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

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 { 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()
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()
const normalizedUsername = String(username || '').trim().toLowerCase()
@@ -68,7 +84,7 @@ export async function loginAdmin(username, password) {
return createAdminSession(user)
}
export async function verifyAdminSessionToken(token) {
export async function verifyAdminSessionToken(token: unknown): Promise<AdminSession> {
ensureAdminAuthConfigured()
const normalizedToken = String(token || '').trim()
@@ -95,7 +111,7 @@ export async function verifyAdminSessionToken(token) {
})
}
let payload = null
let payload: JsonObject | null = null
try {
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)
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)) {
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 pageSize = normalizePageSize(query.pageSize)
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 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 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 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 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 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 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 expiresAt = addHours(issuedAt, Number(runtimeConfig.admin.sessionTtlHours || 12))
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 derived = crypto.scryptSync(password, salt, 64).toString('hex')
return `scrypt$${salt}$${derived}`
}
function verifyAdminPassword(password, storedHash) {
function verifyAdminPassword(password: string, storedHash: string): boolean {
const [algorithm, salt, expectedHash] = String(storedHash || '').split('$')
if (algorithm !== 'scrypt' || !salt || !expectedHash) {
return false
@@ -351,14 +379,14 @@ function verifyAdminPassword(password, storedHash) {
return safeCompare(actualHash, expectedHash)
}
function signPayload(encodedPayload) {
function signPayload(encodedPayload: string): string {
return crypto
.createHmac('sha256', String(runtimeConfig.admin.sessionSecret || ''))
.update(encodedPayload)
.digest('base64url')
}
export function normalizeAdminRole(role) {
export function normalizeAdminRole(role: unknown): AdminRole {
const normalized = String(role || '').trim().toLowerCase()
if (normalized === 'admin') {
@@ -372,11 +400,11 @@ export function normalizeAdminRole(role) {
return 'operator'
}
export function normalizeAdminUserStatus(status) {
export function normalizeAdminUserStatus(status: unknown): AdminUserStatus {
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 right = Buffer.from(String(expected || ''), 'utf8')
@@ -387,25 +415,25 @@ function safeCompare(input, expected) {
return crypto.timingSafeEqual(left, right)
}
function normalizeRoleQuery(role) {
function normalizeRoleQuery(role: unknown): string {
const normalized = String(role || '').trim().toLowerCase()
return ['admin', 'operator', 'support'].includes(normalized) ? normalized : ''
}
function normalizeStatusQuery(status) {
function normalizeStatusQuery(status: unknown): string {
const normalized = String(status || '').trim().toLowerCase()
return ['active', 'disabled'].includes(normalized) ? normalized : ''
}
function normalizeUsername(username) {
function normalizeUsername(username: unknown): string {
return String(username || '').trim().toLowerCase()
}
function normalizePassword(password) {
function normalizePassword(password: unknown): string {
return String(password || '').trim()
}
function validateUsername(username) {
function validateUsername(username: string): void {
if (!/^[a-zA-Z0-9._-]{3,32}$/.test(username)) {
throw createHttpError('后台账号格式无效,需为 3-32 位字母数字或 ._-', {
statusCode: 400,
@@ -414,7 +442,7 @@ function validateUsername(username) {
}
}
function validatePassword(password) {
function validatePassword(password: string): void {
if (password.length < 8) {
throw createHttpError('后台密码至少 8 位', {
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))
if (!user) {
@@ -436,7 +464,11 @@ async function getRequiredAdminUser(userId) {
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 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 {
userId: Number(user.id),
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 : [])
.map((value) => String(value || '').trim())
.filter(Boolean)))
@@ -21,21 +21,24 @@ import { randomId } from '../../utils/random.js'
import { closeAdminTask, confirmAdminTaskAssistedRole, redeemAdminTaskAssisted } from './admin-write-service.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_PLATFORM = 'manual_redeem'
const MANUAL_SOURCE_TYPE = 'admin_manual_redeem'
const ASSISTED_PROFILE_KEY = 'tencent_claim_assisted'
/** @typedef {import('../../types/admin-read-inputs.js').AdminEntityIdInput} AdminEntityIdInput */
/** @typedef {import('../../types/admin-read-inputs.js').AdminViewerSessionInput} AdminViewerSessionInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateInput */
type JsonObject = Record<string, any>
/** @param {AdminManualRedeemCreateInput} [payload] */
/** @param {AdminViewerSessionInput | null} [session] */
export async function createAdminManualRedeemTask(
payload = /** @type {AdminManualRedeemCreateInput} */ ({}),
session = null,
) {
payload: AdminManualRedeemCreateInput = {},
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const viewerContext = createAdminViewerContext(session)
const proofValue = normalizeManualProofValue(payload.proofValue)
const skuCode = String(payload.skuCode || '').trim()
@@ -285,72 +288,83 @@ export async function createAdminManualRedeemTask(
return getAdminManualRedeemDetail(createdTask.id, session)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function getAdminManualRedeemDetail(taskId, session = null) {
export async function getAdminManualRedeemDetail(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await getClaimDetailForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {{ loginType?: string, forceRecreate?: boolean }} [payload] */
/** @param {AdminViewerSessionInput | null} [session] */
export async function createAdminManualRedeemSession(taskId, payload = {}, session = null) {
export async function createAdminManualRedeemSession(
taskId: AdminEntityIdInput,
payload: { loginType?: string, forceRecreate?: boolean } = {},
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await createClaimSessionForAdminTask(task.id, payload)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function getAdminManualRedeemSessionSummary(taskId, session = null) {
export async function getAdminManualRedeemSessionSummary(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await getClaimSessionSummaryForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function reloadAdminManualRedeemSession(taskId, session = null) {
export async function reloadAdminManualRedeemSession(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await reloadClaimSessionForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function closeAdminManualRedeemSession(taskId, session = null) {
export async function closeAdminManualRedeemSession(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session)
const detail = await closeClaimSessionForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function closeAdminManualRedeemTask(taskId, session = null) {
export async function closeAdminManualRedeemTask(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session)
await closeAdminTask(task.id)
return getAdminManualRedeemDetail(task.id, session)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function confirmAdminManualRedeemRole(taskId, session = null) {
export async function confirmAdminManualRedeemRole(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session)
await confirmAdminTaskAssistedRole(task.id, session)
return getAdminManualRedeemDetail(task.id, session)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function redeemAdminManualRedeemTask(taskId, session = null) {
export async function redeemAdminManualRedeemTask(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<JsonObject> {
const task = await getRequiredManualRedeemTask(taskId, session)
await redeemAdminTaskAssisted(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))
if (!task) {
@@ -392,7 +406,7 @@ async function getRequiredManualRedeemTask(taskId, session = null) {
return task
}
function decorateManualRedeemDetail(task, detail) {
function decorateManualRedeemDetail(task: TaskRow, detail: any): JsonObject {
const taskContext = parseTaskContext(task)
const manualRedeem = taskContext.manualRedeem && typeof taskContext.manualRedeem === 'object'
? 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()
}