Nryn215's picture
Upload folder using huggingface_hub
077865a verified
Raw
History Blame Contribute Delete
4.02 kB
import { Router } from 'express';
import { z } from 'zod';
import { userCount, createUser, verifyCredentials, createSession, validateSession, deleteSession, } from '../services/auth.js';
export const authRouter = Router();
// Dashboard auth (#35). These routes are mounted BEFORE requireAuth, so
// /status, /setup and /login are reachable without a session (bootstrap);
// /logout and /me validate the token themselves.
const credentialsSchema = z.object({
email: z.string().email('A valid email is required'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
// ── Brute-force throttle ──────────────────────────────────────────────────
// Simple in-memory per-email limiter. A local single-user tool doesn't need a
// distributed store; this just blunts online password guessing.
const MAX_ATTEMPTS = 5;
const LOCKOUT_MS = 15 * 60 * 1000;
const attempts = new Map();
function isLockedOut(email) {
const a = attempts.get(email.toLowerCase());
return !!a && a.lockedUntil > Date.now();
}
function recordFailure(email) {
const key = email.toLowerCase();
const a = attempts.get(key) ?? { count: 0, lockedUntil: 0 };
a.count++;
if (a.count >= MAX_ATTEMPTS) {
a.lockedUntil = Date.now() + LOCKOUT_MS;
a.count = 0;
}
attempts.set(key, a);
}
function clearFailures(email) {
attempts.delete(email.toLowerCase());
}
function bearer(req) {
return req.headers.authorization?.replace(/^Bearer\s+/i, '')
?? req.headers['x-dashboard-token'];
}
// Has the dashboard been set up yet, and is this caller authenticated?
authRouter.get('/status', (req, res) => {
const session = validateSession(bearer(req));
res.json({
needsSetup: userCount() === 0,
authenticated: !!session,
email: session?.email ?? null,
});
});
// First-run account creation. Only allowed while there are zero users, so it
// can't be used to add accounts once the dashboard is claimed.
authRouter.post('/setup', (req, res) => {
if (userCount() > 0) {
res.status(409).json({ error: { message: 'Setup already completed. Use login instead.', type: 'setup_complete' } });
return;
}
const parsed = credentialsSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: { message: parsed.error.errors.map(e => e.message).join(', ') } });
return;
}
const user = createUser(parsed.data.email, parsed.data.password);
const token = createSession(user.userId);
res.status(201).json({ token, email: user.email });
});
authRouter.post('/login', (req, res) => {
const parsed = credentialsSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: { message: parsed.error.errors.map(e => e.message).join(', ') } });
return;
}
const { email, password } = parsed.data;
if (isLockedOut(email)) {
res.status(429).json({ error: { message: 'Too many failed attempts. Try again later.', type: 'rate_limit_error' } });
return;
}
const user = verifyCredentials(email, password);
if (!user) {
recordFailure(email);
// Same message whether the email exists or not β€” don't leak which.
res.status(401).json({ error: { message: 'Invalid email or password', type: 'authentication_error' } });
return;
}
clearFailures(email);
const token = createSession(user.userId);
res.json({ token, email: user.email });
});
authRouter.post('/logout', (req, res) => {
deleteSession(bearer(req));
res.json({ success: true });
});
authRouter.get('/me', (req, res) => {
const session = validateSession(bearer(req));
if (!session) {
res.status(401).json({ error: { message: 'Authentication required', type: 'authentication_error' } });
return;
}
res.json({ email: session.email });
});
//# sourceMappingURL=auth.js.map