- 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
166 lines
5.6 KiB
JavaScript
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;
|