fix(auth+bugs): optional auth bypass, login routes, conform column name, panel metadata fields, login page: auth.js
This commit is contained in:
parent
069c20ad43
commit
ada5597f79
1 changed files with 136 additions and 0 deletions
136
services/mam-api/src/routes/auth.js
Normal file
136
services/mam-api/src/routes/auth.js
Normal 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;
|
||||
Loading…
Reference in a new issue