spb/maclustr-console
Public
TypeScript 96.1%
CSS 3.1%
JavaScript 0.8%
1// Lightweight auth for the MacLustr web console.2// 10 fixed accounts: user1..user10 with passwords admin1..admin10.3// Sessions are HMAC-SHA256 signed tokens stored in an httpOnly cookie.4// Works in both the Edge (middleware) and Node (route handler) runtimes5// using Web Crypto + base64url (no Buffer dependency).67const SECRET = process.env.AUTH_SECRET || "maclustr-console-7f3a9c2e8b14-secret";8export const SESSION_COOKIE = "ml_session";9const SESSION_TTL_SECONDS = 60 * 60 * 12; // 12h1011export const USERS: Record<string, string> = Object.fromEntries(12 Array.from({ length: 10 }, (_, i) => [`user${i + 1}`, `admin${i + 1}`])13);1415export function checkCredentials(user: string, pass: string): boolean {16 const expected = USERS[user];17 return expected !== undefined && expected === pass;18}1920function b64url(bytes: Uint8Array): string {21 let s = "";22 for (const b of bytes) s += String.fromCharCode(b);23 return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");24}2526async function hmac(data: string): Promise<string> {27 const key = await crypto.subtle.importKey(28 "raw",29 new TextEncoder().encode(SECRET),30 { name: "HMAC", hash: "SHA-256" },31 false,32 ["sign"]33 );34 const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));35 return b64url(new Uint8Array(sig));36}3738export async function signSession(user: string): Promise<string> {39 const exp = Math.floor(Date.now() / 1000) + SESSION_TTL_SECONDS;40 const payload = `${user}.${exp}`;41 const sig = await hmac(payload);42 return `${payload}.${sig}`;43}4445/** Returns the username if the token is valid and unexpired, else null. */46export async function verifySession(token: string | undefined): Promise<string | null> {47 if (!token) return null;48 const parts = token.split(".");49 if (parts.length !== 3) return null;50 const [user, expStr, sig] = parts;51 const expected = await hmac(`${user}.${expStr}`);52 if (sig.length !== expected.length) return null;53 let diff = 0;54 for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);55 if (diff !== 0) return null;56 const exp = parseInt(expStr, 10);57 if (!Number.isFinite(exp) || exp < Math.floor(Date.now() / 1000)) return null;58 return user;59}6061/** Validate an HTTP Basic Auth header (used by API clients such as the iOS app). */62export function checkBasicAuth(header: string | null): boolean {63 if (!header || !header.startsWith("Basic ")) return false;64 try {65 const decoded = atob(header.slice(6));66 const idx = decoded.indexOf(":");67 if (idx < 0) return false;68 return checkCredentials(decoded.slice(0, idx), decoded.slice(idx + 1));69 } catch {70 return false;71 }72}7374export const SESSION_MAX_AGE = SESSION_TTL_SECONDS;75