fix(auth+bugs): optional auth bypass, login routes, conform column name, panel metadata fields, login page: auth.js

This commit is contained in:
Zac Gaetano 2026-05-15 23:40:11 -04:00
parent 069c20ad43
commit ada5597f79

View file

@ -0,0 +1,136 @@
/**
* Authentication routes
*
* POST /api/v1/auth/login exchange username+password for a session cookie
* POST /api/v1/auth/logout destroy the current session
* GET /api/v1/auth/me return the currently authenticated user
* POST /api/v1/auth/setup one-time admin bootstrap (disabled after first user exists)
*/
import express from 'express';
import bcrypt from 'bcrypt';
import pool from '../db/pool.js';
const router = express.Router();
// ---------------------------------------------------------------------------
// POST /login
// ---------------------------------------------------------------------------
router.post('/login', async (req, res, next) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
const result = await pool.query(
'SELECT * FROM users WHERE username = $1',
[username.trim().toLowerCase()]
);
if (result.rows.length === 0) {
// Timing-safe: still run compare on a dummy hash so response time is constant
await bcrypt.compare(password, '$2b$12$invalidhashpadding000000000000000000000000000000000000');
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Regenerate session ID to prevent fixation attacks
req.session.regenerate((err) => {
if (err) return next(err);
req.session.userId = user.id;
req.session.username = user.username;
req.session.role = user.role;
res.json({
id: user.id,
username: user.username,
display_name: user.display_name,
role: user.role,
});
});
} catch (err) {
next(err);
}
});
// ---------------------------------------------------------------------------
// POST /logout
// ---------------------------------------------------------------------------
router.post('/logout', (req, res, next) => {
req.session.destroy((err) => {
if (err) return next(err);
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
// ---------------------------------------------------------------------------
// GET /me
// ---------------------------------------------------------------------------
router.get('/me', async (req, res) => {
if (!req.session || !req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
const result = await pool.query(
'SELECT id, username, display_name, role FROM users WHERE id = $1',
[req.session.userId]
);
if (result.rows.length === 0) {
req.session.destroy(() => {});
return res.status(401).json({ error: 'User not found' });
}
res.json(result.rows[0]);
} catch (err) {
// Fallback to session data if DB unreachable
res.json({
id: req.session.userId,
username: req.session.username,
role: req.session.role,
});
}
});
// ---------------------------------------------------------------------------
// POST /setup — one-time first-admin bootstrap
// ---------------------------------------------------------------------------
router.post('/setup', async (req, res, next) => {
try {
const { username, password, display_name } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
// Block if any user already exists
const count = await pool.query('SELECT COUNT(*) FROM users');
if (parseInt(count.rows[0].count, 10) > 0) {
return res.status(403).json({
error: 'Setup is already complete. Use an existing admin account to add more users.',
});
}
const hash = await bcrypt.hash(password, 12);
const result = await pool.query(
`INSERT INTO users (username, password_hash, display_name, role)
VALUES ($1, $2, $3, 'admin')
RETURNING id, username, display_name, role`,
[username.trim().toLowerCase(), hash, display_name || username]
);
res.status(201).json(result.rows[0]);
} catch (err) {
next(err);
}
});
export default router;