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>
This commit is contained in:
53
src/lib/auth-edge.ts
Normal file
53
src/lib/auth-edge.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user