feat(dcauth): 新增后台手机号与设备认证登录

登录链路改为账号密码 + 短信验证码 + DCAuth 设备签名,并新增后台手机号绑定、
登录日志、设备绑定与重置接口。
This commit is contained in:
yml2213
2026-10-21 20:46:24 +08:00
parent 7542d547bf
commit 040100f290
36 changed files with 3128 additions and 41 deletions
+105
View File
@@ -5,13 +5,17 @@ import {
getAdminSessionSummary,
loginAdmin,
logoutAdmin,
prepareAdminLogin,
completeAdminLogin,
} from '../../services/admin/admin-auth-service.js'
import { sendAdminLoginSmsCode } from '../../services/admin/admin-sms-service.js'
import {
resolveClientIp,
resolveClientLocation,
resolveUserAgent,
} from '../../services/admin/admin-login-log-service.js'
import { createJsonHandler, extractBearerToken } from './session.js'
import { createHttpError } from '../../utils/http.js'
const router = Router()
@@ -47,6 +51,63 @@ router.post(
),
)
router.post(
'/auth/login/sms-code',
adminLoginIpRateLimit,
adminLoginAccountRateLimit,
createJsonHandler(
(req) =>
sendAdminLoginSmsCode(req.body?.username, req.body?.password, {
ip: resolveClientIp(req),
userAgent: resolveUserAgent(req),
location: resolveClientLocation(req),
}),
{
successMessage: '验证码已发送',
errorMessage: '验证码发送失败',
scope: '[admin/auth/login/sms-code]',
},
),
)
router.post(
'/auth/login/prepare',
adminLoginIpRateLimit,
adminLoginAccountRateLimit,
createJsonHandler(
(req) =>
prepareAdminLogin(req.body?.username, req.body?.password, req.body?.smsCode, {
ip: resolveClientIp(req),
userAgent: resolveUserAgent(req),
location: resolveClientLocation(req),
apiBase: resolveApiBase(req, req.body?.apiBase),
}),
{
successMessage: '请在本机 DCAuth 中确认登录',
errorMessage: '准备设备认证失败',
scope: '[admin/auth/login/prepare]',
},
),
)
router.post(
'/auth/login/complete',
adminLoginIpRateLimit,
createJsonHandler(
(req) =>
completeAdminLogin(req.body, {
ip: resolveClientIp(req),
userAgent: resolveUserAgent(req),
location: resolveClientLocation(req),
}),
{
successMessage: '登录成功',
errorMessage: '完成设备登录失败',
scope: '[admin/auth/login/complete]',
},
),
)
router.get(
'/auth/session',
createJsonHandler((req) => getAdminSessionSummary(extractBearerToken(req)), {
@@ -66,3 +127,47 @@ router.post(
)
export default router
function resolveApiBase(
req: { headers: Record<string, unknown>; protocol?: string },
value: unknown,
) {
const forwardedProto = String(req.headers['x-forwarded-proto'] || '')
.split(',')[0]
?.trim()
const forwardedHost = String(req.headers['x-forwarded-host'] || '')
.split(',')[0]
?.trim()
const origin = String(value || req.headers.origin || '').trim()
const raw =
origin ||
`${forwardedProto || req.protocol || 'http'}://${forwardedHost || String(req.headers.host || '')}`
let parsed: URL
try {
parsed = new URL(raw)
} catch {
throw createHttpError('登录来源地址无效', {
statusCode: 400,
errorCode: 'admin_api_base_invalid',
})
}
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
throw createHttpError('登录来源地址必须使用 http 或 https', {
statusCode: 400,
errorCode: 'admin_api_base_invalid',
})
}
if (
parsed.pathname !== '/' ||
parsed.search ||
parsed.hash ||
parsed.username ||
parsed.password
) {
throw createHttpError('登录来源地址必须是不带路径的 origin', {
statusCode: 400,
errorCode: 'admin_api_base_invalid',
})
}
return `${parsed.protocol}//${parsed.host}`
}
+72
View File
@@ -4,7 +4,10 @@ import {
createManagedAdminUser,
getAdminUserList,
resetManagedAdminUserPassword,
resetManagedAdminUserDevice,
updateManagedAdminUserDisplayName,
updateManagedAdminUserPhone,
updateManagedAdminUserTestAccount,
updateManagedAdminUserRole,
updateManagedAdminUserStatus,
} from '../../services/admin/admin-auth-service.js'
@@ -160,4 +163,73 @@ router.post(
),
)
router.post(
'/users/:userId/phone',
createJsonHandler(
(req) =>
updateManagedAdminUserPhone(
String(req.params.userId || ''),
req.body,
getRequiredAdminSession(req),
),
{
successMessage: '手机号已绑定',
errorMessage: '绑定手机号失败',
scope: '[admin/users/:userId/phone]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_phone_updated',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: { username: result.user.username },
}
},
},
),
)
router.post(
'/users/:userId/device/reset',
createJsonHandler(
(req) =>
resetManagedAdminUserDevice(String(req.params.userId || ''), getRequiredAdminSession(req)),
{
successMessage: '设备绑定已重置',
errorMessage: '重置设备绑定失败',
scope: '[admin/users/:userId/device/reset]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_device_reset',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: { username: result.user.username },
}
},
},
),
)
router.post(
'/users/:userId/test-account',
createJsonHandler(
(req) => updateManagedAdminUserTestAccount(String(req.params.userId || ''), req.body),
{
successMessage: '测试账号设置已更新',
errorMessage: '更新测试账号设置失败',
scope: '[admin/users/:userId/test-account]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_test_account_updated',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: { username: result.user.username },
}
},
},
),
)
export default router