后端迁移后台鉴权与人工兑换
This commit is contained in:
+62
-30
@@ -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)))
|
||||
+50
-36
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user