diff --git a/scripts/seed-test-users-prod.ps1 b/scripts/seed-test-users-prod.ps1 new file mode 100644 index 0000000..41a8855 --- /dev/null +++ b/scripts/seed-test-users-prod.ps1 @@ -0,0 +1,148 @@ +# Seed 20 test user prod (Session 22+2 — UAT testing scenarios) +# +# Moi phong con trong tao 3 user (NV/PP/TP) + BOD bo sung 2 user. +# Skip CCM (7 user da du) + PRO (5 user da du). +# +# Pattern: goi API admin token, idempotent (skip 409 conflict email exists). +# Sau tao user -> PATCH /position-level + /bypass-review + /allow-skip-final flag. +# +# ASCII-only per gotcha #30 (PS 5.1 diacritics parser error). FullName trong API +# se la ASCII; bro co the rename qua UI Users page neu can dau tieng Viet. +# +# Usage: +# .\scripts\seed-test-users-prod.ps1 +# +# Pre-req: Admin credentials admin@solutions.com.vn / Admin@123456 prod. + +$ErrorActionPreference = 'Stop' +$ApiBase = 'https://api.solutions.com.vn' +$AdminEmail = 'admin@solutions.com.vn' +$AdminPassword = 'Admin@123456' +$DefaultPassword = 'TestUser@2026' # >=12 chars per Identity policy (gotcha discovered S22+2) + +Write-Host "==> Authenticating admin..." -ForegroundColor Cyan +$authBody = @{ email = $AdminEmail; password = $AdminPassword } | ConvertTo-Json +$authResp = Invoke-RestMethod -Method POST -Uri "$ApiBase/api/auth/login" ` + -ContentType 'application/json' -Body $authBody +$token = $authResp.accessToken +Write-Host " Token acquired (length=$($token.Length))" -ForegroundColor Green + +$headers = @{ Authorization = "Bearer $token" } + +Write-Host "==> Fetching departments..." -ForegroundColor Cyan +$depts = Invoke-RestMethod -Method GET -Uri "$ApiBase/api/departments?page=1&pageSize=50" -Headers $headers +$deptMap = @{} +foreach ($d in $depts.items) { $deptMap[$d.code] = $d.id } +Write-Host " Mapped $($deptMap.Count) departments" -ForegroundColor Green + +# === USER SEEDS (20 total) === +$users = @( + # ACT - Phong Ke toan (3) + @{ Dept='ACT'; Email='hoa.nguyen@solutions.com.vn'; FullName='Nguyen Thi Hoa'; Position='Nhan vien Ke toan'; Roles=@('Drafter','Accounting'); PositionLevel=1; CanBypass=$false; AllowSkip=$false }, + @{ Dept='ACT'; Email='lan.pham@solutions.com.vn'; FullName='Pham Thi Lan'; Position='Pho phong Ke toan'; Roles=@('Drafter','Accounting'); PositionLevel=2; CanBypass=$false; AllowSkip=$false }, + @{ Dept='ACT'; Email='minh.le@solutions.com.vn'; FullName='Le Van Minh'; Position='Truong phong Ke toan'; Roles=@('DeptManager','Accounting'); PositionLevel=3; CanBypass=$true; AllowSkip=$false }, + + # BOD - Ban Giam doc (2 - khong hierarchy) + @{ Dept='BOD'; Email='tuan.tran@solutions.com.vn'; FullName='Tran Anh Tuan'; Position='Giam doc'; Roles=@('Director'); PositionLevel=$null; CanBypass=$false; AllowSkip=$false }, + @{ Dept='BOD'; Email='hung.do@solutions.com.vn'; FullName='Do Quoc Hung'; Position='Pho Giam doc'; Roles=@('Director','AuthorizedSigner'); PositionLevel=$null; CanBypass=$false; AllowSkip=$false }, + + # EQU - Phong Thiet bi (3) + @{ Dept='EQU'; Email='dung.bui@solutions.com.vn'; FullName='Bui Van Dung'; Position='Nhan vien Thiet bi'; Roles=@('Drafter','Equipment'); PositionLevel=1; CanBypass=$false; AllowSkip=$false }, + @{ Dept='EQU'; Email='hai.vu@solutions.com.vn'; FullName='Vu Thanh Hai'; Position='Pho phong Thiet bi'; Roles=@('Drafter','Equipment'); PositionLevel=2; CanBypass=$false; AllowSkip=$false }, + @{ Dept='EQU'; Email='tho.do@solutions.com.vn'; FullName='Do Van Tho'; Position='Truong phong Thiet bi'; Roles=@('DeptManager','Equipment'); PositionLevel=3; CanBypass=$false; AllowSkip=$false }, + + # FIN - Phong Tai chinh (3) + @{ Dept='FIN'; Email='linh.dao@solutions.com.vn'; FullName='Dao Thi Linh'; Position='Nhan vien Tai chinh'; Roles=@('Drafter','Finance'); PositionLevel=1; CanBypass=$false; AllowSkip=$false }, + @{ Dept='FIN'; Email='nga.bui@solutions.com.vn'; FullName='Bui Hong Nga'; Position='Pho phong Tai chinh'; Roles=@('Drafter','Finance'); PositionLevel=2; CanBypass=$false; AllowSkip=$true }, + @{ Dept='FIN'; Email='thu.pham@solutions.com.vn'; FullName='Pham Anh Thu'; Position='Truong phong Tai chinh'; Roles=@('DeptManager','Finance'); PositionLevel=3; CanBypass=$false; AllowSkip=$false }, + + # HRA - Phong Nhan su - Hanh chinh (3) + @{ Dept='HRA'; Email='mai.tran@solutions.com.vn'; FullName='Tran Thi Mai'; Position='Nhan vien HC-NS'; Roles=@('Drafter','HrAdmin'); PositionLevel=1; CanBypass=$false; AllowSkip=$false }, + @{ Dept='HRA'; Email='hong.le@solutions.com.vn'; FullName='Le Thi Hong'; Position='Pho phong HC-NS'; Roles=@('Drafter','HrAdmin'); PositionLevel=2; CanBypass=$false; AllowSkip=$false }, + @{ Dept='HRA'; Email='tam.nguyen@solutions.com.vn'; FullName='Nguyen Van Tam'; Position='Truong phong HC-NS'; Roles=@('DeptManager','HrAdmin'); PositionLevel=3; CanBypass=$true; AllowSkip=$false }, + + # PM - Ban Quan ly Du an (3) + @{ Dept='PM'; Email='khoi.do@solutions.com.vn'; FullName='Do Dang Khoi'; Position='Nhan vien QLDA'; Roles=@('Drafter','ProjectManager'); PositionLevel=1; CanBypass=$false; AllowSkip=$true }, + @{ Dept='PM'; Email='phong.vu@solutions.com.vn'; FullName='Vu Trong Phong'; Position='Pho BQL Du an'; Roles=@('Drafter','ProjectManager'); PositionLevel=2; CanBypass=$false; AllowSkip=$false }, + @{ Dept='PM'; Email='quan.bui@solutions.com.vn'; FullName='Bui Huu Quan'; Position='Truong BQL Du an'; Roles=@('DeptManager','ProjectManager'); PositionLevel=3; CanBypass=$false; AllowSkip=$false }, + + # QS - Phong Quantity Surveyor (3 - khong kem role chuyen) + @{ Dept='QS'; Email='hieu.nguyen@solutions.com.vn'; FullName='Nguyen Van Hieu'; Position='Nhan vien QS'; Roles=@('Drafter'); PositionLevel=1; CanBypass=$false; AllowSkip=$false }, + @{ Dept='QS'; Email='thanh.pham@solutions.com.vn'; FullName='Pham Van Thanh'; Position='Pho phong QS'; Roles=@('Drafter'); PositionLevel=2; CanBypass=$false; AllowSkip=$false }, + @{ Dept='QS'; Email='duc.le@solutions.com.vn'; FullName='Le Quang Duc'; Position='Truong phong QS'; Roles=@('DeptManager'); PositionLevel=3; CanBypass=$false; AllowSkip=$false } +) + +Write-Host "==> Seeding $($users.Count) users..." -ForegroundColor Cyan +$createdCount = 0 +$skippedCount = 0 +$errorCount = 0 + +foreach ($u in $users) { + $deptId = $deptMap[$u.Dept] + if (-not $deptId) { + Write-Host " [SKIP] $($u.Email): department $($u.Dept) not found" -ForegroundColor Yellow + $skippedCount++ + continue + } + + $createBody = @{ + email = $u.Email + fullName = $u.FullName + password = $DefaultPassword + roles = $u.Roles + departmentId = $deptId + position = $u.Position + } | ConvertTo-Json -Compress + + try { + $createResp = Invoke-RestMethod -Method POST -Uri "$ApiBase/api/users" ` + -Headers $headers -ContentType 'application/json' -Body $createBody + $userId = $createResp.id + Write-Host " [+] Created $($u.Email) [$($u.Dept)] id=$userId" -ForegroundColor Green + $createdCount++ + + # Apply position-level flag + if ($null -ne $u.PositionLevel) { + $plBody = @{ positionLevel = $u.PositionLevel } | ConvertTo-Json -Compress + Invoke-RestMethod -Method PATCH -Uri "$ApiBase/api/users/$userId/position-level" ` + -Headers $headers -ContentType 'application/json' -Body $plBody | Out-Null + Write-Host " PositionLevel=$($u.PositionLevel)" -ForegroundColor DarkGray + } + # Apply canBypassReview flag (Mig 16) + if ($u.CanBypass) { + $bpBody = @{ canBypassReview = $true } | ConvertTo-Json -Compress + Invoke-RestMethod -Method PATCH -Uri "$ApiBase/api/users/$userId/bypass-review" ` + -Headers $headers -ContentType 'application/json' -Body $bpBody | Out-Null + Write-Host " CanBypassReview=true (2-stage bypass)" -ForegroundColor DarkGray + } + # Apply allowDrafterSkipToFinal flag (Mig 29 F2) + if ($u.AllowSkip) { + $skBody = @{ allowDrafterSkipToFinal = $true } | ConvertTo-Json -Compress + Invoke-RestMethod -Method PATCH -Uri "$ApiBase/api/users/$userId/allow-skip-final" ` + -Headers $headers -ContentType 'application/json' -Body $skBody | Out-Null + Write-Host " AllowDrafterSkipToFinal=true (F2)" -ForegroundColor DarkGray + } + } + catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + if ($statusCode -eq 409) { + Write-Host " [SKIP] $($u.Email): already exists (409)" -ForegroundColor Yellow + $skippedCount++ + } + else { + Write-Host " [ERR] $($u.Email): $($_.Exception.Message)" -ForegroundColor Red + $errorCount++ + } + } + + # Rate limit backoff (CICD Monitor S22 discovery - auth ~5 req/min) + Start-Sleep -Milliseconds 500 +} + +Write-Host "" +Write-Host "==> Summary" -ForegroundColor Cyan +Write-Host " Created: $createdCount" -ForegroundColor Green +Write-Host " Skipped (already exists): $skippedCount" -ForegroundColor Yellow +Write-Host " Errors: $errorCount" -ForegroundColor Red +Write-Host "" +Write-Host "==> All users password: $DefaultPassword" -ForegroundColor Magenta