后端迁移后台基础路由
This commit is contained in:
-2
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
|
|
||||||
import { getAdminDashboardSummary } from '../../services/admin/admin-dashboard-service.js'
|
import { getAdminDashboardSummary } from '../../services/admin/admin-dashboard-service.js'
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js'
|
import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js'
|
||||||
import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js'
|
import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js'
|
||||||
import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js'
|
import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js'
|
||||||
import { logWarn } from '../../utils/logger.js'
|
import { logWarn } from '../../utils/logger.js'
|
||||||
|
import type { AdminSession } from '../../services/admin/admin-auth-service.js'
|
||||||
|
|
||||||
/** @typedef {import('express').NextFunction} NextFunction */
|
type NextFunction = (error?: unknown) => void
|
||||||
/** @typedef {import('express').Request} ExpressRequest */
|
type ExpressResponse = {
|
||||||
/** @typedef {import('express').Response} ExpressResponse */
|
json: (body: unknown) => unknown
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminRouteAdminSession} AdminRouteAdminSession */
|
sendFile: (path: string) => unknown
|
||||||
/** @typedef {ExpressRequest & { adminSession?: AdminRouteAdminSession | null }} AdminExpressRequest */
|
}
|
||||||
|
export type AdminExpressRequest = {
|
||||||
|
headers: { authorization?: string | string[] }
|
||||||
|
body?: any
|
||||||
|
params?: Record<string, string | undefined>
|
||||||
|
query?: Record<string, unknown>
|
||||||
|
adminSession?: AdminSession | null
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
type AdminAuditPayload = {
|
||||||
* @typedef {{
|
action?: string
|
||||||
* action?: string
|
targetType?: string
|
||||||
* targetType?: string
|
targetId?: string
|
||||||
* targetId?: string
|
data?: Record<string, unknown>
|
||||||
* data?: Record<string, unknown>
|
}
|
||||||
* }} AdminAuditPayload
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
type AdminJsonHandlerOptions = {
|
||||||
* @typedef {{
|
successMessage?: string
|
||||||
* successMessage?: string
|
errorMessage?: string
|
||||||
* errorMessage?: string
|
scope?: string
|
||||||
* scope?: string
|
audit?: (req: AdminExpressRequest, data: unknown) => AdminAuditPayload | null | undefined
|
||||||
* audit?: (req: AdminExpressRequest, data: unknown) => (AdminAuditPayload | null | undefined)
|
}
|
||||||
* }} AdminJsonHandlerOptions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
type AdminFileHandlerOptions = {
|
||||||
* @typedef {{
|
errorMessage?: string
|
||||||
* errorMessage?: string
|
scope?: string
|
||||||
* scope?: string
|
}
|
||||||
* }} AdminFileHandlerOptions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
export function createJsonHandler(
|
||||||
* @param {(req: AdminExpressRequest, res: ExpressResponse) => unknown | Promise<unknown>} action
|
action: (req: AdminExpressRequest, res: ExpressResponse) => unknown | Promise<unknown>,
|
||||||
* @param {AdminJsonHandlerOptions} [options]
|
{ successMessage = 'ok', errorMessage, scope, audit }: AdminJsonHandlerOptions = {},
|
||||||
*/
|
) {
|
||||||
export function createJsonHandler(action, { successMessage = 'ok', errorMessage, scope, audit } = {}) {
|
|
||||||
return async (req, res) => {
|
return async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const data = await action(req, res)
|
const data = await action(req, res)
|
||||||
@@ -52,11 +52,10 @@ export function createJsonHandler(action, { successMessage = 'ok', errorMessage,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function createFileHandler(
|
||||||
* @param {(req: AdminExpressRequest, res: ExpressResponse) => string | Promise<string>} action
|
action: (req: AdminExpressRequest, res: ExpressResponse) => string | Promise<string>,
|
||||||
* @param {AdminFileHandlerOptions} [options]
|
{ errorMessage, scope }: AdminFileHandlerOptions = {},
|
||||||
*/
|
) {
|
||||||
export function createFileHandler(action, { errorMessage, scope } = {}) {
|
|
||||||
return async (req, res) => {
|
return async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const filePath = await action(req, res)
|
const filePath = await action(req, res)
|
||||||
@@ -67,10 +66,7 @@ export function createFileHandler(action, { errorMessage, scope } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminExpressRequest} req */
|
export async function requireAdminSession(req: AdminExpressRequest, res: ExpressResponse, next: NextFunction) {
|
||||||
/** @param {ExpressResponse} res */
|
|
||||||
/** @param {NextFunction} next */
|
|
||||||
export async function requireAdminSession(req, res, next) {
|
|
||||||
try {
|
try {
|
||||||
req.adminSession = await verifyAdminSessionToken(extractBearerToken(req))
|
req.adminSession = await verifyAdminSessionToken(extractBearerToken(req))
|
||||||
next()
|
next()
|
||||||
@@ -79,8 +75,7 @@ export async function requireAdminSession(req, res, next) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string[]} allowedRoles */
|
export function requireAdminRoles(allowedRoles: string[]) {
|
||||||
export function requireAdminRoles(allowedRoles) {
|
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
requireAdminRole(req.adminSession, allowedRoles)
|
requireAdminRole(req.adminSession, allowedRoles)
|
||||||
@@ -91,7 +86,12 @@ export function requireAdminRoles(allowedRoles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recordAdminAudit(session, req, data, audit) {
|
async function recordAdminAudit(
|
||||||
|
session: AdminSession | null | undefined,
|
||||||
|
req: AdminExpressRequest,
|
||||||
|
data: unknown,
|
||||||
|
audit: AdminJsonHandlerOptions['audit'],
|
||||||
|
) {
|
||||||
if (!audit) {
|
if (!audit) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ async function recordAdminAudit(session, req, data, audit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractBearerToken(req) {
|
export function extractBearerToken(req: AdminExpressRequest) {
|
||||||
const authorization = String(req.headers.authorization || '').trim()
|
const authorization = String(req.headers.authorization || '').trim()
|
||||||
const matched = authorization.match(/^Bearer\s+(.+)$/i)
|
const matched = authorization.match(/^Bearer\s+(.+)$/i)
|
||||||
|
|
||||||
@@ -10,17 +10,15 @@ import {
|
|||||||
} from '../../services/admin/admin-auth-service.js'
|
} from '../../services/admin/admin-auth-service.js'
|
||||||
import { createJsonHandler, requireAdminRoles } from './shared.js'
|
import { createJsonHandler, requireAdminRoles } from './shared.js'
|
||||||
|
|
||||||
/**
|
type AdminUserMutationResult = {
|
||||||
* @typedef {{
|
user: {
|
||||||
* user: {
|
userId: number
|
||||||
* userId: number
|
username: string
|
||||||
* username: string
|
role: string
|
||||||
* role: string
|
status?: string
|
||||||
* status?: string
|
inventoryGroupCodes?: string[]
|
||||||
* inventoryGroupCodes?: string[]
|
}
|
||||||
* }
|
}
|
||||||
* }} AdminUserMutationResult
|
|
||||||
*/
|
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
@@ -42,16 +40,16 @@ router.post('/users', createJsonHandler(
|
|||||||
errorMessage: '创建后台用户失败',
|
errorMessage: '创建后台用户失败',
|
||||||
scope: '[admin/users:create]',
|
scope: '[admin/users:create]',
|
||||||
audit: (_req, data) => {
|
audit: (_req, data) => {
|
||||||
const result = /** @type {AdminUserMutationResult} */ (data)
|
const result = data as AdminUserMutationResult
|
||||||
return {
|
return {
|
||||||
action: 'admin_user_created',
|
action: 'admin_user_created',
|
||||||
targetType: 'admin_user',
|
targetType: 'admin_user',
|
||||||
targetId: String(result.user.userId),
|
targetId: String(result.user.userId),
|
||||||
data: {
|
data: {
|
||||||
username: result.user.username,
|
username: result.user.username,
|
||||||
role: result.user.role,
|
role: result.user.role,
|
||||||
status: result.user.status,
|
status: result.user.status,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -64,15 +62,15 @@ router.post('/users/:userId/role', createJsonHandler(
|
|||||||
errorMessage: '更新用户角色失败',
|
errorMessage: '更新用户角色失败',
|
||||||
scope: '[admin/users/:userId/role]',
|
scope: '[admin/users/:userId/role]',
|
||||||
audit: (_req, data) => {
|
audit: (_req, data) => {
|
||||||
const result = /** @type {AdminUserMutationResult} */ (data)
|
const result = data as AdminUserMutationResult
|
||||||
return {
|
return {
|
||||||
action: 'admin_user_role_updated',
|
action: 'admin_user_role_updated',
|
||||||
targetType: 'admin_user',
|
targetType: 'admin_user',
|
||||||
targetId: String(result.user.userId),
|
targetId: String(result.user.userId),
|
||||||
data: {
|
data: {
|
||||||
username: result.user.username,
|
username: result.user.username,
|
||||||
role: result.user.role,
|
role: result.user.role,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -85,15 +83,15 @@ router.post('/users/:userId/status', createJsonHandler(
|
|||||||
errorMessage: '更新用户状态失败',
|
errorMessage: '更新用户状态失败',
|
||||||
scope: '[admin/users/:userId/status]',
|
scope: '[admin/users/:userId/status]',
|
||||||
audit: (_req, data) => {
|
audit: (_req, data) => {
|
||||||
const result = /** @type {AdminUserMutationResult} */ (data)
|
const result = data as AdminUserMutationResult
|
||||||
return {
|
return {
|
||||||
action: 'admin_user_status_updated',
|
action: 'admin_user_status_updated',
|
||||||
targetType: 'admin_user',
|
targetType: 'admin_user',
|
||||||
targetId: String(result.user.userId),
|
targetId: String(result.user.userId),
|
||||||
data: {
|
data: {
|
||||||
username: result.user.username,
|
username: result.user.username,
|
||||||
status: result.user.status,
|
status: result.user.status,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -106,15 +104,15 @@ router.post('/users/:userId/inventory-groups', createJsonHandler(
|
|||||||
errorMessage: '更新用户库存组失败',
|
errorMessage: '更新用户库存组失败',
|
||||||
scope: '[admin/users/:userId/inventory-groups]',
|
scope: '[admin/users/:userId/inventory-groups]',
|
||||||
audit: (_req, data) => {
|
audit: (_req, data) => {
|
||||||
const result = /** @type {AdminUserMutationResult} */ (data)
|
const result = data as AdminUserMutationResult
|
||||||
return {
|
return {
|
||||||
action: 'admin_user_inventory_groups_updated',
|
action: 'admin_user_inventory_groups_updated',
|
||||||
targetType: 'admin_user',
|
targetType: 'admin_user',
|
||||||
targetId: String(result.user.userId),
|
targetId: String(result.user.userId),
|
||||||
data: {
|
data: {
|
||||||
username: result.user.username,
|
username: result.user.username,
|
||||||
inventoryGroupCodes: result.user.inventoryGroupCodes,
|
inventoryGroupCodes: result.user.inventoryGroupCodes,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -127,14 +125,14 @@ router.post('/users/:userId/reset-password', createJsonHandler(
|
|||||||
errorMessage: '重置用户密码失败',
|
errorMessage: '重置用户密码失败',
|
||||||
scope: '[admin/users/:userId/reset-password]',
|
scope: '[admin/users/:userId/reset-password]',
|
||||||
audit: (_req, data) => {
|
audit: (_req, data) => {
|
||||||
const result = /** @type {AdminUserMutationResult} */ (data)
|
const result = data as AdminUserMutationResult
|
||||||
return {
|
return {
|
||||||
action: 'admin_user_password_reset',
|
action: 'admin_user_password_reset',
|
||||||
targetType: 'admin_user',
|
targetType: 'admin_user',
|
||||||
targetId: String(result.user.userId),
|
targetId: String(result.user.userId),
|
||||||
data: {
|
data: {
|
||||||
username: result.user.username,
|
username: result.user.username,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -18,7 +18,7 @@ type AdminUserRow = NonNullable<Awaited<ReturnType<typeof getAdminUserById>>>
|
|||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
|
|
||||||
type AdminSession = {
|
export type AdminSession = {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
userId: number
|
userId: number
|
||||||
username: string
|
username: string
|
||||||
|
|||||||
@@ -808,6 +808,16 @@
|
|||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 139 个用例通过
|
- `npm test` 共 139 个用例通过
|
||||||
208. `src/services` 目录下非测试 `.js` 服务文件已全部迁移为 `.ts`
|
208. `src/services` 目录下非测试 `.js` 服务文件已全部迁移为 `.ts`
|
||||||
|
209. 第一批后台 Admin 路由迁移到 `.ts`:
|
||||||
|
- `src/routes/admin/shared.ts`
|
||||||
|
- `src/routes/admin/auth.ts`
|
||||||
|
- `src/routes/admin/dashboard.ts`
|
||||||
|
- `src/routes/admin/users.ts`
|
||||||
|
210. 后台 JSON / 文件 handler、session 鉴权中间件、角色中间件、登录 / session / logout、概览摘要和后台用户管理路由已进入 TS 编译链路;Admin session、route request、审计 payload、用户 mutation result 与 handler options 补齐类型,并导出真实 `AdminSession` 给路由层复用
|
||||||
|
211. Docker 内验证通过:
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run build`
|
||||||
|
- `npm test` 共 139 个用例通过
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user