File size: 4,024 Bytes
077865a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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