后端迁移后台鉴权与人工兑换

This commit is contained in:
yml
2026-05-21 17:08:37 +08:00
parent 16200b422b
commit 807c0f95e9
3 changed files with 122 additions and 66 deletions
@@ -14,7 +14,23 @@ import { addHours, nowIso } from '../../utils/time.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import { normalizePage, normalizePageSize } from './admin-query-utils.js' import { normalizePage, normalizePageSize } from './admin-query-utils.js'
export async function ensureAdminUsersBootstrapped() { type AdminUserRow = NonNullable<Awaited<ReturnType<typeof getAdminUserById>>>
type JsonObject = Record<string, any>
type AdminSession = {
sessionId: string
userId: number
username: string
role: AdminRole
allowedInventoryGroups: string[]
expiresAt: string
}
type AdminRole = 'admin' | 'operator' | 'support'
type AdminUserStatus = 'active' | 'disabled'
export async function ensureAdminUsersBootstrapped(): Promise<void> {
ensureAdminAuthConfigured() ensureAdminAuthConfigured()
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : [] const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
@@ -44,7 +60,7 @@ export async function ensureAdminUsersBootstrapped() {
} }
} }
export async function loginAdmin(username, password) { export async function loginAdmin(username: unknown, password: unknown): Promise<JsonObject> {
ensureAdminAuthConfigured() ensureAdminAuthConfigured()
const normalizedUsername = String(username || '').trim().toLowerCase() const normalizedUsername = String(username || '').trim().toLowerCase()
@@ -68,7 +84,7 @@ export async function loginAdmin(username, password) {
return createAdminSession(user) return createAdminSession(user)
} }
export async function verifyAdminSessionToken(token) { export async function verifyAdminSessionToken(token: unknown): Promise<AdminSession> {
ensureAdminAuthConfigured() ensureAdminAuthConfigured()
const normalizedToken = String(token || '').trim() const normalizedToken = String(token || '').trim()
@@ -95,7 +111,7 @@ export async function verifyAdminSessionToken(token) {
}) })
} }
let payload = null let payload: JsonObject | null = null
try { try {
payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'))
@@ -133,7 +149,7 @@ export async function verifyAdminSessionToken(token) {
} }
} }
export async function getAdminSessionSummary(token) { export async function getAdminSessionSummary(token: unknown): Promise<JsonObject> {
const session = await verifyAdminSessionToken(token) const session = await verifyAdminSessionToken(token)
return { return {
@@ -148,7 +164,7 @@ export async function getAdminSessionSummary(token) {
} }
} }
export function requireAdminRole(session, allowedRoles) { export function requireAdminRole(session: { role?: string }, allowedRoles: string[]): void {
if (allowedRoles.includes(session.role)) { if (allowedRoles.includes(session.role)) {
return return
} }
@@ -159,7 +175,7 @@ export function requireAdminRole(session, allowedRoles) {
}) })
} }
export async function getAdminUserList(query = {}) { export async function getAdminUserList(query: JsonObject = {}): Promise<JsonObject> {
const page = normalizePage(query.page) const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize) const pageSize = normalizePageSize(query.pageSize)
const { items, total } = await listAdminUsers({ const { items, total } = await listAdminUsers({
@@ -176,7 +192,7 @@ export async function getAdminUserList(query = {}) {
} }
} }
export async function createManagedAdminUser(payload = {}) { export async function createManagedAdminUser(payload: JsonObject = {}): Promise<JsonObject> {
const username = normalizeUsername(payload.username) const username = normalizeUsername(payload.username)
const password = normalizePassword(payload.password) const password = normalizePassword(payload.password)
@@ -219,7 +235,11 @@ export async function createManagedAdminUser(payload = {}) {
} }
} }
export async function updateManagedAdminUserInventoryGroups(userId, payload = {}, session) { export async function updateManagedAdminUserInventoryGroups(
userId: number | string,
payload: JsonObject = {},
session: AdminSession,
): Promise<JsonObject> {
const user = await getRequiredAdminUser(userId) const user = await getRequiredAdminUser(userId)
const inventoryGroupCodes = normalizeInventoryGroupCodes(payload.inventoryGroupCodes) const inventoryGroupCodes = normalizeInventoryGroupCodes(payload.inventoryGroupCodes)
@@ -231,7 +251,11 @@ export async function updateManagedAdminUserInventoryGroups(userId, payload = {}
} }
} }
export async function updateManagedAdminUserRole(userId, payload = {}, session) { export async function updateManagedAdminUserRole(
userId: number | string,
payload: JsonObject = {},
session: AdminSession,
): Promise<JsonObject> {
const user = await getRequiredAdminUser(userId) const user = await getRequiredAdminUser(userId)
const role = normalizeAdminRole(payload.role) const role = normalizeAdminRole(payload.role)
@@ -252,7 +276,11 @@ export async function updateManagedAdminUserRole(userId, payload = {}, session)
} }
} }
export async function updateManagedAdminUserStatus(userId, payload = {}, session) { export async function updateManagedAdminUserStatus(
userId: number | string,
payload: JsonObject = {},
session: AdminSession,
): Promise<JsonObject> {
const user = await getRequiredAdminUser(userId) const user = await getRequiredAdminUser(userId)
const status = normalizeAdminUserStatus(payload.status) const status = normalizeAdminUserStatus(payload.status)
@@ -273,7 +301,7 @@ export async function updateManagedAdminUserStatus(userId, payload = {}, session
} }
} }
export async function resetManagedAdminUserPassword(userId, payload = {}) { export async function resetManagedAdminUserPassword(userId: number | string, payload: JsonObject = {}): Promise<JsonObject> {
const user = await getRequiredAdminUser(userId) const user = await getRequiredAdminUser(userId)
const password = normalizePassword(payload.password) const password = normalizePassword(payload.password)
@@ -295,7 +323,7 @@ export async function resetManagedAdminUserPassword(userId, payload = {}) {
} }
} }
export function ensureAdminAuthConfigured() { export function ensureAdminAuthConfigured(): void {
const sessionSecret = String(runtimeConfig.admin?.sessionSecret || '').trim() const sessionSecret = String(runtimeConfig.admin?.sessionSecret || '').trim()
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : [] const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
@@ -309,7 +337,7 @@ export function ensureAdminAuthConfigured() {
}) })
} }
function createAdminSession(user) { function createAdminSession(user: AdminUserRow): JsonObject {
const issuedAt = nowIso() const issuedAt = nowIso()
const expiresAt = addHours(issuedAt, Number(runtimeConfig.admin.sessionTtlHours || 12)) const expiresAt = addHours(issuedAt, Number(runtimeConfig.admin.sessionTtlHours || 12))
const payload = { const payload = {
@@ -335,13 +363,13 @@ function createAdminSession(user) {
} }
} }
export function hashAdminPassword(password) { export function hashAdminPassword(password: string): string {
const salt = crypto.randomBytes(16).toString('hex') const salt = crypto.randomBytes(16).toString('hex')
const derived = crypto.scryptSync(password, salt, 64).toString('hex') const derived = crypto.scryptSync(password, salt, 64).toString('hex')
return `scrypt$${salt}$${derived}` return `scrypt$${salt}$${derived}`
} }
function verifyAdminPassword(password, storedHash) { function verifyAdminPassword(password: string, storedHash: string): boolean {
const [algorithm, salt, expectedHash] = String(storedHash || '').split('$') const [algorithm, salt, expectedHash] = String(storedHash || '').split('$')
if (algorithm !== 'scrypt' || !salt || !expectedHash) { if (algorithm !== 'scrypt' || !salt || !expectedHash) {
return false return false
@@ -351,14 +379,14 @@ function verifyAdminPassword(password, storedHash) {
return safeCompare(actualHash, expectedHash) return safeCompare(actualHash, expectedHash)
} }
function signPayload(encodedPayload) { function signPayload(encodedPayload: string): string {
return crypto return crypto
.createHmac('sha256', String(runtimeConfig.admin.sessionSecret || '')) .createHmac('sha256', String(runtimeConfig.admin.sessionSecret || ''))
.update(encodedPayload) .update(encodedPayload)
.digest('base64url') .digest('base64url')
} }
export function normalizeAdminRole(role) { export function normalizeAdminRole(role: unknown): AdminRole {
const normalized = String(role || '').trim().toLowerCase() const normalized = String(role || '').trim().toLowerCase()
if (normalized === 'admin') { if (normalized === 'admin') {
@@ -372,11 +400,11 @@ export function normalizeAdminRole(role) {
return 'operator' return 'operator'
} }
export function normalizeAdminUserStatus(status) { export function normalizeAdminUserStatus(status: unknown): AdminUserStatus {
return String(status || '').trim().toLowerCase() === 'disabled' ? 'disabled' : 'active' return String(status || '').trim().toLowerCase() === 'disabled' ? 'disabled' : 'active'
} }
function safeCompare(input, expected) { function safeCompare(input: unknown, expected: unknown): boolean {
const left = Buffer.from(String(input || ''), 'utf8') const left = Buffer.from(String(input || ''), 'utf8')
const right = Buffer.from(String(expected || ''), 'utf8') const right = Buffer.from(String(expected || ''), 'utf8')
@@ -387,25 +415,25 @@ function safeCompare(input, expected) {
return crypto.timingSafeEqual(left, right) return crypto.timingSafeEqual(left, right)
} }
function normalizeRoleQuery(role) { function normalizeRoleQuery(role: unknown): string {
const normalized = String(role || '').trim().toLowerCase() const normalized = String(role || '').trim().toLowerCase()
return ['admin', 'operator', 'support'].includes(normalized) ? normalized : '' return ['admin', 'operator', 'support'].includes(normalized) ? normalized : ''
} }
function normalizeStatusQuery(status) { function normalizeStatusQuery(status: unknown): string {
const normalized = String(status || '').trim().toLowerCase() const normalized = String(status || '').trim().toLowerCase()
return ['active', 'disabled'].includes(normalized) ? normalized : '' return ['active', 'disabled'].includes(normalized) ? normalized : ''
} }
function normalizeUsername(username) { function normalizeUsername(username: unknown): string {
return String(username || '').trim().toLowerCase() return String(username || '').trim().toLowerCase()
} }
function normalizePassword(password) { function normalizePassword(password: unknown): string {
return String(password || '').trim() return String(password || '').trim()
} }
function validateUsername(username) { function validateUsername(username: string): void {
if (!/^[a-zA-Z0-9._-]{3,32}$/.test(username)) { if (!/^[a-zA-Z0-9._-]{3,32}$/.test(username)) {
throw createHttpError('后台账号格式无效,需为 3-32 位字母数字或 ._-', { throw createHttpError('后台账号格式无效,需为 3-32 位字母数字或 ._-', {
statusCode: 400, statusCode: 400,
@@ -414,7 +442,7 @@ function validateUsername(username) {
} }
} }
function validatePassword(password) { function validatePassword(password: string): void {
if (password.length < 8) { if (password.length < 8) {
throw createHttpError('后台密码至少 8 位', { throw createHttpError('后台密码至少 8 位', {
statusCode: 400, statusCode: 400,
@@ -423,7 +451,7 @@ function validatePassword(password) {
} }
} }
async function getRequiredAdminUser(userId) { async function getRequiredAdminUser(userId: number | string): Promise<AdminUserRow> {
const user = await getAdminUserById(Number(userId)) const user = await getAdminUserById(Number(userId))
if (!user) { if (!user) {
@@ -436,7 +464,11 @@ async function getRequiredAdminUser(userId) {
return user return user
} }
async function ensureAdminUserChangeAllowed(user, options = {}, session) { async function ensureAdminUserChangeAllowed(
user: AdminUserRow,
options: { nextRole?: AdminRole, nextStatus?: AdminUserStatus } = {},
session: AdminSession,
): Promise<void> {
const nextRole = options.nextRole || user.role const nextRole = options.nextRole || user.role
const nextStatus = options.nextStatus || user.status const nextStatus = options.nextStatus || user.status
@@ -455,7 +487,7 @@ async function ensureAdminUserChangeAllowed(user, options = {}, session) {
} }
} }
function mapAdminUser(user) { function mapAdminUser(user: AdminUserRow | null): JsonObject {
return { return {
userId: Number(user.id), userId: Number(user.id),
username: String(user.username || ''), username: String(user.username || ''),
@@ -467,7 +499,7 @@ function mapAdminUser(user) {
} }
} }
function normalizeInventoryGroupCodes(values) { function normalizeInventoryGroupCodes(values: unknown): string[] {
return Array.from(new Set((Array.isArray(values) ? values : []) return Array.from(new Set((Array.isArray(values) ? values : [])
.map((value) => String(value || '').trim()) .map((value) => String(value || '').trim())
.filter(Boolean))) .filter(Boolean)))
@@ -21,21 +21,24 @@ import { randomId } from '../../utils/random.js'
import { closeAdminTask, confirmAdminTaskAssistedRole, redeemAdminTaskAssisted } from './admin-write-service.js' import { closeAdminTask, confirmAdminTaskAssistedRole, redeemAdminTaskAssisted } from './admin-write-service.js'
import { createAdminViewerContext, isAssistedClaimTask, parseTaskContext } from './admin-read-shared-helpers.js' import { createAdminViewerContext, isAssistedClaimTask, parseTaskContext } from './admin-read-shared-helpers.js'
import type {
AdminEntityIdInput,
AdminViewerSessionInput,
} from '../../types/admin-read-inputs.js'
import type { AdminManualRedeemCreateInput } from '../../types/admin-write-inputs.js'
import type { TaskRow } from '../../types/repository-rows.js'
const MANUAL_PROVIDER = 'manual' const MANUAL_PROVIDER = 'manual'
const MANUAL_PLATFORM = 'manual_redeem' const MANUAL_PLATFORM = 'manual_redeem'
const MANUAL_SOURCE_TYPE = 'admin_manual_redeem' const MANUAL_SOURCE_TYPE = 'admin_manual_redeem'
const ASSISTED_PROFILE_KEY = 'tencent_claim_assisted' const ASSISTED_PROFILE_KEY = 'tencent_claim_assisted'
/** @typedef {import('../../types/admin-read-inputs.js').AdminEntityIdInput} AdminEntityIdInput */ type JsonObject = Record<string, any>
/** @typedef {import('../../types/admin-read-inputs.js').AdminViewerSessionInput} AdminViewerSessionInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateInput */
/** @param {AdminManualRedeemCreateInput} [payload] */
/** @param {AdminViewerSessionInput | null} [session] */
export async function createAdminManualRedeemTask( export async function createAdminManualRedeemTask(
payload = /** @type {AdminManualRedeemCreateInput} */ ({}), payload: AdminManualRedeemCreateInput = {},
session = null, session: AdminViewerSessionInput | null = null,
) { ): Promise<JsonObject> {
const viewerContext = createAdminViewerContext(session) const viewerContext = createAdminViewerContext(session)
const proofValue = normalizeManualProofValue(payload.proofValue) const proofValue = normalizeManualProofValue(payload.proofValue)
const skuCode = String(payload.skuCode || '').trim() const skuCode = String(payload.skuCode || '').trim()
@@ -285,72 +288,83 @@ export async function createAdminManualRedeemTask(
return getAdminManualRedeemDetail(createdTask.id, session) return getAdminManualRedeemDetail(createdTask.id, session)
} }
/** @param {AdminEntityIdInput} taskId */ export async function getAdminManualRedeemDetail(
/** @param {AdminViewerSessionInput | null} [session] */ taskId: AdminEntityIdInput,
export async function getAdminManualRedeemDetail(taskId, session = null) { session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session) const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await getClaimDetailForAdminTask(task.id) const detail = await getClaimDetailForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail) return decorateManualRedeemDetail(task, detail)
} }
/** @param {AdminEntityIdInput} taskId */ export async function createAdminManualRedeemSession(
/** @param {{ loginType?: string, forceRecreate?: boolean }} [payload] */ taskId: AdminEntityIdInput,
/** @param {AdminViewerSessionInput | null} [session] */ payload: { loginType?: string, forceRecreate?: boolean } = {},
export async function createAdminManualRedeemSession(taskId, payload = {}, session = null) { session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session) const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await createClaimSessionForAdminTask(task.id, payload) const detail = await createClaimSessionForAdminTask(task.id, payload)
return decorateManualRedeemDetail(task, detail) return decorateManualRedeemDetail(task, detail)
} }
/** @param {AdminEntityIdInput} taskId */ export async function getAdminManualRedeemSessionSummary(
/** @param {AdminViewerSessionInput | null} [session] */ taskId: AdminEntityIdInput,
export async function getAdminManualRedeemSessionSummary(taskId, session = null) { session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session) const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await getClaimSessionSummaryForAdminTask(task.id) const detail = await getClaimSessionSummaryForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail) return decorateManualRedeemDetail(task, detail)
} }
/** @param {AdminEntityIdInput} taskId */ export async function reloadAdminManualRedeemSession(
/** @param {AdminViewerSessionInput | null} [session] */ taskId: AdminEntityIdInput,
export async function reloadAdminManualRedeemSession(taskId, session = null) { session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session) const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await reloadClaimSessionForAdminTask(task.id) const detail = await reloadClaimSessionForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail) return decorateManualRedeemDetail(task, detail)
} }
/** @param {AdminEntityIdInput} taskId */ export async function closeAdminManualRedeemSession(
/** @param {AdminViewerSessionInput | null} [session] */ taskId: AdminEntityIdInput,
export async function closeAdminManualRedeemSession(taskId, session = null) { session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session) const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await closeClaimSessionForAdminTask(task.id) const detail = await closeClaimSessionForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail) return decorateManualRedeemDetail(task, detail)
} }
/** @param {AdminEntityIdInput} taskId */ export async function closeAdminManualRedeemTask(
/** @param {AdminViewerSessionInput | null} [session] */ taskId: AdminEntityIdInput,
export async function closeAdminManualRedeemTask(taskId, session = null) { session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session) const task = await getRequiredManualRedeemTask(taskId, session)
await closeAdminTask(task.id) await closeAdminTask(task.id)
return getAdminManualRedeemDetail(task.id, session) return getAdminManualRedeemDetail(task.id, session)
} }
/** @param {AdminEntityIdInput} taskId */ export async function confirmAdminManualRedeemRole(
/** @param {AdminViewerSessionInput | null} [session] */ taskId: AdminEntityIdInput,
export async function confirmAdminManualRedeemRole(taskId, session = null) { session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session) const task = await getRequiredManualRedeemTask(taskId, session)
await confirmAdminTaskAssistedRole(task.id, session) await confirmAdminTaskAssistedRole(task.id, session)
return getAdminManualRedeemDetail(task.id, session) return getAdminManualRedeemDetail(task.id, session)
} }
/** @param {AdminEntityIdInput} taskId */ export async function redeemAdminManualRedeemTask(
/** @param {AdminViewerSessionInput | null} [session] */ taskId: AdminEntityIdInput,
export async function redeemAdminManualRedeemTask(taskId, session = null) { session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session) const task = await getRequiredManualRedeemTask(taskId, session)
await redeemAdminTaskAssisted(task.id, session) await redeemAdminTaskAssisted(task.id, session)
return getAdminManualRedeemDetail(task.id, session) return getAdminManualRedeemDetail(task.id, session)
} }
async function getRequiredManualRedeemTask(taskId, session = null) { async function getRequiredManualRedeemTask(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<TaskRow> {
const task = await getTaskById(Number(taskId)) const task = await getTaskById(Number(taskId))
if (!task) { if (!task) {
@@ -392,7 +406,7 @@ async function getRequiredManualRedeemTask(taskId, session = null) {
return task return task
} }
function decorateManualRedeemDetail(task, detail) { function decorateManualRedeemDetail(task: TaskRow, detail: any): JsonObject {
const taskContext = parseTaskContext(task) const taskContext = parseTaskContext(task)
const manualRedeem = taskContext.manualRedeem && typeof taskContext.manualRedeem === 'object' const manualRedeem = taskContext.manualRedeem && typeof taskContext.manualRedeem === 'object'
? taskContext.manualRedeem ? taskContext.manualRedeem
@@ -415,6 +429,6 @@ function decorateManualRedeemDetail(task, detail) {
} }
} }
function normalizeManualProofValue(value) { function normalizeManualProofValue(value: unknown): string {
return String(value || '').replace(/\s+/g, ' ').trim() return String(value || '').replace(/\s+/g, ' ').trim()
} }
+10
View File
@@ -785,6 +785,16 @@
- `npm run typecheck` - `npm run typecheck`
- `npm run build` - `npm run build`
- `npm test` 共 139 个用例通过 - `npm test` 共 139 个用例通过
201. 后台 Admin 鉴权与人工兑换服务迁移到 `.ts`
- `src/services/admin/admin-auth-service.ts`
- `src/services/admin/admin-manual-redeem-service.ts`
202. 后台默认用户初始化、登录 / session token 验证、用户管理、角色 / 状态归一化、人工兑换任务创建、人工兑换 claim session 包装、确认角色 / 兑换 / 关闭操作已进入 TS 编译链路;admin user row、session、role/status、manual redeem input、entity id、viewer session 与动态详情响应补齐类型
203. Docker 内验证通过:
- `src/services/admin/*.test.js` 共 8 个用例通过
- `npm run typecheck`
- `npm run build`
- `npm test` 共 139 个用例通过
204. `src/services/admin` 目录下非测试 `.js` 服务文件已全部迁移为 `.ts`
## 下一步建议 ## 下一步建议