166 lines
5.7 KiB
JavaScript
166 lines
5.7 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
|
|
const { getDb, getSetting, setSetting } = require('../db/database');
|
|
const { login, logout, hashPassword, cookieOpts, COOKIE_NAME } = require('../services/authService');
|
|
const { requireAuth, requireAdmin } = require('../middleware/requireAuth');
|
|
const { getPublicOidcInfo } = require('../services/oidcService');
|
|
const { loginLimiter, passwordLimiter } = require('../middleware/rateLimiter');
|
|
|
|
// ─────────────────────────────────────────
|
|
// PUBLIC AUTH ROUTES
|
|
// ─────────────────────────────────────────
|
|
|
|
// POST /api/auth/login
|
|
router.post('/login', loginLimiter, async (req, res) => {
|
|
// Respect admin-configured login method toggle
|
|
if (getSetting('local_login_enabled') === 'false') {
|
|
return res.status(403).json({ error: 'Local username/password login is not enabled on this server.' });
|
|
}
|
|
|
|
const { username, password } = req.body;
|
|
if (!username || !password) {
|
|
return res.status(400).json({ error: 'Username and password are required' });
|
|
}
|
|
|
|
const result = await login(username, password);
|
|
if (!result) {
|
|
return res.status(401).json({ error: 'Invalid username or password' });
|
|
}
|
|
|
|
res.cookie(COOKIE_NAME, result.sessionId, cookieOpts(req));
|
|
res.json({ user: result.user });
|
|
});
|
|
|
|
// POST /api/auth/logout
|
|
router.post('/logout', requireAuth, (req, res) => {
|
|
logout(req.cookies?.[COOKIE_NAME]);
|
|
res.clearCookie(COOKIE_NAME, { path: '/', ...cookieOpts(req), maxAge: undefined });
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// GET /api/auth/me
|
|
router.get('/me', requireAuth, (req, res) => {
|
|
res.json({
|
|
user: req.user,
|
|
single_user_mode: !!req.singleUserMode,
|
|
});
|
|
});
|
|
|
|
// GET /api/auth/mode
|
|
// Public — tells the login page which options are available.
|
|
// Never returns secrets. local_enabled/oidc_enabled reflect admin settings.
|
|
router.get('/mode', (req, res) => {
|
|
const oidcInfo = getPublicOidcInfo();
|
|
const localEnabled = getSetting('local_login_enabled') !== 'false';
|
|
res.json({
|
|
auth_mode: getSetting('auth_mode') || 'multi',
|
|
local_enabled: localEnabled,
|
|
...oidcInfo,
|
|
});
|
|
});
|
|
|
|
// POST /api/auth/restore-multi-user-mode
|
|
// Recovery path for single-user mode. In single-user mode requireAuth attaches
|
|
// the configured default user, so this lets that Settings page restore normal
|
|
// login without needing access to Admin routes.
|
|
router.post('/restore-multi-user-mode', requireAuth, (req, res) => {
|
|
if (!req.singleUserMode && getSetting('auth_mode') !== 'single') {
|
|
return res.status(400).json({ error: 'Single-user mode is not enabled.' });
|
|
}
|
|
|
|
setSetting('auth_mode', 'multi');
|
|
setSetting('default_user_id', '');
|
|
|
|
res.json({ success: true, auth_mode: 'multi' });
|
|
});
|
|
|
|
// POST /api/auth/acknowledge-privacy
|
|
router.post('/acknowledge-privacy', requireAuth, (req, res) => {
|
|
getDb().prepare(
|
|
"UPDATE users SET first_login = 0, updated_at = datetime('now') WHERE id = ?"
|
|
).run(req.user.id);
|
|
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// POST /api/auth/change-password
|
|
router.post('/change-password', requireAuth, passwordLimiter, async (req, res) => {
|
|
const { current_password, new_password } = req.body;
|
|
|
|
if (!new_password || new_password.length < 8) {
|
|
return res.status(400).json({ error: 'New password must be at least 8 characters' });
|
|
}
|
|
|
|
const db = getDb();
|
|
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
|
|
|
|
if (!user.must_change_password) {
|
|
const bcrypt = require('bcryptjs');
|
|
const valid = await bcrypt.compare(current_password || '', user.password_hash);
|
|
if (!valid) return res.status(401).json({ error: 'Current password is incorrect' });
|
|
}
|
|
|
|
const hash = await hashPassword(new_password);
|
|
|
|
db.prepare(
|
|
"UPDATE users SET password_hash = ?, must_change_password = 0, updated_at = datetime('now') WHERE id = ?"
|
|
).run(hash, req.user.id);
|
|
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// ─────────────────────────────────────────
|
|
// ADMIN ROUTES (MOUNTED AT /api/admin)
|
|
// ─────────────────────────────────────────
|
|
|
|
// GET /api/admin/has-users
|
|
router.get('/has-users', (req, res) => {
|
|
const count = getDb()
|
|
.prepare("SELECT COUNT(*) AS n FROM users WHERE role = 'user'")
|
|
.get().n;
|
|
|
|
res.json({ has_users: count > 0 });
|
|
});
|
|
|
|
// GET /api/admin/users
|
|
router.get('/users', requireAuth, requireAdmin, (req, res) => {
|
|
const users = getDb().prepare(
|
|
"SELECT id, username, role, must_change_password, first_login, created_at FROM users ORDER BY role DESC, username ASC"
|
|
).all();
|
|
|
|
res.json(users);
|
|
});
|
|
|
|
// POST /api/admin/users
|
|
router.post('/users', requireAuth, requireAdmin, async (req, res) => {
|
|
const { username, password } = req.body;
|
|
|
|
if (!username || username.length < 3) {
|
|
return res.status(400).json({ error: 'Username must be at least 3 characters' });
|
|
}
|
|
|
|
if (!password || password.length < 8) {
|
|
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
|
}
|
|
|
|
const db = getDb();
|
|
|
|
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
|
if (existing) return res.status(409).json({ error: 'Username already taken' });
|
|
|
|
const hash = await hashPassword(password);
|
|
|
|
const result = db.prepare(
|
|
"INSERT INTO users (username, password_hash, role, first_login) VALUES (?, ?, 'user', 1)"
|
|
).run(username, hash);
|
|
|
|
const created = db.prepare(
|
|
'SELECT id, username, role, must_change_password, first_login, created_at FROM users WHERE id = ?'
|
|
).get(result.lastInsertRowid);
|
|
|
|
res.status(201).json(created);
|
|
});
|
|
|
|
module.exports = router;
|