# 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)) { @" SOLUTION ERP

SOLUTION ERP

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

"@ | 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 = @' '@ 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"