[CLAUDE] VPS setup scripts + SSL + runner + FE prod config + master runbook
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:
@ -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
|
||||
|
||||
|
||||
149
docs/guides/vps-setup.md
Normal file
149
docs/guides/vps-setup.md
Normal file
@ -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 '<SA_PASSWORD_FROM_SECRETS_VAULT>'
|
||||
|
||||
# 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 '<token>'
|
||||
```
|
||||
|
||||
## 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=<vrapp 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 <pw>' /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
|
||||
1
fe-admin/.env.production
Normal file
1
fe-admin/.env.production
Normal file
@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=https://api.huypham.vn
|
||||
@ -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,
|
||||
})
|
||||
|
||||
|
||||
1
fe-user/.env.production
Normal file
1
fe-user/.env.production
Normal file
@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=https://api.huypham.vn
|
||||
@ -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,
|
||||
})
|
||||
|
||||
|
||||
77
scripts/setup-gitea-runner.ps1
Normal file
77
scripts/setup-gitea-runner.ps1
Normal file
@ -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"
|
||||
186
scripts/setup-iis-sites.ps1
Normal file
186
scripts/setup-iis-sites.ps1
Normal 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 đã tạo, 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"
|
||||
74
scripts/setup-sql-db.ps1
Normal file
74
scripts/setup-sql-db.ps1
Normal file
@ -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=***"
|
||||
113
scripts/setup-ssl.ps1
Normal file
113
scripts/setup-ssl.ps1
Normal file
@ -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 = @'
|
||||
<rewrite>
|
||||
<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 =====================
|
||||
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"
|
||||
Reference in New Issue
Block a user