SPB Git forge
2commits 1branches 0releases
492.0 KBsize
maindefault branch
yesterdaylast push
TypeScript 96.1% CSS 3.1% JavaScript 0.8%
2.7 KB · 75 lines typescript
Raw Blame History
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