[CLAUDE] Rebrand: 3 domain huypham.vn → solutions.com.vn + migrate script
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m52s
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:
@ -1,6 +1,6 @@
|
||||
---
|
||||
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:
|
||||
- "prod 500 error"
|
||||
- "IIS site fail"
|
||||
@ -27,7 +27,7 @@ Internet
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 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 │
|
||||
│ │ → out-of-process │ │ (static SPA, URL │ │ (static SPA, URL │
|
||||
│ │ Kestrel :5443 │ │ Rewrite /api → 5443)│ │ Rewrite...) │
|
||||
@ -46,9 +46,9 @@ Internet
|
||||
|
||||
| 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-Admin` | `*:443:admin.huypham.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-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.solutions.com.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\Admin\` | static (no app pool .NET) | React build fe-admin |
|
||||
| `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:
|
||||
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
|
||||
|
||||
# Từ ngoài
|
||||
curl https://api.huypham.vn/health/ready
|
||||
curl https://api.solutions.com.vn/health/ready
|
||||
```
|
||||
|
||||
## Let's Encrypt cert — win-acme
|
||||
@ -237,9 +237,9 @@ Xem gotcha #26:
|
||||
|
||||
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ó)
|
||||
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
|
||||
@ -349,9 +349,9 @@ VietReport.
|
||||
**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
|
||||
- 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
|
||||
- **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
|
||||
|
||||
|
||||
@ -109,7 +109,7 @@ jobs:
|
||||
run: |
|
||||
Start-Sleep -Seconds 10
|
||||
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)"
|
||||
} catch {
|
||||
Write-Warning "API smoke test: $_"
|
||||
|
||||
@ -363,9 +363,9 @@ Demo users (User@123456):
|
||||
⚠ Rotate ALL passwords trước UAT thật
|
||||
```
|
||||
|
||||
- API prod: https://api.huypham.vn — `/health/live`, `/health/ready`
|
||||
- Admin FE prod: https://admin.huypham.vn
|
||||
- User FE prod: https://user.huypham.vn
|
||||
- API prod: https://api.solutions.com.vn — `/health/live`, `/health/ready`
|
||||
- Admin FE prod: https://admin.solutions.com.vn
|
||||
- User FE prod: https://eoffice.solutions.com.vn
|
||||
- API dev: http://localhost:5443 — `/swagger` (Dev only)
|
||||
- Admin FE dev: http://localhost:8082
|
||||
- User FE dev: http://localhost:8080
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 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) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
╔════════════════╗ ╔════════════════╗ ╔════════════════╗
|
||||
|
||||
@ -8,9 +8,9 @@
|
||||
|
||||
### 🌐 Production URLs
|
||||
|
||||
- https://api.huypham.vn — API (Let's Encrypt, auto-renew via win-acme)
|
||||
- https://admin.huypham.vn — Admin FE (HTTP→HTTPS auto-redirect)
|
||||
- https://user.huypham.vn — User FE (HTTP→HTTPS auto-redirect)
|
||||
- https://api.solutions.com.vn — API (Let's Encrypt, auto-renew via win-acme)
|
||||
- https://admin.solutions.com.vn — Admin 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
|
||||
- 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
|
||||
# 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)
|
||||
|
||||
@ -185,9 +185,9 @@ Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1
|
||||
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`
|
||||
- Admin FE prod: https://admin.huypham.vn · dev `http://localhost:8082`
|
||||
- User FE prod: https://user.huypham.vn · dev `http://localhost:8080`
|
||||
- Admin FE prod: https://admin.solutions.com.vn · dev `http://localhost:8082`
|
||||
- User FE prod: https://eoffice.solutions.com.vn · dev `http://localhost:8080`
|
||||
- SQL prod: `.\SQLEXPRESS` / `SolutionErp` / `vrapp`
|
||||
- SQL dev: `(localdb)\MSSQLLocalDB` / `SolutionErp_Dev`
|
||||
|
||||
@ -328,7 +328,7 @@ subdomain có ARR proxy về `:3000`.
|
||||
|
||||
**SOLUTION_ERP relevance:**
|
||||
- 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
|
||||
deploy Kestrel standalone qua NSSM → PHẢI apply 3 rules trên
|
||||
- Scripts + skill doc đã update `localhost` → `127.0.0.1` để đồng bộ
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
- **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`
|
||||
- **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)
|
||||
|
||||
## 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
|
||||
# Health check
|
||||
curl https://api.huypham.vn/health/live # → Healthy
|
||||
curl https://api.huypham.vn/health/ready # → Healthy (DB probe)
|
||||
curl https://api.solutions.com.vn/health/live # → Healthy
|
||||
curl https://api.solutions.com.vn/health/ready # → Healthy (DB probe)
|
||||
|
||||
# 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" \
|
||||
-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
|
||||
open https://admin.solutions.com.vn # fe-admin login page
|
||||
open https://eoffice.solutions.com.vn # fe-user login page
|
||||
|
||||
# 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)
|
||||
@ -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.
|
||||
- [ ] **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
|
||||
- [ ] **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
|
||||
|
||||
## 8. Co-existence với VIETREPORT — checklist
|
||||
|
||||
@ -1 +1 @@
|
||||
VITE_API_BASE_URL=https://api.huypham.vn
|
||||
VITE_API_BASE_URL=https://api.solutions.com.vn
|
||||
|
||||
@ -5,7 +5,7 @@ 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)
|
||||
// Prod: VITE_API_BASE_URL = https://api.solutions.com.vn (env.production)
|
||||
const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api'
|
||||
|
||||
export const api = axios.create({
|
||||
|
||||
@ -4,7 +4,7 @@ import { TOKEN_KEY } from '@/lib/api'
|
||||
// Hub URL resolution:
|
||||
// - Dev: Vite proxy forwards /api → :5443 but SignalR bypasses axios, so we
|
||||
// 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'
|
||||
|
||||
let connection: HubConnection | null = null
|
||||
|
||||
@ -1 +1 @@
|
||||
VITE_API_BASE_URL=https://api.huypham.vn
|
||||
VITE_API_BASE_URL=https://api.solutions.com.vn
|
||||
|
||||
@ -5,7 +5,7 @@ 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)
|
||||
// Prod: VITE_API_BASE_URL = https://api.solutions.com.vn (env.production)
|
||||
const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api'
|
||||
|
||||
export const api = axios.create({
|
||||
|
||||
@ -4,7 +4,7 @@ import { TOKEN_KEY } from '@/lib/api'
|
||||
// Hub URL resolution:
|
||||
// - Dev: Vite proxy forwards /api → :5443 but SignalR bypasses axios, so we
|
||||
// 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'
|
||||
|
||||
let connection: HubConnection | null = null
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
# - SQL Server with login 'sa' + 'vrapp' exists
|
||||
# - .NET 10 Hosting Bundle installed
|
||||
# - 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()]
|
||||
param(
|
||||
@ -120,7 +120,7 @@ if (Test-Path $example) {
|
||||
if (-not $SkipSsl) {
|
||||
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 " 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)"
|
||||
if ($confirm -eq 'y') {
|
||||
try {
|
||||
@ -151,9 +151,9 @@ Write-Banner "[OK] DEPLOY ALL DONE - $([int]$duration.TotalMinutes)m $([int]($du
|
||||
Write-Host ""
|
||||
Write-Host "Next:"
|
||||
Write-Host " 1. Verify 3 domains:"
|
||||
Write-Host " curl https://api.huypham.vn/health/live"
|
||||
Write-Host " curl -I https://admin.huypham.vn"
|
||||
Write-Host " curl -I https://user.huypham.vn"
|
||||
Write-Host " curl https://api.solutions.com.vn/health/live"
|
||||
Write-Host " curl -I https://admin.solutions.com.vn"
|
||||
Write-Host " curl -I https://eoffice.solutions.com.vn"
|
||||
Write-Host ""
|
||||
Write-Host " 2. Set remaining 2 Gitea secrets (if not done):"
|
||||
Write-Host " https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/settings/actions/secrets"
|
||||
|
||||
210
scripts/migrate-domains.ps1
Normal file
210
scripts/migrate-domains.ps1
Normal 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
|
||||
}
|
||||
@ -24,9 +24,9 @@ $PathTemplates = "$Root\api\wwwroot\templates"
|
||||
|
||||
$AppPoolApi = "SolutionErp-Api"
|
||||
|
||||
$DomainApi = "api.huypham.vn"
|
||||
$DomainAdmin = "admin.huypham.vn"
|
||||
$DomainUser = "user.huypham.vn"
|
||||
$DomainApi = "api.solutions.com.vn"
|
||||
$DomainAdmin = "admin.solutions.com.vn"
|
||||
$DomainUser = "eoffice.solutions.com.vn"
|
||||
|
||||
$SiteApi = "SolutionErp-Api"
|
||||
$SiteAdmin = "SolutionErp-Admin"
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
# Prereq:
|
||||
# - IIS sites created (run setup-iis-sites.ps1 first)
|
||||
# - 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:
|
||||
# - 3 cert in Windows Cert Store (LocalMachine\My)
|
||||
@ -37,9 +37,9 @@ if (-not (Test-Path $WacsExe)) {
|
||||
# ===================== 2. Check IIS sites exist =====================
|
||||
Import-Module WebAdministration
|
||||
$domains = @(
|
||||
@{ Site = "SolutionErp-Api"; HostName = "api.huypham.vn" },
|
||||
@{ Site = "SolutionErp-Admin"; HostName = "admin.huypham.vn" },
|
||||
@{ Site = "SolutionErp-User"; HostName = "user.huypham.vn" }
|
||||
@{ Site = "SolutionErp-Api"; HostName = "api.solutions.com.vn" },
|
||||
@{ Site = "SolutionErp-Admin"; HostName = "admin.solutions.com.vn" },
|
||||
@{ Site = "SolutionErp-User"; HostName = "eoffice.solutions.com.vn" }
|
||||
)
|
||||
|
||||
foreach ($d in $domains) {
|
||||
@ -100,5 +100,5 @@ if ($task) {
|
||||
|
||||
Write-Host ""
|
||||
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 " Or browser: https://api.huypham.vn/health/live"
|
||||
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.solutions.com.vn/health/live"
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
"RefreshTokenExpiryDays": 7
|
||||
},
|
||||
"AllowedOrigins": [
|
||||
"https://admin.huypham.vn",
|
||||
"https://user.huypham.vn"
|
||||
"https://admin.solutions.com.vn",
|
||||
"https://eoffice.solutions.com.vn"
|
||||
],
|
||||
"Identity": {
|
||||
"Password": {
|
||||
|
||||
Reference in New Issue
Block a user