[CLAUDE] Rebrand: 3 domain huypham.vn → solutions.com.vn + migrate script
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m52s

User request: anh trỏ 3 subdomain mới về VPS IP 103.124.94.38:
  - api.huypham.vn        → api.solutions.com.vn
  - admin.huypham.vn      → admin.solutions.com.vn
  - user.huypham.vn       → eoffice.solutions.com.vn

Verified DNS: cả 3 resolve 103.124.94.38 ✓

Update 17 file repo:
FE (4): fe-admin/.env.production + fe-user/.env.production
        (VITE_API_BASE_URL → https://api.solutions.com.vn)
        fe-admin/src/lib/{api,realtime}.ts + fe-user equivalents (comment)
BE (1): appsettings.Production.json.example — CORS AllowedOrigins
CI/CD (1): .gitea/workflows/deploy.yml — smoke test URL
Scripts (3): setup-iis-sites (DomainApi/Admin/User), setup-ssl (3 host),
             deploy-all (verify curls)
Docs (5): STATUS, HANDOFF, PROJECT-MAP, vps-setup, gotchas
Skill (1): iis-deploy-runbook — 3 site table + description
Email admin@huypham.vn giữ nguyên (Let's Encrypt contact — không phải
domain serve).

Thêm scripts/migrate-domains.ps1 — 1-shot VPS migration:
  1. Pre-flight: resolve DNS 3 domain → verify IP VPS khớp
  2. Add HTTP binding mới cho 3 IIS site (giữ binding cũ làm fallback)
  3. Run win-acme xin 3 cert Let's Encrypt qua HTTP-01 challenge
     (auto add HTTPS binding + http→https redirect)
  4. Verify /health/live + /health/ready + 2 FE endpoint
  5. (Optional -RemoveOld) xóa binding huypham.vn sau verify OK
Rollback: nếu fail, binding cũ vẫn active → site serve qua huypham.vn.

Anh chạy trên VPS:
  cd C:\solution-erp\scripts  ;  .\migrate-domains.ps1
  # Sau 1-2 ngày verify stable:
  .\migrate-domains.ps1 -RemoveOld -SkipCert
This commit is contained in:
pqhuy1987
2026-04-24 09:43:05 +07:00
parent 7ca6c914fa
commit 66c1a5c170
18 changed files with 263 additions and 53 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: iis-deploy-runbook name: iis-deploy-runbook
description: Ops runbook cho SOLUTION_ERP deploy trên Windows Server IIS — 3 site (api/admin/user.huypham.vn), win-acme Let's Encrypt, NSSM gitea-runner shared với VIETREPORT, LibreOffice soffice headless. Dùng khi debug 500/502 prod, restart site, rotate cert, fix CI/CD runner, troubleshoot WebSocket, thêm site mới. description: Ops runbook cho SOLUTION_ERP deploy trên Windows Server IIS — 3 site (api/admin/eoffice.solutions.com.vn), win-acme Let's Encrypt, NSSM gitea-runner shared với VIETREPORT, LibreOffice soffice headless. Dùng khi debug 500/502 prod, restart site, rotate cert, fix CI/CD runner, troubleshoot WebSocket, thêm site mới.
when-to-use: when-to-use:
- "prod 500 error" - "prod 500 error"
- "IIS site fail" - "IIS site fail"
@ -27,7 +27,7 @@ Internet
┌─────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐
│ IIS (Windows Server VPS) │ │ IIS (Windows Server VPS) │
│ │ │ │
│ ┌─ api.huypham.vn ─┐ ┌─ admin.huypham.vn ─┐ ┌─ user.huypham.vn ─┐ │ ┌─ api.solutions.com.vn ─┐ ┌─ admin.solutions.com.vn ─┐ ┌─ eoffice.solutions.com.vn ─┐
│ │ SolutionErp-Api │ │ SolutionErp-Admin │ │ SolutionErp-User │ │ │ SolutionErp-Api │ │ SolutionErp-Admin │ │ SolutionErp-User │
│ │ → out-of-process │ │ (static SPA, URL │ │ (static SPA, URL │ │ │ → out-of-process │ │ (static SPA, URL │ │ (static SPA, URL │
│ │ Kestrel :5443 │ │ Rewrite /api → 5443)│ │ Rewrite...) │ │ │ Kestrel :5443 │ │ Rewrite /api → 5443)│ │ Rewrite...) │
@ -46,9 +46,9 @@ Internet
| Site | Binding | Physical path | Apool | Purpose | | Site | Binding | Physical path | Apool | Purpose |
|---|---|---|---|---| |---|---|---|---|---|
| `SolutionErp-Api` | `*:443:api.huypham.vn` HTTPS | `C:\inetpub\apps\SolutionErp\Api\` | out-of-process Kestrel | ASP.NET Core 10 API (port 5443 internal) | | `SolutionErp-Api` | `*:443:api.solutions.com.vn` HTTPS | `C:\inetpub\apps\SolutionErp\Api\` | out-of-process Kestrel | ASP.NET Core 10 API (port 5443 internal) |
| `SolutionErp-Admin` | `*:443:admin.huypham.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\Admin\` | static (no app pool .NET) | React build fe-admin | | `SolutionErp-Admin` | `*:443:admin.solutions.com.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\Admin\` | static (no app pool .NET) | React build fe-admin |
| `SolutionErp-User` | `*:443:user.huypham.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\User\` | static | React build fe-user | | `SolutionErp-User` | `*:443:eoffice.solutions.com.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\User\` | static | React build fe-user |
**SPA web.config:** 2 FE có `URL Rewrite` rule: **SPA web.config:** 2 FE có `URL Rewrite` rule:
1. HTTP → HTTPS redirect (bắt buộc, CORS whitelist chỉ https) 1. HTTP → HTTPS redirect (bắt buộc, CORS whitelist chỉ https)
@ -94,7 +94,7 @@ curl http://127.0.0.1:5443/health/live
curl http://127.0.0.1:5443/health/ready curl http://127.0.0.1:5443/health/ready
# Từ ngoài # Từ ngoài
curl https://api.huypham.vn/health/ready curl https://api.solutions.com.vn/health/ready
``` ```
## Let's Encrypt cert — win-acme ## Let's Encrypt cert — win-acme
@ -237,9 +237,9 @@ Xem gotcha #26:
Xem `docs/gotchas.md` CORS + HTTPS redirect: Xem `docs/gotchas.md` CORS + HTTPS redirect:
``` ```
1. User gõ http://admin.huypham.vn → không redirect → CORS block 1. User gõ http://admin.solutions.com.vn → không redirect → CORS block
2. Fix: SPA web.config PHẢI có HTTP→HTTPS rule (đã có) 2. Fix: SPA web.config PHẢI có HTTP→HTTPS rule (đã có)
3. Test: curl -I http://admin.huypham.vn → expect 301 Location: https://... 3. Test: curl -I http://admin.solutions.com.vn → expect 301 Location: https://...
``` ```
### DB connection fail ### DB connection fail
@ -349,9 +349,9 @@ VietReport.
**Hiện trạng SOLUTION_ERP — risk THẤP:** **Hiện trạng SOLUTION_ERP — risk THẤP:**
- API host trong IIS app pool out-of-process → ANCM quản lý port Kestrel ephemeral - API host trong IIS app pool out-of-process → ANCM quản lý port Kestrel ephemeral
- FE gọi trực tiếp `https://api.huypham.vn` qua CORS (không ARR proxy) - FE gọi trực tiếp `https://api.solutions.com.vn` qua CORS (không ARR proxy)
- Không có standalone Kestrel service trên port cố định - Không có standalone Kestrel service trên port cố định
- **Nhưng** tương lai nếu thêm reverse proxy (fe-admin/user → `/api` → api.huypham.vn, hoặc /hubs for SignalR) → PHẢI dùng 127.0.0.1 không localhost - **Nhưng** tương lai nếu thêm reverse proxy (fe-admin/user → `/api` → api.solutions.com.vn, hoặc /hubs for SignalR) → PHẢI dùng 127.0.0.1 không localhost
## Related ## Related

View File

@ -109,7 +109,7 @@ jobs:
run: | run: |
Start-Sleep -Seconds 10 Start-Sleep -Seconds 10
try { try {
$r = Invoke-WebRequest -Uri 'https://api.huypham.vn/health/live' -TimeoutSec 30 -UseBasicParsing $r = Invoke-WebRequest -Uri 'https://api.solutions.com.vn/health/live' -TimeoutSec 30 -UseBasicParsing
Write-Host "API /health/live -> $($r.StatusCode)" Write-Host "API /health/live -> $($r.StatusCode)"
} catch { } catch {
Write-Warning "API smoke test: $_" Write-Warning "API smoke test: $_"

View File

@ -363,9 +363,9 @@ Demo users (User@123456):
⚠ Rotate ALL passwords trước UAT thật ⚠ Rotate ALL passwords trước UAT thật
``` ```
- API prod: https://api.huypham.vn — `/health/live`, `/health/ready` - API prod: https://api.solutions.com.vn — `/health/live`, `/health/ready`
- Admin FE prod: https://admin.huypham.vn - Admin FE prod: https://admin.solutions.com.vn
- User FE prod: https://user.huypham.vn - User FE prod: https://eoffice.solutions.com.vn
- API dev: http://localhost:5443 — `/swagger` (Dev only) - API dev: http://localhost:5443 — `/swagger` (Dev only)
- Admin FE dev: http://localhost:8082 - Admin FE dev: http://localhost:8082
- User FE dev: http://localhost:8080 - User FE dev: http://localhost:8080

View File

@ -7,7 +7,7 @@
``` ```
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ SOLUTION_ERP │ │ SOLUTION_ERP │
│ 🌐 Prod live: api/admin/user.huypham.vn (HTTPS Let's Encrypt) │ │ 🌐 Prod live: api.solutions.com.vn / admin.solutions.com.vn / eoffice.solutions.com.vn (HTTPS Let's Encrypt) │
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗

View File

@ -8,9 +8,9 @@
### 🌐 Production URLs ### 🌐 Production URLs
- https://api.huypham.vn — API (Let's Encrypt, auto-renew via win-acme) - https://api.solutions.com.vn — API (Let's Encrypt, auto-renew via win-acme)
- https://admin.huypham.vn — Admin FE (HTTP→HTTPS auto-redirect) - https://admin.solutions.com.vn — Admin FE (HTTP→HTTPS auto-redirect)
- https://user.huypham.vn — User FE (HTTP→HTTPS auto-redirect) - https://eoffice.solutions.com.vn — User FE (HTTP→HTTPS auto-redirect)
- https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp — Gitea repo + Actions - https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp — Gitea repo + Actions
- Default admin: `admin@solutionerp.local` / `Admin@123456` ⚠️ **RE-ROTATE sau login đầu** - Default admin: `admin@solutionerp.local` / `Admin@123456` ⚠️ **RE-ROTATE sau login đầu**
@ -54,7 +54,7 @@
Get-Service *gitea-runner* ; & "C:\nssm\nssm.exe" status gitea-runner Get-Service *gitea-runner* ; & "C:\nssm\nssm.exe" status gitea-runner
# Nếu Stopped → Start-Service gitea-runner # Nếu Stopped → Start-Service gitea-runner
``` ```
Sau đó recheck `curl https://api.huypham.vn/api/purchase-evaluations` → 401 = deploy OK. Sau đó recheck `curl https://api.solutions.com.vn/api/purchase-evaluations` → 401 = deploy OK.
## ✅ Recently Done (newest on top) ## ✅ Recently Done (newest on top)
@ -185,9 +185,9 @@ Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1
admin@solutionerp.local / Admin@123456 admin@solutionerp.local / Admin@123456
``` ```
- API prod: https://api.huypham.vn — Health `/health/live` + `/health/ready` - API prod: https://api.solutions.com.vn — Health `/health/live` + `/health/ready`
- API dev: http://localhost:5443 — Swagger `/swagger` - API dev: http://localhost:5443 — Swagger `/swagger`
- Admin FE prod: https://admin.huypham.vn · dev `http://localhost:8082` - Admin FE prod: https://admin.solutions.com.vn · dev `http://localhost:8082`
- User FE prod: https://user.huypham.vn · dev `http://localhost:8080` - User FE prod: https://eoffice.solutions.com.vn · dev `http://localhost:8080`
- SQL prod: `.\SQLEXPRESS` / `SolutionErp` / `vrapp` - SQL prod: `.\SQLEXPRESS` / `SolutionErp` / `vrapp`
- SQL dev: `(localdb)\MSSQLLocalDB` / `SolutionErp_Dev` - SQL dev: `(localdb)\MSSQLLocalDB` / `SolutionErp_Dev`

View File

@ -328,7 +328,7 @@ subdomain có ARR proxy về `:3000`.
**SOLUTION_ERP relevance:** **SOLUTION_ERP relevance:**
- API host trong IIS app pool out-of-process (ANCM tự quản lý port Kestrel ephemeral) → risk THẤP - API host trong IIS app pool out-of-process (ANCM tự quản lý port Kestrel ephemeral) → risk THẤP
- FE gọi trực tiếp `https://api.huypham.vn` (không ARR proxy) → risk THẤP - FE gọi trực tiếp `https://api.solutions.com.vn` (không ARR proxy) → risk THẤP
- **NHƯNG** nếu tương lai thêm ARR reverse proxy (fe-admin/user `/api` proxy) hoặc - **NHƯNG** nếu tương lai thêm ARR reverse proxy (fe-admin/user `/api` proxy) hoặc
deploy Kestrel standalone qua NSSM → PHẢI apply 3 rules trên deploy Kestrel standalone qua NSSM → PHẢI apply 3 rules trên
- Scripts + skill doc đã update `localhost``127.0.0.1` để đồng bộ - Scripts + skill doc đã update `localhost``127.0.0.1` để đồng bộ

View File

@ -7,7 +7,7 @@
- **VPS OS:** Windows Server (có IIS + SQL Server) - **VPS OS:** Windows Server (có IIS + SQL Server)
- **Shared với:** VIETREPORT project — naming isolation bắt buộc - **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` - **DNS đã trỏ:** `api.solutions.com.vn`, `admin.solutions.com.vn`, `eoffice.solutions.com.vn`, `git.baocaogiaoduc.vn``103.124.94.38`
- **Prefix resources:** `SolutionErp-*` (app pool, site), `SolutionErp` (DB), `C:\inetpub\solution-erp\` (path) - **Prefix resources:** `SolutionErp-*` (app pool, site), `SolutionErp` (DB), `C:\inetpub\solution-erp\` (path)
## 1. Prerequisites trên VPS (đã có sẵn với VIETREPORT) ## 1. Prerequisites trên VPS (đã có sẵn với VIETREPORT)
@ -92,21 +92,21 @@ dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;Database=S
```bash ```bash
# Health check # Health check
curl https://api.huypham.vn/health/live # → Healthy curl https://api.solutions.com.vn/health/live # → Healthy
curl https://api.huypham.vn/health/ready # → Healthy (DB probe) curl https://api.solutions.com.vn/health/ready # → Healthy (DB probe)
# Login # Login
curl -X POST https://api.huypham.vn/api/auth/login \ curl -X POST https://api.solutions.com.vn/api/auth/login \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"email":"admin@solutionerp.local","password":"Admin@123456"}' -d '{"email":"admin@solutionerp.local","password":"Admin@123456"}'
# → accessToken JWT # → accessToken JWT
# FE # FE
open https://admin.huypham.vn # fe-admin login page open https://admin.solutions.com.vn # fe-admin login page
open https://user.huypham.vn # fe-user login page open https://eoffice.solutions.com.vn # fe-user login page
# SSL grade # SSL grade
# https://www.ssllabs.com/ssltest/analyze.html?d=api.huypham.vn # https://www.ssllabs.com/ssltest/analyze.html?d=api.solutions.com.vn
``` ```
## 7. Sau go-live (bắt buộc) ## 7. Sau go-live (bắt buộc)
@ -114,7 +114,7 @@ open https://user.huypham.vn # fe-user login page
- [ ] **Đổi password admin** từ `Admin@123456` → mạnh. Warning log xuất hiện khi còn dùng default. - [ ] **Đổ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 - [ ] **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` - [ ] **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 - [ ] **Disable Swagger prod**: Program.cs đã có `if (IsDevelopment())` — verify URL `https://api.solutions.com.vn/swagger` → 404
- [ ] **Monitor**: kiểm `C:\inetpub\solution-erp\logs\` ngày đầu, watch for ERR - [ ] **Monitor**: kiểm `C:\inetpub\solution-erp\logs\` ngày đầu, watch for ERR
## 8. Co-existence với VIETREPORT — checklist ## 8. Co-existence với VIETREPORT — checklist

View File

@ -1 +1 @@
VITE_API_BASE_URL=https://api.huypham.vn VITE_API_BASE_URL=https://api.solutions.com.vn

View File

@ -5,7 +5,7 @@ export const REFRESH_KEY = 'solution-erp-admin-refresh'
export const USER_KEY = 'solution-erp-admin-user' export const USER_KEY = 'solution-erp-admin-user'
// Dev: Vite proxy /api → :5443 (vite.config.ts) // Dev: Vite proxy /api → :5443 (vite.config.ts)
// Prod: VITE_API_BASE_URL = https://api.huypham.vn (env.production) // Prod: VITE_API_BASE_URL = https://api.solutions.com.vn (env.production)
const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api' const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api'
export const api = axios.create({ export const api = axios.create({

View File

@ -4,7 +4,7 @@ import { TOKEN_KEY } from '@/lib/api'
// Hub URL resolution: // Hub URL resolution:
// - Dev: Vite proxy forwards /api → :5443 but SignalR bypasses axios, so we // - Dev: Vite proxy forwards /api → :5443 but SignalR bypasses axios, so we
// hit the API origin directly from the browser. // hit the API origin directly from the browser.
// - Prod: VITE_API_BASE_URL (https://api.huypham.vn) // - Prod: VITE_API_BASE_URL (https://api.solutions.com.vn)
const HUB_URL = (import.meta.env.VITE_API_BASE_URL ?? window.location.origin) + '/hubs/notifications' const HUB_URL = (import.meta.env.VITE_API_BASE_URL ?? window.location.origin) + '/hubs/notifications'
let connection: HubConnection | null = null let connection: HubConnection | null = null

View File

@ -1 +1 @@
VITE_API_BASE_URL=https://api.huypham.vn VITE_API_BASE_URL=https://api.solutions.com.vn

View File

@ -5,7 +5,7 @@ export const REFRESH_KEY = 'solution-erp-user-refresh'
export const USER_KEY = 'solution-erp-user-user' export const USER_KEY = 'solution-erp-user-user'
// Dev: Vite proxy /api → :5443 (vite.config.ts) // Dev: Vite proxy /api → :5443 (vite.config.ts)
// Prod: VITE_API_BASE_URL = https://api.huypham.vn (env.production) // Prod: VITE_API_BASE_URL = https://api.solutions.com.vn (env.production)
const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api' const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api'
export const api = axios.create({ export const api = axios.create({

View File

@ -4,7 +4,7 @@ import { TOKEN_KEY } from '@/lib/api'
// Hub URL resolution: // Hub URL resolution:
// - Dev: Vite proxy forwards /api → :5443 but SignalR bypasses axios, so we // - Dev: Vite proxy forwards /api → :5443 but SignalR bypasses axios, so we
// hit the API origin directly from the browser. // hit the API origin directly from the browser.
// - Prod: VITE_API_BASE_URL (https://api.huypham.vn) // - Prod: VITE_API_BASE_URL (https://api.solutions.com.vn)
const HUB_URL = (import.meta.env.VITE_API_BASE_URL ?? window.location.origin) + '/hubs/notifications' const HUB_URL = (import.meta.env.VITE_API_BASE_URL ?? window.location.origin) + '/hubs/notifications'
let connection: HubConnection | null = null let connection: HubConnection | null = null

View File

@ -15,7 +15,7 @@
# - SQL Server with login 'sa' + 'vrapp' exists # - 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 pointing to VPS # - DNS api/admin/eoffice.solutions.com.vn pointing to VPS
[CmdletBinding()] [CmdletBinding()]
param( param(
@ -120,7 +120,7 @@ if (Test-Path $example) {
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 "WARNING: Port 80 must be reachable from Internet (Let's Encrypt HTTP-01)" Write-Host "WARNING: Port 80 must be reachable from Internet (Let's Encrypt HTTP-01)"
Write-Host " Test from outside: curl http://api.huypham.vn" Write-Host " Test from outside: curl http://api.solutions.com.vn"
$confirm = Read-Host "Continue? (y/N)" $confirm = Read-Host "Continue? (y/N)"
if ($confirm -eq 'y') { if ($confirm -eq 'y') {
try { try {
@ -151,9 +151,9 @@ Write-Banner "[OK] DEPLOY ALL DONE - $([int]$duration.TotalMinutes)m $([int]($du
Write-Host "" Write-Host ""
Write-Host "Next:" Write-Host "Next:"
Write-Host " 1. Verify 3 domains:" Write-Host " 1. Verify 3 domains:"
Write-Host " curl https://api.huypham.vn/health/live" Write-Host " curl https://api.solutions.com.vn/health/live"
Write-Host " curl -I https://admin.huypham.vn" Write-Host " curl -I https://admin.solutions.com.vn"
Write-Host " curl -I https://user.huypham.vn" Write-Host " curl -I https://eoffice.solutions.com.vn"
Write-Host "" Write-Host ""
Write-Host " 2. Set remaining 2 Gitea secrets (if not done):" 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"

210
scripts/migrate-domains.ps1 Normal file
View File

@ -0,0 +1,210 @@
# Migrate domains from *.huypham.vn to solutions.com.vn (Phase 6+ rebrand).
# Run trên VPS với admin privilege. Idempotent — chạy lại nếu fail giữa chừng.
#
# Workflow (3 IIS sites):
# 1. Add binding mới cho domain mới (*:80 + *:443 với cert self-signed tạm)
# 2. Run win-acme xin cert Let's Encrypt qua HTTP-01 challenge (port 80)
# 3. Verify /health/ready + /health/live qua domain mới
# 4. Remove binding cũ (*:huypham.vn + *:user.huypham.vn) — OPTIONAL via -RemoveOld
#
# Usage:
# .\migrate-domains.ps1 # giữ binding cũ, thêm mới
# .\migrate-domains.ps1 -RemoveOld # xóa binding cũ sau verify
# .\migrate-domains.ps1 -SkipCert # bỏ bước win-acme (nếu anh đã có cert)
#
# Prereq:
# - DNS 3 domain mới đã trỏ 103.124.94.38 (user confirm)
# - Port 80+443 firewall open cho internet
# - win-acme đã cài (script setup-ssl.ps1 cài sẵn)
# - IIS binding cũ còn active (huypham.vn)
#
# Rollback: nếu fail, binding cũ vẫn còn → site vẫn phục vụ qua *.huypham.vn
[CmdletBinding()]
param(
[string]$NewApi = "api.solutions.com.vn",
[string]$NewAdmin = "admin.solutions.com.vn",
[string]$NewUser = "eoffice.solutions.com.vn",
[string]$OldApi = "api.huypham.vn",
[string]$OldAdmin = "admin.huypham.vn",
[string]$OldUser = "user.huypham.vn",
[string]$AdminEmail = "admin@huypham.vn",
[switch]$RemoveOld,
[switch]$SkipCert
)
$ErrorActionPreference = 'Stop'
Import-Module WebAdministration
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
function Write-OK($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
function Write-Warn2($msg){ Write-Host " [!!] $msg" -ForegroundColor Yellow }
# ===================== 1. Pre-flight checks =====================
Write-Step "Pre-flight checks"
# 1a. Verify DNS của 3 domain mới trỏ đúng IP VPS
$vpsIp = (Invoke-WebRequest -Uri "https://api.ipify.org" -UseBasicParsing -TimeoutSec 5).Content.Trim()
Write-Host " VPS public IP: $vpsIp"
foreach ($d in @($NewApi, $NewAdmin, $NewUser)) {
try {
$resolved = (Resolve-DnsName -Name $d -Type A -ErrorAction Stop | Select-Object -First 1).IPAddress
if ($resolved -eq $vpsIp) {
Write-OK "DNS $d -> $resolved"
} else {
Write-Warn2 "DNS $d -> $resolved (KHONG KHOP VPS IP $vpsIp) — Let's Encrypt se fail!"
Read-Host "Continue anyway? (Ctrl+C abort, Enter continue)"
}
} catch {
Write-Warn2 "DNS resolve FAIL cho $d$_"
exit 1
}
}
# 1b. Verify 3 IIS site tồn tại
$siteMap = @(
@{ Site = "SolutionErp-Api"; Old = $OldApi; New = $NewApi },
@{ Site = "SolutionErp-Admin"; Old = $OldAdmin; New = $NewAdmin },
@{ Site = "SolutionErp-User"; Old = $OldUser; New = $NewUser }
)
foreach ($s in $siteMap) {
if (-not (Test-Path "IIS:\Sites\$($s.Site)")) {
Write-Error "Site '$($s.Site)' khong ton tai. Chay setup-iis-sites.ps1 truoc."
exit 1
}
Write-OK "IIS site $($s.Site) ready"
}
# ===================== 2. Add new bindings (keep old) =====================
Write-Step "Add binding moi cho 3 site (giu binding cu)"
foreach ($s in $siteMap) {
$site = $s.Site
$new = $s.New
# Check binding HTTP (port 80) cho domain moi
$existingHttp = Get-WebBinding -Name $site -Protocol http -HostHeader $new -ErrorAction SilentlyContinue
if (-not $existingHttp) {
New-WebBinding -Name $site -IPAddress "*" -Port 80 -HostHeader $new -Protocol http | Out-Null
Write-OK "Add HTTP binding: $site *:80:$new"
} else {
Write-Host " HTTP binding exists: $site *:80:$new"
}
}
# ===================== 3. Request cert Let's Encrypt (win-acme) =====================
if (-not $SkipCert) {
Write-Step "Request cert Let's Encrypt cho 3 domain moi"
$WacsExe = "C:\Program Files\win-acme\wacs.exe"
if (-not (Test-Path $WacsExe)) {
Write-Error "win-acme chua cai. Run setup-ssl.ps1 truoc, hoac dùng -SkipCert."
exit 1
}
foreach ($s in $siteMap) {
Write-Host ""
Write-Host "==> Issue cert $($s.New)" -ForegroundColor Cyan
$siteId = (Get-Website $s.Site).Id
$wacsArgs = @(
"--target", "manual",
"--host", $s.New,
"--store", "certificatestore",
"--installation", "iis",
"--installationsiteid", $siteId,
"--accepttos",
"--emailaddress", $AdminEmail
)
& $WacsExe @wacsArgs
if ($LASTEXITCODE -ne 0) {
Write-Warn2 "Cert $($s.New) FAIL exit $LASTEXITCODE — skip, check:"
Write-Warn2 " 1. Port 80 internet -> VPS open?"
Write-Warn2 " 2. DNS $($s.New) -> $vpsIp?"
Write-Warn2 " 3. HTTP binding $($s.Site) *:80:$($s.New) created?"
} else {
Write-OK "Cert $($s.New) installed (auto HTTPS binding + http->https redirect)"
}
}
} else {
Write-Host "==> SkipCert flag — bo qua xin cert Let's Encrypt" -ForegroundColor Yellow
}
# ===================== 4. Verify endpoint moi =====================
Write-Step "Verify endpoint moi"
Start-Sleep -Seconds 3 # cho IIS reload binding
function Test-Endpoint($url, $expectOk) {
try {
$r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 10 -SkipCertificateCheck
if ($r.StatusCode -eq 200) { Write-OK "$url -> 200 OK"; return $true }
Write-Warn2 "$url -> $($r.StatusCode) (expected 200)"; return $false
} catch {
Write-Warn2 "$url FAIL: $_"
return $false
}
}
$apiLive = Test-Endpoint "https://$NewApi/health/live" $true
$apiReady = Test-Endpoint "https://$NewApi/health/ready" $true
$adminOk = Test-Endpoint "https://$NewAdmin" $true
$userOk = Test-Endpoint "https://$NewUser" $true
$allOk = $apiLive -and $apiReady -and $adminOk -and $userOk
# ===================== 5. Remove old bindings (optional) =====================
if ($RemoveOld) {
if (-not $allOk) {
Write-Warn2 "Endpoint moi CHUA verify OK — skip remove old binding (giu fallback)"
} else {
Write-Step "Remove binding cu (huypham.vn)"
foreach ($s in $siteMap) {
$site = $s.Site
$old = $s.Old
# Remove HTTP
$httpOld = Get-WebBinding -Name $site -Protocol http -HostHeader $old -ErrorAction SilentlyContinue
if ($httpOld) {
Remove-WebBinding -Name $site -Protocol http -HostHeader $old -Port 80
Write-OK "Removed HTTP *:80:$old from $site"
}
# Remove HTTPS
$httpsOld = Get-WebBinding -Name $site -Protocol https -HostHeader $old -ErrorAction SilentlyContinue
if ($httpsOld) {
Remove-WebBinding -Name $site -Protocol https -HostHeader $old -Port 443
Write-OK "Removed HTTPS *:443:$old from $site"
}
}
}
} else {
Write-Host ""
Write-Host "[INFO] Binding cu (*.huypham.vn) van giu active. Run -RemoveOld sau khi verify xong." -ForegroundColor Yellow
}
# ===================== 6. Summary =====================
Write-Host ""
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Host " MIGRATION DONE" -ForegroundColor Cyan
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Host ""
Write-Host "3 domain moi:"
Write-Host " - https://$NewApi/health/live $(if ($apiLive) {'[OK]'} else {'[FAIL]'})"
Write-Host " - https://$NewApi/health/ready $(if ($apiReady) {'[OK]'} else {'[FAIL]'})"
Write-Host " - https://$NewAdmin $(if ($adminOk) {'[OK]'} else {'[FAIL]'})"
Write-Host " - https://$NewUser $(if ($userOk) {'[OK]'} else {'[FAIL]'})"
Write-Host ""
Write-Host "Next step:"
Write-Host " 1. Trigger CI/CD redeploy (push empty commit hoac manual workflow dispatch)"
Write-Host " → BE rebuild voi CORS moi (allow https://admin.solutions.com.vn + eoffice.solutions.com.vn)"
Write-Host " → FE rebuild voi VITE_API_BASE_URL=https://api.solutions.com.vn"
Write-Host " 2. Test login qua browser: https://$NewAdmin + https://$NewUser"
Write-Host " 3. Sau 1-2 ngay verify stable → chay lai script voi -RemoveOld"
Write-Host " .\migrate-domains.ps1 -RemoveOld -SkipCert"
Write-Host ""
if (-not $allOk) {
Write-Warn2 "CO ENDPOINT FAIL — kiem tra Event Log + IIS log truoc khi proceed."
exit 1
}

View File

@ -24,9 +24,9 @@ $PathTemplates = "$Root\api\wwwroot\templates"
$AppPoolApi = "SolutionErp-Api" $AppPoolApi = "SolutionErp-Api"
$DomainApi = "api.huypham.vn" $DomainApi = "api.solutions.com.vn"
$DomainAdmin = "admin.huypham.vn" $DomainAdmin = "admin.solutions.com.vn"
$DomainUser = "user.huypham.vn" $DomainUser = "eoffice.solutions.com.vn"
$SiteApi = "SolutionErp-Api" $SiteApi = "SolutionErp-Api"
$SiteAdmin = "SolutionErp-Admin" $SiteAdmin = "SolutionErp-Admin"

View File

@ -8,7 +8,7 @@
# Prereq: # Prereq:
# - IIS sites created (run setup-iis-sites.ps1 first) # - IIS sites created (run setup-iis-sites.ps1 first)
# - Port 80 from Internet -> VPS open (Let's Encrypt HTTP-01 challenge) # - Port 80 from Internet -> VPS open (Let's Encrypt HTTP-01 challenge)
# - 3 domains api/admin/user.huypham.vn pointing DNS to VPS IP # - 3 domains api/admin/eoffice.solutions.com.vn pointing DNS to VPS IP
# #
# Output: # Output:
# - 3 cert in Windows Cert Store (LocalMachine\My) # - 3 cert in Windows Cert Store (LocalMachine\My)
@ -37,9 +37,9 @@ if (-not (Test-Path $WacsExe)) {
# ===================== 2. Check IIS sites exist ===================== # ===================== 2. Check IIS sites exist =====================
Import-Module WebAdministration Import-Module WebAdministration
$domains = @( $domains = @(
@{ Site = "SolutionErp-Api"; HostName = "api.huypham.vn" }, @{ Site = "SolutionErp-Api"; HostName = "api.solutions.com.vn" },
@{ Site = "SolutionErp-Admin"; HostName = "admin.huypham.vn" }, @{ Site = "SolutionErp-Admin"; HostName = "admin.solutions.com.vn" },
@{ Site = "SolutionErp-User"; HostName = "user.huypham.vn" } @{ Site = "SolutionErp-User"; HostName = "eoffice.solutions.com.vn" }
) )
foreach ($d in $domains) { foreach ($d in $domains) {
@ -100,5 +100,5 @@ if ($task) {
Write-Host "" Write-Host ""
Write-Host "[OK] SSL setup DONE" -ForegroundColor Green 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.solutions.com.vn:443 < /dev/null | openssl x509 -noout -subject -dates"
Write-Host " Or browser: https://api.huypham.vn/health/live" Write-Host " Or browser: https://api.solutions.com.vn/health/live"

View File

@ -10,8 +10,8 @@
"RefreshTokenExpiryDays": 7 "RefreshTokenExpiryDays": 7
}, },
"AllowedOrigins": [ "AllowedOrigins": [
"https://admin.huypham.vn", "https://admin.solutions.com.vn",
"https://user.huypham.vn" "https://eoffice.solutions.com.vn"
], ],
"Identity": { "Identity": {
"Password": { "Password": {