${label}
+ ${formatNumber(value)} + ${hint} +From 1a9c518f9666fe35691ec9ac2ecaeed3e07210f8 Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:20:34 +0800 Subject: [PATCH] Add secure administrator operations console Provide TOTP-authenticated, role-controlled user and credit workflows with paginated audit data and SQL-backed statistics so operations can manage growth safely. --- .env.example | 11 + .github/workflows/ci.yml | 5 + admin-web/.gitignore | 3 + admin-web/index.html | 14 + admin-web/package-lock.json | 2238 +++++++++++++++++ admin-web/package.json | 18 + admin-web/src/api/client.ts | 232 ++ admin-web/src/api/types.ts | 161 ++ admin-web/src/app.ts | 289 +++ admin-web/src/components/charts.ts | 80 + admin-web/src/components/ui.ts | 66 + admin-web/src/lib/format.ts | 54 + admin-web/src/main.ts | 7 + admin-web/src/pages/audit.ts | 99 + admin-web/src/pages/auth.ts | 83 + admin-web/src/pages/credits.ts | 78 + admin-web/src/pages/overview.ts | 102 + admin-web/src/pages/referrals.ts | 97 + admin-web/src/pages/security.ts | 585 +++++ admin-web/src/pages/users.ts | 454 ++++ admin-web/src/styles.css | 1428 +++++++++++ admin-web/src/test/charts.test.ts | 24 + admin-web/src/test/client.test.ts | 192 ++ admin-web/src/test/format.test.ts | 29 + admin-web/src/test/pages.test.ts | 191 ++ admin-web/tsconfig.json | 19 + admin-web/vite.config.ts | 19 + build.gradle.kts | 61 +- docs/openapi.yaml | 622 ++++- .../kotlin/com/osglab/account/Application.kt | 94 + .../com/osglab/account/config/AppConfig.kt | 91 + .../admin/grants/models/AdminGrantDtos.kt | 27 + .../AdminCreditGrantRepository.kt | 84 + .../grants/services/AdminGrantService.kt | 32 + .../features/admin/models/AdminModels.kt | 189 ++ .../admin/repositories/AdminRepository.kt | 641 +++++ .../features/admin/routes/AdminRoutes.kt | 887 +++++++ .../features/admin/security/AdminCsrf.kt | 20 + .../admin/security/AdminPasswordHasher.kt | 131 + .../features/admin/security/AdminTotp.kt | 185 ++ .../admin/services/AdminAuditService.kt | 99 + .../admin/services/AdminAuthService.kt | 236 ++ .../admin/services/AdminBootstrapService.kt | 65 + .../admin/services/AdminOperatorService.kt | 414 +++ .../admin/services/AdminSessionService.kt | 94 + .../admin/stats/models/AdminStatsDtos.kt | 70 + .../repositories/AdminStatsRepository.kt | 334 +++ .../admin/stats/services/AdminStatsService.kt | 57 + .../admin/users/models/AdminUserDtos.kt | 56 + .../repositories/AdminUsersRepository.kt | 332 +++ .../admin/users/services/AdminUsersService.kt | 135 + .../features/credits/domain/CreditDomain.kt | 18 + .../repositories/BillingRepositories.kt | 4 + .../ExposedBillingTransactionRunner.kt | 16 + .../credits/services/CreditService.kt | 160 +- .../account/tools/AdminCredentialGenerator.kt | 104 + src/main/resources/application.yaml | 9 + .../V8__admin_authentication_core.sql | 107 + .../osglab/account/config/AppConfigTest.kt | 42 + .../features/admin/InMemoryAdminRepository.kt | 426 ++++ .../AdminOperatorRepositoryIntegrationTest.kt | 246 ++ .../features/admin/routes/AdminRoutesTest.kt | 331 +++ .../admin/security/AdminPasswordHasherTest.kt | 37 + .../features/admin/security/AdminTotpTest.kt | 54 + .../admin/services/AdminAuditServiceTest.kt | 87 + .../admin/services/AdminAuthServiceTest.kt | 226 ++ .../services/AdminBootstrapServiceTest.kt | 54 + .../services/AdminOperatorServiceTest.kt | 284 +++ .../admin/services/AdminSessionServiceTest.kt | 131 + .../AdminStatsRepositoryIntegrationTest.kt | 71 + .../admin/stats/AdminStatsRepositoryTest.kt | 162 ++ .../admin/users/AdminUsersServiceTest.kt | 229 ++ .../features/credits/CreditServiceTest.kt | 157 ++ .../features/credits/TestBillingStore.kt | 45 + .../MySqlSecurityIntegrationTest.kt | 42 + .../tools/AdminCredentialGeneratorTest.kt | 29 + 76 files changed, 14602 insertions(+), 3 deletions(-) create mode 100644 admin-web/.gitignore create mode 100644 admin-web/index.html create mode 100644 admin-web/package-lock.json create mode 100644 admin-web/package.json create mode 100644 admin-web/src/api/client.ts create mode 100644 admin-web/src/api/types.ts create mode 100644 admin-web/src/app.ts create mode 100644 admin-web/src/components/charts.ts create mode 100644 admin-web/src/components/ui.ts create mode 100644 admin-web/src/lib/format.ts create mode 100644 admin-web/src/main.ts create mode 100644 admin-web/src/pages/audit.ts create mode 100644 admin-web/src/pages/auth.ts create mode 100644 admin-web/src/pages/credits.ts create mode 100644 admin-web/src/pages/overview.ts create mode 100644 admin-web/src/pages/referrals.ts create mode 100644 admin-web/src/pages/security.ts create mode 100644 admin-web/src/pages/users.ts create mode 100644 admin-web/src/styles.css create mode 100644 admin-web/src/test/charts.test.ts create mode 100644 admin-web/src/test/client.test.ts create mode 100644 admin-web/src/test/format.test.ts create mode 100644 admin-web/src/test/pages.test.ts create mode 100644 admin-web/tsconfig.json create mode 100644 admin-web/vite.config.ts create mode 100644 src/main/kotlin/com/osglab/account/features/admin/grants/models/AdminGrantDtos.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/grants/repositories/AdminCreditGrantRepository.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/grants/services/AdminGrantService.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/models/AdminModels.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/repositories/AdminRepository.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/routes/AdminRoutes.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/security/AdminCsrf.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/security/AdminPasswordHasher.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/security/AdminTotp.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/services/AdminAuditService.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/services/AdminAuthService.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/services/AdminBootstrapService.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/services/AdminOperatorService.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/services/AdminSessionService.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/stats/models/AdminStatsDtos.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/stats/repositories/AdminStatsRepository.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/stats/services/AdminStatsService.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/users/models/AdminUserDtos.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/users/repositories/AdminUsersRepository.kt create mode 100644 src/main/kotlin/com/osglab/account/features/admin/users/services/AdminUsersService.kt create mode 100644 src/main/kotlin/com/osglab/account/tools/AdminCredentialGenerator.kt create mode 100644 src/main/resources/db/migration/V8__admin_authentication_core.sql create mode 100644 src/test/kotlin/com/osglab/account/features/admin/InMemoryAdminRepository.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/repositories/AdminOperatorRepositoryIntegrationTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/routes/AdminRoutesTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/security/AdminPasswordHasherTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/security/AdminTotpTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/services/AdminAuditServiceTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/services/AdminAuthServiceTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/services/AdminBootstrapServiceTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/services/AdminOperatorServiceTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/services/AdminSessionServiceTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/stats/AdminStatsRepositoryIntegrationTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/stats/AdminStatsRepositoryTest.kt create mode 100644 src/test/kotlin/com/osglab/account/features/admin/users/AdminUsersServiceTest.kt create mode 100644 src/test/kotlin/com/osglab/account/tools/AdminCredentialGeneratorTest.kt diff --git a/.env.example b/.env.example index 54abb0a..99aca20 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,17 @@ FIELD_ENCRYPTION_KEY=replace-with-exactly-32-random-bytes-as-base64 IDENTITY_HMAC_KEY=replace-with-a-distinct-32-random-bytes-as-base64 IDENTITY_TOMBSTONE_RETENTION_DAYS=365 +# Admin console. Enable bootstrap for the first successful startup only, then +# set it back to false and remove all four ADMIN_BOOTSTRAP_* credential values. +ADMIN_ENABLED=false +ADMIN_BOOTSTRAP_ENABLED=false +ADMIN_BOOTSTRAP_OPERATOR_ID=replace-with-random-uuid +ADMIN_BOOTSTRAP_USERNAME=owner +ADMIN_BOOTSTRAP_PASSWORD_HASH=replace-with-argon2id-phc-hash +ADMIN_BOOTSTRAP_TOTP_SECRET_BASE32=replace-with-random-base32-secret +ADMIN_SESSION_HOURS=8 +ADMIN_MAXIMUM_MANUAL_GRANT=100000 + # Apple identifiers are not secrets, but use the values from your own developer account. APPLE_TEAM_ID=replace-with-apple-team-id APPLE_KEY_ID=replace-with-apple-key-id diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8068399..670fb25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,11 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: admin-web/package-lock.json - uses: actions/setup-java@v4 with: distribution: temurin diff --git a/admin-web/.gitignore b/admin-web/.gitignore new file mode 100644 index 0000000..3bdd52e --- /dev/null +++ b/admin-web/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.DS_Store diff --git a/admin-web/index.html b/admin-web/index.html new file mode 100644 index 0000000..59999fd --- /dev/null +++ b/admin-web/index.html @@ -0,0 +1,14 @@ + + +
+ + + + +| UTC 日期 | 新增用户 | 消耗积分 |
|---|---|---|
| ${escapeHtml(point.date)} | ${formatNumber(point.registrations)} | ${formatNumber(point.creditsUsed)} |
${escapeHtml(label)}…
+${escapeHtml(message)}
+ ${retry ? '' : ""} +${escapeHtml(message)}
追踪管理员操作、目标和执行结果。
+| 时间 | 操作员 | 操作 | 目标 | 请求 ID | 结果 |
|---|
${escapeHtml(entry.action)}安全、克制、可追溯的运营管理
+使用管理员凭据和认证器动态验证码登录。凭据不会保存到浏览器。
+ + `); + + const form = root.querySelector按内部用户 ID 查询不可变积分流水。
+| 时间 | 流水号 | 类型 | 变动 | 结余 | 原因 |
|---|---|---|---|---|---|
| ${formatDateTime(entry.createdAt)} | +${escapeHtml(entry.entryId)} | +${statusLabel(entry.type)} | +${formatSignedCredits(entry.amount)} | +${formatNumber(entry.balanceAfter)} | +${escapeHtml(entry.reasonCode)} | +
快速掌握增长与积分消耗情况。
+按 UTC 日期统计注册数
按类型统计,不包含用户内容
当前周期暂无使用记录
' + : ` +| 类型 | 请求 | 消耗积分 | 语音毫秒 | 输入 Token | 输出 Token |
|---|---|---|---|---|---|
${escapeHtml(item.kind)} |
+ ${formatNumber(item.requests)} | +${formatNumber(item.chargedCredits)} | +${formatNumber(item.asrMillis)} | +${formatNumber(item.inputTokens)} | +${formatNumber(item.outputTokens)} | +
${label}
+ ${formatNumber(value)} + ${hint} +奖励以有效使用为前提,不以注册量代替真实转化。
+待资格确认
+ ${formatNumber(data.pendingBindings)} + 等待有效使用 +不符合奖励条件
+ ${formatNumber(data.ineligibleBindings)} + 未发放积分 +从分享至有效使用
按有效邀请数排序
| 名次 | 用户 ID | 邀请 | 有效 | 奖励积分 |
|---|---|---|---|---|
| ${index + 1} | +${escapeHtml(item.userId)} | +${formatNumber(item.invited)} | +${formatNumber(item.qualified)} | +${formatNumber(item.creditsEarned)} | +
管理运营人员、角色、登录锁定、会话与双重认证。
+已加载 ${operators.length} 个账户
| 管理员 | 角色 | 状态 | 最近登录 | 操作 |
|---|
请立即把 TOTP 密钥交给对应管理员,并确认已添加到认证器。
+${escapeHtml(provisioning.totpSecret)}
+
+ 按内部用户 ID 精确查询,不展示 Apple 身份标识。
+| 用户 | 状态 | 积分余额 | 注册时间 | 操作 |
|---|
${escapeHtml(user.userId)}
+不可变账本记录
| 时间 | 类型 | 变动 | 结余 | 原因 |
|---|
按使用类型汇总,不包含用户内容
| 类型 | 请求 | 消耗积分 | 语音时长(毫秒) | 输入 Token | 输出 Token |
|---|---|---|---|---|---|
${escapeHtml(item.kind)} |
+ ${formatNumber(item.requests)} | +${formatNumber(item.chargedCredits)} | +${formatNumber(item.asrMillis)} | +${formatNumber(item.inputTokens)} | +${formatNumber(item.outputTokens)} | +