From 169e268b283dec28023ef300c2785e7ef9c8da2f Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 21 Apr 2026 14:17:36 +0700 Subject: [PATCH] [CLAUDE] Scripts: rewrite 4 deploy PS1 ASCII-only for PS 5.1 compat 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) --- scripts/deploy-all.ps1 | 70 ++++++++++++------------- scripts/setup-gitea-runner.ps1 | 31 +++++------ scripts/setup-iis-sites.ps1 | 73 ++++++++++++++------------ scripts/setup-ssl.ps1 | 94 +++++++++++++++------------------- 4 files changed, 131 insertions(+), 137 deletions(-) diff --git a/scripts/deploy-all.ps1 b/scripts/deploy-all.ps1 index db3fc3a..870b942 100644 --- a/scripts/deploy-all.ps1 +++ b/scripts/deploy-all.ps1 @@ -1,5 +1,5 @@ -# Deploy SOLUTION_ERP all-in-one trên VPS Windows Server. -# Chạy 4 step: SQL → IIS → SSL → Runner. Idempotent. Stop ngay khi step fail. +# Deploy SOLUTION_ERP all-in-one on VPS Windows Server. +# Runs 4 steps: SQL -> IIS -> SSL -> Runner. Idempotent. Stops on first step fail. # # Usage: # .\deploy-all.ps1 ` @@ -9,13 +9,13 @@ # -RunnerToken '' ` # -AdminEmail 'admin@huypham.vn' # -# Prereq (pre-check script sẽ verify): +# Prereq (pre-check will verify): # - Windows Server + Admin PowerShell # - 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 # - Port 80+443 firewall open -# - DNS api/admin/user.huypham.vn đã trỏ VPS +# - DNS api/admin/user.huypham.vn pointing to VPS [CmdletBinding()] param( @@ -43,20 +43,22 @@ function Test-Prereq { $issues = @() 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-Command sqlcmd -ErrorAction SilentlyContinue)) { - $issues += "sqlcmd không có trong PATH (SQL Server Client tools)" + if (-not (Get-Module -ListAvailable -Name SqlServer)) { + if (-not (Get-Command sqlcmd -ErrorAction SilentlyContinue)) { + $issues += "Neither SqlServer PS module nor sqlcmd.exe available. Install-Module SqlServer -Scope AllUsers -Force" + } } 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" 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 @@ -65,13 +67,13 @@ function Test-Prereq { Write-Banner "Pre-check prerequisites" $issues = Test-Prereq 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 } 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 } -Write-Host "✅ Tất cả prerequisite OK" +Write-Host "[OK] All prerequisites present" # ===================== Step 1: SQL DB ===================== Write-Banner "Step 1/4: SQL Database setup" @@ -79,7 +81,7 @@ try { & "$ScriptRoot\setup-sql-db.ps1" -SaPassword $SaPassword if ($LASTEXITCODE -ne 0) { throw "setup-sql-db.ps1 exit $LASTEXITCODE" } } catch { - Write-Host "❌ Step 1 FAIL: $_" -ForegroundColor Red + Write-Host "[FAIL] Step 1: $_" -ForegroundColor Red exit 1 } @@ -89,7 +91,7 @@ try { & "$ScriptRoot\setup-iis-sites.ps1" if ($LASTEXITCODE -ne 0) { throw "setup-iis-sites.ps1 exit $LASTEXITCODE" } } catch { - Write-Host "❌ Step 2 FAIL: $_" -ForegroundColor Red + Write-Host "[FAIL] Step 2: $_" -ForegroundColor Red exit 1 } @@ -100,41 +102,37 @@ $example = "$apiPath\appsettings.Production.json.example" $prod = "$apiPath\appsettings.Production.json" 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) ` -replace [regex]::Escape('__SET_VIA_SECRETS__'), $VrappPassword ` -replace [regex]::Escape('__SET_VIA_USER_SECRETS_OR_ENV__minimum_64_chars_random'), $JwtSecret Set-Content -Path $prod -Value $content -Encoding UTF8 - Write-Host "✅ Wrote $prod" - # ACL: chỉ Administrators + app pool identity đọc + Write-Host "[OK] Wrote $prod" + # ACL: only Administrators + app pool identity can read icacls $prod /inheritance: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)" } 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 "Sau khi CI deploy xong, rerun step này hoặc copy thủ công." + Write-Warning "Template not found at $example - skip (deploy may not have run yet; need Gitea Actions deploy first)" + Write-Warning "After CI deploy, re-run this step or copy manually." } # ===================== Step 4: SSL ===================== if (-not $SkipSsl) { 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 " Test từ máy ngoài: curl http://api.huypham.vn" - $confirm = Read-Host "Tiếp tục? (y/N)" + Write-Host "WARNING: Port 80 must be reachable from Internet (Let's Encrypt HTTP-01)" + Write-Host " Test from outside: curl http://api.huypham.vn" + $confirm = Read-Host "Continue? (y/N)" if ($confirm -eq 'y') { try { & "$ScriptRoot\setup-ssl.ps1" } 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 { - Write-Host "⏭ Skip SSL — chạy setup-ssl.ps1 thủ công sau" + Write-Host "Skip SSL - run setup-ssl.ps1 manually later" } } else { - Write-Host "⏭ Skip SSL (-SkipSsl flag)" + Write-Host "Skip SSL (-SkipSsl flag)" } # ===================== Step 5: Runner ===================== @@ -143,25 +141,25 @@ if (-not $SkipRunner -and $RunnerToken -ne "SKIP") { try { & "$ScriptRoot\setup-gitea-runner.ps1" -RegistrationToken $RunnerToken } 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 ===================== $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 "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 -I https://admin.huypham.vn" Write-Host " curl -I https://user.huypham.vn" 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 " - JWT_SECRET (update với 64-char)" +Write-Host " - JWT_SECRET (update with 64-char)" Write-Host " - IIS_PASSWORD (Windows admin password)" 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 " 4. Đổi admin password mặc định sau login (security-checklist.md)" +Write-Host " 4. Change default admin password after login (security-checklist.md)" diff --git a/scripts/setup-gitea-runner.ps1 b/scripts/setup-gitea-runner.ps1 index 55d143d..8ad75aa 100644 --- a/scripts/setup-gitea-runner.ps1 +++ b/scripts/setup-gitea-runner.ps1 @@ -1,12 +1,12 @@ -# Register Gitea Actions runner trên VPS Windows Server. -# Có thể dùng chung với VIETREPORT (runner có thể serve nhiều repo qua labels). +# Register Gitea Actions runner on VPS Windows Server. +# Can be shared with VIETREPORT (runner serves multiple repos via labels). # # Usage (admin PowerShell): # .\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) -# 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( [Parameter(Mandatory=$true)] [string]$RegistrationToken, @@ -25,7 +25,7 @@ if (-not (Test-Path $RunnerExe)) { Write-Host "==> Download Gitea act_runner" -ForegroundColor Cyan 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" Invoke-WebRequest -Uri $url -OutFile $RunnerExe -UseBasicParsing Write-Host " Downloaded $RunnerExe" @@ -35,7 +35,8 @@ if (-not (Test-Path $RunnerExe)) { Set-Location $InstallDir 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 ` --no-interactive ` --instance $GiteaUrl ` @@ -43,23 +44,22 @@ if (-not (Test-Path (Join-Path $InstallDir ".runner"))) { --name $RunnerName ` --labels $Labels 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 } Write-Host " Registered as '$RunnerName'" } else { - Write-Host " Runner đã register (.runner file exists)" + Write-Host " Runner already registered (.runner file exists)" } # ===================== 3. Install as Windows service ===================== $ServiceName = "gitea-runner" $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if (-not $svc) { - Write-Host "`n==> Install Windows service" -ForegroundColor Cyan - # act_runner không có built-in service install — dùng nssm hoặc sc.exe - # Dùng sc.exe đơn giản: - $escapedPath = $RunnerExe -replace '\\', '\\' - sc.exe create $ServiceName binPath= "`"$escapedPath`" daemon --config `"$InstallDir\config.yml`"" start= auto DisplayName= "Gitea Actions Runner" + Write-Host "" + Write-Host "==> Install Windows service" -ForegroundColor Cyan + # act_runner has no built-in service install - use sc.exe + sc.exe create $ServiceName binPath= "`"$RunnerExe`" daemon --config `"$InstallDir\config.yml`"" start= auto DisplayName= "Gitea Actions Runner" Start-Service $ServiceName Write-Host " Service '$ServiceName' installed + started" } else { @@ -71,7 +71,8 @@ if (-not $svc) { } } -Write-Host "`n✅ Runner setup DONE" -ForegroundColor Green -Write-Host " Check trên Gitea: $GiteaUrl/-/admin/actions/runners (admin) hoặc repo settings > Actions > Runners" +Write-Host "" +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 " Log: Get-Content '$InstallDir\log.txt' -Tail 50 -Wait" diff --git a/scripts/setup-iis-sites.ps1 b/scripts/setup-iis-sites.ps1 index bf8e6b3..bd021a9 100644 --- a/scripts/setup-iis-sites.ps1 +++ b/scripts/setup-iis-sites.ps1 @@ -1,14 +1,14 @@ -# Setup IIS sites + app pools cho SOLUTION_ERP trên VPS Windows Server -# Chia sẻ với VIETREPORT — naming isolation: SolutionErp-* prefix để tránh conflict. -# Chạy trên VPS với admin privilege. Idempotent. +# Setup IIS sites + app pools for SOLUTION_ERP on VPS Windows Server. +# Shared with VIETREPORT - naming isolation: SolutionErp-* prefix to avoid conflict. +# Run on VPS with admin privilege. Idempotent. # # Usage: # .\setup-iis-sites.ps1 # # Prereq: -# - IIS cài + features: Application Initialization, URL Rewrite, ARR (optional) -# - .NET 10 Hosting Bundle cài -# - Port 80/443 firewall đã mở +# - IIS installed + features: Application Initialization, URL Rewrite, ARR (optional) +# - .NET 10 Hosting Bundle installed +# - Port 80/443 firewall open $ErrorActionPreference = 'Stop' Import-Module WebAdministration @@ -45,14 +45,18 @@ foreach ($p in @($Root, $PathApi, $PathAdmin, $PathUser, $PathLogs, $PathUploads } } -# Grant app pool identity write quyền -$acl = Get-Acl $Root +# Grant app pool identity write permission $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( "IIS AppPool\$AppPoolApi", "Modify", "ContainerInherit,ObjectInherit", "None", "Allow") -$acl.SetAccessRule($rule) -try { Set-Acl -Path $Root -AclObject $acl } catch { Write-Warning " ACL set se fail neu app pool chua ton tai — bypass" } +try { + $acl = Get-Acl $Root + $acl.SetAccessRule($rule) + 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 là static) ===================== +# ===================== 2. App pool (Api only, FE is static) ===================== Write-Step "App pool: $AppPoolApi" if (-not (Test-Path "IIS:\AppPools\$AppPoolApi")) { 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" 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 { $acl = Get-Acl $Root $acl.SetAccessRule($rule) 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 { Write-Warning " ACL fail: $_" } @@ -78,15 +82,15 @@ try { # ===================== 3. Sites ===================== function Ensure-Site { 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")) { - # Port 80 HTTP — SSL sẽ thêm sau qua win-acme + # Port 80 HTTP - SSL added later via win-acme $params = @{ Name = $Name - HostHeader = $Host - PhysicalPath = $Path + HostHeader = $HostName + PhysicalPath = $PhysicalPath Port = 80 } if ($AppPool) { $params.ApplicationPool = $AppPool } @@ -94,18 +98,18 @@ function Ensure-Site { Write-Host " Created" } else { Write-Host " Exists" - Set-ItemProperty "IIS:\Sites\$Name" -Name physicalPath -Value $Path + Set-ItemProperty "IIS:\Sites\$Name" -Name physicalPath -Value $PhysicalPath if ($AppPool) { Set-ItemProperty "IIS:\Sites\$Name" -Name applicationPool -Value $AppPool } } } -Ensure-Site -Name $SiteApi -Host $DomainApi -Path $PathApi -AppPool $AppPoolApi -Ensure-Site -Name $SiteAdmin -Host $DomainAdmin -Path $PathAdmin -AppPool "" -Ensure-Site -Name $SiteUser -Host $DomainUser -Path $PathUser -AppPool "" +Ensure-Site -Name $SiteApi -HostName $DomainApi -PhysicalPath $PathApi -AppPool $AppPoolApi +Ensure-Site -Name $SiteAdmin -HostName $DomainAdmin -PhysicalPath $PathAdmin -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)" foreach ($fePath in @($PathAdmin, $PathUser)) { $idx = Join-Path $fePath "index.html" @@ -114,7 +118,7 @@ foreach ($fePath in @($PathAdmin, $PathUser)) { SOLUTION ERP

SOLUTION ERP

-

Site đã tạo, chờ deploy first build qua Gitea CI/CD.

+

Site created, waiting for first deploy via Gitea CI/CD.

"@ | Set-Content -Path $idx -Encoding UTF8 Write-Host " Wrote $idx" @@ -123,8 +127,8 @@ foreach ($fePath in @($PathAdmin, $PathUser)) { } } -# ===================== 5. web.config cho FE SPA routing ===================== -Write-Step "web.config cho FE SPA fallback" +# ===================== 5. web.config for FE SPA routing ===================== +Write-Step "web.config for FE SPA fallback" $spaWebConfig = @' @@ -160,7 +164,7 @@ $spaWebConfig = @' foreach ($fePath in @($PathAdmin, $PathUser)) { 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 ===================== Write-Step "Firewall port 80/443" @@ -174,13 +178,14 @@ if (-not $fwExists) { } # ===================== 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 " - $SiteApi -> $DomainApi -> $PathApi (app pool $AppPoolApi)" -Write-Host " - $SiteAdmin -> $DomainAdmin -> $PathAdmin (no app pool — static)" -Write-Host " - $SiteUser -> $DomainUser -> $PathUser (no app pool — static)" +Write-Host " - $SiteAdmin -> $DomainAdmin -> $PathAdmin (no app pool - static)" +Write-Host " - $SiteUser -> $DomainUser -> $PathUser (no app pool - static)" Write-Host "" -Write-Host " Tiếp theo:" -Write-Host " 1. Chạy .\setup-ssl.ps1 để cài HTTPS cert cho 3 domain" -Write-Host " 2. Deploy first build qua Gitea Actions (push main trigger workflow)" -Write-Host " 3. Verify https://$DomainApi/health/ready → 200" +Write-Host " Next:" +Write-Host " 1. Run .\setup-ssl.ps1 to install HTTPS cert for 3 domains" +Write-Host " 2. Deploy first build via Gitea Actions (push main to trigger workflow)" +Write-Host " 3. Verify https://$DomainApi/health/ready -> 200" diff --git a/scripts/setup-ssl.ps1 b/scripts/setup-ssl.ps1 index aba40e3..14523a1 100644 --- a/scripts/setup-ssl.ps1 +++ b/scripts/setup-ssl.ps1 @@ -1,19 +1,19 @@ -# Cài HTTPS cert Let's Encrypt cho 3 domain SOLUTION_ERP qua win-acme (WACS). -# Chạy trên VPS Windows Server với admin privilege. -# Idempotent: chạy lại sẽ bỏ qua cert còn valid. +# Install HTTPS cert Let's Encrypt for 3 SOLUTION_ERP domains via win-acme (WACS). +# Run on VPS Windows Server with admin privilege. +# Idempotent: re-run skips cert still valid. # # Usage: # .\setup-ssl.ps1 # # Prereq: -# - IIS sites đã tạo (chạy setup-iis-sites.ps1 trước) -# - Port 80 từ Internet → VPS mở (Let's Encrypt HTTP-01 challenge) -# - 3 domain api/admin/user.huypham.vn đã trỏ DNS về VPS IP +# - IIS sites created (run setup-iis-sites.ps1 first) +# - Port 80 from Internet -> VPS open (Let's Encrypt HTTP-01 challenge) +# - 3 domains api/admin/user.huypham.vn pointing DNS to VPS IP # # Output: -# - 3 cert trong Windows Cert Store (LocalMachine\My) -# - HTTPS binding port 443 cho 3 site -# - Scheduled task auto-renew (90 day cycle Let's Encrypt, win-acme tự renew khi còn 30 ngày) +# - 3 cert in Windows Cert Store (LocalMachine\My) +# - HTTPS binding port 443 for 3 sites +# - Scheduled task auto-renew (90 day cycle Let's Encrypt, win-acme auto renew when 30 days left) $ErrorActionPreference = 'Stop' @@ -31,38 +31,39 @@ if (-not (Test-Path $WacsExe)) { Remove-Item $zip Write-Host " Installed to $WacsDir" } else { - Write-Host "==> win-acme đã cài tại $WacsDir" + Write-Host "==> win-acme already installed at $WacsDir" } # ===================== 2. Check IIS sites exist ===================== Import-Module WebAdministration $domains = @( - @{ Site = "SolutionErp-Api"; Host = "api.huypham.vn" }, - @{ Site = "SolutionErp-Admin"; Host = "admin.huypham.vn" }, - @{ Site = "SolutionErp-User"; Host = "user.huypham.vn" } + @{ Site = "SolutionErp-Api"; HostName = "api.huypham.vn" }, + @{ Site = "SolutionErp-Admin"; HostName = "admin.huypham.vn" }, + @{ Site = "SolutionErp-User"; HostName = "user.huypham.vn" } ) foreach ($d in $domains) { 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 } } -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) { - 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: - # --target iis → lấy hostname từ IIS binding - # --host → domain cụ thể - # --installation iis → auto bind HTTPS 443 + http→https redirect - # --accepttos → auto chấp nhận Let's Encrypt terms - # --emailaddress → email contact nhận alert expiry (đổi cho phù hợp) - $args = @( + # --target manual + --host + # --siteid -> IIS site to install on + # --installation iis -> auto bind HTTPS 443 + http->https redirect + # --accepttos -> accept Let's Encrypt terms + # --emailaddress -> contact email for expiry alerts + $wacsArgs = @( "--target", "manual", - "--host", $d.Host, + "--host", $d.HostName, "--siteid", (Get-Website $d.Site).Id, "--store", "certificatestore", "--installation", "iis", @@ -70,44 +71,33 @@ foreach ($d in $domains) { "--emailaddress", "admin@huypham.vn" ) - & $WacsExe @args + & $WacsExe @wacsArgs if ($LASTEXITCODE -ne 0) { - Write-Warning "Issue cert cho $($d.Host) FAIL exit $LASTEXITCODE — kiểm tra:" - Write-Warning " 1. Port 80 Internet → VPS mở (Let's Encrypt reach qua HTTP-01)?" - Write-Warning " 2. DNS $($d.Host) → $((Resolve-DnsName $d.Host -Type A -ErrorAction SilentlyContinue).IPAddress)?" - Write-Warning " 3. IIS site $($d.Site) binding port 80 có host header $($d.Host)?" + Write-Warning "Issue cert for $($d.HostName) FAIL exit $LASTEXITCODE - check:" + Write-Warning " 1. Port 80 Internet -> VPS open (Let's Encrypt reach via HTTP-01)?" + 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 with host header $($d.HostName)?" } else { - Write-Host " ✅ Cert installed" + Write-Host " [OK] Cert installed" } } -# ===================== 4. HTTP → HTTPS redirect rule ===================== -Write-Host "`n==> Setup HTTP → HTTPS redirect (URL Rewrite)" -ForegroundColor Cyan -$redirectConfig = @' - - - - - - - - - - - -'@ -# win-acme --installation iis đã tự add redirect rule khi binding xong — skip manual. -Write-Host " (win-acme tự setup redirect)" +# ===================== 4. HTTP -> HTTPS redirect ===================== +Write-Host "" +Write-Host "==> HTTP -> HTTPS redirect (win-acme auto-adds via --installation iis)" -ForegroundColor Cyan +Write-Host " (skip manual rule - win-acme handled it)" # ===================== 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 if ($task) { - Write-Host " ✅ Task '$($task.TaskName)' exists — auto renew 9h daily" + Write-Host " [OK] Task '$($task.TaskName)' exists - auto renew 9h daily" } 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 " hoặc browser: https://api.huypham.vn/health/live" +Write-Host " Or browser: https://api.huypham.vn/health/live"