Spaces:
Sleeping
Sleeping
Download backend/src/lib/localAuth.ts from bardd/mike-backend: direct link, hf CLI and curl.
- Browser
- Download file 8.58 kB
-
https://huggingface.co/spaces/bardd/mike-backend/resolve/main/backend/src/lib/localAuth.ts
- Command line
-
hf download hf://spaces/bardd/mike-backend/backend/src/lib/localAuth.ts
-
curl -L -o localAuth.ts https://huggingface.co/spaces/bardd/mike-backend/resolve/main/backend/src/lib/localAuth.ts
8.58 kB
| import crypto from "crypto"; | |
| import type { Request } from "express"; | |
| import { getDatabasePool } from "./db"; | |
| const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }; | |
| const SCRYPT_KEYLEN = 32; | |
| const PASSWORD_PREFIX = "scrypt:v1:"; | |
| const PILOT_SIGNUP_LOCK_NAMESPACE = 0x6d696b65; // "mike" | |
| const PILOT_SIGNUP_LOCK_RESOURCE = 0x61757468; // "auth" | |
| export type LocalAuthUser = { | |
| id: string; | |
| email: string; | |
| is_operator: boolean; | |
| disabled: boolean; | |
| }; | |
| function sessionDays(): number { | |
| const parsed = parseInt(process.env.LOCAL_AUTH_SESSION_DAYS ?? "14", 10); | |
| return Number.isFinite(parsed) && parsed > 0 ? parsed : 14; | |
| } | |
| export function hashPassword(password: string): Promise<string> { | |
| const salt = crypto.randomBytes(16); | |
| return new Promise((resolve, reject) => { | |
| crypto.scrypt(password, salt, SCRYPT_KEYLEN, SCRYPT_PARAMS, (err, derived) => { | |
| if (err) { | |
| reject(err); | |
| return; | |
| } | |
| resolve( | |
| `${PASSWORD_PREFIX}${salt.toString("base64url")}:${derived.toString("base64url")}`, | |
| ); | |
| }); | |
| }); | |
| } | |
| export async function verifyPassword( | |
| password: string, | |
| storedHash: string, | |
| ): Promise<boolean> { | |
| if (!storedHash.startsWith(PASSWORD_PREFIX)) { | |
| return false; | |
| } | |
| const payload = storedHash.slice(PASSWORD_PREFIX.length); | |
| const separator = payload.indexOf(":"); | |
| if (separator <= 0) { | |
| return false; | |
| } | |
| const salt = Buffer.from(payload.slice(0, separator), "base64url"); | |
| const expected = Buffer.from(payload.slice(separator + 1), "base64url"); | |
| if (expected.length !== SCRYPT_KEYLEN) { | |
| return false; | |
| } | |
| const derived = await new Promise<Buffer>((resolve, reject) => { | |
| crypto.scrypt(password, salt, SCRYPT_KEYLEN, SCRYPT_PARAMS, (err, key) => { | |
| if (err) { | |
| reject(err); | |
| return; | |
| } | |
| resolve(key); | |
| }); | |
| }); | |
| return crypto.timingSafeEqual(expected, derived); | |
| } | |
| export function hashSessionToken(token: string): string { | |
| return crypto.createHash("sha256").update(token).digest("hex"); | |
| } | |
| export function generateSessionToken(): string { | |
| return crypto.randomBytes(32).toString("base64url"); | |
| } | |
| export function generateUserId(): string { | |
| return crypto.randomUUID(); | |
| } | |
| export function getPilotConfig() { | |
| const enabled = process.env.PILOT_SIGNUP_ENABLED === "1"; | |
| const limit = Math.max( | |
| 0, | |
| parseInt(process.env.PILOT_SIGNUP_LIMIT ?? "6", 10) || 0, | |
| ); | |
| const exemptEmails = (process.env.PILOT_SIGNUP_EXEMPT_EMAILS ?? "") | |
| .split(",") | |
| .map((email) => email.trim().toLowerCase()) | |
| .filter(Boolean); | |
| return { enabled, limit, exemptEmails }; | |
| } | |
| export function isExemptEmail(email: string, exemptEmails: string[]): boolean { | |
| return exemptEmails.includes(email.toLowerCase()); | |
| } | |
| export async function deleteExpiredSessions(): Promise<void> { | |
| await getDatabasePool().query( | |
| `DELETE FROM auth.sessions WHERE expires_at <= now()`, | |
| ); | |
| } | |
| export async function countPilotUsers(exemptEmails: string[]): Promise<number> { | |
| const result = await getDatabasePool().query<{ count: string }>( | |
| `SELECT count(*)::text AS count | |
| FROM auth.users | |
| WHERE disabled = false | |
| AND is_operator = false | |
| AND NOT (lower(email) = ANY($1::text[]))`, | |
| [exemptEmails], | |
| ); | |
| return parseInt(result.rows[0]?.count ?? "0", 10) || 0; | |
| } | |
| export async function getSignupStatus() { | |
| const { enabled, limit, exemptEmails } = getPilotConfig(); | |
| if (!enabled) { | |
| return { enabled: false, limit, used: 0, remaining: 0 }; | |
| } | |
| await deleteExpiredSessions(); | |
| const used = await countPilotUsers(exemptEmails); | |
| const remaining = Math.max(0, limit - used); | |
| return { enabled: true, limit, used, remaining }; | |
| } | |
| type CreateLocalUserOptions = { | |
| isOperator?: boolean; | |
| skipCapCheck?: boolean; | |
| }; | |
| export async function createLocalUser( | |
| email: string, | |
| password: string, | |
| options: CreateLocalUserOptions = {}, | |
| ): Promise<LocalAuthUser> { | |
| const normalizedEmail = email.trim().toLowerCase(); | |
| const passwordHash = await hashPassword(password); | |
| const userId = generateUserId(); | |
| const isOperator = options.isOperator === true; | |
| const pool = getDatabasePool(); | |
| const client = await pool.connect(); | |
| try { | |
| await client.query("BEGIN"); | |
| if (!options.skipCapCheck && !isOperator) { | |
| const { enabled, limit, exemptEmails } = getPilotConfig(); | |
| if (!enabled) { | |
| throw new Error("Pilot signup is closed"); | |
| } | |
| if (isExemptEmail(normalizedEmail, exemptEmails)) { | |
| throw new Error( | |
| "This email is reserved for operator access and cannot use pilot signup", | |
| ); | |
| } | |
| await client.query(`SELECT pg_advisory_xact_lock($1::integer, $2::integer)`, [ | |
| PILOT_SIGNUP_LOCK_NAMESPACE, | |
| PILOT_SIGNUP_LOCK_RESOURCE, | |
| ]); | |
| const countResult = await client.query<{ count: string }>( | |
| `SELECT count(*)::text AS count | |
| FROM auth.users | |
| WHERE disabled = false | |
| AND is_operator = false | |
| AND NOT (lower(email) = ANY($1::text[]))`, | |
| [exemptEmails], | |
| ); | |
| const used = parseInt(countResult.rows[0]?.count ?? "0", 10) || 0; | |
| if (used >= limit) { | |
| throw new Error("Pilot signup is full"); | |
| } | |
| } | |
| const insertResult = await client.query<LocalAuthUser>( | |
| `INSERT INTO auth.users (id, email, password_hash, is_operator) | |
| VALUES ($1, $2, $3, $4) | |
| RETURNING id, email, is_operator, disabled`, | |
| [userId, normalizedEmail, passwordHash, isOperator], | |
| ); | |
| await client.query("COMMIT"); | |
| return insertResult.rows[0]; | |
| } catch (err) { | |
| await client.query("ROLLBACK"); | |
| throw err; | |
| } finally { | |
| client.release(); | |
| } | |
| } | |
| export async function findUserByEmail( | |
| email: string, | |
| ): Promise<(LocalAuthUser & { password_hash: string | null }) | null> { | |
| const result = await getDatabasePool().query< | |
| LocalAuthUser & { password_hash: string | null } | |
| >( | |
| `SELECT id, email, password_hash, is_operator, disabled | |
| FROM auth.users | |
| WHERE lower(email) = lower($1) | |
| LIMIT 1`, | |
| [email.trim()], | |
| ); | |
| return result.rows[0] ?? null; | |
| } | |
| export async function createSession( | |
| userId: string, | |
| reqMeta?: { userAgent?: string; ip?: string }, | |
| ): Promise<{ token: string; expiresAt: Date }> { | |
| const token = generateSessionToken(); | |
| const tokenHash = hashSessionToken(token); | |
| const expiresAt = new Date( | |
| Date.now() + sessionDays() * 24 * 60 * 60 * 1000, | |
| ); | |
| await getDatabasePool().query( | |
| `INSERT INTO auth.sessions (token_hash, user_id, expires_at, user_agent, ip) | |
| VALUES ($1, $2, $3, $4, $5)`, | |
| [ | |
| tokenHash, | |
| userId, | |
| expiresAt.toISOString(), | |
| reqMeta?.userAgent ?? null, | |
| reqMeta?.ip ?? null, | |
| ], | |
| ); | |
| return { token, expiresAt }; | |
| } | |
| export async function verifySessionToken( | |
| token: string, | |
| ): Promise<LocalAuthUser | null> { | |
| const tokenHash = hashSessionToken(token); | |
| const result = await getDatabasePool().query<LocalAuthUser>( | |
| `SELECT u.id, u.email, u.is_operator, u.disabled | |
| FROM auth.sessions s | |
| JOIN auth.users u ON u.id = s.user_id | |
| WHERE s.token_hash = $1 | |
| AND s.expires_at > now() | |
| LIMIT 1`, | |
| [tokenHash], | |
| ); | |
| const user = result.rows[0]; | |
| if (!user || user.disabled) { | |
| return null; | |
| } | |
| return user; | |
| } | |
| export async function deleteSession(token: string): Promise<void> { | |
| const tokenHash = hashSessionToken(token); | |
| await getDatabasePool().query(`DELETE FROM auth.sessions WHERE token_hash = $1`, [ | |
| tokenHash, | |
| ]); | |
| } | |
| export async function deleteUserSessions(userId: string): Promise<void> { | |
| await getDatabasePool().query(`DELETE FROM auth.sessions WHERE user_id = $1`, [ | |
| userId, | |
| ]); | |
| } | |
| export async function updateUserPassword( | |
| email: string, | |
| password: string, | |
| ): Promise<boolean> { | |
| const passwordHash = await hashPassword(password); | |
| const result = await getDatabasePool().query( | |
| `UPDATE auth.users | |
| SET password_hash = $1, updated_at = now() | |
| WHERE lower(email) = lower($2)`, | |
| [passwordHash, email.trim()], | |
| ); | |
| return (result.rowCount ?? 0) > 0; | |
| } | |
| export function getRequestMeta(req: Request) { | |
| const forwarded = req.headers["x-forwarded-for"]; | |
| const ip = | |
| typeof forwarded === "string" | |
| ? forwarded.split(",")[0]?.trim() | |
| : req.socket.remoteAddress; | |
| return { | |
| userAgent: | |
| typeof req.headers["user-agent"] === "string" | |
| ? req.headers["user-agent"] | |
| : undefined, | |
| ip, | |
| }; | |
| } | |
| export function isValidEmail(email: string): boolean { | |
| return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); | |
| } | |