Path A: strip admin authcode generator from public boot repo
Remove gen-authcode.js, admin-authcode-gui.ps1, admin-authcode.ico (authcode-generation source + admin GUI tooling) from the public boot repo. Authcode minting is now server-side only (issuer container /app/admin/gen-authcode.js). build.ps1 -Admin packaging path removed accordingly; Setup.exe path unchanged. HEAD-only removal (git history retained; stripped content carries no plaintext secret, authorization protected by server-side pepper). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0e5a0d9921
commit
5b2dcecaa8
@ -1,482 +0,0 @@
|
||||
<#
|
||||
.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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
64
build.ps1
64
build.ps1
@ -3,25 +3,23 @@
|
||||
Bookworm Portable — 打包工具 (管理员使用)
|
||||
.DESCRIPTION
|
||||
将 auto-setup.ps1 打包为 Bookworm-Setup.exe (PS2EXE)
|
||||
将 gen-authcode.js 打包为 gen-authcode.exe (pkg)
|
||||
输出到 dist\ 目录
|
||||
.USAGE
|
||||
.\build.ps1 # 打包两个
|
||||
.\build.ps1 -Setup # 只打包用户安装器
|
||||
.\build.ps1 -Admin # 只打包管理员工具
|
||||
.\build.ps1 # 打包用户安装器 Bookworm-Setup.exe
|
||||
.NOTES
|
||||
[Path-A 2026-06-30] admin 授权码工具 (gen-authcode.js / admin-authcode-gui.ps1) 已从公开 boot 仓剥离;
|
||||
授权码生成改由服务器端 issuer 容器 /app/admin/gen-authcode.js 负责, -Admin 打包路径已移除。
|
||||
#>
|
||||
param(
|
||||
[switch]$Setup, # 只打 Bookworm-Setup.exe
|
||||
[switch]$Admin # 只打 gen-authcode.exe
|
||||
[switch]$Setup # 保留参数兼容旧调用; 当前仅产出 Bookworm-Setup.exe
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$DistDir = Join-Path $ScriptDir "dist"
|
||||
|
||||
# 默认两个都打
|
||||
$buildSetup = $Setup -or (-not $Setup -and -not $Admin)
|
||||
$buildAdmin = $Admin -or (-not $Setup -and -not $Admin)
|
||||
# 仅产出用户安装器 (admin 授权码工具已剥离, 见 .NOTES)
|
||||
$buildSetup = $true
|
||||
|
||||
function Write-Step($msg) {
|
||||
Write-Host ""
|
||||
@ -148,53 +146,6 @@ if ($buildSetup) {
|
||||
}
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 2. Bookworm-AuthGen.exe (admin-authcode-gui.ps1 → PS2EXE)
|
||||
# ════════════════════════════════════════════════════════
|
||||
if ($buildAdmin) {
|
||||
Write-Step "打包 Bookworm-AuthGen.exe (PS2EXE GUI)"
|
||||
|
||||
$inputPs1 = Join-Path $ScriptDir "admin-authcode-gui.ps1"
|
||||
$outputExe = Join-Path $DistDir "Bookworm-AuthGen.exe"
|
||||
|
||||
if (-not (Test-Path $inputPs1)) {
|
||||
Write-Fail "找不到 admin-authcode-gui.ps1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host " 输入: $inputPs1" -ForegroundColor Gray
|
||||
Write-Host " 输出: $outputExe" -ForegroundColor Gray
|
||||
|
||||
# 优先用书虫学者图标, 回退到 B 圆
|
||||
$adminIcon = Join-Path $ScriptDir "admin-authcode.ico"
|
||||
if (-not (Test-Path $adminIcon)) { $adminIcon = Join-Path $ScriptDir "bookworm-desktop.ico" }
|
||||
|
||||
$ps2exeArgs = @{
|
||||
InputFile = $inputPs1
|
||||
OutputFile = $outputExe
|
||||
Title = "Bookworm AuthCode Generator"
|
||||
Description = "Bookworm 授权码生成器 (管理员工具)"
|
||||
Company = "Bookworm"
|
||||
Version = "1.5.1.0"
|
||||
NoConsole = $true
|
||||
NoOutput = $true
|
||||
NoError = $true
|
||||
}
|
||||
if (Test-Path $adminIcon) {
|
||||
$ps2exeArgs.IconFile = $adminIcon
|
||||
Write-Host " 图标: $adminIcon" -ForegroundColor Gray
|
||||
}
|
||||
Invoke-ps2exe @ps2exeArgs
|
||||
|
||||
if (Test-Path $outputExe) {
|
||||
$sizeKB = [math]::Round((Get-Item $outputExe).Length / 1KB)
|
||||
Write-OK "Bookworm-AuthGen.exe 打包完成 (${sizeKB} KB)"
|
||||
} else {
|
||||
Write-Fail "Bookworm-AuthGen.exe 打包失败"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 完成
|
||||
# ════════════════════════════════════════════════════════
|
||||
@ -226,5 +177,4 @@ Get-ChildItem $DistDir | ForEach-Object {
|
||||
Write-Host ""
|
||||
Write-Host " 分发说明:" -ForegroundColor Gray
|
||||
Write-Host " Bookworm-Setup.exe → 用户安装器 (公开下载)" -ForegroundColor Gray
|
||||
Write-Host " Bookworm-AuthGen.exe → 管理员授权码生成器 (勿对外分发)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
166
gen-authcode.js
166
gen-authcode.js
@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
/**
|
||||
* Bookworm 授权码生成工具 (管理员使用)
|
||||
*
|
||||
* 用法:
|
||||
* node gen-authcode.js <days> [选项]
|
||||
*
|
||||
* 选项:
|
||||
* --relay-key, -k <key> 中转站限额子 Key (替换 ANTHROPIC_API_KEY)
|
||||
* --user, -u <name> 用户标识 (仅用于显示, 不影响加密)
|
||||
* [secrets.txt路径] 明文凭证文件 (默认: ./secrets.txt)
|
||||
*
|
||||
* 示例:
|
||||
* node gen-authcode.js 30 # 共享 Key (单用户)
|
||||
* node gen-authcode.js 30 --relay-key sk-relay-xxx # 独立限额 Key
|
||||
* node gen-authcode.js 90 -k sk-relay-xxx -u alice # 指定用户名
|
||||
*
|
||||
* 原理:
|
||||
* 1. 生成随机 24位Hex Token (96bit 熵)
|
||||
* 2. Token 前8位 = 文件 ID → 输出 secrets-XXXXXXXX.enc (多用户模式)
|
||||
* 无 --relay-key 时 → 输出 secrets.enc (单用户/共享模式)
|
||||
* 3. 授权码 BW-YYYYMMDD-TOKEN (发给用户)
|
||||
* 4. 安全说明: Token = 解密密钥, YYYYMMDD = 客户端到期校验
|
||||
*/
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ALGO = 'aes-256-cbc';
|
||||
const ITERATIONS = 600000;
|
||||
const DIGEST = 'sha256';
|
||||
const SALT_LEN = 16;
|
||||
const KEY_LEN = 32;
|
||||
const IV_LEN = 16;
|
||||
|
||||
function deriveKey(password, salt) {
|
||||
return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN + IV_LEN, DIGEST);
|
||||
}
|
||||
|
||||
function encrypt(plaintext, password) {
|
||||
const salt = crypto.randomBytes(SALT_LEN);
|
||||
const derived = deriveKey(password, salt);
|
||||
const key = derived.slice(0, KEY_LEN);
|
||||
const iv = derived.slice(KEY_LEN, KEY_LEN + IV_LEN);
|
||||
const cipher = crypto.createCipheriv(ALGO, key, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
||||
return Buffer.concat([Buffer.from('BWENC1'), salt, encrypted]);
|
||||
}
|
||||
|
||||
// ─── 解析 CLI 参数 ───────────────────────────────────────
|
||||
const rawArgs = process.argv.slice(2);
|
||||
const DAYS = parseInt(rawArgs[0]);
|
||||
|
||||
if (!DAYS || DAYS < 1 || DAYS > 3650) {
|
||||
console.error('用法: node gen-authcode.js <有效天数> [选项]');
|
||||
console.error('选项:');
|
||||
console.error(' --relay-key, -k <key> 中转站限额子 Key');
|
||||
console.error(' --user, -u <name> 用户标识 (仅显示)');
|
||||
console.error(' [secrets.txt路径] 默认: ./secrets.txt');
|
||||
console.error('');
|
||||
console.error('示例:');
|
||||
console.error(' node gen-authcode.js 30 # 共享模式');
|
||||
console.error(' node gen-authcode.js 30 -k sk-relay-xxx -u alice # 独立限额');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let relayKey = null;
|
||||
let userName = null;
|
||||
let secretsTxtArg = null;
|
||||
|
||||
for (let i = 1; i < rawArgs.length; i++) {
|
||||
const a = rawArgs[i];
|
||||
if (a === '--relay-key' || a === '-k') { relayKey = rawArgs[++i]; }
|
||||
else if (a === '--user' || a === '-u') { userName = rawArgs[++i]; }
|
||||
else if (!a.startsWith('-')) { secretsTxtArg = a; }
|
||||
}
|
||||
|
||||
// pkg 打包后 __filename 指向虚拟快照路径,需用 process.execPath 获取真实目录
|
||||
const SCRIPT_DIR = path.dirname(
|
||||
typeof process.pkg !== 'undefined' ? process.execPath : path.resolve(__filename)
|
||||
);
|
||||
const SECRETS_TXT = secretsTxtArg || path.join(SCRIPT_DIR, 'secrets.txt');
|
||||
|
||||
if (!fs.existsSync(SECRETS_TXT)) {
|
||||
console.error(`[错误] 找不到 secrets.txt: ${SECRETS_TXT}`);
|
||||
console.error('请先创建 secrets.txt, 每行格式: KEY=VALUE');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (relayKey && !/^[A-Za-z0-9\-_.]+$/.test(relayKey)) {
|
||||
console.error('[错误] --relay-key 格式不合法');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ─── 生成到期日 ──────────────────────────────────────────
|
||||
const expiry = new Date();
|
||||
expiry.setDate(expiry.getDate() + DAYS);
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
const expiryStr = `${expiry.getFullYear()}${pad(expiry.getMonth()+1)}${pad(expiry.getDate())}`;
|
||||
const expiryDisplay = `${expiryStr.slice(0,4)}-${expiryStr.slice(4,6)}-${expiryStr.slice(6,8)}`;
|
||||
|
||||
// ─── 生成随机 Token ──────────────────────────────────────
|
||||
const token = crypto.randomBytes(12).toString('hex'); // 小写 24位
|
||||
const authCode = `BW-${expiryStr}-${token.toUpperCase()}`;
|
||||
const fileId = token.slice(0, 8); // 前8位作为文件 ID
|
||||
|
||||
// ─── 构建待加密内容 ──────────────────────────────────────
|
||||
let secretsPlain = fs.readFileSync(SECRETS_TXT, 'utf8').trim();
|
||||
|
||||
const multiUser = !!relayKey;
|
||||
|
||||
if (multiUser) {
|
||||
// 用中转站 relay key 替换 ANTHROPIC_API_KEY
|
||||
if (/^ANTHROPIC_API_KEY=/m.test(secretsPlain)) {
|
||||
secretsPlain = secretsPlain.replace(
|
||||
/^ANTHROPIC_API_KEY=.*/m,
|
||||
`ANTHROPIC_API_KEY=${relayKey}`
|
||||
);
|
||||
} else {
|
||||
secretsPlain = `ANTHROPIC_API_KEY=${relayKey}\n${secretsPlain}`;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 加密并写出 ──────────────────────────────────────────
|
||||
const encBuffer = encrypt(secretsPlain, token);
|
||||
|
||||
let outFileName, outFilePath;
|
||||
if (multiUser) {
|
||||
outFileName = `secrets-${fileId}.enc`;
|
||||
} else {
|
||||
outFileName = 'secrets.enc';
|
||||
}
|
||||
outFilePath = path.join(SCRIPT_DIR, outFileName);
|
||||
fs.writeFileSync(outFilePath, encBuffer);
|
||||
|
||||
// ─── 输出 ────────────────────────────────────────────────
|
||||
const modeLabel = multiUser
|
||||
? `多用户独立 Key (${userName || '未命名'})`
|
||||
: '共享 Key (单/多用户共享)';
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════');
|
||||
console.log(' Bookworm 授权码生成完毕');
|
||||
console.log('═══════════════════════════════════════════════════');
|
||||
console.log('');
|
||||
console.log(` 模式: ${modeLabel}`);
|
||||
if (userName) console.log(` 用户: ${userName}`);
|
||||
console.log(` 授权码: ${authCode}`);
|
||||
console.log(` 有效期: ${DAYS} 天 (至 ${expiryDisplay})`);
|
||||
if (multiUser) {
|
||||
console.log(` Relay Key: ${relayKey.slice(0,12)}... (已替换 ANTHROPIC_API_KEY)`);
|
||||
console.log(` 文件 ID: ${fileId} → ${outFileName}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(' ▶ 操作步骤:');
|
||||
console.log(` 1. 将授权码发给用户: ${authCode}`);
|
||||
console.log(` 2. 推送 ${outFileName} 到 Gitea:`);
|
||||
console.log(` git add ${outFileName} && git commit -m "add user ${userName || fileId}" && git push`);
|
||||
console.log('');
|
||||
console.log(' ⚠ 安全提醒:');
|
||||
console.log(' - 授权码即解密密钥, 请勿通过不安全渠道明文发送');
|
||||
console.log(` - 到期日 (${expiryDisplay}) 后自动失效`);
|
||||
if (multiUser) {
|
||||
console.log(` - 各用户 secrets-XXXXXXXX.enc 独立, 轮换互不影响`);
|
||||
}
|
||||
console.log('═══════════════════════════════════════════════════\n');
|
||||
Loading…
Reference in New Issue
Block a user