diff --git a/apps/backend/src/services/admin/admin-auth-service.js b/apps/backend/src/services/admin/admin-auth-service.ts similarity index 80% rename from apps/backend/src/services/admin/admin-auth-service.js rename to apps/backend/src/services/admin/admin-auth-service.ts index 6a4a63af..4a1d3058 100644 --- a/apps/backend/src/services/admin/admin-auth-service.js +++ b/apps/backend/src/services/admin/admin-auth-service.ts @@ -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>> + +type JsonObject = Record + +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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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))) diff --git a/apps/backend/src/services/admin/admin-manual-redeem-service.js b/apps/backend/src/services/admin/admin-manual-redeem-service.ts similarity index 85% rename from apps/backend/src/services/admin/admin-manual-redeem-service.js rename to apps/backend/src/services/admin/admin-manual-redeem-service.ts index 58e03233..bd58651a 100644 --- a/apps/backend/src/services/admin/admin-manual-redeem-service.js +++ b/apps/backend/src/services/admin/admin-manual-redeem-service.ts @@ -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 -/** @param {AdminManualRedeemCreateInput} [payload] */ -/** @param {AdminViewerSessionInput | null} [session] */ export async function createAdminManualRedeemTask( - payload = /** @type {AdminManualRedeemCreateInput} */ ({}), - session = null, -) { + payload: AdminManualRedeemCreateInput = {}, + session: AdminViewerSessionInput | null = null, +): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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() } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 4f69ea39..4ed247b0 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -785,6 +785,16 @@ - `npm run typecheck` - `npm run build` - `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` ## 下一步建议