62 lines
1.8 KiB
JavaScript
Executable file
62 lines
1.8 KiB
JavaScript
Executable file
'use strict';
|
|
const express = require('express');
|
|
const bcrypt = require('bcryptjs');
|
|
const { getUserByUsername } = require('./db');
|
|
|
|
const router = express.Router();
|
|
|
|
// Dummy hash — used when user is not found to prevent timing attacks
|
|
const DUMMY_HASH = '$2b$12$invaliddummyhashfortimingattttttttttttttttttttttttttt';
|
|
|
|
// POST /auth/login
|
|
router.post('/login', async (req, res) => {
|
|
const username = String(req.body?.username ?? '').trim();
|
|
const password = String(req.body?.password ?? '');
|
|
|
|
if (!username || !password) {
|
|
return res.status(400).json({ error: 'Username and password are required.' });
|
|
}
|
|
|
|
const user = getUserByUsername(username);
|
|
const hash = user?.password_hash ?? DUMMY_HASH;
|
|
|
|
// Always run bcrypt comparison — even for unknown users — to prevent timing-based enumeration
|
|
const valid = await bcrypt.compare(password, hash);
|
|
|
|
if (!valid || !user) {
|
|
return res.status(401).json({ error: 'Invalid username or password.' });
|
|
}
|
|
|
|
// Regenerate session ID to prevent session fixation
|
|
req.session.regenerate((err) => {
|
|
if (err) return res.status(500).json({ error: 'Session error.' });
|
|
|
|
req.session.user = {
|
|
id: user.id,
|
|
username: user.username,
|
|
name: user.name,
|
|
role: user.role,
|
|
syncro_customer_id: user.syncro_customer_id ?? null,
|
|
};
|
|
|
|
res.json({ ok: true, user: req.session.user });
|
|
});
|
|
});
|
|
|
|
// POST /auth/logout
|
|
router.post('/logout', (req, res) => {
|
|
req.session.destroy(() => {
|
|
res.clearCookie('sid');
|
|
res.json({ ok: true });
|
|
});
|
|
});
|
|
|
|
// GET /auth/me — returns current session user (used by the SPA on startup)
|
|
router.get('/me', (req, res) => {
|
|
if (!req.session?.user) {
|
|
return res.status(401).json({ error: 'Not authenticated.' });
|
|
}
|
|
res.json({ user: req.session.user });
|
|
});
|
|
|
|
module.exports = router;
|