Merge branch 'main' into dev

This commit is contained in:
yml2213
2026-10-25 13:33:05 +08:00
37 changed files with 825 additions and 112 deletions
@@ -18,7 +18,12 @@ import {
revokeAdminSession,
touchAdminSession,
} from '../../repositories/admin-session-repo.js'
import { addHours, nowIso } from '../../utils/time.js'
import { nowIso } from '../../utils/time.js'
import {
resolveSessionExpiryWindow,
resolveSlidingSessionExpiresAt,
} from '../../utils/session-window.js'
import { setSessionExpiryHeaders } from '../../utils/session-headers.js'
import { createHttpError } from '../../utils/http.js'
import { normalizePage, normalizePageSize } from './admin-query-utils.js'
import { recordAdminLoginLog } from './admin-login-log-service.js'
@@ -39,6 +44,7 @@ export type AdminSession = {
displayName: string
role: AdminRole
expiresAt: string
absoluteExpiresAt: string
sessionVersion: number
deviceId: string
permissions: string[]
@@ -346,8 +352,8 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
})
}
const expiresAt = String(payload?.exp || '').trim()
if (!expiresAt || Date.parse(expiresAt) <= Date.now()) {
const absoluteExpiresAt = String(payload?.exp || '').trim()
if (!absoluteExpiresAt || Date.parse(absoluteExpiresAt) <= Date.now()) {
throw createHttpError('后台登录已过期,请重新登录', {
statusCode: 401,
errorCode: 'admin_auth_expired',
@@ -362,14 +368,30 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
errorCode: 'admin_auth_invalid',
})
}
const activeSession = await getActiveAdminSession(sessionId, nowIso())
const now = nowIso()
const activeSession = await getActiveAdminSession(sessionId, now)
if (!activeSession || Number(activeSession.user_id) !== userId) {
throw createHttpError('后台登录态已失效,请重新登录', {
statusCode: 401,
errorCode: 'admin_auth_revoked',
})
}
await touchAdminSession(sessionId, nowIso())
const touchedExpiresAt = await touchAdminSession({
sessionId,
now,
idleExpiresAt: resolveSlidingSessionExpiresAt({
now,
idleHours: runtimeConfig.admin.sessionIdleTtlHours,
absoluteExpiresAt,
}),
absoluteExpiresAt,
})
if (!touchedExpiresAt) {
throw createHttpError('后台登录态已失效,请重新登录', {
statusCode: 401,
errorCode: 'admin_auth_revoked',
})
}
const user = await getAdminUserById(userId)
if (!user || user.status !== 'active') {
throw createHttpError('后台账号已不可用,请重新登录', {
@@ -393,7 +415,8 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
username: String(user.username || ''),
displayName: String(user.display_name || user.username || ''),
role: normalizeAdminRole(user.role),
expiresAt,
expiresAt: touchedExpiresAt,
absoluteExpiresAt,
sessionVersion: currentSessionVersion,
deviceId: String(activeSession.device_id || ''),
permissions: await listAdminPermissionKeysForRole(normalizeAdminRole(user.role)),
@@ -406,12 +429,17 @@ export async function logoutAdmin(token: unknown): Promise<{ success: boolean; r
return { success: true, revoked }
}
export async function getAdminSessionSummary(token: unknown): Promise<JsonObject> {
export async function getAdminSessionSummary(
token: unknown,
res?: { setHeader(name: string, value: string): unknown },
): Promise<JsonObject> {
const session = await verifyAdminSessionToken(token)
if (res) setSessionExpiryHeaders(res, session)
return {
authenticated: true,
expiresAt: session.expiresAt,
absoluteExpiresAt: session.absoluteExpiresAt,
user: {
userId: session.userId,
username: session.username,
@@ -738,7 +766,12 @@ export function ensureAdminAuthConfigured(): void {
function createAdminSession(user: AdminUserRow, deviceId = ''): JsonObject {
const issuedAt = nowIso()
const expiresAt = addHours(issuedAt, Number(runtimeConfig.admin.sessionTtlHours || 12))
// Token 的 exp 是绝对上限;DB 会话的 expires_at 是空闲窗口,会在每次访问时滑动。
const { idleExpiresAt, absoluteExpiresAt } = resolveSessionExpiryWindow({
now: issuedAt,
idleHours: Number(runtimeConfig.admin.sessionIdleTtlHours || 24),
absoluteHours: Number(runtimeConfig.admin.sessionAbsoluteTtlHours || 168),
})
const payload = {
sid: crypto.randomBytes(12).toString('hex'),
uid: Number(user.id),
@@ -747,7 +780,7 @@ function createAdminSession(user: AdminUserRow, deviceId = ''): JsonObject {
dev: String(deviceId || ''),
ver: normalizeAdminSessionVersion(user.session_version),
iat: issuedAt,
exp: expiresAt,
exp: absoluteExpiresAt,
}
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
const signature = signPayload(encodedPayload)
@@ -756,7 +789,8 @@ function createAdminSession(user: AdminUserRow, deviceId = ''): JsonObject {
sessionId: payload.sid,
issuedAt,
token: `${encodedPayload}.${signature}`,
expiresAt,
expiresAt: idleExpiresAt,
absoluteExpiresAt,
user: {
userId: Number(user.id),
username: String(user.username || ''),