[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) <noreply@anthropic.com>
This commit is contained in:
@ -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)"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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")
|
||||||
$acl.SetAccessRule($rule)
|
try {
|
||||||
try { Set-Acl -Path $Root -AclObject $acl } catch { Write-Warning " ACL set se fail neu app pool chua ton tai — bypass" }
|
$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"
|
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 đã tạo, 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"
|
||||||
|
|||||||
@ -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 + http→https 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 có 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user