77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
import { Router } from 'express';
|
|
import { db } from '../db/client';
|
|
import { users } from '../db/schema';
|
|
import {
|
|
anyUsersExist,
|
|
createSession,
|
|
destroySession,
|
|
failedLoginDelayMs,
|
|
findUserByUsername,
|
|
getSessionUser,
|
|
hashPassword,
|
|
sleep,
|
|
validateNewCredentials,
|
|
verifyPassword,
|
|
} from '../auth';
|
|
|
|
const router = Router();
|
|
|
|
// Drives the client's auth gate: `configured: false` → show the first-run
|
|
// setup screen; unauthenticated → login screen; otherwise `user` identifies
|
|
// the session for the UI.
|
|
router.get('/status', (req, res) => {
|
|
const user = getSessionUser(req);
|
|
res.json({
|
|
configured: anyUsersExist(),
|
|
authenticated: user !== null,
|
|
user: user ?? null,
|
|
});
|
|
});
|
|
|
|
// First-run onboarding: creates the first account and signs it in. Locked to
|
|
// the empty-users state so it can never be used to add accounts later (that's
|
|
// POST /api/users, which requires a session).
|
|
router.post('/setup', (req, res) => {
|
|
if (anyUsersExist()) {
|
|
res.status(409).json({ error: 'Setup has already been completed' });
|
|
return;
|
|
}
|
|
|
|
const { username, password } = req.body ?? {};
|
|
const invalid = validateNewCredentials(username, password);
|
|
if (invalid) {
|
|
res.status(400).json({ error: invalid });
|
|
return;
|
|
}
|
|
|
|
const created = db
|
|
.insert(users)
|
|
.values({ username: (username as string).trim(), passwordHash: hashPassword(password as string) })
|
|
.returning({ id: users.id, username: users.username })
|
|
.get();
|
|
|
|
createSession(res, created.id);
|
|
res.status(201).json({ authenticated: true, user: created });
|
|
});
|
|
|
|
router.post('/login', async (req, res) => {
|
|
const { username, password } = req.body ?? {};
|
|
const user = typeof username === 'string' ? findUserByUsername(username) : undefined;
|
|
|
|
if (!user || typeof password !== 'string' || !verifyPassword(password, user.passwordHash)) {
|
|
await sleep(failedLoginDelayMs); // flat delay = cheap brute-force damping
|
|
res.status(401).json({ error: 'Incorrect username or password' });
|
|
return;
|
|
}
|
|
|
|
createSession(res, user.id);
|
|
res.json({ authenticated: true, user: { id: user.id, username: user.username } });
|
|
});
|
|
|
|
router.post('/logout', (req, res) => {
|
|
destroySession(req, res);
|
|
res.json({ authenticated: false });
|
|
});
|
|
|
|
export default router;
|