安全加固:增加后台登录 IP 限流并记录验证结果
This commit is contained in:
@@ -71,6 +71,31 @@ test('createRateLimitMiddleware supports custom limit responses', () => {
|
|||||||
assert.deepEqual(res.body, { code: 429, message: 'limited' })
|
assert.deepEqual(res.body, { code: 429, message: 'limited' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('createRateLimitMiddleware can cap an IP across different accounts', () => {
|
||||||
|
resetRateLimitBucketsForTest()
|
||||||
|
|
||||||
|
const limiter = createRateLimitMiddleware({
|
||||||
|
scope: 'test:ip-wide',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 2,
|
||||||
|
})
|
||||||
|
const first = createMockRequest()
|
||||||
|
const second = createMockRequest()
|
||||||
|
const third = createMockRequest()
|
||||||
|
const response = createMockResponse()
|
||||||
|
let nextCount = 0
|
||||||
|
const next: NextFunction = () => {
|
||||||
|
nextCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
limiter(first, response, next)
|
||||||
|
limiter(second, response, next)
|
||||||
|
limiter(third, response, next)
|
||||||
|
|
||||||
|
assert.equal(nextCount, 2)
|
||||||
|
assert.equal(response.statusCode, 429)
|
||||||
|
})
|
||||||
|
|
||||||
function createMockRequest(): Request {
|
function createMockRequest(): Request {
|
||||||
return {
|
return {
|
||||||
ip: '127.0.0.1',
|
ip: '127.0.0.1',
|
||||||
|
|||||||
@@ -15,14 +15,23 @@ import { createJsonHandler, extractBearerToken } from './session.js'
|
|||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.post(
|
const adminLoginIpRateLimit = createRateLimitMiddleware({
|
||||||
'/auth/login',
|
scope: 'admin:login:ip',
|
||||||
createRateLimitMiddleware({
|
windowMs: 60_000,
|
||||||
scope: 'admin:login',
|
max: 20,
|
||||||
|
})
|
||||||
|
|
||||||
|
const adminLoginAccountRateLimit = createRateLimitMiddleware({
|
||||||
|
scope: 'admin:login:account',
|
||||||
windowMs: 60_000,
|
windowMs: 60_000,
|
||||||
max: 10,
|
max: 10,
|
||||||
key: getBodyFieldRateLimitKey('username'),
|
key: getBodyFieldRateLimitKey('username'),
|
||||||
}),
|
})
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/login',
|
||||||
|
adminLoginIpRateLimit,
|
||||||
|
adminLoginAccountRateLimit,
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
loginAdmin(req.body?.username, req.body?.password, {
|
loginAdmin(req.body?.username, req.body?.password, {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
|
||||||
|
import { createRateLimitMiddleware, getBodyFieldRateLimitKey } from '../../middleware/rate-limit.js'
|
||||||
|
import {
|
||||||
|
getAdminSessionSummary,
|
||||||
|
loginAdmin,
|
||||||
|
logoutAdmin,
|
||||||
|
} from '../../services/admin/admin-auth-service.js'
|
||||||
|
import {
|
||||||
|
resolveClientIp,
|
||||||
|
resolveClientLocation,
|
||||||
|
resolveUserAgent,
|
||||||
|
} from '../../services/admin/admin-login-log-service.js'
|
||||||
|
import { createJsonHandler, extractBearerToken } from './session.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/login',
|
||||||
|
createRateLimitMiddleware({
|
||||||
|
scope: 'admin:login',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 10,
|
||||||
|
key: getBodyFieldRateLimitKey('username'),
|
||||||
|
}),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
loginAdmin(req.body?.username, req.body?.password, {
|
||||||
|
ip: resolveClientIp(req),
|
||||||
|
userAgent: resolveUserAgent(req),
|
||||||
|
location: resolveClientLocation(req),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
successMessage: '登录成功',
|
||||||
|
errorMessage: '后台登录失败',
|
||||||
|
scope: '[admin/auth/login]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/auth/session',
|
||||||
|
createJsonHandler((req) => getAdminSessionSummary(extractBearerToken(req)), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取后台登录态失败',
|
||||||
|
scope: '[admin/auth/session]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/logout',
|
||||||
|
createJsonHandler((req) => logoutAdmin(extractBearerToken(req)), {
|
||||||
|
successMessage: '已退出登录',
|
||||||
|
errorMessage: '后台退出失败',
|
||||||
|
scope: '[admin/auth/logout]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import type { NextFunction, Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { createRateLimitMiddleware, resetRateLimitBucketsForTest } from './rate-limit.js'
|
||||||
|
|
||||||
|
test('createRateLimitMiddleware allows requests within the window', () => {
|
||||||
|
resetRateLimitBucketsForTest()
|
||||||
|
|
||||||
|
const limiter = createRateLimitMiddleware({
|
||||||
|
scope: 'test:allow',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 2,
|
||||||
|
})
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
let nextCount = 0
|
||||||
|
const next: NextFunction = () => {
|
||||||
|
nextCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
limiter(req, res, next)
|
||||||
|
limiter(req, res, next)
|
||||||
|
|
||||||
|
assert.equal(nextCount, 2)
|
||||||
|
assert.equal(res.statusCode, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createRateLimitMiddleware blocks requests over the limit', () => {
|
||||||
|
resetRateLimitBucketsForTest()
|
||||||
|
|
||||||
|
const limiter = createRateLimitMiddleware({
|
||||||
|
scope: 'test:block',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 1,
|
||||||
|
})
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
let nextCount = 0
|
||||||
|
const next: NextFunction = () => {
|
||||||
|
nextCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
limiter(req, res, next)
|
||||||
|
limiter(req, res, next)
|
||||||
|
|
||||||
|
assert.equal(nextCount, 1)
|
||||||
|
assert.equal(res.statusCode, 429)
|
||||||
|
assert.equal(res.headers['Retry-After'], '60')
|
||||||
|
assert.equal(res.body.errorCode, 'rate_limited')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createRateLimitMiddleware supports custom limit responses', () => {
|
||||||
|
resetRateLimitBucketsForTest()
|
||||||
|
|
||||||
|
const limiter = createRateLimitMiddleware({
|
||||||
|
scope: 'test:custom',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 1,
|
||||||
|
onLimit: (_req, res) => {
|
||||||
|
res.status(200).json({ code: 429, message: 'limited' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
limiter(req, res, () => {})
|
||||||
|
limiter(req, res, () => {})
|
||||||
|
|
||||||
|
assert.equal(res.statusCode, 200)
|
||||||
|
assert.deepEqual(res.body, { code: 429, message: 'limited' })
|
||||||
|
})
|
||||||
|
|
||||||
|
function createMockRequest(): Request {
|
||||||
|
return {
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
originalUrl: '/test',
|
||||||
|
headers: {},
|
||||||
|
socket: {
|
||||||
|
remoteAddress: '127.0.0.1',
|
||||||
|
},
|
||||||
|
} as Request
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockResponse(): Response & {
|
||||||
|
statusCode: number
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: any
|
||||||
|
} {
|
||||||
|
const res = {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {} as Record<string, string>,
|
||||||
|
body: null as any,
|
||||||
|
setHeader(name: string, value: string) {
|
||||||
|
this.headers[name] = value
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
status(statusCode: number) {
|
||||||
|
this.statusCode = statusCode
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
json(body: any) {
|
||||||
|
this.body = body
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return res as Response & {
|
||||||
|
statusCode: number
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: any
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
diff --git a/apps/backend/src/routes/admin/auth.ts b/apps/backend/src/routes/admin/auth.ts
|
||||||
|
--- a/apps/backend/src/routes/admin/auth.ts
|
||||||
|
+++ b/apps/backend/src/routes/admin/auth.ts
|
||||||
|
@@
|
||||||
|
const router = Router()
|
||||||
|
+
|
||||||
|
+const adminLoginIpRateLimit = createRateLimitMiddleware({
|
||||||
|
+ scope: 'admin:login:ip',
|
||||||
|
+ windowMs: 60_000,
|
||||||
|
+ max: 20,
|
||||||
|
+})
|
||||||
|
+
|
||||||
|
+const adminLoginAccountRateLimit = createRateLimitMiddleware({
|
||||||
|
+ scope: 'admin:login:account',
|
||||||
|
+ windowMs: 60_000,
|
||||||
|
+ max: 10,
|
||||||
|
+ key: getBodyFieldRateLimitKey('username'),
|
||||||
|
+})
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/login',
|
||||||
|
- createRateLimitMiddleware({
|
||||||
|
- scope: 'admin:login',
|
||||||
|
- windowMs: 60_000,
|
||||||
|
- max: 10,
|
||||||
|
- key: getBodyFieldRateLimitKey('username'),
|
||||||
|
- }),
|
||||||
|
+ adminLoginIpRateLimit,
|
||||||
|
+ adminLoginAccountRateLimit,
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
|
||||||
|
import { createRateLimitMiddleware, getBodyFieldRateLimitKey } from '../../middleware/rate-limit.js'
|
||||||
|
import {
|
||||||
|
getAdminSessionSummary,
|
||||||
|
loginAdmin,
|
||||||
|
logoutAdmin,
|
||||||
|
} from '../../services/admin/admin-auth-service.js'
|
||||||
|
import {
|
||||||
|
resolveClientIp,
|
||||||
|
resolveClientLocation,
|
||||||
|
resolveUserAgent,
|
||||||
|
} from '../../services/admin/admin-login-log-service.js'
|
||||||
|
import { createJsonHandler, extractBearerToken } from './session.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
const adminLoginIpRateLimit = createRateLimitMiddleware({
|
||||||
|
scope: 'admin:login:ip',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 20,
|
||||||
|
})
|
||||||
|
|
||||||
|
const adminLoginAccountRateLimit = createRateLimitMiddleware({
|
||||||
|
scope: 'admin:login:account',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 10,
|
||||||
|
key: getBodyFieldRateLimitKey('username'),
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/login',
|
||||||
|
adminLoginIpRateLimit,
|
||||||
|
adminLoginAccountRateLimit,
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
loginAdmin(req.body?.username, req.body?.password, {
|
||||||
|
ip: resolveClientIp(req),
|
||||||
|
userAgent: resolveUserAgent(req),
|
||||||
|
location: resolveClientLocation(req),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
successMessage: '登录成功',
|
||||||
|
errorMessage: '后台登录失败',
|
||||||
|
scope: '[admin/auth/login]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/auth/session',
|
||||||
|
createJsonHandler((req) => getAdminSessionSummary(extractBearerToken(req)), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取后台登录态失败',
|
||||||
|
scope: '[admin/auth/session]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/logout',
|
||||||
|
createJsonHandler((req) => logoutAdmin(extractBearerToken(req)), {
|
||||||
|
successMessage: '已退出登录',
|
||||||
|
errorMessage: '后台退出失败',
|
||||||
|
scope: '[admin/auth/logout]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="${1:?usage: ROLLBACK.sh <workspace-copy>}"
|
||||||
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||||
|
|
||||||
|
cp "$SCRIPT_DIR/BASELINE_admin_auth.ts" "$ROOT/apps/backend/src/routes/admin/auth.ts"
|
||||||
|
cp "$SCRIPT_DIR/BASELINE_rate-limit.test.ts" "$ROOT/apps/backend/src/middleware/rate-limit.test.ts"
|
||||||
|
printf 'restored admin auth route and rate-limit test in %s\n' "$ROOT"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
Security review: admin login brute-force protection
|
||||||
|
|
||||||
|
Changed branch/field:
|
||||||
|
- apps/backend/src/routes/admin/auth.ts, POST /auth/login middleware chain
|
||||||
|
- Added admin:login:ip (20 requests / 60 seconds per client IP)
|
||||||
|
- Kept admin:login:account (10 requests / 60 seconds per client IP + username)
|
||||||
|
|
||||||
|
Artifacts:
|
||||||
|
- MODIFIED_FILE: /Users/yml/codes/order_site/security-artifacts/MODIFIED_FILE.ts
|
||||||
|
- DIFF_FILE: /Users/yml/codes/order_site/security-artifacts/DIFF_FILE.patch
|
||||||
|
- VERIFICATION.txt: /Users/yml/codes/order_site/security-artifacts/VERIFICATION.txt
|
||||||
|
- ROLLBACK.sh: /Users/yml/codes/order_site/security-artifacts/ROLLBACK.sh
|
||||||
|
|
||||||
|
BASELINE
|
||||||
|
Command: npm test -- --test-name-pattern='createRateLimitMiddleware'
|
||||||
|
Input: repository HEAD sources before the route/test edits
|
||||||
|
Literal result: tests 371; pass 369; fail 0; skipped 2; exit status 0
|
||||||
|
|
||||||
|
MODIFIED
|
||||||
|
Command: node --import tsx --test src/middleware/rate-limit.test.ts
|
||||||
|
Input: modified rate-limit middleware test source
|
||||||
|
Literal result: tests 4; pass 4; fail 0; skipped 0; exit status 0
|
||||||
|
|
||||||
|
Command: npm run check
|
||||||
|
Input: modified backend source tree
|
||||||
|
Literal result: format check passed; SQL guard passed; ESLint passed; TypeScript passed; tests 373; pass 371; fail 0; skipped 2; build completed; exit status 0
|
||||||
|
|
||||||
|
ROLLBACK
|
||||||
|
Command: security-artifacts/ROLLBACK.sh security-artifacts/rollback-copy
|
||||||
|
Input: independent copy containing MODIFIED_FILE.ts and BASELINE_rate-limit.test.ts
|
||||||
|
Literal result: restored admin auth route and rate-limit test in security-artifacts/rollback-copy; baseline admin auth SHA-256 d1afed851de0dce8704f4233e1b9339a33a88d1ae9c89d6639aa104be7ff8aff; exit status 0
|
||||||
|
Restored behavior/status: rollback-copy/apps/backend/src/routes/admin/auth.ts matches the baseline hash; the working source remains modified.
|
||||||
|
|
||||||
|
Original source hash before modification:
|
||||||
|
d1afed851de0dce8704f4233e1b9339a33a88d1ae9c89d6639aa104be7ff8aff
|
||||||
|
Modified copy hash:
|
||||||
|
c70ca222c13245a6e1714f1c90f3592f5c4fd3713a1ef3af5fb4073eb9b9db95
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import type { NextFunction, Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { createRateLimitMiddleware, resetRateLimitBucketsForTest } from './rate-limit.js'
|
||||||
|
|
||||||
|
test('createRateLimitMiddleware allows requests within the window', () => {
|
||||||
|
resetRateLimitBucketsForTest()
|
||||||
|
|
||||||
|
const limiter = createRateLimitMiddleware({
|
||||||
|
scope: 'test:allow',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 2,
|
||||||
|
})
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
let nextCount = 0
|
||||||
|
const next: NextFunction = () => {
|
||||||
|
nextCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
limiter(req, res, next)
|
||||||
|
limiter(req, res, next)
|
||||||
|
|
||||||
|
assert.equal(nextCount, 2)
|
||||||
|
assert.equal(res.statusCode, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createRateLimitMiddleware blocks requests over the limit', () => {
|
||||||
|
resetRateLimitBucketsForTest()
|
||||||
|
|
||||||
|
const limiter = createRateLimitMiddleware({
|
||||||
|
scope: 'test:block',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 1,
|
||||||
|
})
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
let nextCount = 0
|
||||||
|
const next: NextFunction = () => {
|
||||||
|
nextCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
limiter(req, res, next)
|
||||||
|
limiter(req, res, next)
|
||||||
|
|
||||||
|
assert.equal(nextCount, 1)
|
||||||
|
assert.equal(res.statusCode, 429)
|
||||||
|
assert.equal(res.headers['Retry-After'], '60')
|
||||||
|
assert.equal(res.body.errorCode, 'rate_limited')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createRateLimitMiddleware supports custom limit responses', () => {
|
||||||
|
resetRateLimitBucketsForTest()
|
||||||
|
|
||||||
|
const limiter = createRateLimitMiddleware({
|
||||||
|
scope: 'test:custom',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 1,
|
||||||
|
onLimit: (_req, res) => {
|
||||||
|
res.status(200).json({ code: 429, message: 'limited' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
limiter(req, res, () => {})
|
||||||
|
limiter(req, res, () => {})
|
||||||
|
|
||||||
|
assert.equal(res.statusCode, 200)
|
||||||
|
assert.deepEqual(res.body, { code: 429, message: 'limited' })
|
||||||
|
})
|
||||||
|
|
||||||
|
function createMockRequest(): Request {
|
||||||
|
return {
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
originalUrl: '/test',
|
||||||
|
headers: {},
|
||||||
|
socket: {
|
||||||
|
remoteAddress: '127.0.0.1',
|
||||||
|
},
|
||||||
|
} as Request
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockResponse(): Response & {
|
||||||
|
statusCode: number
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: any
|
||||||
|
} {
|
||||||
|
const res = {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {} as Record<string, string>,
|
||||||
|
body: null as any,
|
||||||
|
setHeader(name: string, value: string) {
|
||||||
|
this.headers[name] = value
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
status(statusCode: number) {
|
||||||
|
this.statusCode = statusCode
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
json(body: any) {
|
||||||
|
this.body = body
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return res as Response & {
|
||||||
|
statusCode: number
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: any
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
|
||||||
|
import { createRateLimitMiddleware, getBodyFieldRateLimitKey } from '../../middleware/rate-limit.js'
|
||||||
|
import {
|
||||||
|
getAdminSessionSummary,
|
||||||
|
loginAdmin,
|
||||||
|
logoutAdmin,
|
||||||
|
} from '../../services/admin/admin-auth-service.js'
|
||||||
|
import {
|
||||||
|
resolveClientIp,
|
||||||
|
resolveClientLocation,
|
||||||
|
resolveUserAgent,
|
||||||
|
} from '../../services/admin/admin-login-log-service.js'
|
||||||
|
import { createJsonHandler, extractBearerToken } from './session.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/login',
|
||||||
|
createRateLimitMiddleware({
|
||||||
|
scope: 'admin:login',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 10,
|
||||||
|
key: getBodyFieldRateLimitKey('username'),
|
||||||
|
}),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
loginAdmin(req.body?.username, req.body?.password, {
|
||||||
|
ip: resolveClientIp(req),
|
||||||
|
userAgent: resolveUserAgent(req),
|
||||||
|
location: resolveClientLocation(req),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
successMessage: '登录成功',
|
||||||
|
errorMessage: '后台登录失败',
|
||||||
|
scope: '[admin/auth/login]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/auth/session',
|
||||||
|
createJsonHandler((req) => getAdminSessionSummary(extractBearerToken(req)), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取后台登录态失败',
|
||||||
|
scope: '[admin/auth/session]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/logout',
|
||||||
|
createJsonHandler((req) => logoutAdmin(extractBearerToken(req)), {
|
||||||
|
successMessage: '已退出登录',
|
||||||
|
errorMessage: '后台退出失败',
|
||||||
|
scope: '[admin/auth/logout]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
Reference in New Issue
Block a user