Add guestbook with WebAuthn admin and SQLite
Introduce server-rendered guestbook and moderation portal. Persist data in SQLite (better-sqlite3); add WebAuthn YubiKey admin auth, rate-limiting, spam heuristics, and sanitization. Switch Docker image to run Node/standalone Astro (remove nginx), update docker-compose, Dockerfile, astro.config, and package.json. Add .env.example, docs/guestbook.md, gitignore updates, layouts, API routes, and supporting lib/components/pages for the feature.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import db from './db';
|
||||
|
||||
function getSecretKey(): string {
|
||||
const key = process.env.SECRET_KEY;
|
||||
if (!key) throw new Error('SECRET_KEY environment variable is required');
|
||||
return key;
|
||||
}
|
||||
|
||||
export function hashIP(ip: string): string {
|
||||
return createHash('sha256').update(getSecretKey() + ':' + ip).digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
export function generateId(): string {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
export interface AdminSession {
|
||||
id: string;
|
||||
user_id: string;
|
||||
created_at: string;
|
||||
expires_at: string;
|
||||
}
|
||||
|
||||
export function createSession(userId: string): string {
|
||||
const sessionId = generateId();
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO admin_sessions (id, user_id, expires_at) VALUES (?, ?, ?)`
|
||||
).run(sessionId, userId, expiresAt);
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
export function getSession(sessionId: string): AdminSession | undefined {
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT * FROM admin_sessions
|
||||
WHERE id = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`
|
||||
)
|
||||
.get(sessionId) as AdminSession | undefined;
|
||||
}
|
||||
|
||||
export function deleteSession(sessionId: string): void {
|
||||
db.prepare(`DELETE FROM admin_sessions WHERE id = ?`).run(sessionId);
|
||||
}
|
||||
|
||||
export function cleanExpiredSessions(): void {
|
||||
db.prepare(
|
||||
`DELETE FROM admin_sessions WHERE expires_at <= strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`
|
||||
).run();
|
||||
db.prepare(
|
||||
`DELETE FROM webauthn_challenges WHERE expires_at <= strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`
|
||||
).run();
|
||||
}
|
||||
|
||||
export const SESSION_COOKIE = 'admin_session';
|
||||
export const CHALLENGE_COOKIE = 'webauthn_challenge';
|
||||
|
||||
export function sessionCookieOptions(maxAge: number) {
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict' as const,
|
||||
path: '/',
|
||||
maxAge,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user