[CLAUDE] Scripts: rewrite 4 deploy PS1 ASCII-only for PS 5.1 compat
Some checks failed
Deploy SOLUTION_ERP / build-backend (push) Failing after 9s
Deploy SOLUTION_ERP / build-fe-admin (push) Has been cancelled
Deploy SOLUTION_ERP / build-fe-user (push) Has been cancelled
Deploy SOLUTION_ERP / deploy-iis (push) Has been cancelled

PowerShell 5.1 reads .ps1 files as locale codepage (not UTF-8 no BOM),
which corrupts multi-byte Vietnamese chars and breaks parsing. Rewrote
setup-iis-sites.ps1, setup-ssl.ps1, setup-gitea-runner.ps1, deploy-all.ps1
as ASCII-only. Also renamed $Host param to $HostName in Ensure-Site to
avoid collision with PowerShell built-in $Host automatic variable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 14:17:36 +07:00
parent 85acf750b3
commit 169e268b28
4 changed files with 131 additions and 137 deletions

View File

@ -1,5 +1,5 @@
# Deploy SOLUTION_ERP all-in-one trên VPS Windows Server. # Deploy SOLUTION_ERP all-in-one on VPS Windows Server.
# Chạy 4 step: SQL IIS SSL Runner. Idempotent. Stop ngay khi step fail. # Runs 4 steps: SQL -> IIS -> SSL -> Runner. Idempotent. Stops on first step fail.
# #
# Usage: # Usage:
# .\deploy-all.ps1 ` # .\deploy-all.ps1 `
@ -9,13 +9,13 @@
# -RunnerToken '<gitea runner registration token>' ` # -RunnerToken '<gitea runner registration token>' `
# -AdminEmail 'admin@huypham.vn' # -AdminEmail 'admin@huypham.vn'
# #
# Prereq (pre-check script sẽ verify): # Prereq (pre-check will verify):
# - Windows Server + Admin PowerShell # - Windows Server + Admin PowerShell
# - IIS + URL Rewrite installed # - IIS + URL Rewrite installed
# - SQL Server với login 'sa' + 'vrapp' tồn tại # - SQL Server with login 'sa' + 'vrapp' exists
# - .NET 10 Hosting Bundle installed # - .NET 10 Hosting Bundle installed
# - Port 80+443 firewall open # - Port 80+443 firewall open
# - DNS api/admin/user.huypham.vn đã trỏ VPS # - DNS api/admin/user.huypham.vn pointing to VPS
[CmdletBinding()] [CmdletBinding()]
param( param(
@ -43,20 +43,22 @@ function Test-Prereq {
$issues = @() $issues = @()
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
$issues += "Không chạy với Admin privilege" $issues += "Not running with Admin privilege"
} }
if (-not (Get-Module -ListAvailable -Name SqlServer)) {
if (-not (Get-Command sqlcmd -ErrorAction SilentlyContinue)) { if (-not (Get-Command sqlcmd -ErrorAction SilentlyContinue)) {
$issues += "sqlcmd không có trong PATH (SQL Server Client tools)" $issues += "Neither SqlServer PS module nor sqlcmd.exe available. Install-Module SqlServer -Scope AllUsers -Force"
}
} }
if (-not (Get-Module -ListAvailable -Name WebAdministration)) { if (-not (Get-Module -ListAvailable -Name WebAdministration)) {
$issues += "IIS WebAdministration module không cài (Install-WindowsFeature Web-Server,Web-Scripting-Tools)" $issues += "IIS WebAdministration module not installed (Install-WindowsFeature Web-Server,Web-Scripting-Tools)"
} }
$dotnet10 = & dotnet --list-runtimes 2>$null | Select-String "Microsoft.AspNetCore.App 10" $dotnet10 = & dotnet --list-runtimes 2>$null | Select-String "Microsoft.AspNetCore.App 10"
if (-not $dotnet10) { if (-not $dotnet10) {
$issues += ".NET 10 Hosting Bundle chưa cài (https://dotnet.microsoft.com/download/dotnet/10.0)" $issues += ".NET 10 Hosting Bundle not installed (https://dotnet.microsoft.com/download/dotnet/10.0)"
} }
return $issues return $issues
@ -65,13 +67,13 @@ function Test-Prereq {
Write-Banner "Pre-check prerequisites" Write-Banner "Pre-check prerequisites"
$issues = Test-Prereq $issues = Test-Prereq
if ($issues.Count -gt 0) { if ($issues.Count -gt 0) {
Write-Host "❌ Các vấn đề:" -ForegroundColor Red Write-Host "[FAIL] Issues:" -ForegroundColor Red
$issues | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } $issues | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
Write-Host "" Write-Host ""
Write-Host "Fix các vấn đề trên rồi chạy lại." -ForegroundColor Yellow Write-Host "Fix the above then re-run." -ForegroundColor Yellow
exit 1 exit 1
} }
Write-Host "✅ Tất cả prerequisite OK" Write-Host "[OK] All prerequisites present"
# ===================== Step 1: SQL DB ===================== # ===================== Step 1: SQL DB =====================
Write-Banner "Step 1/4: SQL Database setup" Write-Banner "Step 1/4: SQL Database setup"
@ -79,7 +81,7 @@ try {
& "$ScriptRoot\setup-sql-db.ps1" -SaPassword $SaPassword & "$ScriptRoot\setup-sql-db.ps1" -SaPassword $SaPassword
if ($LASTEXITCODE -ne 0) { throw "setup-sql-db.ps1 exit $LASTEXITCODE" } if ($LASTEXITCODE -ne 0) { throw "setup-sql-db.ps1 exit $LASTEXITCODE" }
} catch { } catch {
Write-Host " Step 1 FAIL: $_" -ForegroundColor Red Write-Host "[FAIL] Step 1: $_" -ForegroundColor Red
exit 1 exit 1
} }
@ -89,7 +91,7 @@ try {
& "$ScriptRoot\setup-iis-sites.ps1" & "$ScriptRoot\setup-iis-sites.ps1"
if ($LASTEXITCODE -ne 0) { throw "setup-iis-sites.ps1 exit $LASTEXITCODE" } if ($LASTEXITCODE -ne 0) { throw "setup-iis-sites.ps1 exit $LASTEXITCODE" }
} catch { } catch {
Write-Host " Step 2 FAIL: $_" -ForegroundColor Red Write-Host "[FAIL] Step 2: $_" -ForegroundColor Red
exit 1 exit 1
} }
@ -100,41 +102,37 @@ $example = "$apiPath\appsettings.Production.json.example"
$prod = "$apiPath\appsettings.Production.json" $prod = "$apiPath\appsettings.Production.json"
if (Test-Path $example) { if (Test-Path $example) {
$content = Get-Content $example -Raw
$content = $content -replace '__SET_VIA_SECRETS__', [regex]::Escape($VrappPassword).Replace('\\','\')
$content = $content -replace '__SET_VIA_USER_SECRETS_OR_ENV__minimum_64_chars_random', [regex]::Escape($JwtSecret).Replace('\\','\')
# Unescape khi json serialization — vì json có escape char riêng, replace simple:
$content = (Get-Content $example -Raw) ` $content = (Get-Content $example -Raw) `
-replace [regex]::Escape('__SET_VIA_SECRETS__'), $VrappPassword ` -replace [regex]::Escape('__SET_VIA_SECRETS__'), $VrappPassword `
-replace [regex]::Escape('__SET_VIA_USER_SECRETS_OR_ENV__minimum_64_chars_random'), $JwtSecret -replace [regex]::Escape('__SET_VIA_USER_SECRETS_OR_ENV__minimum_64_chars_random'), $JwtSecret
Set-Content -Path $prod -Value $content -Encoding UTF8 Set-Content -Path $prod -Value $content -Encoding UTF8
Write-Host " Wrote $prod" Write-Host "[OK] Wrote $prod"
# ACL: chỉ Administrators + app pool identity đọc # ACL: only Administrators + app pool identity can read
icacls $prod /inheritance:r | Out-Null icacls $prod /inheritance:r | Out-Null
icacls $prod /grant:r 'Administrators:(R,W)' 'IIS AppPool\SolutionErp-Api:(R)' | Out-Null icacls $prod /grant:r 'Administrators:(R,W)' 'IIS AppPool\SolutionErp-Api:(R)' | Out-Null
Write-Host " ACL restricted (Admins RW + AppPool R only)" Write-Host " ACL restricted (Admins RW + AppPool R only)"
} else { } else {
Write-Warning "Template không tồn tại tại $example skip (maybe deploy chưa chạy, cần Gitea Actions deploy trước)" Write-Warning "Template not found at $example - skip (deploy may not have run yet; need Gitea Actions deploy first)"
Write-Warning "Sau khi CI deploy xong, rerun step này hoặc copy thủ công." Write-Warning "After CI deploy, re-run this step or copy manually."
} }
# ===================== Step 4: SSL ===================== # ===================== Step 4: SSL =====================
if (-not $SkipSsl) { if (-not $SkipSsl) {
Write-Banner "Step 4/4 (SSL): win-acme Let's Encrypt" Write-Banner "Step 4/4 (SSL): win-acme Let's Encrypt"
Write-Host "⚠️ Port 80 phải reachable từ Internet (Let's Encrypt HTTP-01)" Write-Host "WARNING: Port 80 must be reachable from Internet (Let's Encrypt HTTP-01)"
Write-Host " Test từ máy ngoài: curl http://api.huypham.vn" Write-Host " Test from outside: curl http://api.huypham.vn"
$confirm = Read-Host "Tiếp tục? (y/N)" $confirm = Read-Host "Continue? (y/N)"
if ($confirm -eq 'y') { if ($confirm -eq 'y') {
try { try {
& "$ScriptRoot\setup-ssl.ps1" & "$ScriptRoot\setup-ssl.ps1"
} catch { } catch {
Write-Warning "SSL fail: $_ có thể fix sau + rerun setup-ssl.ps1" Write-Warning "SSL fail: $_ - can fix later + re-run setup-ssl.ps1"
} }
} else { } else {
Write-Host "Skip SSL — chạy setup-ssl.ps1 thủ công sau" Write-Host "Skip SSL - run setup-ssl.ps1 manually later"
} }
} else { } else {
Write-Host "Skip SSL (-SkipSsl flag)" Write-Host "Skip SSL (-SkipSsl flag)"
} }
# ===================== Step 5: Runner ===================== # ===================== Step 5: Runner =====================
@ -143,25 +141,25 @@ if (-not $SkipRunner -and $RunnerToken -ne "SKIP") {
try { try {
& "$ScriptRoot\setup-gitea-runner.ps1" -RegistrationToken $RunnerToken & "$ScriptRoot\setup-gitea-runner.ps1" -RegistrationToken $RunnerToken
} catch { } catch {
Write-Warning "Runner setup fail: $_ — chạy setup-gitea-runner.ps1 thủ công sau" Write-Warning "Runner setup fail: $_ - run setup-gitea-runner.ps1 manually later"
} }
} }
# ===================== Summary ===================== # ===================== Summary =====================
$duration = (Get-Date) - $StartTime $duration = (Get-Date) - $StartTime
Write-Banner " DEPLOY ALL DONE $([int]$duration.TotalMinutes)m $([int]($duration.TotalSeconds % 60))s" Write-Banner "[OK] DEPLOY ALL DONE - $([int]$duration.TotalMinutes)m $([int]($duration.TotalSeconds % 60))s"
Write-Host "" Write-Host ""
Write-Host "Next:" Write-Host "Next:"
Write-Host " 1. Verify 3 domain:" Write-Host " 1. Verify 3 domains:"
Write-Host " curl https://api.huypham.vn/health/live" Write-Host " curl https://api.huypham.vn/health/live"
Write-Host " curl -I https://admin.huypham.vn" Write-Host " curl -I https://admin.huypham.vn"
Write-Host " curl -I https://user.huypham.vn" Write-Host " curl -I https://user.huypham.vn"
Write-Host "" Write-Host ""
Write-Host " 2. Set 2 Gitea secrets còn thiếu (nếu chưa):" Write-Host " 2. Set remaining 2 Gitea secrets (if not done):"
Write-Host " https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/settings/actions/secrets" Write-Host " https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/settings/actions/secrets"
Write-Host " - JWT_SECRET (update với 64-char)" Write-Host " - JWT_SECRET (update with 64-char)"
Write-Host " - IIS_PASSWORD (Windows admin password)" Write-Host " - IIS_PASSWORD (Windows admin password)"
Write-Host "" Write-Host ""
Write-Host " 3. Trigger deploy: push commit main → Gitea Actions pick up workflow chạy" Write-Host " 3. Trigger deploy: push main commit -> Gitea Actions pick up -> workflow runs"
Write-Host "" Write-Host ""
Write-Host " 4. Đổi admin password mặc định sau login (security-checklist.md)" Write-Host " 4. Change default admin password after login (security-checklist.md)"

View File

@ -1,12 +1,12 @@
# Register Gitea Actions runner trên VPS Windows Server. # Register Gitea Actions runner on VPS Windows Server.
# Có thể dùng chung với VIETREPORT (runner có thể serve nhiều repo qua labels). # Can be shared with VIETREPORT (runner serves multiple repos via labels).
# #
# Usage (admin PowerShell): # Usage (admin PowerShell):
# .\setup-gitea-runner.ps1 -RegistrationToken 'xxxx' -RunnerName 'vps-win-01' # .\setup-gitea-runner.ps1 -RegistrationToken 'xxxx' -RunnerName 'vps-win-01'
# #
# Lấy RegistrationToken từ: # Get RegistrationToken from:
# https://git.baocaogiaoduc.vn/-/admin/actions/runners (admin only) # https://git.baocaogiaoduc.vn/-/admin/actions/runners (admin only)
# hoặc per-repo: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/settings/actions/runners # or per-repo: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/settings/actions/runners
param( param(
[Parameter(Mandatory=$true)] [string]$RegistrationToken, [Parameter(Mandatory=$true)] [string]$RegistrationToken,
@ -25,7 +25,7 @@ if (-not (Test-Path $RunnerExe)) {
Write-Host "==> Download Gitea act_runner" -ForegroundColor Cyan Write-Host "==> Download Gitea act_runner" -ForegroundColor Cyan
if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null } if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null }
# Latest release từ Gitea # Latest release from Gitea CDN
$url = "https://dl.gitea.com/act_runner/act_runner-windows-amd64.exe" $url = "https://dl.gitea.com/act_runner/act_runner-windows-amd64.exe"
Invoke-WebRequest -Uri $url -OutFile $RunnerExe -UseBasicParsing Invoke-WebRequest -Uri $url -OutFile $RunnerExe -UseBasicParsing
Write-Host " Downloaded $RunnerExe" Write-Host " Downloaded $RunnerExe"
@ -35,7 +35,8 @@ if (-not (Test-Path $RunnerExe)) {
Set-Location $InstallDir Set-Location $InstallDir
if (-not (Test-Path (Join-Path $InstallDir ".runner"))) { if (-not (Test-Path (Join-Path $InstallDir ".runner"))) {
Write-Host "`n==> Register với Gitea $GiteaUrl" -ForegroundColor Cyan Write-Host ""
Write-Host "==> Register with Gitea $GiteaUrl" -ForegroundColor Cyan
& $RunnerExe register ` & $RunnerExe register `
--no-interactive ` --no-interactive `
--instance $GiteaUrl ` --instance $GiteaUrl `
@ -43,23 +44,22 @@ if (-not (Test-Path (Join-Path $InstallDir ".runner"))) {
--name $RunnerName ` --name $RunnerName `
--labels $Labels --labels $Labels
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "Register fail. Check:`n- Token đúng?`n- GiteaUrl reachable?`n- Runner name '$RunnerName' đã dùng?" Write-Error "Register fail. Check: token correct? GiteaUrl reachable? Runner name '$RunnerName' already used?"
exit 1 exit 1
} }
Write-Host " Registered as '$RunnerName'" Write-Host " Registered as '$RunnerName'"
} else { } else {
Write-Host " Runner đã register (.runner file exists)" Write-Host " Runner already registered (.runner file exists)"
} }
# ===================== 3. Install as Windows service ===================== # ===================== 3. Install as Windows service =====================
$ServiceName = "gitea-runner" $ServiceName = "gitea-runner"
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if (-not $svc) { if (-not $svc) {
Write-Host "`n==> Install Windows service" -ForegroundColor Cyan Write-Host ""
# act_runner không có built-in service install — dùng nssm hoặc sc.exe Write-Host "==> Install Windows service" -ForegroundColor Cyan
# Dùng sc.exe đơn giản: # act_runner has no built-in service install - use sc.exe
$escapedPath = $RunnerExe -replace '\\', '\\' sc.exe create $ServiceName binPath= "`"$RunnerExe`" daemon --config `"$InstallDir\config.yml`"" start= auto DisplayName= "Gitea Actions Runner"
sc.exe create $ServiceName binPath= "`"$escapedPath`" daemon --config `"$InstallDir\config.yml`"" start= auto DisplayName= "Gitea Actions Runner"
Start-Service $ServiceName Start-Service $ServiceName
Write-Host " Service '$ServiceName' installed + started" Write-Host " Service '$ServiceName' installed + started"
} else { } else {
@ -71,7 +71,8 @@ if (-not $svc) {
} }
} }
Write-Host "`n✅ Runner setup DONE" -ForegroundColor Green Write-Host ""
Write-Host " Check trên Gitea: $GiteaUrl/-/admin/actions/runners (admin) hoặc repo settings > Actions > Runners" Write-Host "[OK] Runner setup DONE" -ForegroundColor Green
Write-Host " Check on Gitea: $GiteaUrl/-/admin/actions/runners (admin) or repo settings > Actions > Runners"
Write-Host " Labels: $Labels" Write-Host " Labels: $Labels"
Write-Host " Log: Get-Content '$InstallDir\log.txt' -Tail 50 -Wait" Write-Host " Log: Get-Content '$InstallDir\log.txt' -Tail 50 -Wait"

View File

@ -1,14 +1,14 @@
# Setup IIS sites + app pools cho SOLUTION_ERP trên VPS Windows Server # Setup IIS sites + app pools for SOLUTION_ERP on VPS Windows Server.
# Chia sẻ với VIETREPORT naming isolation: SolutionErp-* prefix để tránh conflict. # Shared with VIETREPORT - naming isolation: SolutionErp-* prefix to avoid conflict.
# Chạy trên VPS với admin privilege. Idempotent. # Run on VPS with admin privilege. Idempotent.
# #
# Usage: # Usage:
# .\setup-iis-sites.ps1 # .\setup-iis-sites.ps1
# #
# Prereq: # Prereq:
# - IIS cài + features: Application Initialization, URL Rewrite, ARR (optional) # - IIS installed + features: Application Initialization, URL Rewrite, ARR (optional)
# - .NET 10 Hosting Bundle cài # - .NET 10 Hosting Bundle installed
# - Port 80/443 firewall đã mở # - Port 80/443 firewall open
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Import-Module WebAdministration Import-Module WebAdministration
@ -45,14 +45,18 @@ foreach ($p in @($Root, $PathApi, $PathAdmin, $PathUser, $PathLogs, $PathUploads
} }
} }
# Grant app pool identity write quyền # Grant app pool identity write permission
$acl = Get-Acl $Root
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"IIS AppPool\$AppPoolApi", "Modify", "ContainerInherit,ObjectInherit", "None", "Allow") "IIS AppPool\$AppPoolApi", "Modify", "ContainerInherit,ObjectInherit", "None", "Allow")
try {
$acl = Get-Acl $Root
$acl.SetAccessRule($rule) $acl.SetAccessRule($rule)
try { Set-Acl -Path $Root -AclObject $acl } catch { Write-Warning " ACL set se fail neu app pool chua ton tai — bypass" } Set-Acl -Path $Root -AclObject $acl
} catch {
Write-Warning " ACL set may fail if app pool does not exist yet - will retry"
}
# ===================== 2. App pool (Api only, FE static) ===================== # ===================== 2. App pool (Api only, FE is static) =====================
Write-Step "App pool: $AppPoolApi" Write-Step "App pool: $AppPoolApi"
if (-not (Test-Path "IIS:\AppPools\$AppPoolApi")) { if (-not (Test-Path "IIS:\AppPools\$AppPoolApi")) {
New-WebAppPool -Name $AppPoolApi | Out-Null New-WebAppPool -Name $AppPoolApi | Out-Null
@ -65,12 +69,12 @@ Set-ItemProperty "IIS:\AppPools\$AppPoolApi" -Name processModel.idleTimeout -Val
Set-ItemProperty "IIS:\AppPools\$AppPoolApi" -Name recycling.periodicRestart.time -Value "00:00:00" Set-ItemProperty "IIS:\AppPools\$AppPoolApi" -Name recycling.periodicRestart.time -Value "00:00:00"
Write-Host " Configured (NoManagedCode + AlwaysRunning + no idle timeout + no daily recycle)" Write-Host " Configured (NoManagedCode + AlwaysRunning + no idle timeout + no daily recycle)"
# Re-grant ACL sau khi pool tạo (identity cần tồn tại) # Re-grant ACL after pool created (identity must exist)
try { try {
$acl = Get-Acl $Root $acl = Get-Acl $Root
$acl.SetAccessRule($rule) $acl.SetAccessRule($rule)
Set-Acl -Path $Root -AclObject $acl Set-Acl -Path $Root -AclObject $acl
Write-Host " ACL granted Modify cho IIS AppPool\$AppPoolApi" Write-Host " ACL granted Modify to IIS AppPool\$AppPoolApi"
} catch { } catch {
Write-Warning " ACL fail: $_" Write-Warning " ACL fail: $_"
} }
@ -78,15 +82,15 @@ try {
# ===================== 3. Sites ===================== # ===================== 3. Sites =====================
function Ensure-Site { function Ensure-Site {
param( param(
[string]$Name, [string]$Host, [string]$Path, [string]$AppPool [string]$Name, [string]$HostName, [string]$PhysicalPath, [string]$AppPool
) )
Write-Step "Site: $Name ($Host)" Write-Step "Site: $Name ($HostName)"
if (-not (Test-Path "IIS:\Sites\$Name")) { if (-not (Test-Path "IIS:\Sites\$Name")) {
# Port 80 HTTP SSL sẽ thêm sau qua win-acme # Port 80 HTTP - SSL added later via win-acme
$params = @{ $params = @{
Name = $Name Name = $Name
HostHeader = $Host HostHeader = $HostName
PhysicalPath = $Path PhysicalPath = $PhysicalPath
Port = 80 Port = 80
} }
if ($AppPool) { $params.ApplicationPool = $AppPool } if ($AppPool) { $params.ApplicationPool = $AppPool }
@ -94,18 +98,18 @@ function Ensure-Site {
Write-Host " Created" Write-Host " Created"
} else { } else {
Write-Host " Exists" Write-Host " Exists"
Set-ItemProperty "IIS:\Sites\$Name" -Name physicalPath -Value $Path Set-ItemProperty "IIS:\Sites\$Name" -Name physicalPath -Value $PhysicalPath
if ($AppPool) { if ($AppPool) {
Set-ItemProperty "IIS:\Sites\$Name" -Name applicationPool -Value $AppPool Set-ItemProperty "IIS:\Sites\$Name" -Name applicationPool -Value $AppPool
} }
} }
} }
Ensure-Site -Name $SiteApi -Host $DomainApi -Path $PathApi -AppPool $AppPoolApi Ensure-Site -Name $SiteApi -HostName $DomainApi -PhysicalPath $PathApi -AppPool $AppPoolApi
Ensure-Site -Name $SiteAdmin -Host $DomainAdmin -Path $PathAdmin -AppPool "" Ensure-Site -Name $SiteAdmin -HostName $DomainAdmin -PhysicalPath $PathAdmin -AppPool ""
Ensure-Site -Name $SiteUser -Host $DomainUser -Path $PathUser -AppPool "" Ensure-Site -Name $SiteUser -HostName $DomainUser -PhysicalPath $PathUser -AppPool ""
# ===================== 4. Placeholder index.html cho FE (tạm trước khi deploy thật) ===================== # ===================== 4. Placeholder index.html for FE (temporary until first real deploy) =====================
Write-Step "Placeholder index.html (pre-deploy)" Write-Step "Placeholder index.html (pre-deploy)"
foreach ($fePath in @($PathAdmin, $PathUser)) { foreach ($fePath in @($PathAdmin, $PathUser)) {
$idx = Join-Path $fePath "index.html" $idx = Join-Path $fePath "index.html"
@ -114,7 +118,7 @@ foreach ($fePath in @($PathAdmin, $PathUser)) {
<!DOCTYPE html><html><head><meta charset="utf-8"><title>SOLUTION ERP</title></head> <!DOCTYPE html><html><head><meta charset="utf-8"><title>SOLUTION ERP</title></head>
<body style="font:14px sans-serif;padding:40px;text-align:center;color:#555"> <body style="font:14px sans-serif;padding:40px;text-align:center;color:#555">
<h1>SOLUTION ERP</h1> <h1>SOLUTION ERP</h1>
<p>Site đã to, ch deploy first build qua Gitea CI/CD.</p> <p>Site created, waiting for first deploy via Gitea CI/CD.</p>
</body></html> </body></html>
"@ | Set-Content -Path $idx -Encoding UTF8 "@ | Set-Content -Path $idx -Encoding UTF8
Write-Host " Wrote $idx" Write-Host " Wrote $idx"
@ -123,8 +127,8 @@ foreach ($fePath in @($PathAdmin, $PathUser)) {
} }
} }
# ===================== 5. web.config cho FE SPA routing ===================== # ===================== 5. web.config for FE SPA routing =====================
Write-Step "web.config cho FE SPA fallback" Write-Step "web.config for FE SPA fallback"
$spaWebConfig = @' $spaWebConfig = @'
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
@ -160,7 +164,7 @@ $spaWebConfig = @'
foreach ($fePath in @($PathAdmin, $PathUser)) { foreach ($fePath in @($PathAdmin, $PathUser)) {
Set-Content -Path (Join-Path $fePath "web.config") -Value $spaWebConfig -Encoding UTF8 Set-Content -Path (Join-Path $fePath "web.config") -Value $spaWebConfig -Encoding UTF8
} }
Write-Host " Wrote SPA web.config cho admin + user" Write-Host " Wrote SPA web.config for admin + user"
# ===================== 6. Firewall ===================== # ===================== 6. Firewall =====================
Write-Step "Firewall port 80/443" Write-Step "Firewall port 80/443"
@ -174,13 +178,14 @@ if (-not $fwExists) {
} }
# ===================== 7. Recap ===================== # ===================== 7. Recap =====================
Write-Host "`n✅ IIS setup DONE" -ForegroundColor Green Write-Host ""
Write-Host "[OK] IIS setup DONE" -ForegroundColor Green
Write-Host " 3 sites:" Write-Host " 3 sites:"
Write-Host " - $SiteApi -> $DomainApi -> $PathApi (app pool $AppPoolApi)" Write-Host " - $SiteApi -> $DomainApi -> $PathApi (app pool $AppPoolApi)"
Write-Host " - $SiteAdmin -> $DomainAdmin -> $PathAdmin (no app pool static)" Write-Host " - $SiteAdmin -> $DomainAdmin -> $PathAdmin (no app pool - static)"
Write-Host " - $SiteUser -> $DomainUser -> $PathUser (no app pool static)" Write-Host " - $SiteUser -> $DomainUser -> $PathUser (no app pool - static)"
Write-Host "" Write-Host ""
Write-Host " Tiếp theo:" Write-Host " Next:"
Write-Host " 1. Chạy .\setup-ssl.ps1 để cài HTTPS cert cho 3 domain" Write-Host " 1. Run .\setup-ssl.ps1 to install HTTPS cert for 3 domains"
Write-Host " 2. Deploy first build qua Gitea Actions (push main trigger workflow)" Write-Host " 2. Deploy first build via Gitea Actions (push main to trigger workflow)"
Write-Host " 3. Verify https://$DomainApi/health/ready 200" Write-Host " 3. Verify https://$DomainApi/health/ready -> 200"

View File

@ -1,19 +1,19 @@
# Cài HTTPS cert Let's Encrypt cho 3 domain SOLUTION_ERP qua win-acme (WACS). # Install HTTPS cert Let's Encrypt for 3 SOLUTION_ERP domains via win-acme (WACS).
# Chạy trên VPS Windows Server với admin privilege. # Run on VPS Windows Server with admin privilege.
# Idempotent: chạy lại sẽ bỏ qua cert còn valid. # Idempotent: re-run skips cert still valid.
# #
# Usage: # Usage:
# .\setup-ssl.ps1 # .\setup-ssl.ps1
# #
# Prereq: # Prereq:
# - IIS sites đã tạo (chạy setup-iis-sites.ps1 trước) # - IIS sites created (run setup-iis-sites.ps1 first)
# - Port 80 từ Internet VPS mở (Let's Encrypt HTTP-01 challenge) # - Port 80 from Internet -> VPS open (Let's Encrypt HTTP-01 challenge)
# - 3 domain api/admin/user.huypham.vn đã trỏ DNS về VPS IP # - 3 domains api/admin/user.huypham.vn pointing DNS to VPS IP
# #
# Output: # Output:
# - 3 cert trong Windows Cert Store (LocalMachine\My) # - 3 cert in Windows Cert Store (LocalMachine\My)
# - HTTPS binding port 443 cho 3 site # - HTTPS binding port 443 for 3 sites
# - Scheduled task auto-renew (90 day cycle Let's Encrypt, win-acme tự renew khi còn 30 ngày) # - Scheduled task auto-renew (90 day cycle Let's Encrypt, win-acme auto renew when 30 days left)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@ -31,38 +31,39 @@ if (-not (Test-Path $WacsExe)) {
Remove-Item $zip Remove-Item $zip
Write-Host " Installed to $WacsDir" Write-Host " Installed to $WacsDir"
} else { } else {
Write-Host "==> win-acme đã cài tại $WacsDir" Write-Host "==> win-acme already installed at $WacsDir"
} }
# ===================== 2. Check IIS sites exist ===================== # ===================== 2. Check IIS sites exist =====================
Import-Module WebAdministration Import-Module WebAdministration
$domains = @( $domains = @(
@{ Site = "SolutionErp-Api"; Host = "api.huypham.vn" }, @{ Site = "SolutionErp-Api"; HostName = "api.huypham.vn" },
@{ Site = "SolutionErp-Admin"; Host = "admin.huypham.vn" }, @{ Site = "SolutionErp-Admin"; HostName = "admin.huypham.vn" },
@{ Site = "SolutionErp-User"; Host = "user.huypham.vn" } @{ Site = "SolutionErp-User"; HostName = "user.huypham.vn" }
) )
foreach ($d in $domains) { foreach ($d in $domains) {
if (-not (Test-Path "IIS:\Sites\$($d.Site)")) { if (-not (Test-Path "IIS:\Sites\$($d.Site)")) {
Write-Error "Site '$($d.Site)' chưa tồn tại. Chạy setup-iis-sites.ps1 trước." Write-Error "Site '$($d.Site)' does not exist. Run setup-iis-sites.ps1 first."
exit 1 exit 1
} }
} }
Write-Host " 3 IIS site đã ready" Write-Host " 3 IIS sites ready"
# ===================== 3. Run win-acme cho từng domain ===================== # ===================== 3. Run win-acme per domain =====================
foreach ($d in $domains) { foreach ($d in $domains) {
Write-Host "`n==> Issue cert cho $($d.Host)" -ForegroundColor Cyan Write-Host ""
Write-Host "==> Issue cert for $($d.HostName)" -ForegroundColor Cyan
# win-acme CLI non-interactive: # win-acme CLI non-interactive:
# --target iis → lấy hostname từ IIS binding # --target manual + --host <domain>
# --host → domain cụ thể # --siteid -> IIS site to install on
# --installation iis auto bind HTTPS 443 + httphttps redirect # --installation iis -> auto bind HTTPS 443 + http->https redirect
# --accepttos → auto chấp nhận Let's Encrypt terms # --accepttos -> accept Let's Encrypt terms
# --emailaddress → email contact nhận alert expiry (đổi cho phù hợp) # --emailaddress -> contact email for expiry alerts
$args = @( $wacsArgs = @(
"--target", "manual", "--target", "manual",
"--host", $d.Host, "--host", $d.HostName,
"--siteid", (Get-Website $d.Site).Id, "--siteid", (Get-Website $d.Site).Id,
"--store", "certificatestore", "--store", "certificatestore",
"--installation", "iis", "--installation", "iis",
@ -70,44 +71,33 @@ foreach ($d in $domains) {
"--emailaddress", "admin@huypham.vn" "--emailaddress", "admin@huypham.vn"
) )
& $WacsExe @args & $WacsExe @wacsArgs
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Warning "Issue cert cho $($d.Host) FAIL exit $LASTEXITCODE — kiểm tra:" Write-Warning "Issue cert for $($d.HostName) FAIL exit $LASTEXITCODE - check:"
Write-Warning " 1. Port 80 Internet VPS mở (Let's Encrypt reach qua HTTP-01)?" Write-Warning " 1. Port 80 Internet -> VPS open (Let's Encrypt reach via HTTP-01)?"
Write-Warning " 2. DNS $($d.Host) $((Resolve-DnsName $d.Host -Type A -ErrorAction SilentlyContinue).IPAddress)?" Write-Warning " 2. DNS $($d.HostName) -> $((Resolve-DnsName $d.HostName -Type A -ErrorAction SilentlyContinue).IPAddress)?"
Write-Warning " 3. IIS site $($d.Site) binding port 80 host header $($d.Host)?" Write-Warning " 3. IIS site $($d.Site) binding port 80 with host header $($d.HostName)?"
} else { } else {
Write-Host " Cert installed" Write-Host " [OK] Cert installed"
} }
} }
# ===================== 4. HTTP HTTPS redirect rule ===================== # ===================== 4. HTTP -> HTTPS redirect =====================
Write-Host "`n==> Setup HTTP → HTTPS redirect (URL Rewrite)" -ForegroundColor Cyan Write-Host ""
$redirectConfig = @' Write-Host "==> HTTP -> HTTPS redirect (win-acme auto-adds via --installation iis)" -ForegroundColor Cyan
<rewrite> Write-Host " (skip manual rule - win-acme handled it)"
<rules>
<rule name="Redirect HTTP to HTTPS" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="^OFF$" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
</rules>
</rewrite>
'@
# win-acme --installation iis đã tự add redirect rule khi binding xong — skip manual.
Write-Host " (win-acme tự setup redirect)"
# ===================== 5. Verify scheduled task ===================== # ===================== 5. Verify scheduled task =====================
Write-Host "`n==> Verify scheduled task auto-renew" Write-Host ""
Write-Host "==> Verify scheduled task auto-renew"
$task = Get-ScheduledTask -TaskName "win-acme renew (acme-v02.api.letsencrypt.org)" -ErrorAction SilentlyContinue $task = Get-ScheduledTask -TaskName "win-acme renew (acme-v02.api.letsencrypt.org)" -ErrorAction SilentlyContinue
if ($task) { if ($task) {
Write-Host " Task '$($task.TaskName)' exists auto renew 9h daily" Write-Host " [OK] Task '$($task.TaskName)' exists - auto renew 9h daily"
} else { } else {
Write-Warning " Task chưa tạo — chạy tay: $WacsExe --renew --baseuri https://acme-v02.api.letsencrypt.org/" Write-Warning " Task not created - run manually: $WacsExe --renew --baseuri https://acme-v02.api.letsencrypt.org/"
} }
Write-Host "`n✅ SSL setup DONE" -ForegroundColor Green Write-Host ""
Write-Host "[OK] SSL setup DONE" -ForegroundColor Green
Write-Host " Test: openssl s_client -connect api.huypham.vn:443 < /dev/null | openssl x509 -noout -subject -dates" Write-Host " Test: openssl s_client -connect api.huypham.vn:443 < /dev/null | openssl x509 -noout -subject -dates"
Write-Host " hoặc browser: https://api.huypham.vn/health/live" Write-Host " Or browser: https://api.huypham.vn/health/live"