Project-Image-Uploader/backend/src/routes/auth.js
matthias.lotz 6332b82c6a Feature Request: admin session security
- replace bearer auth with session+CSRF flow and add admin user directory

- update frontend moderation flow, force password change gate, and new CLI

- refresh changelog/docs/feature plan + ensure swagger dev experience
2025-11-23 21:18:42 +01:00

166 lines
5.6 KiB
JavaScript

const express = require('express');
const router = express.Router();
const AdminAuthService = require('../services/AdminAuthService');
const { requireAdminAuth } = require('../middlewares/auth');
const { requireCsrf } = require('../middlewares/csrf');
router.get('/setup/status', async (req, res) => {
try {
const needsSetup = await AdminAuthService.needsInitialSetup();
const sessionUser = req.session && req.session.user
? {
id: req.session.user.id,
username: req.session.user.username,
role: req.session.user.role,
requiresPasswordChange: Boolean(req.session.user.requiresPasswordChange)
}
: null;
res.json({
needsSetup,
hasSession: Boolean(sessionUser),
user: sessionUser
});
} catch (error) {
console.error('[Auth] setup/status error:', error);
res.status(500).json({ error: 'SETUP_STATUS_FAILED' });
}
});
router.post('/setup/initial-admin', async (req, res) => {
try {
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ error: 'USERNAME_AND_PASSWORD_REQUIRED' });
}
const user = await AdminAuthService.createInitialAdmin({ username, password });
const csrfToken = AdminAuthService.startSession(req, {
...user,
requiresPasswordChange: false
});
res.status(201).json({
success: true,
user: {
id: user.id,
username: user.username,
role: user.role
},
csrfToken
});
} catch (error) {
console.error('[Auth] initial setup error:', error.message);
switch (error.message) {
case 'SETUP_ALREADY_COMPLETED':
return res.status(409).json({ error: 'SETUP_ALREADY_COMPLETED' });
case 'USERNAME_REQUIRED':
return res.status(400).json({ error: 'USERNAME_REQUIRED' });
case 'PASSWORD_TOO_WEAK':
return res.status(400).json({ error: 'PASSWORD_TOO_WEAK' });
default:
if (error.message && error.message.includes('UNIQUE')) {
return res.status(409).json({ error: 'USERNAME_IN_USE' });
}
return res.status(500).json({ error: 'INITIAL_SETUP_FAILED' });
}
}
});
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ error: 'USERNAME_AND_PASSWORD_REQUIRED' });
}
if (await AdminAuthService.needsInitialSetup()) {
return res.status(409).json({ error: 'SETUP_REQUIRED' });
}
const user = await AdminAuthService.verifyCredentials(username, password);
if (!user) {
return res.status(401).json({ error: 'INVALID_CREDENTIALS' });
}
const csrfToken = AdminAuthService.startSession(req, user);
res.json({
success: true,
user: {
id: user.id,
username: user.username,
role: user.role,
requiresPasswordChange: user.requiresPasswordChange
},
csrfToken
});
} catch (error) {
console.error('[Auth] login error:', error);
res.status(500).json({ error: 'LOGIN_FAILED' });
}
});
router.post('/logout', async (req, res) => {
try {
await AdminAuthService.destroySession(req);
res.clearCookie('sid');
res.status(204).send();
} catch (error) {
console.error('[Auth] logout error:', error);
res.status(500).json({ error: 'LOGOUT_FAILED' });
}
});
router.get('/csrf-token', requireAdminAuth, (req, res) => {
if (!req.session.csrfToken) {
req.session.csrfToken = AdminAuthService.generateCsrfToken();
}
res.json({ csrfToken: req.session.csrfToken });
});
router.post('/change-password', requireAdminAuth, requireCsrf, async (req, res) => {
try {
const { currentPassword, newPassword } = req.body || {};
if (!currentPassword || !newPassword) {
return res.status(400).json({ error: 'CURRENT_AND_NEW_PASSWORD_REQUIRED' });
}
const user = await AdminAuthService.changePassword({
userId: req.session.user.id,
currentPassword,
newPassword
});
req.session.user = {
...req.session.user,
requiresPasswordChange: false
};
res.json({
success: true,
user: {
id: user.id,
username: user.username,
role: user.role,
requiresPasswordChange: false
}
});
} catch (error) {
console.error('[Auth] change password error:', error.message || error);
switch (error.message) {
case 'CURRENT_PASSWORD_REQUIRED':
return res.status(400).json({ error: 'CURRENT_PASSWORD_REQUIRED' });
case 'PASSWORD_TOO_WEAK':
return res.status(400).json({ error: 'PASSWORD_TOO_WEAK' });
case 'INVALID_CURRENT_PASSWORD':
return res.status(400).json({ error: 'INVALID_CURRENT_PASSWORD' });
case 'USER_NOT_FOUND':
return res.status(404).json({ error: 'USER_NOT_FOUND' });
default:
return res.status(500).json({ error: 'PASSWORD_CHANGE_FAILED' });
}
}
});
module.exports = router;