<# .SYNOPSIS Bookworm 授权码生成器 (管理员 GUI 工具) .DESCRIPTION 内部调用 node gen-authcode.js, 提供可视化界面生成多用户授权码。 打包命令: 见 build.ps1 -Admin #> # 全局错误捕获 (PS2EXE -NoOutput 会吞掉所有错误, 这里确保弹窗显示) try { $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing [System.Windows.Forms.Application]::EnableVisualStyles() # ─── 路径 ───────────────────────────────────────────── $ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } elseif ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName -match '\.exe$') { Split-Path -Parent ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName) } elseif ($MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path } else { $PWD.Path } # gen-authcode.js / secrets.txt 查找: 当前目录 → 父目录 (dist/ 内运行时) $GenScript = Join-Path $ScriptDir "gen-authcode.js" if (-not (Test-Path $GenScript)) { $parentDir = Split-Path $ScriptDir -Parent $GenScript = Join-Path $parentDir "gen-authcode.js" if (Test-Path $GenScript) { $ScriptDir = $parentDir } # 切到父目录 } $SecretsTxt = Join-Path $ScriptDir "secrets.txt" # ─── 品牌色 ─────────────────────────────────────────── $brandBlue = [System.Drawing.Color]::FromArgb(88, 101, 242) $brandDark = [System.Drawing.Color]::FromArgb(24, 25, 38) $brandLight = [System.Drawing.Color]::FromArgb(245, 246, 250) $cardBg = [System.Drawing.Color]::FromArgb(248, 249, 253) $successGreen = [System.Drawing.Color]::FromArgb(35, 134, 54) $warningOrange = [System.Drawing.Color]::FromArgb(227, 137, 29) $textPrimary = [System.Drawing.Color]::FromArgb(36, 41, 47) $textSecondary = [System.Drawing.Color]::FromArgb(110, 119, 129) $inputBorder = [System.Drawing.Color]::FromArgb(208, 215, 222) # ─── 主窗口 ─────────────────────────────────────────── $form = New-Object System.Windows.Forms.Form $form.Text = "Bookworm 授权码生成器" $form.ClientSize = New-Object System.Drawing.Size(540, 594) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedSingle" $form.MaximizeBox = $false $form.BackColor = [System.Drawing.Color]::White $form.Font = New-Object System.Drawing.Font("Segoe UI", 9) # ─── 标题栏 (深色渐变 + 副标题) ────────────────────── $header = New-Object System.Windows.Forms.Panel $header.Dock = "Top" $header.Size = New-Object System.Drawing.Size(540, 70) $header.BackColor = $brandDark $form.Controls.Add($header) $titleLabel = New-Object System.Windows.Forms.Label $titleLabel.Location = New-Object System.Drawing.Point(24, 12) $titleLabel.Size = New-Object System.Drawing.Size(500, 28) $titleLabel.Text = "Bookworm 授权码生成器" $titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Bold) $titleLabel.ForeColor = [System.Drawing.Color]::White $header.Controls.Add($titleLabel) $subtitleLabel = New-Object System.Windows.Forms.Label $subtitleLabel.Location = New-Object System.Drawing.Point(24, 42) $subtitleLabel.Size = New-Object System.Drawing.Size(500, 18) $subtitleLabel.Text = "管理员专用 · 为每位用户生成独立授权码与加密凭证" $subtitleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 8.5) $subtitleLabel.ForeColor = [System.Drawing.Color]::FromArgb(160, 170, 200) $header.Controls.Add($subtitleLabel) # ═══ 输入卡片 (Panel 模拟卡片) ═══════════════════════ $inputCard = New-Object System.Windows.Forms.Panel $inputCard.Location = New-Object System.Drawing.Point(20, 84) $inputCard.Size = New-Object System.Drawing.Size(500, 186) $inputCard.BackColor = $cardBg $form.Controls.Add($inputCard) # ── 卡片标题 $inputTitle = New-Object System.Windows.Forms.Label $inputTitle.Location = New-Object System.Drawing.Point(16, 10) $inputTitle.Size = New-Object System.Drawing.Size(200, 20) $inputTitle.Text = "用户信息" $inputTitle.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold) $inputTitle.ForeColor = $brandBlue $inputCard.Controls.Add($inputTitle) # ── 用户名 $lblUser = New-Object System.Windows.Forms.Label $lblUser.Location = New-Object System.Drawing.Point(16, 40) $lblUser.Size = New-Object System.Drawing.Size(90, 22) $lblUser.Text = "用户标识" $lblUser.ForeColor = $textPrimary $lblUser.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight $inputCard.Controls.Add($lblUser) $txtUser = New-Object System.Windows.Forms.TextBox $txtUser.Location = New-Object System.Drawing.Point(114, 38) $txtUser.Size = New-Object System.Drawing.Size(370, 26) $txtUser.Font = New-Object System.Drawing.Font("Segoe UI", 10) $txtUser.BorderStyle = "FixedSingle" $inputCard.Controls.Add($txtUser) # ── Relay Key $lblKey = New-Object System.Windows.Forms.Label $lblKey.Location = New-Object System.Drawing.Point(16, 74) $lblKey.Size = New-Object System.Drawing.Size(90, 22) $lblKey.Text = "Relay Key" $lblKey.ForeColor = $textPrimary $lblKey.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight $inputCard.Controls.Add($lblKey) $txtKey = New-Object System.Windows.Forms.TextBox $txtKey.Location = New-Object System.Drawing.Point(114, 72) $txtKey.Size = New-Object System.Drawing.Size(370, 26) $txtKey.Font = New-Object System.Drawing.Font("Segoe UI", 10) $txtKey.PasswordChar = '*' $txtKey.BorderStyle = "FixedSingle" $inputCard.Controls.Add($txtKey) $chkShowKey = New-Object System.Windows.Forms.CheckBox $chkShowKey.Location = New-Object System.Drawing.Point(114, 102) $chkShowKey.Size = New-Object System.Drawing.Size(100, 20) $chkShowKey.Text = "显示 Key" $chkShowKey.ForeColor = $textSecondary $chkShowKey.Font = New-Object System.Drawing.Font("Segoe UI", 8) $chkShowKey.Add_CheckedChanged({ $txtKey.PasswordChar = if ($chkShowKey.Checked) { [char]0 } else { '*' } }) $inputCard.Controls.Add($chkShowKey) # ── 有效期 $lblDays = New-Object System.Windows.Forms.Label $lblDays.Location = New-Object System.Drawing.Point(16, 132) $lblDays.Size = New-Object System.Drawing.Size(90, 22) $lblDays.Text = "有效期" $lblDays.ForeColor = $textPrimary $lblDays.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight $inputCard.Controls.Add($lblDays) $cmbDays = New-Object System.Windows.Forms.ComboBox $cmbDays.Location = New-Object System.Drawing.Point(114, 130) $cmbDays.Size = New-Object System.Drawing.Size(80, 26) $cmbDays.DropDownStyle = "DropDownList" $cmbDays.Font = New-Object System.Drawing.Font("Segoe UI", 10) $null = $cmbDays.Items.AddRange(@(7, 14, 30, 60, 90, 180, 365)) $cmbDays.SelectedIndex = 2 $inputCard.Controls.Add($cmbDays) $lblDaysUnit = New-Object System.Windows.Forms.Label $lblDaysUnit.Location = New-Object System.Drawing.Point(198, 132) $lblDaysUnit.Size = New-Object System.Drawing.Size(30, 22) $lblDaysUnit.Text = "天" $lblDaysUnit.ForeColor = $textSecondary $inputCard.Controls.Add($lblDaysUnit) $lblDaysHint = New-Object System.Windows.Forms.Label $lblDaysHint.Location = New-Object System.Drawing.Point(240, 132) $lblDaysHint.Size = New-Object System.Drawing.Size(240, 22) $lblDaysHint.Text = "" $lblDaysHint.ForeColor = $textSecondary $lblDaysHint.Font = New-Object System.Drawing.Font("Segoe UI", 8.5) $inputCard.Controls.Add($lblDaysHint) $cmbDays.Add_SelectedIndexChanged({ $d = [int]$cmbDays.SelectedItem $lblDaysHint.Text = "-> 到期: " + (Get-Date).AddDays($d).ToString("yyyy-MM-dd") }) $cmbDays.SelectedIndex = 2 # ── 环境状态行 ── $nodeOK = [bool](Get-Command node -ErrorAction SilentlyContinue) $secretsOK = Test-Path $SecretsTxt $lblStatus1 = New-Object System.Windows.Forms.Label $lblStatus1.Location = New-Object System.Drawing.Point(16, 160) $lblStatus1.Size = New-Object System.Drawing.Size(230, 18) $lblStatus1.Font = New-Object System.Drawing.Font("Segoe UI", 8) if ($secretsOK) { $lineCount = (Get-Content $SecretsTxt -ErrorAction SilentlyContinue | Where-Object { $_ -match '=' }).Count $lblStatus1.Text = "secrets.txt: $lineCount 个凭证" $lblStatus1.ForeColor = $successGreen } else { $lblStatus1.Text = "secrets.txt: 未找到" $lblStatus1.ForeColor = [System.Drawing.Color]::Red } $inputCard.Controls.Add($lblStatus1) $lblStatus2 = New-Object System.Windows.Forms.Label $lblStatus2.Location = New-Object System.Drawing.Point(250, 160) $lblStatus2.Size = New-Object System.Drawing.Size(230, 18) $lblStatus2.Font = New-Object System.Drawing.Font("Segoe UI", 8) if ($nodeOK) { $nodeVer = try { (& node --version 2>$null) } catch { "" } $lblStatus2.Text = "Node.js: $nodeVer" $lblStatus2.ForeColor = $successGreen } else { $lblStatus2.Text = "Node.js: 未安装" $lblStatus2.ForeColor = [System.Drawing.Color]::Red } $inputCard.Controls.Add($lblStatus2) # ═══ 操作按钮 ═════════════════════════════════════════ $btnGenerate = New-Object System.Windows.Forms.Button $btnGenerate.Location = New-Object System.Drawing.Point(138, 282) $btnGenerate.Size = New-Object System.Drawing.Size(180, 42) $btnGenerate.Text = " 生成授权码" $btnGenerate.Font = New-Object System.Drawing.Font("Segoe UI", 11, [System.Drawing.FontStyle]::Bold) $btnGenerate.FlatStyle = "Flat" $btnGenerate.BackColor = $brandBlue $btnGenerate.ForeColor = [System.Drawing.Color]::White $btnGenerate.FlatAppearance.BorderSize = 0 $btnGenerate.Cursor = [System.Windows.Forms.Cursors]::Hand $btnGenerate.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter $form.Controls.Add($btnGenerate) $btnClear = New-Object System.Windows.Forms.Button $btnClear.Location = New-Object System.Drawing.Point(330, 282) $btnClear.Size = New-Object System.Drawing.Size(72, 42) $btnClear.Text = "清空" $btnClear.Font = New-Object System.Drawing.Font("Segoe UI", 9) $btnClear.FlatStyle = "Flat" $btnClear.BackColor = [System.Drawing.Color]::White $btnClear.ForeColor = $textSecondary $btnClear.FlatAppearance.BorderColor = $inputBorder $btnClear.FlatAppearance.BorderSize = 1 $form.Controls.Add($btnClear) # ═══ 结果卡片 ═════════════════════════════════════════ $resultCard = New-Object System.Windows.Forms.Panel $resultCard.Location = New-Object System.Drawing.Point(20, 338) $resultCard.Size = New-Object System.Drawing.Size(500, 220) $resultCard.BackColor = $cardBg $form.Controls.Add($resultCard) $lblResultTitle = New-Object System.Windows.Forms.Label $lblResultTitle.Location = New-Object System.Drawing.Point(16, 10) $lblResultTitle.Size = New-Object System.Drawing.Size(200, 20) $lblResultTitle.Text = "生成结果" $lblResultTitle.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold) $lblResultTitle.ForeColor = $brandBlue $resultCard.Controls.Add($lblResultTitle) $txtAuthCode = New-Object System.Windows.Forms.TextBox $txtAuthCode.Location = New-Object System.Drawing.Point(16, 38) $txtAuthCode.Size = New-Object System.Drawing.Size(380, 34) $txtAuthCode.Font = New-Object System.Drawing.Font("Consolas", 13, [System.Drawing.FontStyle]::Bold) $txtAuthCode.ReadOnly = $true $txtAuthCode.BackColor = [System.Drawing.Color]::White $txtAuthCode.ForeColor = $brandDark $txtAuthCode.BorderStyle = "FixedSingle" $txtAuthCode.Text = "" $resultCard.Controls.Add($txtAuthCode) $btnCopy = New-Object System.Windows.Forms.Button $btnCopy.Location = New-Object System.Drawing.Point(404, 38) $btnCopy.Size = New-Object System.Drawing.Size(80, 34) $btnCopy.Text = "复制" $btnCopy.Font = New-Object System.Drawing.Font("Segoe UI", 9) $btnCopy.FlatStyle = "Flat" $btnCopy.BackColor = $brandBlue $btnCopy.ForeColor = [System.Drawing.Color]::White $btnCopy.FlatAppearance.BorderSize = 0 $btnCopy.Cursor = [System.Windows.Forms.Cursors]::Hand $btnCopy.Enabled = $false $resultCard.Controls.Add($btnCopy) $lblDetails = New-Object System.Windows.Forms.Label $lblDetails.Location = New-Object System.Drawing.Point(16, 82) $lblDetails.Size = New-Object System.Drawing.Size(468, 66) $lblDetails.Text = "点击「生成授权码」后,结果将显示在此处。" $lblDetails.ForeColor = $textSecondary $lblDetails.Font = New-Object System.Drawing.Font("Segoe UI", 8.5) $resultCard.Controls.Add($lblDetails) # 推送到 Gitea 按钮 $btnPush = New-Object System.Windows.Forms.Button $btnPush.Location = New-Object System.Drawing.Point(16, 156) $btnPush.Size = New-Object System.Drawing.Size(468, 38) $btnPush.Text = "推送到 Gitea (git add + commit + push)" $btnPush.Font = New-Object System.Drawing.Font("Segoe UI", 10) $btnPush.FlatStyle = "Flat" $btnPush.BackColor = $successGreen $btnPush.ForeColor = [System.Drawing.Color]::White $btnPush.FlatAppearance.BorderSize = 0 $btnPush.Cursor = [System.Windows.Forms.Cursors]::Hand $btnPush.Enabled = $false $resultCard.Controls.Add($btnPush) # ═══ 状态栏 ═══════════════════════════════════════════ $statusBar = New-Object System.Windows.Forms.Label $statusBar.Location = New-Object System.Drawing.Point(0, 568) $statusBar.Size = New-Object System.Drawing.Size(540, 26) $statusBar.BackColor = $brandDark $statusBar.ForeColor = [System.Drawing.Color]::FromArgb(160, 170, 200) $statusBar.Text = " 就绪 | $ScriptDir" $statusBar.Font = New-Object System.Drawing.Font("Segoe UI", 8) $statusBar.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $form.Controls.Add($statusBar) # ─── 事件处理 ───────────────────────────────────────── $btnCopy.Add_Click({ if ($txtAuthCode.Text) { [System.Windows.Forms.Clipboard]::SetText($txtAuthCode.Text) $statusBar.Text = " 已复制到剪贴板" $statusBar.ForeColor = $successGreen } }) $btnPush.Add_Click({ $btnPush.Enabled = $false $btnPush.Text = "推送中..." $statusBar.Text = " git add + commit + push ..." $statusBar.ForeColor = $warningOrange [System.Windows.Forms.Application]::DoEvents() try { $gitExe = (Get-Command git -ErrorAction Stop).Source $userName = if ($global:lastGenUser) { $global:lastGenUser } else { "user" } # git add secrets-*.enc $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $gitExe $psi.Arguments = "add secrets-*.enc" $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 $psi.CreateNoWindow = $true $psi.WorkingDirectory = $ScriptDir $p = [System.Diagnostics.Process]::Start($psi) $p.WaitForExit(15000) # git commit $psi.Arguments = "commit -m `"add user $userName`"" $p = [System.Diagnostics.Process]::Start($psi) $p.StandardOutput.ReadToEnd() | Out-Null $p.WaitForExit(15000) # git push $statusBar.Text = " git push 中 (可能需要几秒)..." [System.Windows.Forms.Application]::DoEvents() $psi.Arguments = "push" $p = [System.Diagnostics.Process]::Start($psi) $pushOut = $p.StandardError.ReadToEnd() # git push 输出在 stderr $p.WaitForExit(30000) if ($p.ExitCode -eq 0) { $btnPush.Text = "已推送" $btnPush.BackColor = [System.Drawing.Color]::FromArgb(200, 220, 200) $btnPush.ForeColor = $successGreen $statusBar.Text = " 推送成功 — 现在可以把授权码发给 $userName" $statusBar.ForeColor = $successGreen } else { throw "git push 失败: $pushOut" } } catch { $btnPush.Text = "推送失败 (点击重试)" $btnPush.BackColor = [System.Drawing.Color]::FromArgb(220, 200, 200) $btnPush.ForeColor = [System.Drawing.Color]::Red $btnPush.Enabled = $true $statusBar.Text = " 推送失败: $_" $statusBar.ForeColor = [System.Drawing.Color]::Red [System.Windows.Forms.MessageBox]::Show("推送失败:`n$_`n`n请检查 Git 配置和网络。", "Git 错误", "OK", "Error") } }) $btnClear.Add_Click({ $txtUser.Text = "" $txtKey.Text = "" $txtAuthCode.Text = "" $lblDetails.Text = "" $btnCopy.Enabled = $false $statusBar.Text = " 已清空" $statusBar.ForeColor = [System.Drawing.Color]::Gray }) $btnGenerate.Add_Click({ # 校验 $user = $txtUser.Text.Trim() $key = $txtKey.Text.Trim() $days = $cmbDays.SelectedItem.ToString() if (-not $user) { [System.Windows.Forms.MessageBox]::Show("请输入用户标识", "缺少用户名", "OK", "Warning") $txtUser.Focus(); return } if (-not $key) { [System.Windows.Forms.MessageBox]::Show("请输入 Relay Sub-Key`n(从中转站后台为用户创建)", "缺少 Sub-Key", "OK", "Warning") $txtKey.Focus(); return } if (-not (Test-Path $SecretsTxt)) { [System.Windows.Forms.MessageBox]::Show("secrets.txt 不存在:`n$SecretsTxt`n`n请先创建 secrets.txt (每行 KEY=VALUE)", "缺少凭证文件", "OK", "Error") return } if (-not $nodeOK) { [System.Windows.Forms.MessageBox]::Show("Node.js 未安装。`n请先安装: https://nodejs.org", "缺少 Node.js", "OK", "Error") return } $statusBar.Text = " 生成中..." $statusBar.ForeColor = $warningOrange $btnGenerate.Enabled = $false [System.Windows.Forms.Application]::DoEvents() try { # 用 Process + UTF8 编码读取 (PS2EXE 默认 GBK 会乱码) $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = (Get-Command node -ErrorAction Stop).Source $psi.Arguments = "`"$GenScript`" $days -k `"$key`" -u `"$user`"" $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 $psi.CreateNoWindow = $true $psi.WorkingDirectory = $ScriptDir $proc = [System.Diagnostics.Process]::Start($psi) $stdout = $proc.StandardOutput.ReadToEnd() $stderr = $proc.StandardError.ReadToEnd() $proc.WaitForExit() if ($proc.ExitCode -ne 0) { throw "gen-authcode.js 退出码 $($proc.ExitCode)`n$stderr" } # 解析输出 (编码无关: PS2EXE 下中文可能乱码, 只匹配 ASCII 格式) $authCode = if ($stdout -match '(BW-\d{8}-[A-F0-9]{24})') { $Matches[1] } else { "" } $fileId = if ($stdout -match '(secrets-[a-f0-9]{8}\.enc)') { $Matches[1] } else { "" } $expiry = if ($stdout -match '(\d{4}-\d{2}-\d{2})') { $Matches[1] } else { "" } if ($authCode) { $txtAuthCode.Text = $authCode $lblDetails.Text = "用户: $user`n加密文件: $fileId`n有效期: $days 天 (至 $expiry)`n路径: $ScriptDir\$fileId" $btnCopy.Enabled = $true $btnPush.Enabled = $true $global:lastGenUser = $user $global:lastGenFile = $fileId $statusBar.Text = " 生成成功 — 点击「推送到 Gitea」完成部署" $statusBar.ForeColor = $successGreen # 追加历史记录 (仅本机, 已在 .gitignore) $historyFile = Join-Path $ScriptDir "authcode-history.log" $logLine = "$(Get-Date -Format 'yyyy-MM-dd HH:mm') $($user.PadRight(12)) $fileId $($days)天 至$expiry $authCode" try { Add-Content -Path $historyFile -Value $logLine -Encoding utf8 } catch {} } else { throw "无法解析授权码输出:`n$stdout" } } catch { $txtAuthCode.Text = "" $lblDetails.Text = "错误: $_" $btnCopy.Enabled = $false $statusBar.Text = " 生成失败" $statusBar.ForeColor = [System.Drawing.Color]::Red [System.Windows.Forms.MessageBox]::Show("生成失败:`n$_", "错误", "OK", "Error") } finally { $btnGenerate.Enabled = $true } }) # ─── 启动 ───────────────────────────────────────────── $form.Add_Shown({ $txtUser.Focus() }) [System.Windows.Forms.Application]::Run($form) } catch { # 全局错误弹窗 (PS2EXE 下唯一的错误可见方式) $errMsg = "Bookworm AuthGen 启动失败:`n`n" + "错误: $($_.Exception.Message)`n" + "行号: $($_.InvocationInfo.ScriptLineNumber)`n" + "代码: $($_.InvocationInfo.Line.Trim())`n`n" + "ScriptDir: $ScriptDir`n" + "GenScript: $GenScript`n" + "SecretsTxt: $SecretsTxt" try { [System.Windows.Forms.MessageBox]::Show($errMsg, "AuthGen 错误", "OK", "Error") } catch { # 如果连 MsgBox 都失败 (WinForms 未加载), 写文件 $errMsg | Out-File "$env:TEMP\bookworm-authgen-error.txt" -Encoding utf8 } }