apps/web was never deployed, so /billing on the public domain returned the landing page and d3ro.dev (desktop "upgrade") did not resolve. - apps/web runs with basePath /app and output standalone; /download and /releases redirect to the site's #download. A Dockerfile and a d3ro-web compose service (port 3002) deploy it to the NAS with the other images. - The site bridge worker forwards /app/* to WEB_APP_ORIGIN (the tunnel host) and rewrites upstream redirects; everything else still goes to Pages. With no origin configured /app answers 503 instead of the landing page. - Desktop upgrade, desktop Stripe return, mobile subscription management, the web checkout/portal returns and the site all use billingUrl(); the return query is success=1 / canceled=1, which the billing page reads. The billing page highlights ?tier=pro|pro_plus, and signing in from a billing link returns to the same plan. - auth/callback pins the redirect origin in production and rejects protocol-relative next= values (open redirect). - Mobile legal links use SITE_URLS (fixes the missing slash on /terms). - Compose drops the unused NEXT_PUBLIC_API_URL and the dead wwwroot legal mounts; deploy scripts add the web image and the SUPABASE_* values the NAS compose already required; .dockerignore keeps app .env files out of images. - Supabase auth redirects allow /app/** (remote dashboard must match). Policy: docs/REFACTOR_POLICY.md Wave 3, W3-3 and W3-4.
251 lines
12 KiB
PowerShell
251 lines
12 KiB
PowerShell
# ============================================================================
|
|
# scripts/deploy-nas.ps1
|
|
# D3RO Voice — Automated NAS Docker Deployment & Packaging Script (PowerShell)
|
|
# ============================================================================
|
|
|
|
param (
|
|
[string]$NasHost = "",
|
|
[string]$NasUser = "",
|
|
[int]$NasSshPort = 0,
|
|
[string]$NasPath = "",
|
|
[int]$Port = 0,
|
|
[switch]$SkipBuild = $false,
|
|
[switch]$DeploySsh = $false
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# Load variables from root .env if present
|
|
$rootEnvFile = Join-Path $PSScriptRoot "..\.env"
|
|
$envDict = @{}
|
|
if (Test-Path $rootEnvFile) {
|
|
Get-Content $rootEnvFile | ForEach-Object {
|
|
$line = $_.Trim()
|
|
if ($line -and -not $line.StartsWith("#") -and $line.Contains("=")) {
|
|
$parts = $line.Split("=", 2)
|
|
$envDict[$parts[0].Trim()] = $parts[1].Trim()
|
|
}
|
|
}
|
|
}
|
|
|
|
# Fallback to .env values if parameters are not provided
|
|
if (-not $NasHost -and $envDict.ContainsKey("NAS_HOST")) { $NasHost = $envDict["NAS_HOST"] }
|
|
if (-not $NasUser -and $envDict.ContainsKey("NAS_USER")) { $NasUser = $envDict["NAS_USER"] }
|
|
if ($NasSshPort -eq 0 -and $envDict.ContainsKey("NAS_SSH_PORT")) { [int]::TryParse($envDict["NAS_SSH_PORT"], [ref]$NasSshPort) | Out-Null }
|
|
if (-not $NasPath -and $envDict.ContainsKey("NAS_DEPLOY_PATH")) { $NasPath = $envDict["NAS_DEPLOY_PATH"] }
|
|
if ($Port -eq 0 -and $envDict.ContainsKey("PORT")) { [int]::TryParse($envDict["PORT"], [ref]$Port) | Out-Null }
|
|
|
|
# Defaults if still empty
|
|
if (-not $NasUser) { $NasUser = "yunchan" }
|
|
if ($NasSshPort -eq 0) { $NasSshPort = 22 }
|
|
if (-not $NasPath) { $NasPath = "/volume1/docker/d3ro" }
|
|
if ($Port -eq 0) { $Port = 5000 }
|
|
|
|
Write-Host "========================================================" -ForegroundColor Cyan
|
|
Write-Host " D3RO Voice — NAS Docker Deployment & Packaging Tool " -ForegroundColor Cyan
|
|
Write-Host "========================================================" -ForegroundColor Cyan
|
|
|
|
if ($NasHost) {
|
|
Write-Host " [Target NAS] $NasUser@$($NasHost):$NasSshPort (Path: $NasPath)" -ForegroundColor Gray
|
|
}
|
|
|
|
# 1. Output directory setup
|
|
$outDir = Join-Path $PSScriptRoot "..\out"
|
|
$nasPkgDir = Join-Path $outDir "nas-package"
|
|
if (-not (Test-Path $nasPkgDir)) {
|
|
New-Item -ItemType Directory -Path $nasPkgDir -Force | Out-Null
|
|
}
|
|
|
|
# 2. Build Docker Image
|
|
if (-not $SkipBuild) {
|
|
Write-Host "`n[1/4] Building D3RO Voice API Image..." -ForegroundColor Yellow
|
|
docker build -t d3ro-voice-api:latest ./apps/api-server
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "API Docker image build failed!"
|
|
exit 1
|
|
}
|
|
Write-Host " Docker image built: d3ro-voice-api:latest" -ForegroundColor Green
|
|
|
|
Write-Host "`n[1.5/4] Building D3RO Voice Next.js Admin CRM Image..." -ForegroundColor Yellow
|
|
docker build -t d3ro-voice-admin:latest -f apps/admin/Dockerfile .
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Admin Docker image build failed!"
|
|
exit 1
|
|
}
|
|
Write-Host " Docker image built: d3ro-voice-admin:latest" -ForegroundColor Green
|
|
|
|
# Web app (@d3ro/web, served at /app). NEXT_PUBLIC_* values are baked in at build time.
|
|
Write-Host "`n[1.6/4] Building D3RO Voice Next.js Web App Image..." -ForegroundColor Yellow
|
|
$webBuildArgs = @()
|
|
foreach ($name in @("NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY", "NEXT_PUBLIC_PAYPLE_CLIENT_KEY")) {
|
|
$value = if ($envDict.ContainsKey($name)) { $envDict[$name] } else { [Environment]::GetEnvironmentVariable($name) }
|
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
|
if ($name -eq "NEXT_PUBLIC_PAYPLE_CLIENT_KEY") {
|
|
Write-Warning "$name is empty: the web app will build with Payple checkout disabled."
|
|
continue
|
|
}
|
|
throw "$name is required to build the web app image."
|
|
}
|
|
if ($value.Contains("`r") -or $value.Contains("`n")) {
|
|
throw "$name must be a single-line value."
|
|
}
|
|
$webBuildArgs += @("--build-arg", "$name=$value")
|
|
}
|
|
docker build -t d3ro-voice-web:latest -f apps/web/Dockerfile @webBuildArgs .
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Web Docker image build failed!"
|
|
exit 1
|
|
}
|
|
Write-Host " Docker image built: d3ro-voice-web:latest" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "`n[1/4] Skipping Docker build (-SkipBuild specified)" -ForegroundColor Gray
|
|
}
|
|
|
|
# 3. Export Docker Image Archives
|
|
$tarPath = Join-Path $nasPkgDir "d3ro-voice-api.tar"
|
|
$adminTarPath = Join-Path $nasPkgDir "d3ro-voice-admin.tar"
|
|
$webTarPath = Join-Path $nasPkgDir "d3ro-voice-web.tar"
|
|
Write-Host "`n[2/4] Exporting Docker Images to Archives..." -ForegroundColor Yellow
|
|
docker save -o $tarPath d3ro-voice-api:latest
|
|
docker save -o $adminTarPath d3ro-voice-admin:latest
|
|
docker save -o $webTarPath d3ro-voice-web:latest
|
|
Write-Host " Image archives exported: $tarPath, $adminTarPath, $webTarPath" -ForegroundColor Green
|
|
|
|
# 4. Prepare Standalone NAS Package
|
|
Write-Host "`n[3/4] Packaging NAS deployment files..." -ForegroundColor Yellow
|
|
|
|
# Copy docker-compose.nas.yml as docker-compose.yml in package
|
|
Copy-Item (Join-Path $PSScriptRoot "..\docker-compose.nas.yml") (Join-Path $nasPkgDir "docker-compose.yml") -Force
|
|
|
|
# Generate an allowlisted deployment environment. Never copy the repository
|
|
# .env because it can also contain Git, SSH, and NAS credentials.
|
|
$requiredDeploymentSecrets = @(
|
|
"JWT_SECRET",
|
|
"JWT_ISSUER",
|
|
"JWT_AUDIENCE",
|
|
"ADMIN_BOOTSTRAP_TOKEN",
|
|
"D3RO_API_TOKEN",
|
|
"ADMIN_SESSION_SECRET",
|
|
"API_SERVER_URL",
|
|
"CORS_ALLOWED_ORIGINS",
|
|
"ALLOWED_HOSTS",
|
|
"SUPABASE_URL",
|
|
"SUPABASE_SERVICE_ROLE_KEY"
|
|
)
|
|
$deploymentSecrets = @{}
|
|
foreach ($name in $requiredDeploymentSecrets) {
|
|
$value = if ($envDict.ContainsKey($name)) { $envDict[$name] } else { [Environment]::GetEnvironmentVariable($name) }
|
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
|
throw "$name is required for a NAS deployment package."
|
|
}
|
|
if ($value.Contains("`r") -or $value.Contains("`n")) {
|
|
throw "$name must be a single-line value."
|
|
}
|
|
$deploymentSecrets[$name] = $value
|
|
}
|
|
if ([Text.Encoding]::UTF8.GetByteCount($deploymentSecrets["D3RO_API_TOKEN"]) -lt 32) {
|
|
throw "D3RO_API_TOKEN must contain at least 32 UTF-8 bytes."
|
|
}
|
|
$envContent = @"
|
|
PORT=$Port
|
|
DATA_PATH=./data
|
|
JWT_SECRET=$($deploymentSecrets["JWT_SECRET"])
|
|
JWT_ISSUER=$($deploymentSecrets["JWT_ISSUER"])
|
|
JWT_AUDIENCE=$($deploymentSecrets["JWT_AUDIENCE"])
|
|
ADMIN_BOOTSTRAP_TOKEN=$($deploymentSecrets["ADMIN_BOOTSTRAP_TOKEN"])
|
|
D3RO_API_TOKEN=$($deploymentSecrets["D3RO_API_TOKEN"])
|
|
ADMIN_SESSION_SECRET=$($deploymentSecrets["ADMIN_SESSION_SECRET"])
|
|
API_SERVER_URL=$($deploymentSecrets["API_SERVER_URL"])
|
|
CORS_ALLOWED_ORIGINS=$($deploymentSecrets["CORS_ALLOWED_ORIGINS"])
|
|
ALLOWED_HOSTS=$($deploymentSecrets["ALLOWED_HOSTS"])
|
|
SUPABASE_URL=$($deploymentSecrets["SUPABASE_URL"])
|
|
SUPABASE_SERVICE_ROLE_KEY=$($deploymentSecrets["SUPABASE_SERVICE_ROLE_KEY"])
|
|
TZ=Asia/Seoul
|
|
"@
|
|
$deploymentEnvPath = Join-Path $nasPkgDir ".env"
|
|
Set-Content -Path $deploymentEnvPath -Value $envContent -Encoding UTF8
|
|
$deploymentEnvAcl = Get-Acl -LiteralPath $deploymentEnvPath
|
|
$deploymentEnvAcl.SetAccessRuleProtection($true, $false)
|
|
$deploymentEnvAcl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
))
|
|
Set-Acl -LiteralPath $deploymentEnvPath -AclObject $deploymentEnvAcl
|
|
|
|
# Copy control script
|
|
Copy-Item (Join-Path $PSScriptRoot "nas-control.sh") (Join-Path $nasPkgDir "nas-control.sh") -Force -ErrorAction SilentlyContinue
|
|
|
|
# Create Quick Instructions
|
|
$readmeContent = @"
|
|
========================================================================
|
|
D3RO Voice — Standalone NAS Deployment Package
|
|
========================================================================
|
|
|
|
[Synology / QNAP / Linux NAS 배포 방법]
|
|
|
|
1. 이 폴더의 모든 파일 (d3ro-voice-api.tar, d3ro-voice-admin.tar, d3ro-voice-web.tar,
|
|
docker-compose.yml, .env, nas-control.sh)을 NAS의 Docker 작업 폴더 ($NasPath)에 업로드합니다.
|
|
|
|
2. NAS SSH 터미널에 접속하여 해당 폴더로 이동합니다:
|
|
cd $NasPath
|
|
|
|
3. Docker 이미지를 로드합니다:
|
|
docker load < d3ro-voice-api.tar
|
|
docker load < d3ro-voice-admin.tar
|
|
docker load < d3ro-voice-web.tar
|
|
|
|
4. 서비스를 시작합니다:
|
|
docker compose up -d
|
|
|
|
5. 브라우저에서 접속 확인:
|
|
- 포털 메인: http://<NAS_IP>:$Port/
|
|
- 관리자 백오피스: http://<NAS_IP>:$Port/admin
|
|
- API Swagger: http://<NAS_IP>:$Port/swagger
|
|
- 헬스체크: http://<NAS_IP>:$Port/health
|
|
- 웹앱: http://<NAS_IP>:3002/app/login (공개 주소 https://d3ro.chanpaca.net/app 는 사이트 브리지 워커 경유)
|
|
"@
|
|
Set-Content -Path (Join-Path $nasPkgDir "README.txt") -Value $readmeContent -Encoding UTF8
|
|
|
|
Write-Host " NAS Deployment Package ready at: $nasPkgDir" -ForegroundColor Green
|
|
|
|
# 5. Remote SSH Deployment (Optional or if -DeploySsh is specified)
|
|
if ($DeploySsh -and $NasHost) {
|
|
Write-Host "`n[4/4] Deploying to Remote NAS ($NasUser@$($NasHost):$NasPath on SSH Port $NasSshPort)..." -ForegroundColor Yellow
|
|
|
|
$sshCmd = "ssh -p $NasSshPort"
|
|
$scpCmd = "scp -O -P $NasSshPort"
|
|
|
|
Write-Host " [1/3] Creating remote directory on NAS ($NasPath/data)..." -ForegroundColor Gray
|
|
ssh -p $NasSshPort "$NasUser@$NasHost" "mkdir -p $NasPath/data"
|
|
|
|
Write-Host " [2/3] Uploading deployment package to NAS (SCP)..." -ForegroundColor Gray
|
|
scp -O -P $NasSshPort "$tarPath" "$adminTarPath" "$webTarPath" (Join-Path $nasPkgDir "docker-compose.yml") (Join-Path $nasPkgDir ".env") (Join-Path $nasPkgDir "nas-control.sh") "$NasUser@$($NasHost):$NasPath/"
|
|
|
|
Write-Host " [3/3] Loading Docker images and launching services on NAS..." -ForegroundColor Gray
|
|
ssh -p $NasSshPort "$NasUser@$NasHost" "cd $NasPath && chmod +x nas-control.sh 2>/dev/null; docker load < d3ro-voice-api.tar && docker load < d3ro-voice-admin.tar && docker load < d3ro-voice-web.tar && docker compose down 2>/dev/null; docker compose up -d"
|
|
|
|
Write-Host "`n========================================================" -ForegroundColor Green
|
|
Write-Host " NAS Deployment Completed Successfully!" -ForegroundColor Green
|
|
Write-Host " Access your D3RO Cloud Services at:" -ForegroundColor Cyan
|
|
Write-Host " - Next.js Admin CRM: http://$($NasHost):3001/" -ForegroundColor White
|
|
Write-Host " - Next.js Web App: http://$($NasHost):3002/app/login" -ForegroundColor White
|
|
Write-Host " - Swagger API Docs: http://$($NasHost):$Port/swagger" -ForegroundColor White
|
|
Write-Host " - Health Check: http://$($NasHost):$Port/health" -ForegroundColor White
|
|
Write-Host "========================================================" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "`n========================================================" -ForegroundColor Green
|
|
Write-Host " NAS Package Ready!" -ForegroundColor Green
|
|
Write-Host " Output Folder: $nasPkgDir" -ForegroundColor White
|
|
Write-Host " Files generated:" -ForegroundColor White
|
|
Write-Host " - d3ro-voice-api.tar (Docker image archive)" -ForegroundColor Gray
|
|
Write-Host " - d3ro-voice-admin.tar (Docker image archive)" -ForegroundColor Gray
|
|
Write-Host " - d3ro-voice-web.tar (Docker image archive)" -ForegroundColor Gray
|
|
Write-Host " - docker-compose.yml (NAS Compose configuration)" -ForegroundColor Gray
|
|
Write-Host " - .env (Environment variables)" -ForegroundColor Gray
|
|
Write-Host " - nas-control.sh (Container management script)" -ForegroundColor Gray
|
|
Write-Host " - README.txt (Deployment guide)" -ForegroundColor Gray
|
|
Write-Host "`n To deploy automatically via SSH, run:" -ForegroundColor Cyan
|
|
Write-Host " .\scripts\deploy-nas.ps1 -DeploySsh" -ForegroundColor Yellow
|
|
Write-Host "========================================================" -ForegroundColor Green
|
|
}
|