feat: 我的页整合扫一扫/设置与提现、all-chapters去重、内容上传API、文档与后台登录
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
92
lib/admin-auth.ts
Normal file
92
lib/admin-auth.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 后台管理员登录鉴权:生成/校验签名 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
|
||||
}
|
||||
Reference in New Issue
Block a user