feat(dcauth): 新增后台手机号与设备认证登录
登录链路改为账号密码 + 短信验证码 + DCAuth 设备签名,并新增后台手机号绑定、 登录日志、设备绑定与重置接口。
This commit is contained in:
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user