320 lines
9.8 KiB
JavaScript
320 lines
9.8 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../db');
|
|
const auth = require('../auth');
|
|
const { requestLimit } = require('../services/requestLimits');
|
|
const authTokens = require('../services/authTokens');
|
|
const loginLimit = requestLimit(20);
|
|
|
|
// Configure Passport strategies
|
|
auth.configureLocalStrategy(
|
|
async (username) => await db.users.getByUsername(username),
|
|
async (password, hash) => await auth.verifyPassword(password, hash)
|
|
);
|
|
|
|
auth.configureJwtStrategy(
|
|
async (id) => await db.users.getById(id)
|
|
);
|
|
|
|
// Configure Passport session serialization (required for OIDC)
|
|
auth.configureSessionSerialization(
|
|
async (id) => await db.users.getById(id)
|
|
);
|
|
|
|
// Configure OIDC once; SSO routes wait until discovery and registration finish.
|
|
let oidcAvailable = false;
|
|
const oidcReady = auth.configureOidcStrategy(
|
|
async (oidcId) => await db.users.getByOidcId(oidcId),
|
|
async (email) => await db.users.getByEmail(email),
|
|
async (userData) => await db.users.create(userData)
|
|
).then(available => { oidcAvailable = available; }).catch(error => {
|
|
console.error('[OIDC] SSO unavailable:', error.message);
|
|
});
|
|
router.oidcReady = oidcReady;
|
|
async function requireOidc(req, res, next) {
|
|
await oidcReady;
|
|
if (!oidcAvailable) return res.status(503).json({ error: 'Single sign-on is unavailable. Contact your administrator.' });
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Start OIDC Login
|
|
* GET /api/auth/oidc/login
|
|
*/
|
|
router.get('/oidc/login', requireOidc, auth.passport.authenticate('openidconnect'));
|
|
|
|
/**
|
|
* OIDC Callback
|
|
* GET /api/auth/oidc/callback
|
|
*/
|
|
router.get('/oidc/callback', requireOidc,
|
|
auth.passport.authenticate('openidconnect', { session: false, failureRedirect: '/login.html?error=SSO+Failed' }),
|
|
(req, res) => {
|
|
// Successful authentication
|
|
const token = auth.generateToken(req.user);
|
|
|
|
auth.setAuthCookie(req, res, token);
|
|
req.session.regenerate(error => error ? res.status(500).json({ error: 'Login failed' }) : res.redirect('/'));
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Check if initial setup is required
|
|
* GET /api/auth/setup-required
|
|
*/
|
|
router.get('/setup-required', async (req, res) => {
|
|
try {
|
|
const userCount = (await db.checkHealth()).users.length;
|
|
res.json({ setupRequired: userCount === 0 });
|
|
} catch (err) {
|
|
console.error('Error in /setup-required:', err);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Initial setup - Create admin user
|
|
* POST /api/auth/setup
|
|
*/
|
|
router.post('/setup', loginLimit, async (req, res) => {
|
|
try {
|
|
const userCount = (await db.checkHealth()).users.length;
|
|
|
|
// Check if setup already done
|
|
if (userCount > 0) {
|
|
return res.status(400).json({ error: 'Setup already completed' });
|
|
}
|
|
|
|
const { username, password } = req.body;
|
|
|
|
if (typeof username !== 'string' || !username.trim() || username.length > 64 || typeof password !== 'string') {
|
|
return res.status(400).json({ error: 'Username and password required' });
|
|
}
|
|
|
|
if (password.length < 6 || Buffer.byteLength(password) > 72) {
|
|
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
|
}
|
|
|
|
// Create admin user
|
|
const passwordHash = await auth.hashPassword(password);
|
|
const adminUser = await db.users.create({
|
|
username,
|
|
passwordHash,
|
|
role: 'admin'
|
|
}, { initialSetup: true });
|
|
|
|
// Generate token for immediate login
|
|
const token = auth.generateToken(adminUser);
|
|
auth.setAuthCookie(req, res, token);
|
|
|
|
res.status(201).json({
|
|
message: 'Admin user created successfully',
|
|
token,
|
|
user: adminUser
|
|
});
|
|
} catch (err) {
|
|
console.error('Error in /setup:', err);
|
|
res.status(500).json({ error: err.message || 'Server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Login with Passport Local Strategy
|
|
* POST /api/auth/login
|
|
*/
|
|
router.post('/login', loginLimit, (req, res, next) => {
|
|
if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string' || req.body.password.length > 1024) {
|
|
return res.status(400).json({ error: 'Username and password required' });
|
|
}
|
|
auth.passport.authenticate('local', { session: false }, (err, user, info) => {
|
|
if (err) {
|
|
console.error('Login error:', err);
|
|
return res.status(500).json({ error: 'Server error' });
|
|
}
|
|
|
|
if (!user) {
|
|
return res.status(401).json({ error: info?.message || 'Invalid credentials' });
|
|
}
|
|
|
|
// Generate JWT token
|
|
const token = auth.generateToken(user);
|
|
auth.setAuthCookie(req, res, token);
|
|
|
|
res.json({
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
role: user.role
|
|
}
|
|
});
|
|
})(req, res, next);
|
|
});
|
|
|
|
/**
|
|
* Logout (client-side handles token removal)
|
|
* POST /api/auth/logout
|
|
*/
|
|
router.post('/logout', (req, res) => {
|
|
const payload = auth.verifyToken(auth.extractToken(req));
|
|
if (payload) authTokens.revoke(payload);
|
|
auth.clearAuthCookie(req, res);
|
|
if (req.session) req.session.destroy(() => {});
|
|
res.json({ success: true, message: 'Logged out successfully' });
|
|
});
|
|
|
|
// Exchange a valid API token for a browser cookie during migration.
|
|
router.post('/session', auth.requireAuth, (req, res) => {
|
|
auth.setAuthCookie(req, res, auth.extractToken(req));
|
|
res.json({ success: true });
|
|
});
|
|
|
|
/**
|
|
* Get current user
|
|
* GET /api/auth/me
|
|
*/
|
|
router.get('/me', auth.requireAuth, async (req, res) => {
|
|
try {
|
|
const user = await db.users.getById(req.user.id);
|
|
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
res.json({
|
|
id: user.id,
|
|
username: user.username,
|
|
role: user.role
|
|
});
|
|
} catch (err) {
|
|
console.error('Error in /me:', err);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get all users (admin only)
|
|
* GET /api/auth/users
|
|
*/
|
|
router.get('/users', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
|
try {
|
|
const allUsers = await db.users.getAll();
|
|
|
|
// Remove password hashes
|
|
const users = allUsers.map(u => {
|
|
const { passwordHash, ...userWithoutPassword } = u;
|
|
return userWithoutPassword;
|
|
});
|
|
|
|
res.json(users);
|
|
} catch (err) {
|
|
console.error('Error fetching users:', err);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Create a new user (admin only)
|
|
* POST /api/auth/users
|
|
*/
|
|
router.post('/users', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
|
try {
|
|
const { username, password, role } = req.body;
|
|
|
|
if (!username || !password || !role) {
|
|
return res.status(400).json({ error: 'Username, password, and role are required' });
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
|
}
|
|
|
|
if (!['admin', 'viewer'].includes(role)) {
|
|
return res.status(400).json({ error: 'Role must be either "admin" or "viewer"' });
|
|
}
|
|
|
|
const passwordHash = await auth.hashPassword(password);
|
|
const newUser = await db.users.create({
|
|
username,
|
|
passwordHash,
|
|
role
|
|
});
|
|
|
|
res.status(201).json(newUser);
|
|
} catch (err) {
|
|
console.error('Error creating user:', err);
|
|
res.status(500).json({ error: err.message || 'Server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Update a user (admin only)
|
|
* PUT /api/auth/users/:id
|
|
*/
|
|
router.put('/users/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { username, password, role } = req.body;
|
|
|
|
const updates = {};
|
|
|
|
if (username) {
|
|
updates.username = username;
|
|
}
|
|
|
|
if (password) {
|
|
if (password.length < 6) {
|
|
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
|
}
|
|
updates.passwordHash = await auth.hashPassword(password);
|
|
}
|
|
|
|
if (role) {
|
|
if (!['admin', 'viewer'].includes(role)) {
|
|
return res.status(400).json({ error: 'Role must be either "admin" or "viewer"' });
|
|
}
|
|
|
|
// Prevent removing admin role from the last admin
|
|
const user = await db.users.getById(id);
|
|
if (user && user.role === 'admin' && role !== 'admin') {
|
|
const allUsers = await db.users.getAll();
|
|
const adminCount = allUsers.filter(u => u.role === 'admin').length;
|
|
if (adminCount <= 1) {
|
|
return res.status(400).json({ error: 'Cannot remove admin role from the last admin user' });
|
|
}
|
|
}
|
|
|
|
updates.role = role;
|
|
}
|
|
|
|
const updatedUser = await db.users.update(id, updates);
|
|
res.json(updatedUser);
|
|
} catch (err) {
|
|
console.error('Error updating user:', err);
|
|
res.status(500).json({ error: err.message || 'Server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Delete a user (admin only)
|
|
* DELETE /api/auth/users/:id
|
|
*/
|
|
router.delete('/users/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// Prevent deleting yourself
|
|
if (parseInt(id) === req.user.id) {
|
|
return res.status(400).json({ error: 'Cannot delete your own account' });
|
|
}
|
|
|
|
await db.users.delete(id);
|
|
res.json({ success: true, message: 'User deleted successfully' });
|
|
} catch (err) {
|
|
console.error('Error deleting user:', err);
|
|
res.status(500).json({ error: err.message || 'Server error' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|