289 lines
11 KiB
PowerShell
289 lines
11 KiB
PowerShell
# 许可证服务器历史内部部署脚本
|
||
# 状态:默认禁止执行,不作为客户交付入口
|
||
|
||
param(
|
||
[string]$Server = "",
|
||
[string]$User = "root",
|
||
[string]$SshKey = "",
|
||
[string]$RemotePath = "/opt/license-server",
|
||
[string]$PremiumModuleRegistryBase = "",
|
||
[string]$PremiumModuleVersion = "1.0.0",
|
||
[switch]$SkipBuild = $false,
|
||
[switch]$AcknowledgeLegacyInternalDeploy
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
if (-not $AcknowledgeLegacyInternalDeploy) {
|
||
Write-Host "[ERROR] license-server/deploy.ps1 是历史内部部署脚本,默认禁止执行。" -ForegroundColor Red
|
||
Write-Host "许可证服务是授权中枢,正式部署前必须先确认目标服务器、密钥、版本镜像和回滚方案。" -ForegroundColor Yellow
|
||
Write-Host "如确需排查历史链路,请追加 -AcknowledgeLegacyInternalDeploy,并显式传入 -Server 和 -SshKey。" -ForegroundColor Yellow
|
||
exit 1
|
||
}
|
||
|
||
if ([string]::IsNullOrWhiteSpace($Server) -or [string]::IsNullOrWhiteSpace($SshKey)) {
|
||
throw "请显式传入 -Server 和 -SshKey,脚本不再默认指向旧服务器或旧 SSH key。"
|
||
}
|
||
|
||
function New-RandomHex {
|
||
param([int]$Bytes = 32)
|
||
$buffer = New-Object byte[] $Bytes
|
||
[System.Security.Cryptography.RandomNumberGenerator]::Fill($buffer)
|
||
return ([System.BitConverter]::ToString($buffer)).Replace("-", "").ToLower()
|
||
}
|
||
|
||
function New-RandomBase64Url {
|
||
param([int]$Bytes = 18)
|
||
$buffer = New-Object byte[] $Bytes
|
||
[System.Security.Cryptography.RandomNumberGenerator]::Fill($buffer)
|
||
$base64 = [Convert]::ToBase64String($buffer)
|
||
return $base64.TrimEnd('=').Replace('+', '-').Replace('/', '_')
|
||
}
|
||
|
||
function Convert-ToEscapedEnvMultiline {
|
||
param([string]$Value)
|
||
return ($Value.Trim() -replace "`r", "" -replace "`n", "\n")
|
||
}
|
||
|
||
function New-RsaKeyPair {
|
||
$rsa = [System.Security.Cryptography.RSA]::Create(2048)
|
||
try {
|
||
return @{
|
||
PrivateKey = $rsa.ExportPkcs8PrivateKeyPem()
|
||
PublicKey = $rsa.ExportSubjectPublicKeyInfoPem()
|
||
}
|
||
} finally {
|
||
$rsa.Dispose()
|
||
}
|
||
}
|
||
|
||
function Get-EnvValue {
|
||
param(
|
||
[string]$Content,
|
||
[string]$Key
|
||
)
|
||
|
||
$match = [regex]::Match($Content, "(?m)^" + [regex]::Escape($Key) + "=(.*)$")
|
||
if (-not $match.Success) {
|
||
return $null
|
||
}
|
||
|
||
$value = $match.Groups[1].Value.Trim()
|
||
if ($value.StartsWith('"') -and $value.EndsWith('"')) {
|
||
return $value.Substring(1, $value.Length - 2)
|
||
}
|
||
return $value
|
||
}
|
||
|
||
function Test-EnvConfig {
|
||
param([string]$Content)
|
||
|
||
$requiredKeys = @(
|
||
"POSTGRES_USER",
|
||
"POSTGRES_PASSWORD",
|
||
"POSTGRES_DB",
|
||
"MAIN_DATABASE_URL",
|
||
"MAIN_DB_ENCRYPTION_KEY",
|
||
"DATABASE_URL",
|
||
"RSA_PRIVATE_KEY",
|
||
"RSA_PUBLIC_KEY",
|
||
"AES_SECRET_KEY",
|
||
"LICENSE_REQUEST_SIGNING_SECRET",
|
||
"MODULE_PULL_TOKEN_SECRET",
|
||
"PREMIUM_MODULE_REGISTRY_BASE",
|
||
"PREMIUM_MODULE_VERSION",
|
||
"JWT_SECRET",
|
||
"ADMIN_INITIAL_PASSWORD"
|
||
)
|
||
|
||
foreach ($key in $requiredKeys) {
|
||
$value = Get-EnvValue -Content $Content -Key $key
|
||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||
throw ".env 缺少必填项: $key"
|
||
}
|
||
}
|
||
|
||
$blockedPatterns = @(
|
||
"your-jwt-secret",
|
||
"your-rsa-private-key",
|
||
"your-main-db-encryption-key",
|
||
"your-32-byte-hex-key",
|
||
"replace-with",
|
||
"registry.example.com",
|
||
"admin123",
|
||
"license_secret"
|
||
)
|
||
|
||
foreach ($pattern in $blockedPatterns) {
|
||
if ($Content -match [regex]::Escape($pattern)) {
|
||
throw ".env 中仍包含不安全占位值: $pattern"
|
||
}
|
||
}
|
||
|
||
$jwtSecret = Get-EnvValue -Content $Content -Key "JWT_SECRET"
|
||
if ($jwtSecret.Length -lt 32) {
|
||
throw "JWT_SECRET 长度不足 32 字节"
|
||
}
|
||
|
||
$mainDbEncryptionKey = Get-EnvValue -Content $Content -Key "MAIN_DB_ENCRYPTION_KEY"
|
||
if ($mainDbEncryptionKey.Length -lt 32) {
|
||
throw "MAIN_DB_ENCRYPTION_KEY 长度不足 32 字节"
|
||
}
|
||
|
||
$aesSecretKey = Get-EnvValue -Content $Content -Key "AES_SECRET_KEY"
|
||
if ($aesSecretKey.Length -lt 64) {
|
||
throw "AES_SECRET_KEY 需要至少 64 个十六进制字符"
|
||
}
|
||
}
|
||
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host "许可证服务器部署" -ForegroundColor Cyan
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host ""
|
||
Write-Host "目标服务器: $User@$Server" -ForegroundColor Yellow
|
||
Write-Host "远程路径: $RemotePath" -ForegroundColor Yellow
|
||
Write-Host ""
|
||
|
||
# SSH 命令封装
|
||
function Invoke-RemoteCommand {
|
||
param([string]$Command)
|
||
ssh -i $SshKey "$User@$Server" $Command
|
||
if ($LASTEXITCODE -ne 0) { throw "远程命令执行失败: $Command" }
|
||
}
|
||
|
||
# SCP 上传
|
||
function Upload-File {
|
||
param([string]$Local, [string]$Remote)
|
||
scp -i $SshKey -r $Local "$User@$Server`:$Remote"
|
||
if ($LASTEXITCODE -ne 0) { throw "文件上传失败: $Local" }
|
||
}
|
||
|
||
# 1. 检查 SSH 连接
|
||
Write-Host "1. 检查 SSH 连接..." -ForegroundColor White
|
||
try {
|
||
Invoke-RemoteCommand "echo 'SSH 连接成功'"
|
||
Write-Host " SSH 连接成功 ✓" -ForegroundColor Green
|
||
} catch {
|
||
Write-Host " SSH 连接失败,请检查密钥和网络" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
|
||
# 2. 创建远程目录
|
||
Write-Host "2. 创建远程目录..." -ForegroundColor White
|
||
Invoke-RemoteCommand "mkdir -p $RemotePath"
|
||
Write-Host " 目录创建成功 ✓" -ForegroundColor Green
|
||
|
||
# 3. 上传文件
|
||
Write-Host "3. 上传项目文件..." -ForegroundColor White
|
||
|
||
# 创建临时目录存放要上传的文件
|
||
$tempDir = Join-Path $env:TEMP "license-server-deploy"
|
||
if (Test-Path $tempDir) { Remove-Item -Recurse -Force $tempDir }
|
||
New-Item -ItemType Directory -Path $tempDir | Out-Null
|
||
|
||
# 复制必要文件
|
||
Copy-Item -Path "docker-compose.yml" -Destination $tempDir
|
||
Copy-Item -Path "Dockerfile" -Destination $tempDir
|
||
Copy-Item -Path "package*.json" -Destination $tempDir
|
||
Copy-Item -Path "next.config.js" -Destination $tempDir
|
||
Copy-Item -Path "tsconfig.json" -Destination $tempDir
|
||
Copy-Item -Path "tailwind.config.ts" -Destination $tempDir
|
||
Copy-Item -Path "postcss.config.js" -Destination $tempDir
|
||
Copy-Item -Path "prisma" -Destination $tempDir -Recurse
|
||
Copy-Item -Path "app" -Destination $tempDir -Recurse
|
||
Copy-Item -Path "lib" -Destination $tempDir -Recurse
|
||
Copy-Item -Path "components" -Destination $tempDir -Recurse -ErrorAction SilentlyContinue
|
||
if (Test-Path "public") { Copy-Item -Path "public" -Destination $tempDir -Recurse }
|
||
|
||
# 上传
|
||
Upload-File "$tempDir/*" $RemotePath
|
||
Write-Host " 文件上传成功 ✓" -ForegroundColor Green
|
||
|
||
# 清理临时目录
|
||
Remove-Item -Recurse -Force $tempDir
|
||
|
||
# 4. 创建 .env 文件(如果不存在)
|
||
Write-Host "4. 检查环境变量配置..." -ForegroundColor White
|
||
$envExists = ssh -i $SshKey "$User@$Server" "test -f $RemotePath/.env && echo 'exists'"
|
||
if ($envExists -ne "exists") {
|
||
Write-Host " 生成安全 .env 配置..." -ForegroundColor Yellow
|
||
if ([string]::IsNullOrWhiteSpace($PremiumModuleRegistryBase)) {
|
||
throw "请通过 -PremiumModuleRegistryBase 指定授权模块镜像站路径,例如 registry.example.com/flnxi/premium"
|
||
}
|
||
|
||
$postgresUser = "license"
|
||
$postgresPassword = New-RandomBase64Url
|
||
$postgresDb = "license_db"
|
||
$jwtSecret = New-RandomHex
|
||
$mainDbEncryptionKey = New-RandomHex
|
||
$aesSecretKey = New-RandomHex
|
||
$licenseRequestSigningSecret = New-RandomHex
|
||
$modulePullTokenSecret = New-RandomHex
|
||
$adminInitialPassword = New-RandomBase64Url
|
||
$rsaKeys = New-RsaKeyPair
|
||
$mainDatabaseUrl = "postgresql://${postgresUser}:${postgresPassword}@postgres:5432/${postgresDb}?schema=main"
|
||
$projectDatabaseUrl = "postgresql://${postgresUser}:${postgresPassword}@postgres:5432/${postgresDb}?schema=public"
|
||
|
||
$envContent = @"
|
||
# 许可证服务器环境变量
|
||
POSTGRES_USER=$postgresUser
|
||
POSTGRES_PASSWORD=$postgresPassword
|
||
POSTGRES_DB=$postgresDb
|
||
MAIN_DATABASE_URL=$mainDatabaseUrl
|
||
MAIN_DB_ENCRYPTION_KEY=$mainDbEncryptionKey
|
||
DATABASE_URL=$projectDatabaseUrl
|
||
RSA_PRIVATE_KEY=$(Convert-ToEscapedEnvMultiline $rsaKeys.PrivateKey)
|
||
RSA_PUBLIC_KEY=$(Convert-ToEscapedEnvMultiline $rsaKeys.PublicKey)
|
||
AES_SECRET_KEY=$aesSecretKey
|
||
JWT_SECRET=$jwtSecret
|
||
LICENSE_REQUEST_SIGNING_SECRET=$licenseRequestSigningSecret
|
||
MODULE_PULL_TOKEN_SECRET=$modulePullTokenSecret
|
||
PREMIUM_MODULE_REGISTRY_BASE=$PremiumModuleRegistryBase
|
||
PREMIUM_MODULE_VERSION=$PremiumModuleVersion
|
||
ADMIN_INITIAL_PASSWORD=$adminInitialPassword
|
||
"@
|
||
ssh -i $SshKey "$User@$Server" "cat > $RemotePath/.env << 'EOF'
|
||
$envContent
|
||
EOF"
|
||
Write-Host " 已生成安全 .env 文件 ✓" -ForegroundColor Green
|
||
Write-Host " 初始管理员密码已写入远程 .env 的 ADMIN_INITIAL_PASSWORD,不在部署日志中输出明文" -ForegroundColor Yellow
|
||
} else {
|
||
$remoteEnv = ssh -i $SshKey "$User@$Server" "cat $RemotePath/.env"
|
||
Test-EnvConfig -Content $remoteEnv
|
||
Write-Host " .env 文件已存在且通过安全检查 ✓" -ForegroundColor Green
|
||
}
|
||
|
||
# 5. 构建和启动
|
||
Write-Host "5. 构建和启动服务..." -ForegroundColor White
|
||
Invoke-RemoteCommand "cd $RemotePath && docker compose down 2>/dev/null || true"
|
||
Invoke-RemoteCommand "cd $RemotePath && docker compose build --no-cache"
|
||
Invoke-RemoteCommand "cd $RemotePath && docker compose up -d"
|
||
Write-Host " 服务启动成功 ✓" -ForegroundColor Green
|
||
|
||
# 6. 等待服务就绪
|
||
Write-Host "6. 等待服务就绪..." -ForegroundColor White
|
||
Start-Sleep -Seconds 10
|
||
|
||
# 7. 运行数据库迁移
|
||
Write-Host "7. 运行数据库迁移..." -ForegroundColor White
|
||
Invoke-RemoteCommand "cd $RemotePath && docker compose exec -T license-server npm run db:push"
|
||
Write-Host " 数据库迁移成功 ✓" -ForegroundColor Green
|
||
|
||
# 8. 检查服务状态
|
||
Write-Host "8. 检查服务状态..." -ForegroundColor White
|
||
$status = ssh -i $SshKey "$User@$Server" "docker compose -f $RemotePath/docker-compose.yml ps --format 'table {{.Name}}\t{{.Status}}'"
|
||
Write-Host $status
|
||
|
||
Write-Host ""
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host "部署完成!" -ForegroundColor Green
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host ""
|
||
Write-Host "许可证服务器地址: http://$Server`:9527" -ForegroundColor Yellow
|
||
Write-Host ""
|
||
Write-Host "后续步骤:" -ForegroundColor White
|
||
Write-Host "1. 查看安全配置: ssh -i $SshKey $User@$Server 'sed -n \"1,20p\" $RemotePath/.env'" -ForegroundColor Gray
|
||
Write-Host "2. 重启服务: ssh -i $SshKey $User@$Server 'cd $RemotePath && docker compose restart'" -ForegroundColor Gray
|
||
Write-Host "3. 初始化超管: ssh -i $SshKey $User@$Server 'cd $RemotePath && docker compose exec license-server npm run init-admin -- admin'" -ForegroundColor Gray
|
||
Write-Host "4. 初始化项目: ssh -i $SshKey $User@$Server 'cd $RemotePath && docker compose exec license-server npm run init-project -- flnxi-sucai'" -ForegroundColor Gray
|