sciagent code + Gitea Actions CI/CD
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Thinh Lam
2026-06-30 09:38:30 +07:00
commit 688fac73e9
1167 changed files with 158244 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# ============================================================================
# DYD — Deployment environment variables (TEMPLATE)
#
# Copy file này thành .env.deploy.local (đã trong .gitignore) và điền secrets.
# KHÔNG commit file .env.deploy.local vào git.
# ============================================================================
# --- VPS ---------------------------------------------------------------------
VPS_IP=103.124.94.58
VPS_USER=Administrator
# VPS_PASSWORD=... # Không khuyến khích lưu file — nhập khi RDP
# --- DOMAINS -----------------------------------------------------------------
API_DOMAIN=api.ski-ump.com.vn
USER_DOMAIN=ski-ump.com.vn
ADMIN_DOMAIN=admin.ski-ump.com.vn
# --- SQL SERVER --------------------------------------------------------------
SQL_SERVER=103.124.94.58,1433
SQL_DATABASE=DYD_Prod
# Login admin (chỉ để SETUP lần đầu — KHÔNG dùng cho app)
# SQL_SA_USER=sa
# SQL_SA_PASSWORD=...
# Login app (tạo bằng scripts/deployment/sql/01-create-database.sql)
SQL_APP_USER=dyd_app
# SQL_APP_PASSWORD=... # Generate: [System.Web.Security.Membership]::GeneratePassword(32,8)
# Connection string cho .NET (KHÔNG commit)
# DB_CONNECTION_STRING="Server=103.124.94.58,1433;Database=DYD_Prod;User Id=dyd_app;Password=XXX;TrustServerCertificate=True;MultipleActiveResultSets=True;"
# --- JWT ---------------------------------------------------------------------
# Generate: [Convert]::ToBase64String([byte[]]::new(48)) sau [System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
# Hoặc online: https://generate-random.org/api-token-generator (64 char)
# JWT_SIGNING_KEY=...
# --- AI SERVICE --------------------------------------------------------------
AI_SERVICE_URL=http://localhost:4402
# AI_SERVICE_API_KEY=... # Random hex 32: [BitConverter]::ToString((New-Object byte[] 32).Tap({[Security.Cryptography.RandomNumberGenerator]::Fill($_)})) -replace '-'
# --- GITEA -------------------------------------------------------------------
GITEA_URL=http://103.124.94.58:3000
# GITEA_ADMIN_USER=admin
# GITEA_ADMIN_PASSWORD=... # Set qua web installer
# GITEA_RUNNER_TOKEN=... # Lấy ở Site Admin → Actions → Runner Management
# --- EMAIL (for win-acme Let's Encrypt + SMTP future) -----------------------
ADMIN_EMAIL=admin@ski-ump.com.vn
# SMTP_HOST=smtp.sendgrid.net
# SMTP_PORT=587
# SMTP_USER=apikey
# SMTP_PASSWORD=...
@@ -0,0 +1,25 @@
# ============================================================================
# COPY TOÀN BỘ block PowerShell BÊN DƯỚI, paste vào PowerShell Admin trên VPS
# (Nhỏ gọn ~4KB, clipboard RDP paste tốt)
# ============================================================================
$ErrorActionPreference='Stop'; Write-Host "[DYD SSH] Start..." -F Cyan
$cap = Get-WindowsCapability -Online | ? Name -like 'OpenSSH.Server*'
if ($cap.State -ne 'Installed') { Add-WindowsCapability -Online -Name $cap.Name | Out-Null; Write-Host "[OK] OpenSSH installed" -F Green } else { Write-Host "[OK] OpenSSH already installed" -F Green }
Start-Service sshd; Set-Service sshd -StartupType Automatic; Set-Service ssh-agent -StartupType Automatic; Start-Service ssh-agent -ErrorAction SilentlyContinue
if (-not (Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue)) { New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH SSH Server' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null }
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType String -Force | Out-Null
$pub = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM/SmlEVa41JmeIAwQOtEkdzUo1BLPJbJ+oDqDYm1ywQ dyd-vps-deploy-20260415'
$authFile = 'C:\ProgramData\ssh\administrators_authorized_keys'
if (-not (Test-Path (Split-Path $authFile))) { New-Item -ItemType Directory -Path (Split-Path $authFile) -Force | Out-Null }
$existing = if (Test-Path $authFile) { Get-Content $authFile -Raw } else { '' }
if ($existing -notmatch [regex]::Escape($pub)) { Add-Content -Path $authFile -Value $pub -Encoding UTF8 }
icacls $authFile /inheritance:r | Out-Null; icacls $authFile /grant 'Administrators:F' /grant 'SYSTEM:F' | Out-Null
Restart-Service sshd
$sshd = Get-Service sshd; $listen = Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue
Write-Host ""; Write-Host "=====================================" -F Green; Write-Host " DONE — SSH server ready" -F Green; Write-Host "=====================================" -F Green
Write-Host " sshd status : $($sshd.Status)" -F Green
Write-Host " port 22 : $(if($listen){'LISTENING'}else{'NOT listening'})" -F $(if($listen){'Green'}else{'Red'})
Write-Host " public key : added to $authFile" -F Green
Write-Host ""; Write-Host "Dev can test:" -F Yellow
Write-Host " ssh -i ~/.ssh/dyd_vps Administrator@103.124.94.58 hostname" -F Yellow
+143
View File
@@ -0,0 +1,143 @@
# ============================================================================
# 00 — Enable OpenSSH Server trên Windows Server
# USAGE:
# 1. RDP vào VPS (103.124.94.58:3389)
# 2. Mở PowerShell AS ADMINISTRATOR
# 3. Copy-paste TOÀN BỘ file này vào PowerShell rồi Enter
# 4. Chờ ~1 phút, script sẽ print "DONE" khi xong
# 5. Báo lại cho dev để test SSH
#
# Script idempotent — chạy nhiều lần OK.
# ============================================================================
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Cyan
Write-Host " DYD — Enable OpenSSH Server on Windows" -ForegroundColor Cyan
Write-Host "=========================================================" -ForegroundColor Cyan
# --- 1. Install OpenSSH Server capability ---
Write-Host ""
Write-Host "[1/6] Install OpenSSH.Server capability ..." -ForegroundColor Yellow
$cap = Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'
if ($cap.State -ne 'Installed') {
Add-WindowsCapability -Online -Name $cap.Name | Out-Null
Write-Host " [OK] Installed" -ForegroundColor Green
}
else {
Write-Host " [OK] Already installed" -ForegroundColor Green
}
# --- 2. Start sshd + auto-start ---
Write-Host ""
Write-Host "[2/6] Start sshd service ..." -ForegroundColor Yellow
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
# Start ssh-agent too (tùy, cho key management)
Set-Service -Name ssh-agent -StartupType Automatic
Start-Service ssh-agent -ErrorAction SilentlyContinue
Write-Host " [OK] sshd running, auto-start enabled" -ForegroundColor Green
# --- 3. Firewall rule port 22 ---
Write-Host ""
Write-Host "[3/6] Firewall rule port 22 ..." -ForegroundColor Yellow
$rule = Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue
if (-not $rule) {
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' `
-DisplayName 'OpenSSH SSH Server (sshd)' `
-Enabled True -Direction Inbound -Protocol TCP -Action Allow `
-LocalPort 22 | Out-Null
Write-Host " [OK] Firewall rule created" -ForegroundColor Green
}
else {
Enable-NetFirewallRule -Name 'OpenSSH-Server-In-TCP'
Write-Host " [OK] Firewall rule enabled" -ForegroundColor Green
}
# --- 4. Set DefaultShell = PowerShell (thay cho cmd) ---
Write-Host ""
Write-Host "[4/6] Set DefaultShell = PowerShell ..." -ForegroundColor Yellow
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' `
-Name DefaultShell `
-Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' `
-PropertyType String -Force | Out-Null
Write-Host " [OK] DefaultShell set to PowerShell" -ForegroundColor Green
# --- 5. Add dev machine public key to authorized_keys ---
Write-Host ""
Write-Host "[5/6] Add dev public key ..." -ForegroundColor Yellow
# === PUBLIC KEY ĐÃ EMBED — KHÔNG commit private key ===
$PublicKey = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM/SmlEVa41JmeIAwQOtEkdzUo1BLPJbJ+oDqDYm1ywQ dyd-vps-deploy-20260415'
# For Administrator account, dùng C:\ProgramData\ssh\administrators_authorized_keys
# (KHÔNG dùng ~/.ssh/authorized_keys)
$authFile = 'C:\ProgramData\ssh\administrators_authorized_keys'
# Ensure directory exists
$authDir = Split-Path $authFile
if (-not (Test-Path $authDir)) {
New-Item -ItemType Directory -Path $authDir -Force | Out-Null
}
# Append key nếu chưa có (idempotent)
$existing = if (Test-Path $authFile) { Get-Content $authFile -Raw } else { '' }
if ($existing -notmatch [regex]::Escape($PublicKey)) {
Add-Content -Path $authFile -Value $PublicKey -Encoding UTF8
Write-Host " [OK] Public key added" -ForegroundColor Green
}
else {
Write-Host " [OK] Public key already present" -ForegroundColor Green
}
# Fix permission — QUAN TRỌNG, sai permission = SSH silently reject key
# Chỉ Administrators + SYSTEM được đọc
icacls $authFile /inheritance:r | Out-Null
icacls $authFile /grant 'Administrators:F' /grant 'SYSTEM:F' | Out-Null
Write-Host " [OK] Permission locked (Admin + SYSTEM only)" -ForegroundColor Green
# --- 6. Verify ---
Write-Host ""
Write-Host "[6/6] Verify ..." -ForegroundColor Yellow
$sshd = Get-Service sshd
if ($sshd.Status -eq 'Running') {
Write-Host " [OK] sshd: Running" -ForegroundColor Green
}
else {
Write-Host " [FAIL] sshd: $($sshd.Status)" -ForegroundColor Red
}
# Test listener
$listening = Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue
if ($listening) {
Write-Host " [OK] Port 22: listening on $($listening[0].LocalAddress)" -ForegroundColor Green
}
else {
Write-Host " [WARN] Port 22: not listening (may need restart)" -ForegroundColor Yellow
}
# Test firewall
$fw = Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue
if ($fw -and $fw.Enabled -eq 'True') {
Write-Host " [OK] Firewall: allowed" -ForegroundColor Green
}
# --- DONE ---
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Green
Write-Host " DONE — SSH server ready" -ForegroundColor Green
Write-Host "=========================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Dev machine can now connect:"
Write-Host " ssh -i ~/.ssh/dyd_vps Administrator@103.124.94.58" -ForegroundColor Yellow
Write-Host ""
Write-Host "Test từ máy dev:"
Write-Host " ssh -i ~/.ssh/dyd_vps Administrator@103.124.94.58 hostname" -ForegroundColor Yellow
Write-Host ""
Write-Host "[!] Nếu đổi ý muốn disable SSH sau, chạy:"
Write-Host " Stop-Service sshd; Set-Service sshd -StartupType Disabled"
@@ -0,0 +1,167 @@
# ============================================================================
# 01 — Install prerequisites on Windows Server (Run as Administrator)
# Target: Windows Server 2019/2022 with IIS
# Usage: Run via RDP on VPS (103.124.94.58)
# ============================================================================
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Write-Step($msg) {
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " $msg" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
}
function Test-CommandExists($cmd) {
$null -ne (Get-Command $cmd -ErrorAction SilentlyContinue)
}
# ---------------------------------------------------------------------------
Write-Step "1/7 — Enable IIS features"
# ---------------------------------------------------------------------------
$features = @(
'IIS-WebServerRole', 'IIS-WebServer', 'IIS-CommonHttpFeatures',
'IIS-HttpErrors', 'IIS-HttpRedirect', 'IIS-StaticContent',
'IIS-DefaultDocument', 'IIS-DirectoryBrowsing',
'IIS-ApplicationDevelopment', 'IIS-ISAPIExtensions', 'IIS-ISAPIFilter',
'IIS-Security', 'IIS-RequestFiltering', 'IIS-BasicAuthentication',
'IIS-WindowsAuthentication', 'IIS-HealthAndDiagnostics', 'IIS-HttpLogging',
'IIS-LoggingLibraries', 'IIS-Performance', 'IIS-HttpCompressionStatic',
'IIS-HttpCompressionDynamic', 'IIS-WebServerManagementTools',
'IIS-ManagementConsole', 'IIS-ManagementScriptingTools',
'IIS-WebSockets', 'NetFx4Extended-ASPNET45',
'IIS-ASPNET45', 'IIS-NetFxExtensibility45'
)
foreach ($feature in $features) {
$state = (Get-WindowsOptionalFeature -Online -FeatureName $feature -ErrorAction SilentlyContinue).State
if ($state -ne 'Enabled') {
Write-Host " Enabling $feature ..." -ForegroundColor Yellow
Enable-WindowsOptionalFeature -Online -FeatureName $feature -NoRestart -ErrorAction SilentlyContinue | Out-Null
}
else {
Write-Host " [OK] $feature" -ForegroundColor Green
}
}
# ---------------------------------------------------------------------------
Write-Step "2/7 — Install ASP.NET Core 10 Hosting Bundle"
# ---------------------------------------------------------------------------
if (Test-Path 'HKLM:\SOFTWARE\Microsoft\ASP.NET Core\Hosting Bundle\v10.0') {
Write-Host " [OK] ASP.NET Core 10 Hosting Bundle already installed" -ForegroundColor Green
}
else {
$url = 'https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.6/dotnet-hosting-10.0.6-win.exe'
$exe = "$env:TEMP\dotnet-hosting-10.0.6-win.exe"
Write-Host " Downloading $url ..." -ForegroundColor Yellow
Invoke-WebRequest -Uri $url -OutFile $exe
Write-Host " Installing (silent)..." -ForegroundColor Yellow
Start-Process -FilePath $exe -ArgumentList '/quiet','/install','/norestart' -Wait
Remove-Item $exe -Force
Write-Host " [OK] ASP.NET Core 10 Hosting Bundle installed" -ForegroundColor Green
}
# ---------------------------------------------------------------------------
Write-Step "3/7 — Install IIS URL Rewrite 2.1"
# ---------------------------------------------------------------------------
if (Test-Path 'HKLM:\SOFTWARE\Microsoft\IIS Extensions\URL Rewrite') {
Write-Host " [OK] URL Rewrite already installed" -ForegroundColor Green
}
else {
$url = 'https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi'
$msi = "$env:TEMP\rewrite_amd64_en-US.msi"
Write-Host " Downloading URL Rewrite ..." -ForegroundColor Yellow
Invoke-WebRequest -Uri $url -OutFile $msi
Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /quiet /norestart" -Wait
Remove-Item $msi -Force
Write-Host " [OK] URL Rewrite installed" -ForegroundColor Green
}
# ---------------------------------------------------------------------------
Write-Step "4/7 — Install .NET 10 SDK (for Gitea runner builds)"
# ---------------------------------------------------------------------------
if (Test-CommandExists 'dotnet') {
$v = (dotnet --list-sdks) -match '^10\.'
if ($v) {
Write-Host " [OK] .NET 10 SDK installed" -ForegroundColor Green
}
}
if (-not (Test-CommandExists 'dotnet') -or -not ((dotnet --list-sdks) -match '^10\.')) {
$url = 'https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.104/dotnet-sdk-10.0.104-win-x64.exe'
$exe = "$env:TEMP\dotnet-sdk-10.0.104-win-x64.exe"
Write-Host " Downloading .NET 10 SDK ..." -ForegroundColor Yellow
Invoke-WebRequest -Uri $url -OutFile $exe
Start-Process $exe -ArgumentList '/quiet','/norestart' -Wait
Remove-Item $exe -Force
Write-Host " [OK] .NET 10 SDK installed" -ForegroundColor Green
}
# ---------------------------------------------------------------------------
Write-Step "5/7 — Install Node.js 20 LTS"
# ---------------------------------------------------------------------------
if (Test-CommandExists 'node') {
$nodeV = (node --version) -replace 'v', ''
if ([version]$nodeV -ge [version]'20.0.0') {
Write-Host " [OK] Node.js $nodeV installed" -ForegroundColor Green
}
}
if (-not (Test-CommandExists 'node') -or ([version]((node --version) -replace 'v','')) -lt [version]'20.0.0') {
$url = 'https://nodejs.org/dist/v20.18.0/node-v20.18.0-x64.msi'
$msi = "$env:TEMP\node-v20.18.0-x64.msi"
Write-Host " Downloading Node.js 20 ..." -ForegroundColor Yellow
Invoke-WebRequest -Uri $url -OutFile $msi
Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /quiet /norestart" -Wait
Remove-Item $msi -Force
Write-Host " [OK] Node.js 20 installed" -ForegroundColor Green
}
# ---------------------------------------------------------------------------
Write-Step "6/7 — Install Git for Windows"
# ---------------------------------------------------------------------------
if (Test-CommandExists 'git') {
Write-Host " [OK] Git installed" -ForegroundColor Green
}
else {
$url = 'https://github.com/git-for-windows/git/releases/download/v2.46.0.windows.1/Git-2.46.0-64-bit.exe'
$exe = "$env:TEMP\Git-2.46.0-64-bit.exe"
Write-Host " Downloading Git ..." -ForegroundColor Yellow
Invoke-WebRequest -Uri $url -OutFile $exe
Start-Process $exe -ArgumentList '/VERYSILENT','/NORESTART' -Wait
Remove-Item $exe -Force
Write-Host " [OK] Git installed" -ForegroundColor Green
}
# ---------------------------------------------------------------------------
Write-Step "7/7 — Install win-acme (Let's Encrypt client)"
# ---------------------------------------------------------------------------
$wacsPath = 'C:\Tools\win-acme'
if (Test-Path "$wacsPath\wacs.exe") {
Write-Host " [OK] win-acme already installed at $wacsPath" -ForegroundColor Green
}
else {
New-Item -ItemType Directory -Force -Path $wacsPath | Out-Null
$url = 'https://github.com/win-acme/win-acme/releases/download/v2.2.9.1701/win-acme.v2.2.9.1701.x64.pluggable.zip'
$zip = "$env:TEMP\wacs.zip"
Write-Host " Downloading win-acme ..." -ForegroundColor Yellow
Invoke-WebRequest -Uri $url -OutFile $zip
Expand-Archive -Path $zip -DestinationPath $wacsPath -Force
Remove-Item $zip -Force
Write-Host " [OK] win-acme installed at $wacsPath\wacs.exe" -ForegroundColor Green
}
# ---------------------------------------------------------------------------
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Green
Write-Host " DONE — Prerequisites installed" -ForegroundColor Green
Write-Host "=========================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:"
Write-Host " 1. Restart IIS: iisreset"
Write-Host " 2. Run: .\scripts\deployment\02-setup-sqlserver.ps1"
Write-Host " 3. Run: .\scripts\deployment\03-setup-iis-sites.ps1"
Write-Host ""
Write-Host "Refresh PATH in new PowerShell session to use dotnet/node/git"
+159
View File
@@ -0,0 +1,159 @@
# ============================================================================
# 03 — Setup 3 IIS sites: DYD.Api / DYD.User / DYD.Admin
# Run as Administrator on VPS after 01-install-prerequisites.ps1
# ============================================================================
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Stop'
Import-Module WebAdministration -ErrorAction Stop
# --- CONFIG ------------------------------------------------------------------
$BaseDir = 'C:\inetpub'
$Sites = @(
@{
Name = 'DYD.Api'
AppPool = 'DYD.ApiPool'
Path = "$BaseDir\DYD.Api"
HttpPort = 5443
HttpsPort = 443
Host = 'api.ski-ump.com.vn'
LogsPath = "$BaseDir\DYD.Api\logs"
Managed = $true # .NET Core → No Managed Code
},
@{
Name = 'DYD.User'
AppPool = '' # static site, no app pool
Path = "$BaseDir\DYD.User"
HttpPort = 8080
HttpsPort = 443
Host = 'ski-ump.com.vn'
Managed = $false
},
@{
Name = 'DYD.Admin'
AppPool = ''
Path = "$BaseDir\DYD.Admin"
HttpPort = 8082
HttpsPort = 443
Host = 'admin.ski-ump.com.vn'
Managed = $false
}
)
# --- HELPER ------------------------------------------------------------------
function New-Dir($p) {
if (-not (Test-Path $p)) {
New-Item -ItemType Directory -Path $p -Force | Out-Null
Write-Host " Created: $p" -ForegroundColor Green
}
}
function New-ApiAppPool($name) {
if (Test-Path "IIS:\AppPools\$name") {
Write-Host " [OK] AppPool $name exists" -ForegroundColor Green
return
}
New-WebAppPool -Name $name | Out-Null
Set-ItemProperty "IIS:\AppPools\$name" -name managedRuntimeVersion -value ''
Set-ItemProperty "IIS:\AppPools\$name" -name startMode -value 'AlwaysRunning'
Set-ItemProperty "IIS:\AppPools\$name" -name processModel.idleTimeout -value ([TimeSpan]::Zero)
Write-Host " Created AppPool: $name (No Managed Code, AlwaysRunning)" -ForegroundColor Green
}
# --- DIRECTORIES -------------------------------------------------------------
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " 1/4 — Create directories" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
foreach ($s in $Sites) {
New-Dir $s.Path
if ($s.LogsPath) { New-Dir $s.LogsPath }
}
New-Dir "$BaseDir\backups"
# --- APP POOLS ---------------------------------------------------------------
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " 2/4 — Create App Pools" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
foreach ($s in $Sites) {
if ($s.Managed -and $s.AppPool) {
New-ApiAppPool $s.AppPool
}
}
# --- SITES -------------------------------------------------------------------
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " 3/4 — Create Sites + Bindings" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
foreach ($s in $Sites) {
# Remove existing site if port conflict
$existing = Get-Website -Name $s.Name -ErrorAction SilentlyContinue
if ($existing) {
Write-Host " Site $($s.Name) exists. Skipping creation." -ForegroundColor Yellow
continue
}
if ($s.Managed) {
New-Website -Name $s.Name `
-PhysicalPath $s.Path `
-ApplicationPool $s.AppPool `
-Port $s.HttpPort `
-HostHeader $s.Host `
-Force | Out-Null
}
else {
New-Website -Name $s.Name `
-PhysicalPath $s.Path `
-Port $s.HttpPort `
-HostHeader $s.Host `
-Force | Out-Null
}
Write-Host " Created site: $($s.Name) on :$($s.HttpPort) — host: $($s.Host)" -ForegroundColor Green
}
# --- PLACEHOLDER INDEX -------------------------------------------------------
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " 4/4 — Placeholder index.html" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
foreach ($s in $Sites) {
$indexPath = Join-Path $s.Path 'index.html'
if (-not (Test-Path $indexPath)) {
@"
<!DOCTYPE html>
<html><head><title>$($s.Name)</title></head>
<body><h1>$($s.Name) placeholder</h1>
<p>Site chưa đưc deploy. Chy pipeline hoc deploy th công.</p></body></html>
"@ | Set-Content $indexPath -Encoding UTF8
}
}
# --- FIREWALL ----------------------------------------------------------------
Write-Host ""
Write-Host "Firewall rules (HTTP 80, HTTPS 443, custom ports):" -ForegroundColor Cyan
$ports = @(80, 443, 5443, 8080, 8082, 3000) # 3000 cho Gitea sau
foreach ($p in $ports) {
$rule = "DYD-TCP-$p"
if (-not (Get-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Protocol TCP -LocalPort $p -Action Allow | Out-Null
Write-Host " [+] TCP $p allowed" -ForegroundColor Green
}
}
# --- SUMMARY -----------------------------------------------------------------
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Green
Write-Host " DONE — 3 IIS sites ready" -ForegroundColor Green
Write-Host "=========================================================" -ForegroundColor Green
Write-Host ""
Get-Website | Where-Object { $_.Name -like 'DYD.*' } | Format-Table -AutoSize Name, State, PhysicalPath, @{L='Bindings';E={($_.Bindings.Collection.bindingInformation) -join ', '}}
Write-Host "HTTP URLs (cần bind host file hoặc DNS):"
foreach ($s in $Sites) {
Write-Host " http://$($s.Host):$($s.HttpPort)" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Next: 04-install-gitea.ps1 + 05-setup-ssl.ps1 (sau khi DNS trỏ IP)"
+147
View File
@@ -0,0 +1,147 @@
# ============================================================================
# 04 — Install Gitea 1.23 + NSSM service (port 3000, SQLite)
# Run as Administrator. Pattern từ NamGroup skill cicd-gitea-windows.
# ============================================================================
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$GiteaDir = 'C:\Gitea'
$DataDir = 'C:\Gitea\data'
$LogsDir = 'C:\Gitea\log'
$NSSMDir = 'C:\Tools\nssm'
# --- DOWNLOAD GITEA ---------------------------------------------------------
Write-Host "1/5 — Download Gitea 1.23.7" -ForegroundColor Cyan
New-Item -ItemType Directory -Force -Path $GiteaDir, $DataDir, $LogsDir | Out-Null
$giteaExe = "$GiteaDir\gitea.exe"
if (-not (Test-Path $giteaExe)) {
$url = 'https://dl.gitea.com/gitea/1.23.7/gitea-1.23.7-windows-4.0-amd64.exe'
Invoke-WebRequest -Uri $url -OutFile $giteaExe
Write-Host " [OK] gitea.exe downloaded" -ForegroundColor Green
} else {
Write-Host " [OK] gitea.exe exists" -ForegroundColor Green
}
# --- DOWNLOAD NSSM ----------------------------------------------------------
Write-Host "2/5 — Download NSSM" -ForegroundColor Cyan
New-Item -ItemType Directory -Force -Path $NSSMDir | Out-Null
$nssmExe = "$NSSMDir\nssm.exe"
if (-not (Test-Path $nssmExe)) {
$zip = "$env:TEMP\nssm.zip"
Invoke-WebRequest -Uri 'https://nssm.cc/release/nssm-2.24.zip' -OutFile $zip
Expand-Archive -Path $zip -DestinationPath "$env:TEMP\nssm-extract" -Force
Copy-Item "$env:TEMP\nssm-extract\nssm-2.24\win64\nssm.exe" $nssmExe -Force
Remove-Item $zip, "$env:TEMP\nssm-extract" -Recurse -Force
Write-Host " [OK] nssm.exe ready" -ForegroundColor Green
} else {
Write-Host " [OK] nssm.exe exists" -ForegroundColor Green
}
# --- CONFIG FILE -------------------------------------------------------------
Write-Host "3/5 — Create app.ini" -ForegroundColor Cyan
$appIni = "$GiteaDir\custom\conf\app.ini"
New-Item -ItemType Directory -Force -Path (Split-Path $appIni) | Out-Null
if (-not (Test-Path $appIni)) {
@'
APP_NAME = DYD Git (Gitea)
RUN_USER = LOCAL SYSTEM
RUN_MODE = prod
[server]
PROTOCOL = http
DOMAIN = 103.124.94.58
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3000
ROOT_URL = http://103.124.94.58:3000/
DISABLE_SSH = true
OFFLINE_MODE = false
[database]
DB_TYPE = sqlite3
PATH = C:/Gitea/data/gitea.db
LOG_SQL = false
[repository]
ROOT = C:/Gitea/data/gitea-repositories
[log]
MODE = file
LEVEL = info
ROOT_PATH = C:/Gitea/log
[security]
INSTALL_LOCK = false
; SECRET_KEY sẽ được tự sinh khi chạy lần đầu qua web installer
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
ENABLE_NOTIFY_MAIL = false
REGISTER_EMAIL_CONFIRM = false
[picture]
DISABLE_GRAVATAR = true
[actions]
ENABLED = true
'@ | Set-Content -Path $appIni -Encoding UTF8
Write-Host " [OK] app.ini created" -ForegroundColor Green
} else {
Write-Host " [OK] app.ini exists (skip)" -ForegroundColor Green
}
# --- INSTALL SERVICE --------------------------------------------------------
Write-Host "4/5 — Install NSSM service 'gitea'" -ForegroundColor Cyan
$existingService = Get-Service -Name 'gitea' -ErrorAction SilentlyContinue
if (-not $existingService) {
& $nssmExe install gitea $giteaExe web --config "$appIni" --work-path "$GiteaDir"
& $nssmExe set gitea AppStdout "$LogsDir\stdout.log"
& $nssmExe set gitea AppStderr "$LogsDir\stderr.log"
& $nssmExe set gitea AppRotateFiles 1
& $nssmExe set gitea AppRotateBytes 10485760
& $nssmExe set gitea DisplayName 'Gitea Service (DYD)'
& $nssmExe set gitea Description 'Gitea self-hosted git + Actions for DYD project'
& $nssmExe set gitea Start SERVICE_AUTO_START
Write-Host " [OK] Service installed" -ForegroundColor Green
} else {
Write-Host " [OK] Service 'gitea' exists" -ForegroundColor Green
}
# --- START -------------------------------------------------------------------
Write-Host "5/5 — Start Gitea service" -ForegroundColor Cyan
Start-Service gitea
Start-Sleep 5
$svc = Get-Service gitea
Write-Host " Service status: $($svc.Status)" -ForegroundColor $(if ($svc.Status -eq 'Running') {'Green'} else {'Red'})
# --- Test HTTP ---------------------------------------------------------------
try {
$r = Invoke-WebRequest 'http://127.0.0.1:3000' -UseBasicParsing -TimeoutSec 10
Write-Host " Gitea web responds: HTTP $($r.StatusCode)" -ForegroundColor Green
} catch {
Write-Host " [WARN] Gitea chưa response, check log: $LogsDir" -ForegroundColor Yellow
}
# --- SUMMARY -----------------------------------------------------------------
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Green
Write-Host " Gitea installed" -ForegroundColor Green
Write-Host "=========================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps MANUAL:" -ForegroundColor Yellow
Write-Host " 1. Mở browser: http://103.124.94.58:3000"
Write-Host " 2. Complete installer wizard (SQLite OK, admin account)"
Write-Host " 3. Sau khi setup: Site Admin → Disable Registration"
Write-Host " 4. Tạo repo 'DYD' và push code"
Write-Host " 5. Site Admin → Actions → Runner Management → Create Runner"
Write-Host " → copy registration token → chạy 05-install-act-runner.ps1"
Write-Host ""
Write-Host "Config file: $appIni"
Write-Host "Logs: $LogsDir"
@@ -0,0 +1,98 @@
# ============================================================================
# 05 — Install act_runner (Gitea Actions runner) as Windows service
# PREREQUISITE:
# 1. Gitea đã chạy (04-install-gitea.ps1)
# 2. Đã lấy registration token từ Gitea:
# Site Admin → Actions → Runner Management → Create new Runner → Copy token
# Usage:
# .\05-install-act-runner.ps1 -Token <registration-token>
# ============================================================================
param(
[Parameter(Mandatory=$true)]
[string]$Token,
[string]$GiteaUrl = 'http://127.0.0.1:3000',
[string]$RunnerName = 'dyd-windows-runner',
[string]$Labels = 'windows:host,windows-latest:host'
)
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Stop'
$RunnerDir = 'C:\Tools\act_runner'
$NSSM = 'C:\Tools\nssm\nssm.exe'
if (-not (Test-Path $NSSM)) {
Write-Error "NSSM chưa cài. Chạy 04-install-gitea.ps1 trước."
exit 1
}
# --- DOWNLOAD ---------------------------------------------------------------
Write-Host "1/4 — Download act_runner" -ForegroundColor Cyan
New-Item -ItemType Directory -Force -Path $RunnerDir | Out-Null
$runnerExe = "$RunnerDir\act_runner.exe"
if (-not (Test-Path $runnerExe)) {
$url = 'https://dl.gitea.com/act_runner/0.2.11/act_runner-0.2.11-windows-amd64.exe'
Invoke-WebRequest -Uri $url -OutFile $runnerExe
Write-Host " [OK] downloaded" -ForegroundColor Green
}
# --- REGISTER ---------------------------------------------------------------
Write-Host "2/4 — Register with Gitea" -ForegroundColor Cyan
Set-Location $RunnerDir
if (-not (Test-Path "$RunnerDir\.runner")) {
& $runnerExe register --no-interactive `
--instance $GiteaUrl `
--token $Token `
--name $RunnerName `
--labels $Labels
if ($LASTEXITCODE -ne 0) {
Write-Error "Register failed. Check token + Gitea URL."
exit 1
}
Write-Host " [OK] Registered as $RunnerName" -ForegroundColor Green
} else {
Write-Host " [OK] Already registered (.runner exists)" -ForegroundColor Green
}
# --- CONFIG -----------------------------------------------------------------
Write-Host "3/4 — Generate config.yaml" -ForegroundColor Cyan
$cfgFile = "$RunnerDir\config.yaml"
if (-not (Test-Path $cfgFile)) {
& $runnerExe generate-config | Set-Content $cfgFile -Encoding UTF8
Write-Host " [OK] config.yaml ready — edit nếu cần tùy chỉnh" -ForegroundColor Green
}
# --- SERVICE ----------------------------------------------------------------
Write-Host "4/4 — Install NSSM service 'act_runner'" -ForegroundColor Cyan
$svc = Get-Service -Name 'act_runner' -ErrorAction SilentlyContinue
if (-not $svc) {
& $NSSM install act_runner $runnerExe daemon --config "$cfgFile"
& $NSSM set act_runner AppDirectory $RunnerDir
& $NSSM set act_runner AppStdout "$RunnerDir\runner-stdout.log"
& $NSSM set act_runner AppStderr "$RunnerDir\runner-stderr.log"
& $NSSM set act_runner AppRotateFiles 1
& $NSSM set act_runner AppRotateBytes 10485760
& $NSSM set act_runner DisplayName 'Gitea Actions Runner (DYD)'
& $NSSM set act_runner Start SERVICE_AUTO_START
Write-Host " [OK] Service installed" -ForegroundColor Green
}
Start-Service act_runner
Start-Sleep 3
$svc = Get-Service act_runner
Write-Host " Service status: $($svc.Status)" -ForegroundColor $(if ($svc.Status -eq 'Running') {'Green'} else {'Red'})
# --- DONE -------------------------------------------------------------------
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Green
Write-Host " act_runner ready — Gitea Actions sẵn sàng" -ForegroundColor Green
Write-Host "=========================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Verify:"
Write-Host " Gitea UI → Site Admin → Actions → Runner — phải thấy '$RunnerName' online"
Write-Host ""
Write-Host "Labels: $Labels"
Write-Host "Logs: $RunnerDir\runner-*.log"
+114
View File
@@ -0,0 +1,114 @@
# ============================================================================
# 06 — Setup Let's Encrypt SSL cho 3 domain qua win-acme
# PREREQUISITE:
# - DNS đã trỏ 3 domain về IP VPS (103.124.94.58)
# - IIS sites đã bind với host header đúng
# - Port 80 mở từ internet (cho HTTP-01 challenge)
# ============================================================================
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Stop'
$wacs = 'C:\Tools\win-acme\wacs.exe'
$email = 'admin@ski-ump.com.vn' # TODO: đổi email contact
if (-not (Test-Path $wacs)) {
Write-Error "win-acme chưa cài. Chạy 01-install-prerequisites.ps1 trước."
exit 1
}
# --- CHECK DNS ---------------------------------------------------------------
$domains = @('api.ski-ump.com.vn', 'ski-ump.com.vn', 'admin.ski-ump.com.vn')
$vpsIp = '103.124.94.58'
Write-Host "Checking DNS resolution (phải trỏ về $vpsIp) ..." -ForegroundColor Cyan
$allOk = $true
foreach ($d in $domains) {
try {
$resolved = (Resolve-DnsName $d -Type A -ErrorAction Stop | Where-Object { $_.Type -eq 'A' } | Select-Object -First 1).IPAddress
if ($resolved -eq $vpsIp) {
Write-Host " [OK] $d -> $resolved" -ForegroundColor Green
} else {
Write-Host " [FAIL] $d -> $resolved (expected $vpsIp)" -ForegroundColor Red
$allOk = $false
}
} catch {
Write-Host " [FAIL] $d -> DNS not resolvable" -ForegroundColor Red
$allOk = $false
}
}
if (-not $allOk) {
Write-Host ""
Write-Host "DNS chưa sẵn sàng. Cần cấu hình A record cho 3 domain về $vpsIp trước." -ForegroundColor Red
Write-Host "Vào DNS provider (Cloudflare/GoDaddy/...) tạo:" -ForegroundColor Yellow
Write-Host " api.ski-ump.com.vn A $vpsIp"
Write-Host " ski-ump.com.vn A $vpsIp"
Write-Host " admin.ski-ump.com.vn A $vpsIp"
Write-Host " (plus) www.ski-ump.com.vn CNAME ski-ump.com.vn"
Write-Host ""
Write-Host "Đợi ~15 phút cho DNS propagation rồi chạy lại script này."
exit 1
}
# --- ISSUE CERTIFICATE -------------------------------------------------------
Write-Host ""
Write-Host "Issuing Let's Encrypt cert cho 3 domain..." -ForegroundColor Cyan
$sites = @(
@{ Domain = 'api.ski-ump.com.vn'; IISSite = 'DYD.Api' },
@{ Domain = 'ski-ump.com.vn'; IISSite = 'DYD.User' },
@{ Domain = 'admin.ski-ump.com.vn'; IISSite = 'DYD.Admin' }
)
foreach ($s in $sites) {
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " SSL for $($s.Domain) -> $($s.IISSite)" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
# Non-interactive mode
& $wacs `
--target manual `
--host $s.Domain `
--installation iis `
--installationsiteid (Get-Website -Name $s.IISSite).Id `
--emailaddress $email `
--accepttos
if ($LASTEXITCODE -ne 0) {
Write-Host " [FAIL] Issue cert for $($s.Domain) (exit $LASTEXITCODE)" -ForegroundColor Red
} else {
Write-Host " [OK] Cert issued and bound to $($s.IISSite)" -ForegroundColor Green
}
}
# --- FORCE HTTPS REDIRECT ----------------------------------------------------
Write-Host ""
Write-Host "Adding HTTP→HTTPS redirect on all 3 sites..." -ForegroundColor Cyan
# Trên mỗi site, URL Rewrite rule để 301 redirect HTTP → HTTPS
# Có thể làm ở web.config nhưng cho API phải cẩn thận (stdout log)
# Simplest: enable HTTPS only binding, remove HTTP binding
foreach ($s in $sites) {
# Không remove HTTP binding vì Let's Encrypt cần port 80 để renew
# Dùng URL Rewrite trong web.config hoặc HSTS
Write-Host " $($s.IISSite): HTTPS cert đã bound. (HTTP binding để Let's Encrypt renew)" -ForegroundColor Yellow
}
# --- VERIFY ------------------------------------------------------------------
Write-Host ""
Write-Host "Verify bindings:" -ForegroundColor Cyan
Get-WebBinding | Where-Object { $_.bindingInformation -match 'dyd' } | Format-Table protocol, bindingInformation -AutoSize
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Green
Write-Host " SSL setup done" -ForegroundColor Green
Write-Host "=========================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Test:"
foreach ($s in $sites) {
Write-Host " curl -I https://$($s.Domain)"
}
Write-Host ""
Write-Host "Auto-renewal: win-acme tạo Scheduled Task tự động (daily check, renew 30 ngày trước expiry)."
@@ -0,0 +1,52 @@
# Hướng dẫn mở SSH trên VPS (1 lần duy nhất)
## Cách dễ nhất — copy script vào RDP
1. **RDP vào VPS:**
- Windows + R → `mstsc`
- Computer: `103.124.94.58:3389`
- User: `Administrator`
- Password: (dùng password đã cung cấp)
2. **Mở PowerShell AS ADMINISTRATOR:**
- Start → gõ "PowerShell" → right-click → **Run as Administrator**
3. **Copy-paste toàn bộ file `scripts/deployment/00-enable-ssh-server.ps1`** vào PowerShell → Enter.
4. **Đợi ~1 phút**, khi thấy `DONE — SSH server ready` là xong.
5. **Báo lại cho dev**, dev sẽ test connection:
```bash
ssh -i ~/.ssh/dyd_vps Administrator@103.124.94.58 hostname
```
## Nếu copy-paste qua RDP clipboard không được
**Option A — Upload file qua RDP mapped drive:**
1. Trong mstsc → More → Local Resources → Drives → check `D:\` (hoặc drive chứa repo)
2. Sau khi RDP, mở File Explorer → `\\tsclient\D\...\scripts\deployment\00-enable-ssh-server.ps1`
3. Right-click file → Run with PowerShell (As Admin)
**Option B — Tải từ Gitea/GitHub (sau khi push):**
```powershell
Invoke-WebRequest -Uri '<raw-url>/scripts/deployment/00-enable-ssh-server.ps1' -OutFile C:\Temp\00-enable-ssh.ps1
powershell -ExecutionPolicy Bypass -File C:\Temp\00-enable-ssh.ps1
```
## Script làm gì?
1. Install OpenSSH Server capability (Windows feature)
2. Start `sshd` service + set auto-start
3. Open firewall port 22
4. Set default SSH shell = PowerShell (chứ không phải cmd)
5. Add public key `dyd-vps-deploy-20260415` vào `administrators_authorized_keys`
6. Fix file permission (chỉ Admin + SYSTEM được đọc)
**Idempotent** — chạy nhiều lần OK, không side effect.
## Security
- Public key đã embed trong script
- Private key nằm trên máy dev (`~/.ssh/dyd_vps`) — KHÔNG rời máy
- Nếu mất private key → chạy lại script sau khi đổi `$PublicKey` với key mới
- Disable SSH sau khi xong deploy: `Stop-Service sshd; Set-Service sshd -StartupType Disabled`
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
web.config cho DYD.Api — ASP.NET Core 10 out-of-process / in-process hosting
Đặt ở: C:\inetpub\DYD.Api\web.config
Robocopy /XF web.config để không bị overwrite khi deploy.
-->
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\DYD.Api.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
hostingModel="InProcess"
forwardWindowsAuthToken="false">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
<!-- Nếu dùng User Secrets / env var cho JWT key, set ở đây hoặc qua IIS Configuration Editor -->
<!--
<environmentVariable name="ConnectionStrings__DefaultConnection" value="Server=..." />
<environmentVariable name="Jwt__SigningKey" value="..." />
-->
</environmentVariables>
</aspNetCore>
<!-- Security headers -->
<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" />
<add name="Permissions-Policy" value="geolocation=(), microphone=(), camera=()" />
</customHeaders>
</httpProtocol>
<!-- Gzip compression -->
<httpCompression>
<dynamicTypes>
<add mimeType="application/json" enabled="true" />
<add mimeType="application/json; charset=utf-8" enabled="true" />
</dynamicTypes>
</httpCompression>
<!-- Larger file upload (cho InitiativeAttachments) -->
<security>
<requestFiltering>
<!-- 50MB = 52428800 bytes -->
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
</location>
</configuration>
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
web.config cho React SPA (fe0 / fe-admin)
Đặt ở: C:\inetpub\DYD.User\web.config và C:\inetpub\DYD.Admin\web.config
Mục đích:
- URL Rewrite: React Router HTML5 history API → fallback /index.html
- Cache static assets 1 năm (hash filename đảm bảo bust cache khi build mới)
- Security headers
- MIME types cho font/webmanifest
Deploy: Robocopy /XF web.config để không bị overwrite.
-->
<configuration>
<system.webServer>
<!-- React Router rewrite -->
<rewrite>
<rules>
<rule name="React Router SPA" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<!-- Không rewrite /api/* (nếu có reverse proxy) -->
<add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
</conditions>
<action type="Rewrite" url="/index.html" />
</rule>
</rules>
</rewrite>
<!-- MIME types -->
<staticContent>
<remove fileExtension=".webmanifest" />
<mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
<!-- Cache static 1 năm (Vite build hash filename bust cache tự động) -->
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" />
</staticContent>
<!-- Security headers -->
<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" />
<add name="Permissions-Policy" value="geolocation=(), microphone=(), camera=()" />
<!-- CSP strict (điều chỉnh nếu cần CDN): -->
<!-- <add name="Content-Security-Policy" value="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.ski-ump.com.vn" /> -->
</customHeaders>
</httpProtocol>
<!-- Gzip compression -->
<httpCompression>
<dynamicTypes>
<add mimeType="text/*" enabled="true" />
<add mimeType="application/javascript" enabled="true" />
<add mimeType="application/json" enabled="true" />
</dynamicTypes>
<staticTypes>
<add mimeType="text/*" enabled="true" />
<add mimeType="application/javascript" enabled="true" />
<add mimeType="application/json" enabled="true" />
<add mimeType="application/manifest+json" enabled="true" />
</staticTypes>
</httpCompression>
<!-- Default document -->
<defaultDocument>
<files>
<clear />
<add value="index.html" />
</files>
</defaultDocument>
</system.webServer>
</configuration>
+56
View File
@@ -0,0 +1,56 @@
# VPS inventory check
$ErrorActionPreference = 'Continue'
Write-Host "=== OS ===" -ForegroundColor Cyan
$os = Get-WmiObject Win32_OperatingSystem
$os | Select Caption, Version, OSArchitecture | Format-List
"Memory: $([math]::Round($os.TotalVisibleMemorySize / 1MB, 2)) GB"
Write-Host "=== CPU ===" -ForegroundColor Cyan
Get-WmiObject Win32_Processor | Select Name, NumberOfCores | Format-List
Write-Host "=== Disks ===" -ForegroundColor Cyan
Get-PSDrive -PSProvider FileSystem | Where-Object Used -gt 0 | ForEach-Object {
"Drive $($_.Name): Used $([math]::Round($_.Used / 1GB, 2)) GB, Free $([math]::Round($_.Free / 1GB, 2)) GB"
}
Write-Host "=== IIS ===" -ForegroundColor Cyan
$iis = Get-WindowsFeature Web-Server -ErrorAction SilentlyContinue
if ($iis) { "IIS: $($iis.InstallState)" } else { "IIS: Not available via WindowsFeature" }
$w3svc = Get-Service W3SVC -ErrorAction SilentlyContinue
if ($w3svc) { "W3SVC service: $($w3svc.Status)" }
Write-Host "=== .NET SDKs ===" -ForegroundColor Cyan
try { dotnet --list-sdks } catch { "dotnet not found" }
Write-Host "=== Node.js ===" -ForegroundColor Cyan
try { node --version } catch { "node not found" }
try { npm --version } catch { "npm not found" }
Write-Host "=== Git ===" -ForegroundColor Cyan
try { git --version } catch { "git not found" }
Write-Host "=== SQL Server services ===" -ForegroundColor Cyan
Get-Service MSSQL* -ErrorAction SilentlyContinue | Select Name, Status, StartType | Format-Table -AutoSize
Get-Service SQL* -ErrorAction SilentlyContinue | Select Name, Status, StartType | Format-Table -AutoSize
Write-Host "=== PowerShell ===" -ForegroundColor Cyan
$PSVersionTable.PSVersion
Write-Host "=== Listening ports ===" -ForegroundColor Cyan
Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $_.LocalPort -in 22,80,443,1433,3000,3389,5443,5985,8080,8082 } |
Select-Object LocalAddress, LocalPort |
Sort-Object LocalPort |
Get-Unique -AsString |
Format-Table -AutoSize
Write-Host "=== ASP.NET Core Module ===" -ForegroundColor Cyan
$hb = Test-Path 'HKLM:\SOFTWARE\Microsoft\ASP.NET Core\Hosting Bundle\v10.0'
"ASP.NET Core 10 Hosting Bundle: $(if ($hb) {'Installed'} else {'NOT installed'})"
$hb9 = Test-Path 'HKLM:\SOFTWARE\Microsoft\ASP.NET Core\Hosting Bundle\v9.0'
"ASP.NET Core 9 Hosting Bundle: $(if ($hb9) {'Installed'} else {'NOT installed'})"
Write-Host "=== URL Rewrite ===" -ForegroundColor Cyan
$ur = Test-Path 'HKLM:\SOFTWARE\Microsoft\IIS Extensions\URL Rewrite'
"URL Rewrite: $(if ($ur) {'Installed'} else {'NOT installed'})"
@@ -0,0 +1,78 @@
-- ============================================================================
-- DYD — Create Production Database + Dedicated Login
-- Run on SQL Server as sysadmin (sa)
-- ============================================================================
USE [master];
GO
-- 1. Create database
IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = N'DYD_Prod')
BEGIN
CREATE DATABASE [DYD_Prod]
COLLATE Vietnamese_CI_AS;
-- Optional: set recovery model (Simple cho dev/staging, Full cho prod backup log)
ALTER DATABASE [DYD_Prod] SET RECOVERY SIMPLE;
PRINT 'Database [DYD_Prod] created.';
END
ELSE
BEGIN
PRINT 'Database [DYD_Prod] already exists. Skipping.';
END
GO
-- 2. Create dedicated login for the app (KHÔNG dùng sa trong production)
-- TODO: Thay '<APP_DB_PASSWORD>' bằng password mạnh (32+ char, random)
-- Generate: [System.Web.Security.Membership]::GeneratePassword(32, 8)
IF NOT EXISTS (SELECT 1 FROM sys.sql_logins WHERE name = N'dyd_app')
BEGIN
CREATE LOGIN [dyd_app] WITH
PASSWORD = N'<APP_DB_PASSWORD>',
DEFAULT_DATABASE = [DYD_Prod],
CHECK_EXPIRATION = OFF,
CHECK_POLICY = ON;
PRINT 'Login [dyd_app] created.';
END
ELSE
BEGIN
PRINT 'Login [dyd_app] already exists. Skipping.';
END
GO
-- 3. Map login to database user + assign roles
USE [DYD_Prod];
GO
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N'dyd_app')
BEGIN
CREATE USER [dyd_app] FOR LOGIN [dyd_app];
PRINT 'User [dyd_app] created in [DYD_Prod].';
END
GO
-- Grant roles:
-- db_datareader — SELECT
-- db_datawriter — INSERT, UPDATE, DELETE
-- db_ddladmin — CREATE/ALTER/DROP (cho EF migrations)
ALTER ROLE db_datareader ADD MEMBER [dyd_app];
ALTER ROLE db_datawriter ADD MEMBER [dyd_app];
ALTER ROLE db_ddladmin ADD MEMBER [dyd_app];
PRINT 'Roles granted to [dyd_app].';
GO
-- 4. Verify
SELECT
DB_NAME() AS Database_Name,
USER_NAME() AS Current_User,
@@VERSION AS Server_Version;
GO
PRINT '';
PRINT '==========================================';
PRINT ' DONE — DYD_Prod ready.';
PRINT '==========================================';
PRINT '';
PRINT 'Connection string cho .NET (điền password đã tạo):';
PRINT 'Server=103.124.94.58,1433;Database=DYD_Prod;User Id=dyd_app;Password=<APP_DB_PASSWORD>;TrustServerCertificate=True;MultipleActiveResultSets=True;';
GO
+653
View File
@@ -0,0 +1,653 @@
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
END;
GO
BEGIN TRANSACTION;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AppraisalTeams] (
[Id] uniqueidentifier NOT NULL,
[Name] nvarchar(200) NOT NULL,
[Description] nvarchar(max) NULL,
[IsActive] bit NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_AppraisalTeams] PRIMARY KEY ([Id])
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AspNetRoles] (
[Id] nvarchar(450) NOT NULL,
[Name] nvarchar(256) NULL,
[NormalizedName] nvarchar(256) NULL,
[ConcurrencyStamp] nvarchar(max) NULL,
CONSTRAINT [PK_AspNetRoles] PRIMARY KEY ([Id])
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AspNetUsers] (
[Id] nvarchar(450) NOT NULL,
[FullName] nvarchar(max) NOT NULL,
[IsEnabled] bit NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[LastLoginAt] datetime2 NULL,
[RefreshToken] nvarchar(max) NULL,
[RefreshTokenExpiresAt] datetime2 NULL,
[UnitId] uniqueidentifier NULL,
[UserName] nvarchar(256) NULL,
[NormalizedUserName] nvarchar(256) NULL,
[Email] nvarchar(256) NULL,
[NormalizedEmail] nvarchar(256) NULL,
[EmailConfirmed] bit NOT NULL,
[PasswordHash] nvarchar(max) NULL,
[SecurityStamp] nvarchar(max) NULL,
[ConcurrencyStamp] nvarchar(max) NULL,
[PhoneNumber] nvarchar(max) NULL,
[PhoneNumberConfirmed] bit NOT NULL,
[TwoFactorEnabled] bit NOT NULL,
[LockoutEnd] datetimeoffset NULL,
[LockoutEnabled] bit NOT NULL,
[AccessFailedCount] int NOT NULL,
CONSTRAINT [PK_AspNetUsers] PRIMARY KEY ([Id])
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AuditLogs] (
[Id] uniqueidentifier NOT NULL,
[EntityName] nvarchar(100) NOT NULL,
[EntityId] nvarchar(100) NOT NULL,
[Action] nvarchar(50) NOT NULL,
[ActorUserId] nvarchar(max) NULL,
[ActorDisplayName] nvarchar(max) NULL,
[PreviousValue] nvarchar(max) NULL,
[NewValue] nvarchar(max) NULL,
[Metadata] nvarchar(max) NULL,
[IpAddress] nvarchar(max) NULL,
[UserAgent] nvarchar(max) NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
CONSTRAINT [PK_AuditLogs] PRIMARY KEY ([Id])
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [Notifications] (
[Id] uniqueidentifier NOT NULL,
[RecipientUserId] nvarchar(450) NOT NULL,
[Title] nvarchar(200) NOT NULL,
[Message] nvarchar(max) NOT NULL,
[Type] nvarchar(20) NOT NULL,
[Link] nvarchar(max) NULL,
[IsRead] bit NOT NULL,
[ReadAt] datetime2 NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
CONSTRAINT [PK_Notifications] PRIMARY KEY ([Id])
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [SystemSettings] (
[Id] uniqueidentifier NOT NULL,
[Key] nvarchar(100) NOT NULL,
[Value] nvarchar(max) NOT NULL,
[Description] nvarchar(max) NULL,
[Category] nvarchar(50) NOT NULL,
[IsSecret] bit NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_SystemSettings] PRIMARY KEY ([Id])
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [Units] (
[Id] uniqueidentifier NOT NULL,
[Name] nvarchar(200) NOT NULL,
[Code] nvarchar(50) NOT NULL,
[Description] nvarchar(max) NULL,
[ParentUnitId] uniqueidentifier NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_Units] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Units_Units_ParentUnitId] FOREIGN KEY ([ParentUnitId]) REFERENCES [Units] ([Id]) ON DELETE NO ACTION
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AppraisalTeamMembers] (
[Id] uniqueidentifier NOT NULL,
[AppraisalTeamId] uniqueidentifier NOT NULL,
[UserId] nvarchar(450) NOT NULL,
[Role] nvarchar(50) NOT NULL,
[IsChair] bit NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
CONSTRAINT [PK_AppraisalTeamMembers] PRIMARY KEY ([Id]),
CONSTRAINT [FK_AppraisalTeamMembers_AppraisalTeams_AppraisalTeamId] FOREIGN KEY ([AppraisalTeamId]) REFERENCES [AppraisalTeams] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AspNetRoleClaims] (
[Id] int NOT NULL IDENTITY,
[RoleId] nvarchar(450) NOT NULL,
[ClaimType] nvarchar(max) NULL,
[ClaimValue] nvarchar(max) NULL,
CONSTRAINT [PK_AspNetRoleClaims] PRIMARY KEY ([Id]),
CONSTRAINT [FK_AspNetRoleClaims_AspNetRoles_RoleId] FOREIGN KEY ([RoleId]) REFERENCES [AspNetRoles] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AspNetUserClaims] (
[Id] int NOT NULL IDENTITY,
[UserId] nvarchar(450) NOT NULL,
[ClaimType] nvarchar(max) NULL,
[ClaimValue] nvarchar(max) NULL,
CONSTRAINT [PK_AspNetUserClaims] PRIMARY KEY ([Id]),
CONSTRAINT [FK_AspNetUserClaims_AspNetUsers_UserId] FOREIGN KEY ([UserId]) REFERENCES [AspNetUsers] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AspNetUserLogins] (
[LoginProvider] nvarchar(450) NOT NULL,
[ProviderKey] nvarchar(450) NOT NULL,
[ProviderDisplayName] nvarchar(max) NULL,
[UserId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_AspNetUserLogins] PRIMARY KEY ([LoginProvider], [ProviderKey]),
CONSTRAINT [FK_AspNetUserLogins_AspNetUsers_UserId] FOREIGN KEY ([UserId]) REFERENCES [AspNetUsers] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AspNetUserRoles] (
[UserId] nvarchar(450) NOT NULL,
[RoleId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_AspNetUserRoles] PRIMARY KEY ([UserId], [RoleId]),
CONSTRAINT [FK_AspNetUserRoles_AspNetRoles_RoleId] FOREIGN KEY ([RoleId]) REFERENCES [AspNetRoles] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_AspNetUserRoles_AspNetUsers_UserId] FOREIGN KEY ([UserId]) REFERENCES [AspNetUsers] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [AspNetUserTokens] (
[UserId] nvarchar(450) NOT NULL,
[LoginProvider] nvarchar(450) NOT NULL,
[Name] nvarchar(450) NOT NULL,
[Value] nvarchar(max) NULL,
CONSTRAINT [PK_AspNetUserTokens] PRIMARY KEY ([UserId], [LoginProvider], [Name]),
CONSTRAINT [FK_AspNetUserTokens_AspNetUsers_UserId] FOREIGN KEY ([UserId]) REFERENCES [AspNetUsers] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [Authors] (
[Id] uniqueidentifier NOT NULL,
[FullName] nvarchar(200) NOT NULL,
[Email] nvarchar(200) NOT NULL,
[PhoneNumber] nvarchar(max) NULL,
[Position] nvarchar(max) NULL,
[AcademicTitle] nvarchar(max) NULL,
[UnitId] uniqueidentifier NULL,
[UserId] nvarchar(max) NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_Authors] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Authors_Units_UnitId] FOREIGN KEY ([UnitId]) REFERENCES [Units] ([Id]) ON DELETE SET NULL
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [Initiatives] (
[Id] uniqueidentifier NOT NULL,
[Code] nvarchar(50) NOT NULL,
[Title] nvarchar(500) NOT NULL,
[Description] nvarchar(max) NOT NULL,
[ShortSummary] nvarchar(max) NULL,
[Objectives] nvarchar(max) NULL,
[ScopeOfApplication] nvarchar(max) NULL,
[ExpectedOutcomes] nvarchar(max) NULL,
[ActualOutcomes] nvarchar(max) NULL,
[EstimatedBudget] decimal(18,2) NULL,
[ActualBudget] decimal(18,2) NULL,
[StartDate] datetime2 NULL,
[EndDate] datetime2 NULL,
[SubmissionDate] datetime2 NULL,
[ApprovalDate] datetime2 NULL,
[Status] int NOT NULL,
[Category] int NOT NULL,
[Group] int NOT NULL,
[OwningUnitId] uniqueidentifier NOT NULL,
[SubmittedByUserId] nvarchar(max) NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_Initiatives] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Initiatives_Units_OwningUnitId] FOREIGN KEY ([OwningUnitId]) REFERENCES [Units] ([Id]) ON DELETE NO ACTION
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [InitiativeAttachments] (
[Id] uniqueidentifier NOT NULL,
[InitiativeId] uniqueidentifier NOT NULL,
[FileName] nvarchar(260) NOT NULL,
[StoragePath] nvarchar(500) NOT NULL,
[ContentType] nvarchar(100) NOT NULL,
[FileSize] bigint NOT NULL,
[Description] nvarchar(max) NULL,
[Category] nvarchar(max) NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_InitiativeAttachments] PRIMARY KEY ([Id]),
CONSTRAINT [FK_InitiativeAttachments_Initiatives_InitiativeId] FOREIGN KEY ([InitiativeId]) REFERENCES [Initiatives] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [InitiativeAuthors] (
[Id] uniqueidentifier NOT NULL,
[InitiativeId] uniqueidentifier NOT NULL,
[AuthorId] uniqueidentifier NOT NULL,
[ContributionPercentage] decimal(5,2) NOT NULL,
[IsLeadAuthor] bit NOT NULL,
[ContributionDescription] nvarchar(max) NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
CONSTRAINT [PK_InitiativeAuthors] PRIMARY KEY ([Id]),
CONSTRAINT [FK_InitiativeAuthors_Authors_AuthorId] FOREIGN KEY ([AuthorId]) REFERENCES [Authors] ([Id]) ON DELETE NO ACTION,
CONSTRAINT [FK_InitiativeAuthors_Initiatives_InitiativeId] FOREIGN KEY ([InitiativeId]) REFERENCES [Initiatives] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [InitiativeStatusHistories] (
[Id] uniqueidentifier NOT NULL,
[InitiativeId] uniqueidentifier NOT NULL,
[FromStatus] int NOT NULL,
[ToStatus] int NOT NULL,
[Comment] nvarchar(max) NULL,
[ChangedByUserId] nvarchar(max) NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
CONSTRAINT [PK_InitiativeStatusHistories] PRIMARY KEY ([Id]),
CONSTRAINT [FK_InitiativeStatusHistories_Initiatives_InitiativeId] FOREIGN KEY ([InitiativeId]) REFERENCES [Initiatives] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE TABLE [Reviews] (
[Id] uniqueidentifier NOT NULL,
[InitiativeId] uniqueidentifier NOT NULL,
[ReviewerUserId] nvarchar(max) NOT NULL,
[AppraisalTeamId] uniqueidentifier NOT NULL,
[Score] decimal(5,2) NULL,
[NoveltyScore] decimal(5,2) NULL,
[FeasibilityScore] decimal(5,2) NULL,
[ImpactScore] decimal(5,2) NULL,
[EfficiencyScore] decimal(5,2) NULL,
[Decision] int NOT NULL,
[Comments] nvarchar(max) NULL,
[Strengths] nvarchar(max) NULL,
[Weaknesses] nvarchar(max) NULL,
[Recommendations] nvarchar(max) NULL,
[ReviewedAt] datetime2 NULL,
[DueDate] datetime2 NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_Reviews] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Reviews_AppraisalTeams_AppraisalTeamId] FOREIGN KEY ([AppraisalTeamId]) REFERENCES [AppraisalTeams] ([Id]) ON DELETE NO ACTION,
CONSTRAINT [FK_Reviews_Initiatives_InitiativeId] FOREIGN KEY ([InitiativeId]) REFERENCES [Initiatives] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE UNIQUE INDEX [IX_AppraisalTeamMembers_AppraisalTeamId_UserId] ON [AppraisalTeamMembers] ([AppraisalTeamId], [UserId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_AspNetRoleClaims_RoleId] ON [AspNetRoleClaims] ([RoleId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
EXEC(N'CREATE UNIQUE INDEX [RoleNameIndex] ON [AspNetRoles] ([NormalizedName]) WHERE [NormalizedName] IS NOT NULL');
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_AspNetUserClaims_UserId] ON [AspNetUserClaims] ([UserId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_AspNetUserLogins_UserId] ON [AspNetUserLogins] ([UserId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_AspNetUserRoles_RoleId] ON [AspNetUserRoles] ([RoleId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [EmailIndex] ON [AspNetUsers] ([NormalizedEmail]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
EXEC(N'CREATE UNIQUE INDEX [UserNameIndex] ON [AspNetUsers] ([NormalizedUserName]) WHERE [NormalizedUserName] IS NOT NULL');
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_AuditLogs_CreatedAt] ON [AuditLogs] ([CreatedAt]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_AuditLogs_EntityName_EntityId] ON [AuditLogs] ([EntityName], [EntityId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Authors_Email] ON [Authors] ([Email]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Authors_UnitId] ON [Authors] ([UnitId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_InitiativeAttachments_InitiativeId] ON [InitiativeAttachments] ([InitiativeId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_InitiativeAuthors_AuthorId] ON [InitiativeAuthors] ([AuthorId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE UNIQUE INDEX [IX_InitiativeAuthors_InitiativeId_AuthorId] ON [InitiativeAuthors] ([InitiativeId], [AuthorId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Initiatives_Category] ON [Initiatives] ([Category]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE UNIQUE INDEX [IX_Initiatives_Code] ON [Initiatives] ([Code]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Initiatives_OwningUnitId] ON [Initiatives] ([OwningUnitId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Initiatives_Status] ON [Initiatives] ([Status]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_InitiativeStatusHistories_InitiativeId] ON [InitiativeStatusHistories] ([InitiativeId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Notifications_RecipientUserId_IsRead] ON [Notifications] ([RecipientUserId], [IsRead]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Reviews_AppraisalTeamId] ON [Reviews] ([AppraisalTeamId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Reviews_InitiativeId] ON [Reviews] ([InitiativeId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE UNIQUE INDEX [IX_SystemSettings_Key] ON [SystemSettings] ([Key]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE UNIQUE INDEX [IX_Units_Code] ON [Units] ([Code]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
CREATE INDEX [IX_Units_ParentUnitId] ON [Units] ([ParentUnitId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260415141734_InitialCreate'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260415141734_InitialCreate', N'10.0.6');
END;
COMMIT;
GO
@@ -0,0 +1,175 @@
BEGIN TRANSACTION;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE TABLE [InitiativeReports] (
[Id] uniqueidentifier NOT NULL,
[Code] nvarchar(50) NOT NULL,
[InitiativeId] uniqueidentifier NOT NULL,
[ActualOutcomes] nvarchar(4000) NULL,
[ActualBudget] decimal(18,2) NULL,
[ImplementationNotes] nvarchar(4000) NULL,
[Challenges] nvarchar(4000) NULL,
[LessonsLearned] nvarchar(4000) NULL,
[Status] int NOT NULL,
[SubmissionDate] datetime2 NULL,
[ApprovalDate] datetime2 NULL,
[SubmittedByUserId] nvarchar(450) NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_InitiativeReports] PRIMARY KEY ([Id]),
CONSTRAINT [FK_InitiativeReports_Initiatives_InitiativeId] FOREIGN KEY ([InitiativeId]) REFERENCES [Initiatives] ([Id]) ON DELETE NO ACTION
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE TABLE [RecognitionDocuments] (
[Id] uniqueidentifier NOT NULL,
[Code] nvarchar(50) NOT NULL,
[ReportId] uniqueidentifier NOT NULL,
[Type] int NOT NULL,
[Content] nvarchar(max) NULL,
[Summary] nvarchar(1000) NULL,
[Status] int NOT NULL,
[SubmissionDate] datetime2 NULL,
[ApprovalDate] datetime2 NULL,
[SubmittedByUserId] nvarchar(450) NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_RecognitionDocuments] PRIMARY KEY ([Id]),
CONSTRAINT [FK_RecognitionDocuments_InitiativeReports_ReportId] FOREIGN KEY ([ReportId]) REFERENCES [InitiativeReports] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE TABLE [ReportStatusHistories] (
[Id] uniqueidentifier NOT NULL,
[ReportId] uniqueidentifier NOT NULL,
[FromStatus] int NOT NULL,
[ToStatus] int NOT NULL,
[Comment] nvarchar(2000) NULL,
[ChangedByUserId] nvarchar(450) NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_ReportStatusHistories] PRIMARY KEY ([Id]),
CONSTRAINT [FK_ReportStatusHistories_InitiativeReports_ReportId] FOREIGN KEY ([ReportId]) REFERENCES [InitiativeReports] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE TABLE [DocumentStatusHistories] (
[Id] uniqueidentifier NOT NULL,
[DocumentId] uniqueidentifier NOT NULL,
[FromStatus] int NOT NULL,
[ToStatus] int NOT NULL,
[Comment] nvarchar(2000) NULL,
[ChangedByUserId] nvarchar(450) NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_DocumentStatusHistories] PRIMARY KEY ([Id]),
CONSTRAINT [FK_DocumentStatusHistories_RecognitionDocuments_DocumentId] FOREIGN KEY ([DocumentId]) REFERENCES [RecognitionDocuments] ([Id]) ON DELETE CASCADE
);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE INDEX [IX_DocumentStatusHistories_DocumentId] ON [DocumentStatusHistories] ([DocumentId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE UNIQUE INDEX [IX_InitiativeReports_Code] ON [InitiativeReports] ([Code]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE INDEX [IX_InitiativeReports_InitiativeId] ON [InitiativeReports] ([InitiativeId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE INDEX [IX_InitiativeReports_Status] ON [InitiativeReports] ([Status]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE UNIQUE INDEX [IX_RecognitionDocuments_Code] ON [RecognitionDocuments] ([Code]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE UNIQUE INDEX [IX_RecognitionDocuments_ReportId_Type] ON [RecognitionDocuments] ([ReportId], [Type]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
CREATE INDEX [IX_ReportStatusHistories_ReportId] ON [ReportStatusHistories] ([ReportId]);
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260416081839_AddReportAndDocumentEntities'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260416081839_AddReportAndDocumentEntities', N'10.0.6');
END;
COMMIT;
GO