param( [string]$ApiBase = "http://192.168.3.90:8001/api/v1", [string]$AdminUsername = "admin@example.com", [string]$ArtifactRoot = "" ) $ErrorActionPreference = "Stop" $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") if (-not $ArtifactRoot) { $ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase4-password-rotation" } $timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss" $artifactDir = Join-Path $ArtifactRoot $timestamp $summaryJsonPath = Join-Path $artifactDir "admin-password-rotation-summary.json" $summaryMdPath = Join-Path $artifactDir "admin-password-rotation-summary.md" New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null function Convert-SecureStringToPlainText { param([Security.SecureString]$SecureValue) $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureValue) try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) } } function Invoke-JsonPost { param( [string]$Url, [hashtable]$Payload ) $body = $Payload | ConvertTo-Json -Depth 8 Invoke-RestMethod -Uri $Url -Method Post -ContentType "application/json; charset=utf-8" -Body $body } function Test-Login { param([string]$Password) try { $response = Invoke-JsonPost -Url "$ApiBase/auth/login" -Payload @{ account = $AdminUsername; password = $Password } return @{ ok = $response.code -eq 0 response = $response status = 200 } } catch { $status = $null if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode } return @{ ok = $false response = $null status = $status } } } function Write-Summary { param([hashtable]$Summary) $Summary | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 -Path $summaryJsonPath @( "# Admin Password Rotation", "", "- API: ``$($Summary.api_base)``", "- Admin: ``$($Summary.admin_username)``", "- Status: $($Summary.status)", "- Old password rejected: $($Summary.old_password_rejected)", "- New password login: $($Summary.new_password_login_pass)", "- Readiness: $($Summary.readiness_status)", "- Artifact: ``$artifactDir``", "" ) | Set-Content -Encoding UTF8 -Path $summaryMdPath } try { Write-Host "AI合同平台管理员密码轮换" -ForegroundColor Cyan Write-Host "API: $ApiBase" Write-Host "管理员账号: $AdminUsername" Write-Host "密码不会写入日志或产物。" -ForegroundColor Yellow $currentPasswordSecure = Read-Host "请输入当前管理员密码" -AsSecureString $newPasswordSecure = Read-Host "请输入新管理员密码" -AsSecureString $confirmPasswordSecure = Read-Host "请再次输入新管理员密码" -AsSecureString $currentPassword = Convert-SecureStringToPlainText $currentPasswordSecure $newPassword = Convert-SecureStringToPlainText $newPasswordSecure $confirmPassword = Convert-SecureStringToPlainText $confirmPasswordSecure if ($newPassword.Length -lt 6) { throw "新密码至少需要 6 位。" } if ($newPassword -eq "123456") { throw "新密码不能继续使用默认验收密码 123456。" } if ($newPassword -ne $confirmPassword) { throw "两次输入的新密码不一致。" } $currentLogin = Test-Login -Password $currentPassword if (-not $currentLogin.ok) { throw "当前管理员密码校验失败,未执行轮换。" } $token = $currentLogin.response.data.access_token $displayName = $currentLogin.response.data.user.display_name $headers = @{ Authorization = "Bearer $token" } $updateBody = @{ display_name = $displayName password = $newPassword } $updateResponse = Invoke-RestMethod -Uri "$ApiBase/auth/me" -Method Put -Headers $headers -ContentType "application/x-www-form-urlencoded" -Body $updateBody if ($updateResponse.code -ne 0) { throw "管理员密码更新失败。" } $oldPasswordLogin = Test-Login -Password $currentPassword $newPasswordLogin = Test-Login -Password $newPassword if ($oldPasswordLogin.ok) { throw "旧密码仍可登录,轮换未达到安全闭环。" } if (-not $newPasswordLogin.ok) { throw "新密码无法登录,轮换后验证失败。" } $readiness = Invoke-RestMethod -Uri "$ApiBase/system/readiness" -Method Get $summary = @{ status = "PASS" api_base = $ApiBase admin_username = $AdminUsername rotated_at = (Get-Date).ToString("o") old_password_rejected = -not $oldPasswordLogin.ok old_password_reject_status = $oldPasswordLogin.status new_password_login_pass = $newPasswordLogin.ok readiness_status = $readiness.data.status readiness_warnings = $readiness.data.summary.warnings artifact_dir = $artifactDir } Write-Summary -Summary $summary Write-Host "ADMIN_PASSWORD_ROTATION_PASS $artifactDir" -ForegroundColor Green } catch { $summary = @{ status = "FAIL" api_base = $ApiBase admin_username = $AdminUsername rotated_at = (Get-Date).ToString("o") error = $_.Exception.Message artifact_dir = $artifactDir } Write-Summary -Summary $summary Write-Host "ADMIN_PASSWORD_ROTATION_FAIL $($_.Exception.Message)" -ForegroundColor Red exit 1 } finally { if ($currentPassword) { $currentPassword = $null } if ($newPassword) { $newPassword = $null } if ($confirmPassword) { $confirmPassword = $null } Read-Host "按 Enter 关闭窗口" }