initiation librodrome
This commit is contained in:
56
server/utils/admin-auth.ts
Normal file
56
server/utils/admin-auth.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto'
|
||||
|
||||
const TOKEN_COOKIE = 'admin_token'
|
||||
const TOKEN_MAX_AGE = 60 * 60 * 24 // 24h
|
||||
|
||||
export function signToken(payload: string, secret: string): string {
|
||||
const hmac = createHmac('sha256', secret)
|
||||
hmac.update(payload)
|
||||
return payload + '.' + hmac.digest('hex')
|
||||
}
|
||||
|
||||
export function verifyToken(token: string, secret: string): string | null {
|
||||
const dotIndex = token.lastIndexOf('.')
|
||||
if (dotIndex === -1) return null
|
||||
|
||||
const payload = token.slice(0, dotIndex)
|
||||
const expected = signToken(payload, secret)
|
||||
|
||||
const a = Buffer.from(token)
|
||||
const b = Buffer.from(expected)
|
||||
|
||||
if (a.length !== b.length) return null
|
||||
if (!timingSafeEqual(a, b)) return null
|
||||
|
||||
// Check expiry
|
||||
try {
|
||||
const data = JSON.parse(payload)
|
||||
if (data.exp && data.exp < Math.floor(Date.now() / 1000)) return null
|
||||
return payload
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function setAdminCookie(event: any, secret: string): void {
|
||||
const exp = Math.floor(Date.now() / 1000) + TOKEN_MAX_AGE
|
||||
const payload = JSON.stringify({ role: 'admin', exp })
|
||||
const token = signToken(payload, secret)
|
||||
|
||||
setCookie(event, TOKEN_COOKIE, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: TOKEN_MAX_AGE,
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
export function clearAdminCookie(event: any): void {
|
||||
deleteCookie(event, TOKEN_COOKIE, { path: '/' })
|
||||
}
|
||||
|
||||
export function getAdminToken(event: any): string | null {
|
||||
return getCookie(event, TOKEN_COOKIE) ?? null
|
||||
}
|
||||
37
server/utils/content.ts
Normal file
37
server/utils/content.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import yaml from 'yaml'
|
||||
|
||||
const dataDir = join(process.cwd(), 'data')
|
||||
|
||||
const cache = new Map<string, { data: unknown; mtime: number }>()
|
||||
|
||||
export async function readYaml<T = unknown>(relativePath: string): Promise<T> {
|
||||
const filePath = join(dataDir, relativePath)
|
||||
const cached = cache.get(filePath)
|
||||
|
||||
if (cached && Date.now() - cached.mtime < 5000) {
|
||||
return cached.data as T
|
||||
}
|
||||
|
||||
const raw = await readFile(filePath, 'utf-8')
|
||||
const data = yaml.parse(raw) as T
|
||||
cache.set(filePath, { data, mtime: Date.now() })
|
||||
return data
|
||||
}
|
||||
|
||||
export async function writeYaml(relativePath: string, data: unknown): Promise<void> {
|
||||
const filePath = join(dataDir, relativePath)
|
||||
const raw = yaml.stringify(data, { lineWidth: 120 })
|
||||
await writeFile(filePath, raw, 'utf-8')
|
||||
cache.delete(filePath)
|
||||
}
|
||||
|
||||
export function invalidateCache(relativePath?: string): void {
|
||||
if (relativePath) {
|
||||
cache.delete(join(dataDir, relativePath))
|
||||
}
|
||||
else {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user