diff --git a/CLAUDE.md b/CLAUDE.md index 0d065b2..ce5fb00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,6 +76,7 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser | [`docs/flows/README.md`](docs/flows/README.md) | Index 6 flow (auth, permission, contract, form, SLA) | | [`docs/gotchas.md`](docs/gotchas.md) | ⭐ 26 bẫy đã gặp — đọc trước khi debug tương tự | | [`.claude/skills/`](.claude/skills/README.md) | 3 skill: contract-workflow, form-engine, permission-matrix | +| [`docs/guides/vps-setup.md`](docs/guides/vps-setup.md) | ⭐ Master runbook deploy VPS shared với VIETREPORT | ## ⚠️ Kết thúc session diff --git a/docs/guides/vps-setup.md b/docs/guides/vps-setup.md new file mode 100644 index 0000000..c25e503 --- /dev/null +++ b/docs/guides/vps-setup.md @@ -0,0 +1,149 @@ +# VPS Setup — Master Runbook + +> Step-by-step deploy SOLUTION_ERP lên VPS Windows Server shared với VIETREPORT. +> VPS: `103.124.94.38` (cùng chỗ với Gitea, SQL, IIS). + +## 0. Context + +- **VPS OS:** Windows Server (có IIS + SQL Server) +- **Shared với:** VIETREPORT project — naming isolation bắt buộc +- **DNS đã trỏ:** `api.huypham.vn`, `admin.huypham.vn`, `user.huypham.vn`, `git.baocaogiaoduc.vn` → `103.124.94.38` +- **Prefix resources:** `SolutionErp-*` (app pool, site), `SolutionErp` (DB), `C:\inetpub\solution-erp\` (path) + +## 1. Prerequisites trên VPS (đã có sẵn với VIETREPORT) + +- [x] Windows Server 2019/2022 +- [x] IIS + URL Rewrite + Application Initialization (VIETREPORT đã dùng) +- [x] SQL Server với login `sa` + `vrapp` (shared app user) +- [x] Gitea tại `git.baocaogiaoduc.vn` +- [x] Port 80/443 firewall open + +Cần **verify trên VPS**: +- [ ] .NET 10 Hosting Bundle cài chưa? Check: `dotnet --list-runtimes | grep 'Microsoft.AspNetCore.App 10'` +- [ ] Nếu chưa → tải từ https://dotnet.microsoft.com/en-us/download/dotnet/10.0 → "Hosting Bundle" +- [ ] Sau cài → `iisreset` + +## 2. Run setup scripts trên VPS + +Copy folder `scripts/` từ repo lên VPS (vd `C:\solution-erp\scripts\`): + +```powershell +# 1. SQL DB + grant vrapp +cd C:\solution-erp\scripts +.\setup-sql-db.ps1 -SaPassword 'AtA9pPJ9A031txk4zfRXh3aV' + +# 2. IIS sites + app pool +.\setup-iis-sites.ps1 +# → Tạo SolutionErp-Api pool + 3 site (SolutionErp-Api/Admin/User) +# → C:\inetpub\solution-erp\{api,fe-admin,fe-user,logs,uploads} + +# 3. HTTPS cert (win-acme, Let's Encrypt) +.\setup-ssl.ps1 +# → Issue cert cho 3 domain + auto-renew scheduled task + +# 4. Gitea runner (registration token lấy từ Gitea admin settings) +.\setup-gitea-runner.ps1 -RegistrationToken '' +``` + +## 3. Set Gitea Actions secrets + +Vào https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/settings/actions/secrets — 5 secret: + +| Name | Value | Ghi chú | +|---|---|---| +| `IIS_HOST` | `103.124.94.38` | ✅ đã set | +| `IIS_USER` | `Administrator` | ✅ đã set (hoặc username Windows khác) | +| `IIS_PASSWORD` | _(Windows admin password)_ | ⚠️ user set tay — token/secret chat không commit | +| `JWT_SECRET` | _(64 chars từ `vps-jwt-key.txt`)_ | ⚠️ cần update — placeholder hiện tại | +| `DB_CONNECTION` | `Server=localhost;Database=SolutionErp;User Id=vrapp;Password=...` | ✅ đã set với vrapp password | + +## 4. First deploy + +```powershell +# Trên máy dev — push main trigger workflow +git push origin main + +# Trên Gitea: +# https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions +# → 1 run đang chạy (build BE + 2 FE + deploy) +``` + +## 5. Set `appsettings.Production.json` trên VPS + +```powershell +cd C:\inetpub\solution-erp\api +# Copy từ template +cp appsettings.Production.json.example appsettings.Production.json + +# Edit thay placeholder: +# - ConnectionStrings.Default → Password= +# - Jwt.Secret → <64 chars từ vps-jwt-key.txt> +``` + +Hoặc dùng user-secrets (safer — không file trên disk): + +```powershell +cd C:\inetpub\solution-erp\api +dotnet user-secrets set "Jwt:Secret" "<64-char-from-file>" +dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;Database=SolutionErp;User Id=vrapp;Password=...;..." +``` + +## 6. Smoke test sau deploy + +```bash +# Health check +curl https://api.huypham.vn/health/live # → Healthy +curl https://api.huypham.vn/health/ready # → Healthy (DB probe) + +# Login +curl -X POST https://api.huypham.vn/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"admin@solutionerp.local","password":"Admin@123456"}' +# → accessToken JWT + +# FE +open https://admin.huypham.vn # fe-admin login page +open https://user.huypham.vn # fe-user login page + +# SSL grade +# https://www.ssllabs.com/ssltest/analyze.html?d=api.huypham.vn +``` + +## 7. Sau go-live (bắt buộc) + +- [ ] **Đổi password admin** từ `Admin@123456` → mạnh. Warning log xuất hiện khi còn dùng default. +- [ ] **Rotate secrets** đã post trong chat (SA, vrapp, Gitea token, JWT) — tất cả đã vượt khỏi VPS, cần đổi mới +- [ ] **Backup SQL** daily schedule: `schtasks /Create /TN 'SolutionErp SQL Backup' /TR 'powershell -File C:\solution-erp\scripts\backup-sql.ps1 -SaPassword ' /SC DAILY /ST 02:00 /RU SYSTEM` +- [ ] **Disable Swagger prod**: Program.cs đã có `if (IsDevelopment())` — verify URL `https://api.huypham.vn/swagger` → 404 +- [ ] **Monitor**: kiểm `C:\inetpub\solution-erp\logs\` ngày đầu, watch for ERR + +## 8. Co-existence với VIETREPORT — checklist + +| Resource | SOLUTION_ERP | VIETREPORT | Conflict? | +|---|---|---|---| +| App pool name | `SolutionErp-Api` | `VietReport-*` (assumed) | ❌ | +| Site name | `SolutionErp-Api/Admin/User` | `VietReport-*` | ❌ | +| Path | `C:\inetpub\solution-erp\` | `C:\inetpub\vietreport\` (assumed) | ❌ | +| SQL DB | `SolutionErp` | `VietReport` (assumed) | ❌ | +| SQL user | `vrapp` (shared db_owner) | `vrapp` | ✅ Shared — OK (cùng login, khác DB) | +| Port 80/443 | Host header routing | Host header routing | ✅ IIS cho phép share qua Host header | +| Firewall rule | `HTTP In (SolutionErp)` | `HTTP In (VietReport)` | ❌ | +| Gitea runner | `vps-win-*` labels `windows-latest` | shared | ✅ Runner serve multi repo qua labels | + +## 9. Troubleshooting + +Xem [`runbook.md`](runbook.md) cho operations hàng ngày + [`deployment-iis.md`](deployment-iis.md) cho troubleshoot chi tiết. + +Common issues khi co-exist: +- **Port 80 conflict** — check `netstat -ano | findstr :80` → kill service ngoài IIS +- **IIS default site block new site** — disable "Default Web Site" hoặc đổi binding +- **win-acme fail HTTP-01** — port 80 phải reachable từ Internet (không local firewall block) +- **SQL user vrapp không có quyền DB SolutionErp** — script `setup-sql-db.ps1` đã handle, re-run nếu skip + +## 10. Liên quan + +- [`deployment-iis.md`](deployment-iis.md) — IIS setup chi tiết +- [`cicd.md`](cicd.md) — Gitea Actions +- [`security-checklist.md`](security-checklist.md) — pre go-live +- [`runbook.md`](runbook.md) — operations +- `scripts/setup-*.ps1` — automation scripts diff --git a/fe-admin/.env.production b/fe-admin/.env.production new file mode 100644 index 0000000..68e8bc8 --- /dev/null +++ b/fe-admin/.env.production @@ -0,0 +1 @@ +VITE_API_BASE_URL=https://api.huypham.vn diff --git a/fe-admin/src/lib/api.ts b/fe-admin/src/lib/api.ts index e7c7009..f11407e 100644 --- a/fe-admin/src/lib/api.ts +++ b/fe-admin/src/lib/api.ts @@ -4,8 +4,12 @@ export const TOKEN_KEY = 'solution-erp-admin-token' export const REFRESH_KEY = 'solution-erp-admin-refresh' export const USER_KEY = 'solution-erp-admin-user' +// Dev: Vite proxy /api → :5443 (vite.config.ts) +// Prod: VITE_API_BASE_URL = https://api.huypham.vn (env.production) +const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api' + export const api = axios.create({ - baseURL: '/api', + baseURL: BASE_URL, timeout: 30000, }) diff --git a/fe-user/.env.production b/fe-user/.env.production new file mode 100644 index 0000000..68e8bc8 --- /dev/null +++ b/fe-user/.env.production @@ -0,0 +1 @@ +VITE_API_BASE_URL=https://api.huypham.vn diff --git a/fe-user/src/lib/api.ts b/fe-user/src/lib/api.ts index 18073ef..f3913ff 100644 --- a/fe-user/src/lib/api.ts +++ b/fe-user/src/lib/api.ts @@ -4,8 +4,12 @@ export const TOKEN_KEY = 'solution-erp-user-token' export const REFRESH_KEY = 'solution-erp-user-refresh' export const USER_KEY = 'solution-erp-user-user' +// Dev: Vite proxy /api → :5443 (vite.config.ts) +// Prod: VITE_API_BASE_URL = https://api.huypham.vn (env.production) +const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api' + export const api = axios.create({ - baseURL: '/api', + baseURL: BASE_URL, timeout: 30000, }) diff --git a/scripts/setup-gitea-runner.ps1 b/scripts/setup-gitea-runner.ps1 new file mode 100644 index 0000000..55d143d --- /dev/null +++ b/scripts/setup-gitea-runner.ps1 @@ -0,0 +1,77 @@ +# 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). +# +# Usage (admin PowerShell): +# .\setup-gitea-runner.ps1 -RegistrationToken 'xxxx' -RunnerName 'vps-win-01' +# +# Lấy RegistrationToken từ: +# https://git.baocaogiaoduc.vn/-/admin/actions/runners (admin only) +# hoặc per-repo: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/settings/actions/runners + +param( + [Parameter(Mandatory=$true)] [string]$RegistrationToken, + [string]$RunnerName = "vps-win-$(Get-Date -Format 'yyyyMMdd')", + [string]$InstallDir = "C:\gitea-runner", + [string]$GiteaUrl = "https://git.baocaogiaoduc.vn", + [string]$Labels = "windows-latest,self-hosted,windows,x64" +) + +$ErrorActionPreference = 'Stop' + +# ===================== 1. Download runner ===================== +$RunnerExe = Join-Path $InstallDir "act_runner.exe" + +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 + $url = "https://dl.gitea.com/act_runner/act_runner-windows-amd64.exe" + Invoke-WebRequest -Uri $url -OutFile $RunnerExe -UseBasicParsing + Write-Host " Downloaded $RunnerExe" +} + +# ===================== 2. Register ===================== +Set-Location $InstallDir + +if (-not (Test-Path (Join-Path $InstallDir ".runner"))) { + Write-Host "`n==> Register với Gitea $GiteaUrl" -ForegroundColor Cyan + & $RunnerExe register ` + --no-interactive ` + --instance $GiteaUrl ` + --token $RegistrationToken ` + --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?" + exit 1 + } + Write-Host " Registered as '$RunnerName'" +} else { + Write-Host " Runner đã register (.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" + Start-Service $ServiceName + Write-Host " Service '$ServiceName' installed + started" +} else { + if ($svc.Status -ne 'Running') { + Start-Service $ServiceName + Write-Host " Service started" + } else { + Write-Host " Service already running" + } +} + +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 " 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 new file mode 100644 index 0000000..bf8e6b3 --- /dev/null +++ b/scripts/setup-iis-sites.ps1 @@ -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)) { + @" +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" diff --git a/scripts/setup-sql-db.ps1 b/scripts/setup-sql-db.ps1 new file mode 100644 index 0000000..927d20c --- /dev/null +++ b/scripts/setup-sql-db.ps1 @@ -0,0 +1,74 @@ +# Setup SQL Server DB cho SOLUTION_ERP trên VPS chia sẻ với VIETREPORT. +# Tạo database + grant quyền cho user vrapp (đã có sẵn trên server). +# Idempotent: chạy lại không phá gì. +# +# Usage (chạy trên VPS với admin privilege): +# .\setup-sql-db.ps1 -SaPassword 'your-sa-password' +# +# Prereq: +# - SQL Server cài sẵn +# - Login vrapp đã tồn tại (dùng chung với VIETREPORT) +# - sqlcmd CLI available (đi kèm SQL Server) + +param( + [Parameter(Mandatory=$true)] [string]$SaPassword, + [string]$Server = ".", + [string]$Database = "SolutionErp", + [string]$AppUser = "vrapp" +) + +$ErrorActionPreference = 'Stop' + +function Invoke-Sql($query) { + $output = sqlcmd -S $Server -U sa -P $SaPassword -Q $query -b 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "SQL fail (exit $LASTEXITCODE):`n$output" + } + return $output +} + +Write-Host "==> Check SQL Server reachable" +Invoke-Sql "SELECT @@VERSION" | Select-Object -First 2 + +Write-Host "`n==> Check login '$AppUser' exists" +$check = Invoke-Sql "SELECT name FROM sys.sql_logins WHERE name = '$AppUser'" +if ($check -notmatch $AppUser) { + Write-Error "Login '$AppUser' KHONG ton tai. Tao tay truoc: CREATE LOGIN [$AppUser] WITH PASSWORD='...'" + exit 1 +} +Write-Host " OK" + +Write-Host "`n==> Create database '$Database' (if not exists)" +Invoke-Sql @" +IF DB_ID(N'$Database') IS NULL +BEGIN + CREATE DATABASE [$Database]; + PRINT 'Created database $Database'; +END +ELSE PRINT 'Database $Database already exists'; +"@ + +Write-Host "`n==> Grant db_owner cho '$AppUser' tren '$Database'" +Invoke-Sql @" +USE [$Database]; +IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = '$AppUser') +BEGIN + CREATE USER [$AppUser] FOR LOGIN [$AppUser]; + PRINT 'Created user $AppUser in DB'; +END +ELSE PRINT 'User $AppUser already exists in DB'; +ALTER ROLE db_owner ADD MEMBER [$AppUser]; +PRINT 'Added $AppUser to db_owner'; +"@ + +Write-Host "`n==> Verify kết nối với vrapp (test login)" +Write-Host " (skip — tự test khi app chạy)" + +Write-Host "`n==> Setup backup schedule" +Write-Host " Chạy: schtasks /Create /TN 'SolutionErp SQL Backup' /TR 'pwsh -File C:\solution-erp\scripts\backup-sql.ps1 -Server . -Database $Database' /SC DAILY /ST 02:00 /RU SYSTEM /F" +Write-Host " (hoặc tạo tay qua Task Scheduler)" + +Write-Host "`n✅ SQL Server setup DONE" -ForegroundColor Green +Write-Host " Database: $Database" +Write-Host " App user: $AppUser (db_owner)" +Write-Host " Connection string production: Server=localhost;Database=$Database;User Id=$AppUser;Password=***" diff --git a/scripts/setup-ssl.ps1 b/scripts/setup-ssl.ps1 new file mode 100644 index 0000000..aba40e3 --- /dev/null +++ b/scripts/setup-ssl.ps1 @@ -0,0 +1,113 @@ +# 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. +# +# 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 +# +# 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) + +$ErrorActionPreference = 'Stop' + +$WacsDir = "C:\Program Files\win-acme" +$WacsExe = Join-Path $WacsDir "wacs.exe" + +# ===================== 1. Download + install win-acme ===================== +if (-not (Test-Path $WacsExe)) { + Write-Host "==> Download win-acme (WACS)..." -ForegroundColor Cyan + $url = "https://github.com/win-acme/win-acme/releases/download/v2.2.9.1701/win-acme.v2.2.9.1701.x64.trimmed.zip" + $zip = "$env:TEMP\wacs.zip" + Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing + if (-not (Test-Path $WacsDir)) { New-Item -ItemType Directory -Force -Path $WacsDir | Out-Null } + Expand-Archive -Path $zip -DestinationPath $WacsDir -Force + Remove-Item $zip + Write-Host " Installed to $WacsDir" +} else { + Write-Host "==> win-acme đã cài tại $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" } +) + +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." + exit 1 + } +} +Write-Host " 3 IIS site đã ready" + +# ===================== 3. Run win-acme cho từng domain ===================== +foreach ($d in $domains) { + Write-Host "`n==> Issue cert cho $($d.Host)" -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", $d.Host, + "--siteid", (Get-Website $d.Site).Id, + "--store", "certificatestore", + "--installation", "iis", + "--accepttos", + "--emailaddress", "admin@huypham.vn" + ) + + & $WacsExe @args + 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)?" + } else { + Write-Host " ✅ 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)" + +# ===================== 5. Verify scheduled task ===================== +Write-Host "`n==> 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" +} else { + Write-Warning " Task chưa tạo — chạy tay: $WacsExe --renew --baseuri https://acme-v02.api.letsencrypt.org/" +} + +Write-Host "`n✅ 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"