Files
blackheart-website/src/lib/auth-edge.ts
diana.dolgolyova 27c1348f89 feat: admin panel with SQLite, auth, and calendar-style schedule editor
Complete admin panel for content management:
- SQLite database with better-sqlite3, seed script from content.ts
- Simple password auth with HMAC-signed cookies (Edge + Node compatible)
- 9 section editors: meta, hero, about, team, classes, schedule, pricing, FAQ, contact
- Team CRUD with image upload and drag reorder
- Schedule editor with Google Calendar-style visual timeline (colored blocks, overlap detection, click-to-add)
- All public components refactored to accept data props from DB (with fallback to static content)
- Middleware protecting /admin/* and /api/admin/* routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:59:12 +03:00

54 lines
1.4 KiB
TypeScript

/**
* Edge-compatible auth helpers (for middleware).
* Uses Web Crypto API instead of Node.js crypto.
*/
const COOKIE_NAME = "bh-admin-token";
function getSecret(): string {
const secret = process.env.AUTH_SECRET;
if (!secret) throw new Error("AUTH_SECRET is not set");
return secret;
}
function base64urlEncode(buf: ArrayBuffer): string {
const bytes = new Uint8Array(buf);
let binary = "";
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
async function hmacSign(data: string, secret: string): Promise<string> {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
return base64urlEncode(sig);
}
export async function verifyToken(token: string): Promise<boolean> {
try {
const [data, sig] = token.split(".");
if (!data || !sig) return false;
const expectedSig = await hmacSign(data, getSecret());
if (sig !== expectedSig) return false;
const payload = JSON.parse(atob(data.replace(/-/g, "+").replace(/_/g, "/"))) as {
role: string;
exp: number;
};
return payload.role === "admin" && payload.exp > Date.now();
} catch {
return false;
}
}
export { COOKIE_NAME };