[CLAUDE] VPS setup scripts + SSL + runner + FE prod config + master runbook
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

Scripts moi (PowerShell admin trên VPS Windows Server):
- setup-sql-db.ps1: tao DB SolutionErp + grant db_owner cho vrapp (user shared voi VIETREPORT). Idempotent.
- setup-iis-sites.ps1: app pool SolutionErp-Api (NoManagedCode + AlwaysRunning + no idle) + 3 site (SolutionErp-Api/Admin/User) voi host header, C:\inetpub\solution-erp\{api,fe-admin,fe-user,logs,uploads}. Placeholder index.html + SPA web.config voi URL rewrite fallback + security headers. Firewall rule. ACL grant AppPool identity Modify. Naming prefix SolutionErp-* tranh conflict VIETREPORT.
- setup-ssl.ps1: download win-acme v2.2.9 → issue cert Let's Encrypt 3 domain (api/admin/user.huypham.vn) qua HTTP-01 challenge + auto install IIS binding + HTTP→HTTPS redirect + scheduled task 90d renew.
- setup-gitea-runner.ps1: download act_runner.exe → register voi Gitea git.baocaogiaoduc.vn, install Windows service, labels windows-latest,self-hosted,windows,x64 (cho phep share voi VIETREPORT).

FE production config:
- fe-admin/.env.production + fe-user/.env.production: VITE_API_BASE_URL=https://api.huypham.vn
- fe-admin/src/lib/api.ts + fe-user/src/lib/api.ts: BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api'
  - Dev: empty prefix → /api qua Vite proxy :5443
  - Prod: https://api.huypham.vn/api (cross-origin CORS da config AllowedOrigins)

Docs:
- docs/guides/vps-setup.md MOI (master runbook): prereq, 4 script chay theo thu tu, set 5 Gitea secrets, first deploy, appsettings.Production.json pattern (file hoac user-secrets), smoke test 3 curl, post go-live checklist (doi admin password, rotate secrets chat-exposed, backup schedule, disable Swagger prod, monitor logs), table co-existence VIETREPORT
- CLAUDE.md root: add vps-setup.md reference

Gitea repo da setup (extern):
- https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp (private)
- Secrets set via API: IIS_HOST=103.124.94.38, IIS_USER=Administrator, DB_CONNECTION (voi vrapp password), JWT_SECRET placeholder
- CON THIEU: IIS_PASSWORD (Windows admin — user cung cap), JWT_SECRET real value (64-char tu vps-jwt-key.txt — user update qua Gitea UI)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 13:34:26 +07:00
parent fba0754110
commit b1a4571c86
10 changed files with 612 additions and 2 deletions

186
scripts/setup-iis-sites.ps1 Normal file
View File

@ -0,0 +1,186 @@
# 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.
#
# 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ở
$ErrorActionPreference = 'Stop'
Import-Module WebAdministration
# ===================== Config =====================
$Root = "C:\inetpub\solution-erp"
$PathApi = "$Root\api"
$PathAdmin = "$Root\fe-admin"
$PathUser = "$Root\fe-user"
$PathLogs = "$Root\logs"
$PathUploads = "$Root\uploads"
$PathTemplates = "$Root\api\wwwroot\templates"
$AppPoolApi = "SolutionErp-Api"
$DomainApi = "api.huypham.vn"
$DomainAdmin = "admin.huypham.vn"
$DomainUser = "user.huypham.vn"
$SiteApi = "SolutionErp-Api"
$SiteAdmin = "SolutionErp-Admin"
$SiteUser = "SolutionErp-User"
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
# ===================== 1. Directories =====================
Write-Step "Create directories under $Root"
foreach ($p in @($Root, $PathApi, $PathAdmin, $PathUser, $PathLogs, $PathUploads, $PathTemplates)) {
if (-not (Test-Path $p)) {
New-Item -ItemType Directory -Force -Path $p | Out-Null
Write-Host " Created $p"
} else {
Write-Host " Exists $p"
}
}
# Grant app pool identity write quyền
$acl = Get-Acl $Root
$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" }
# ===================== 2. App pool (Api only, FE là static) =====================
Write-Step "App pool: $AppPoolApi"
if (-not (Test-Path "IIS:\AppPools\$AppPoolApi")) {
New-WebAppPool -Name $AppPoolApi | Out-Null
Write-Host " Created"
}
Set-ItemProperty "IIS:\AppPools\$AppPoolApi" -Name managedRuntimeVersion -Value ""
Set-ItemProperty "IIS:\AppPools\$AppPoolApi" -Name startMode -Value "AlwaysRunning"
Set-ItemProperty "IIS:\AppPools\$AppPoolApi" -Name processModel.identityType -Value "ApplicationPoolIdentity"
Set-ItemProperty "IIS:\AppPools\$AppPoolApi" -Name processModel.idleTimeout -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)"
# Re-grant ACL sau khi pool tạo (identity cần tồn tại)
try {
$acl = Get-Acl $Root
$acl.SetAccessRule($rule)
Set-Acl -Path $Root -AclObject $acl
Write-Host " ACL granted Modify cho IIS AppPool\$AppPoolApi"
} catch {
Write-Warning " ACL fail: $_"
}
# ===================== 3. Sites =====================
function Ensure-Site {
param(
[string]$Name, [string]$Host, [string]$Path, [string]$AppPool
)
Write-Step "Site: $Name ($Host)"
if (-not (Test-Path "IIS:\Sites\$Name")) {
# Port 80 HTTP — SSL sẽ thêm sau qua win-acme
$params = @{
Name = $Name
HostHeader = $Host
PhysicalPath = $Path
Port = 80
}
if ($AppPool) { $params.ApplicationPool = $AppPool }
New-WebSite @params | Out-Null
Write-Host " Created"
} else {
Write-Host " Exists"
Set-ItemProperty "IIS:\Sites\$Name" -Name physicalPath -Value $Path
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 ""
# ===================== 4. Placeholder index.html cho FE (tạm trước khi deploy thật) =====================
Write-Step "Placeholder index.html (pre-deploy)"
foreach ($fePath in @($PathAdmin, $PathUser)) {
$idx = Join-Path $fePath "index.html"
if (-not (Test-Path $idx)) {
@"
<!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">
<h1>SOLUTION ERP</h1>
<p>Site đã to, ch deploy first build qua Gitea CI/CD.</p>
</body></html>
"@ | Set-Content -Path $idx -Encoding UTF8
Write-Host " Wrote $idx"
} else {
Write-Host " Exists $idx"
}
}
# ===================== 5. web.config cho FE SPA routing =====================
Write-Step "web.config cho FE SPA fallback"
$spaWebConfig = @'
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="SPA Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<add input="{REQUEST_URI}" pattern="^/api" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
<staticContent>
<remove fileExtension=".webmanifest" />
<mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
</staticContent>
<httpProtocol>
<customHeaders>
<add name="X-Content-Type-Options" value="nosniff" />
<add name="X-Frame-Options" value="DENY" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
'@
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"
# ===================== 6. Firewall =====================
Write-Step "Firewall port 80/443"
$fwExists = Get-NetFirewallRule -DisplayName "HTTP In (SolutionErp)" -ErrorAction SilentlyContinue
if (-not $fwExists) {
New-NetFirewallRule -DisplayName "HTTP In (SolutionErp)" -Direction Inbound -LocalPort 80 -Protocol TCP -Action Allow | Out-Null
New-NetFirewallRule -DisplayName "HTTPS In (SolutionErp)" -Direction Inbound -LocalPort 443 -Protocol TCP -Action Allow | Out-Null
Write-Host " Created 2 rules"
} else {
Write-Host " Exists"
}
# ===================== 7. Recap =====================
Write-Host "`n✅ 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 ""
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"