Files
soul/lib/admin-auth.ts

93 lines
3.0 KiB
TypeScript
Raw Normal View History

/**
* / Cookie
* admin / key123456 .cursorrules
*/
import { createHmac, timingSafeEqual } from 'crypto'
const COOKIE_NAME = 'admin_session'
const MAX_AGE_SEC = 7 * 24 * 3600 // 7 天
const SECRET = process.env.ADMIN_SESSION_SECRET || 'soul-admin-secret-change-in-prod'
export function getAdminCredentials() {
return {
username: process.env.ADMIN_USERNAME || 'admin',
password: process.env.ADMIN_PASSWORD || 'key123456',
}
}
export function verifyAdminCredentials(username: string, password: string): boolean {
const { username: u, password: p } = getAdminCredentials()
return username === u && password === p
}
function sign(payload: string): string {
return createHmac('sha256', SECRET).update(payload).digest('base64url')
}
/** 生成签名 token写入 Cookie 用 */
export function createAdminToken(): string {
const exp = Math.floor(Date.now() / 1000) + MAX_AGE_SEC
const payload = `${exp}`
const sig = sign(payload)
return `${payload}.${sig}`
}
/** 校验 Cookie 中的 token */
export function verifyAdminToken(token: string | null | undefined): boolean {
if (!token || typeof token !== 'string') return false
const dot = token.indexOf('.')
if (dot === -1) return false
const payload = token.slice(0, dot)
const sig = token.slice(dot + 1)
const exp = parseInt(payload, 10)
if (Number.isNaN(exp) || exp < Math.floor(Date.now() / 1000)) return false
const expected = sign(payload)
if (typeof expected !== 'string' || typeof sig !== 'string') return false
if (sig.length !== expected.length) return false
try {
return timingSafeEqual(Buffer.from(sig, 'base64url'), Buffer.from(expected, 'base64url'))
} catch {
return false
}
}
export function getAdminCookieName() {
return COOKIE_NAME
}
export function getAdminCookieOptions() {
return {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' as const,
maxAge: MAX_AGE_SEC,
path: '/',
}
}
/** 从请求中读取 admin cookie 并校验,未通过时返回 null */
export function getAdminTokenFromRequest(request: Request): string | null {
const cookieHeader = request.headers.get('cookie')
if (!cookieHeader) return null
const name = COOKIE_NAME + '='
const start = cookieHeader.indexOf(name)
if (start === -1) return null
const valueStart = start + name.length
const end = cookieHeader.indexOf(';', valueStart)
const value = end === -1 ? cookieHeader.slice(valueStart) : cookieHeader.slice(valueStart, end)
return value.trim() || null
}
/** 若未登录则返回 401 Response供各 admin API 使用 */
export function requireAdminResponse(request: Request): Response | null {
const token = getAdminTokenFromRequest(request)
if (!verifyAdminToken(token)) {
return new Response(JSON.stringify({ error: '未授权访问,请先登录' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
return null
}