516 lines
19 KiB
JavaScript
516 lines
19 KiB
JavaScript
|
|
const express = require('express');
|
|||
|
|
const router = express.Router();
|
|||
|
|
const { getDb, getSetting, setSetting } = require('../db/database');
|
|||
|
|
const { hashPassword } = require('../services/authService');
|
|||
|
|
const {
|
|||
|
|
createBackup,
|
|||
|
|
deleteBackup,
|
|||
|
|
getBackupFile,
|
|||
|
|
importBackupBuffer,
|
|||
|
|
listBackups,
|
|||
|
|
restoreBackup,
|
|||
|
|
} = require('../services/backupService');
|
|||
|
|
const {
|
|||
|
|
getScheduleStatus,
|
|||
|
|
runScheduledBackupNow,
|
|||
|
|
saveSettings: saveBackupScheduleSettings,
|
|||
|
|
} = require('../services/backupScheduler');
|
|||
|
|
const {
|
|||
|
|
getCleanupStatus,
|
|||
|
|
runAllCleanup,
|
|||
|
|
validateAndApplySettings: applyCleanupSettings,
|
|||
|
|
} = require('../services/cleanupService');
|
|||
|
|
|
|||
|
|
// All routes mounted at /api/admin (requireAuth + requireAdmin applied at server level)
|
|||
|
|
|
|||
|
|
function sendError(res, err) {
|
|||
|
|
const status = err.status || 500;
|
|||
|
|
res.status(status).json({ error: status === 500 ? 'Backup operation failed' : err.message });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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', (req, res) => {
|
|||
|
|
res.json(
|
|||
|
|
getDb().prepare(
|
|||
|
|
'SELECT id, username, role, must_change_password, first_login, created_at FROM users ORDER BY role DESC, username ASC'
|
|||
|
|
).all()
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// POST /api/admin/backups
|
|||
|
|
router.post('/backups', async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const backup = await createBackup();
|
|||
|
|
res.status(201).json(backup);
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// GET /api/admin/backups
|
|||
|
|
router.get('/backups', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
res.json({ backups: listBackups() });
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// GET /api/admin/backups/settings
|
|||
|
|
router.get('/backups/settings', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
res.json(getScheduleStatus());
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// PUT /api/admin/backups/settings
|
|||
|
|
router.put('/backups/settings', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
res.json(saveBackupScheduleSettings(req.body));
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// POST /api/admin/backups/run-scheduled-now
|
|||
|
|
router.post('/backups/run-scheduled-now', async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
res.status(201).json(await runScheduledBackupNow());
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// POST /api/admin/backups/import
|
|||
|
|
router.post(
|
|||
|
|
'/backups/import',
|
|||
|
|
express.raw({
|
|||
|
|
type: ['application/octet-stream', 'application/x-sqlite3', 'application/vnd.sqlite3'],
|
|||
|
|
limit: '100mb',
|
|||
|
|
}),
|
|||
|
|
async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const backup = await importBackupBuffer(req.body);
|
|||
|
|
res.status(201).json(backup);
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// GET /api/admin/backups/:id/download
|
|||
|
|
router.get('/backups/:id/download', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const backup = getBackupFile(req.params.id);
|
|||
|
|
res.setHeader('Content-Type', 'application/octet-stream');
|
|||
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|||
|
|
res.download(backup.path, backup.metadata.id, (err) => {
|
|||
|
|
if (err && !res.headersSent) sendError(res, err);
|
|||
|
|
});
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// POST /api/admin/backups/:id/restore
|
|||
|
|
router.post('/backups/:id/restore', async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
res.json(await restoreBackup(req.params.id));
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// DELETE /api/admin/backups/:id
|
|||
|
|
router.delete('/backups/:id', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
res.json(deleteBackup(req.params.id));
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// POST /api/admin/users
|
|||
|
|
router.post('/users', 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();
|
|||
|
|
if (db.prepare('SELECT id FROM users WHERE username = ?').get(username))
|
|||
|
|
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);
|
|||
|
|
|
|||
|
|
res.status(201).json(
|
|||
|
|
db.prepare('SELECT id, username, role, must_change_password, first_login, created_at FROM users WHERE id = ?')
|
|||
|
|
.get(result.lastInsertRowid)
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// PUT /api/admin/users/:id/password
|
|||
|
|
router.put('/users/:id/password', async (req, res) => {
|
|||
|
|
const { password } = req.body;
|
|||
|
|
if (!password || password.length < 8)
|
|||
|
|
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
|||
|
|
|
|||
|
|
const db = getDb();
|
|||
|
|
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
|||
|
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|||
|
|
if (user.role === 'admin') return res.status(403).json({ error: 'Cannot reset admin password this way' });
|
|||
|
|
|
|||
|
|
const hash = await hashPassword(password);
|
|||
|
|
db.prepare("UPDATE users SET password_hash=?, must_change_password=1, updated_at=datetime('now') WHERE id=?")
|
|||
|
|
.run(hash, req.params.id);
|
|||
|
|
db.prepare('DELETE FROM sessions WHERE user_id = ?').run(req.params.id);
|
|||
|
|
res.json({ success: true });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// PUT /api/admin/users/:id/role
|
|||
|
|
// Promote/demote an existing user. Prevents removing the last admin or
|
|||
|
|
// changing your own role mid-session.
|
|||
|
|
router.put('/users/:id/role', (req, res) => {
|
|||
|
|
const { role } = req.body;
|
|||
|
|
if (!['admin', 'user'].includes(role)) {
|
|||
|
|
return res.status(400).json({ error: 'role must be "admin" or "user"' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const targetId = Number(req.params.id);
|
|||
|
|
const db = getDb();
|
|||
|
|
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(targetId);
|
|||
|
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|||
|
|
|
|||
|
|
if (req.user?.id === targetId) {
|
|||
|
|
return res.status(400).json({ error: 'You cannot change your own admin role.' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (user.role === 'admin' && role === 'user') {
|
|||
|
|
const adminCount = db.prepare("SELECT COUNT(*) AS n FROM users WHERE role = 'admin'").get().n;
|
|||
|
|
if (adminCount <= 1) {
|
|||
|
|
return res.status(400).json({ error: 'Cannot remove the last admin account.' });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
db.prepare("UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?")
|
|||
|
|
.run(role, targetId);
|
|||
|
|
|
|||
|
|
const updated = db.prepare(
|
|||
|
|
'SELECT id, username, role, must_change_password, first_login, created_at FROM users WHERE id = ?'
|
|||
|
|
).get(targetId);
|
|||
|
|
|
|||
|
|
res.json(updated);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// DELETE /api/admin/users/:id
|
|||
|
|
router.delete('/users/:id', (req, res) => {
|
|||
|
|
const db = getDb();
|
|||
|
|
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
|||
|
|
if (!user) return res.status(404).json({ error: 'User not found' });
|
|||
|
|
if (user.role === 'admin') return res.status(403).json({ error: 'Cannot delete the admin account' });
|
|||
|
|
db.prepare('DELETE FROM users WHERE id = ?').run(req.params.id);
|
|||
|
|
res.json({ success: true });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ── Cleanup endpoints ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
// GET /api/admin/cleanup
|
|||
|
|
// Returns current cleanup settings and the result of the last cleanup run.
|
|||
|
|
router.get('/cleanup', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
res.json(getCleanupStatus());
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// PUT /api/admin/cleanup
|
|||
|
|
// Updates one or more cleanup settings. Accepts partial objects.
|
|||
|
|
// import_sessions_enabled boolean prune expired import preview sessions
|
|||
|
|
// temp_exports_enabled boolean prune stale SQLite export temp files
|
|||
|
|
// temp_export_max_age_hours 1–72 hours before an orphaned export file is removed
|
|||
|
|
// backup_partials_enabled boolean prune orphaned .partial/.upload backup files
|
|||
|
|
// import_history_enabled boolean prune old import history rows (disabled by default)
|
|||
|
|
// import_history_max_age_days 30–3650 age threshold for import history rows
|
|||
|
|
router.put('/cleanup', (req, res) => {
|
|||
|
|
try {
|
|||
|
|
res.json(applyCleanupSettings(req.body));
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// POST /api/admin/cleanup/run
|
|||
|
|
// Runs all enabled cleanup tasks immediately and returns the result.
|
|||
|
|
router.post('/cleanup/run', async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const result = await runAllCleanup();
|
|||
|
|
res.json(result);
|
|||
|
|
} catch (err) {
|
|||
|
|
sendError(res, err);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ── Auth-mode helpers ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
const {
|
|||
|
|
getAdminOidcSettings,
|
|||
|
|
getOidcConfigStatus,
|
|||
|
|
invalidateClientCache,
|
|||
|
|
testOidcConfiguration,
|
|||
|
|
} = require('../services/oidcService');
|
|||
|
|
|
|||
|
|
function trimOrEmpty(value) {
|
|||
|
|
if (value === undefined || value === null) return '';
|
|||
|
|
return String(value).trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function boolSetting(value, fallback) {
|
|||
|
|
if (value === undefined) return fallback;
|
|||
|
|
if (typeof value === 'string') return value === 'true';
|
|||
|
|
return !!value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function computeSubmittedOidcConfigured(body) {
|
|||
|
|
const current = getAdminOidcSettings();
|
|||
|
|
const next = {
|
|||
|
|
issuer: body.oidc_issuer_url !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_issuer_url)
|
|||
|
|
: current.oidc_issuer_url,
|
|||
|
|
clientId: body.oidc_client_id !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_client_id)
|
|||
|
|
: current.oidc_client_id,
|
|||
|
|
redirectUri: body.oidc_redirect_uri !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_redirect_uri)
|
|||
|
|
: current.oidc_redirect_uri,
|
|||
|
|
clientSecret: current.oidc_client_secret_set ? 'set' : '',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (body.oidc_client_secret_clear === true) {
|
|||
|
|
next.clientSecret = process.env.OIDC_CLIENT_SECRET ? 'set' : '';
|
|||
|
|
}
|
|||
|
|
if (trimOrEmpty(body.oidc_client_secret)) {
|
|||
|
|
next.clientSecret = 'set';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return !!(next.issuer && next.clientId && next.clientSecret && next.redirectUri);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildSubmittedOidcConfig(body) {
|
|||
|
|
const current = getAdminOidcSettings();
|
|||
|
|
const status = getOidcConfigStatus();
|
|||
|
|
|
|||
|
|
const issuerUrl = body.oidc_issuer_url !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_issuer_url)
|
|||
|
|
: current.oidc_issuer_url;
|
|||
|
|
const clientId = body.oidc_client_id !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_client_id)
|
|||
|
|
: current.oidc_client_id;
|
|||
|
|
const redirectUri = body.oidc_redirect_uri !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_redirect_uri)
|
|||
|
|
: current.oidc_redirect_uri;
|
|||
|
|
const tokenAuthMethod = body.oidc_token_auth_method !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_token_auth_method)
|
|||
|
|
: current.oidc_token_auth_method;
|
|||
|
|
const scopes = body.oidc_scopes !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_scopes)
|
|||
|
|
: current.oidc_scopes;
|
|||
|
|
const providerName = body.oidc_provider_name !== undefined
|
|||
|
|
? trimOrEmpty(body.oidc_provider_name)
|
|||
|
|
: current.oidc_provider_name;
|
|||
|
|
|
|||
|
|
let clientSecret = status.oidc_client_secret_set ? '__saved__' : '';
|
|||
|
|
if (body.oidc_client_secret_clear === true) clientSecret = process.env.OIDC_CLIENT_SECRET || '';
|
|||
|
|
if (trimOrEmpty(body.oidc_client_secret)) clientSecret = trimOrEmpty(body.oidc_client_secret);
|
|||
|
|
|
|||
|
|
if (!issuerUrl || !clientId || !clientSecret || !redirectUri) return null;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
enabled: true,
|
|||
|
|
issuerUrl,
|
|||
|
|
clientId,
|
|||
|
|
clientSecret: clientSecret === '__saved__'
|
|||
|
|
? (getSetting('oidc_client_secret') || process.env.OIDC_CLIENT_SECRET || '')
|
|||
|
|
: clientSecret,
|
|||
|
|
tokenEndpointAuthMethod: tokenAuthMethod === 'client_secret_post'
|
|||
|
|
? 'client_secret_post'
|
|||
|
|
: 'client_secret_basic',
|
|||
|
|
redirectUri,
|
|||
|
|
scopes: (scopes || 'openid email profile groups').split(/\s+/).filter(Boolean),
|
|||
|
|
adminGroup: body.oidc_admin_group !== undefined ? trimOrEmpty(body.oidc_admin_group) : current.oidc_admin_group,
|
|||
|
|
defaultRole: 'user',
|
|||
|
|
autoProvision: body.oidc_auto_provision !== undefined ? !!body.oidc_auto_provision : current.oidc_auto_provision,
|
|||
|
|
providerName: providerName || 'authentik',
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildAuthModeStatus() {
|
|||
|
|
const oidcConfigured = getOidcConfigStatus().oidc_configured;
|
|||
|
|
const localEnabled = getSetting('local_login_enabled') !== 'false';
|
|||
|
|
const oidcEnabled = getSetting('oidc_login_enabled') === 'true';
|
|||
|
|
const oidcAdminGroup = getAdminOidcSettings().oidc_admin_group;
|
|||
|
|
|
|||
|
|
// Disabling local is only safe if OIDC is configured, enabled, and has an admin path.
|
|||
|
|
const canDisableLocal = oidcConfigured && oidcEnabled && !!oidcAdminGroup;
|
|||
|
|
|
|||
|
|
const warnings = [];
|
|||
|
|
if (!localEnabled && !oidcConfigured) {
|
|||
|
|
warnings.push('Local login is disabled but OIDC is not configured; users may be locked out.');
|
|||
|
|
}
|
|||
|
|
if (!localEnabled && !oidcEnabled) {
|
|||
|
|
warnings.push('No login method is enabled. Re-enable local login or configure OIDC.');
|
|||
|
|
}
|
|||
|
|
if (oidcEnabled && !oidcConfigured) {
|
|||
|
|
warnings.push('authentik/OIDC login is enabled but configuration is incomplete, so the login button will stay hidden.');
|
|||
|
|
}
|
|||
|
|
if (!localEnabled && !oidcAdminGroup) {
|
|||
|
|
warnings.push('Local login is disabled but no OIDC admin group is configured.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
auth_mode: getSetting('auth_mode') || 'multi',
|
|||
|
|
default_user_id: getSetting('default_user_id') || null,
|
|||
|
|
local_login_enabled: localEnabled,
|
|||
|
|
oidc_login_enabled: oidcEnabled,
|
|||
|
|
oidc_configured: oidcConfigured,
|
|||
|
|
...getOidcConfigStatus(),
|
|||
|
|
...getAdminOidcSettings(),
|
|||
|
|
can_disable_local: canDisableLocal,
|
|||
|
|
warnings,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GET /api/admin/auth-mode
|
|||
|
|
router.get('/auth-mode', (req, res) => {
|
|||
|
|
res.json(buildAuthModeStatus());
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// POST /api/admin/auth-mode/oidc-test
|
|||
|
|
// Tests submitted or saved OIDC provider settings with OIDC discovery.
|
|||
|
|
// Never returns client secret or token material.
|
|||
|
|
router.post('/auth-mode/oidc-test', async (req, res) => {
|
|||
|
|
const config = buildSubmittedOidcConfig(req.body || {});
|
|||
|
|
const result = await testOidcConfiguration(config);
|
|||
|
|
res.status(result.ok ? 200 : 400).json(result);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// PUT /api/admin/auth-mode
|
|||
|
|
// Accepts legacy auth_mode/default_user_id fields plus new auth method settings.
|
|||
|
|
// Validates lockout protection before saving.
|
|||
|
|
router.put('/auth-mode', (req, res) => {
|
|||
|
|
const {
|
|||
|
|
auth_mode, default_user_id,
|
|||
|
|
local_login_enabled, oidc_login_enabled, oidc_enabled,
|
|||
|
|
oidc_provider_name, oidc_issuer_url, oidc_client_id, oidc_client_secret,
|
|||
|
|
oidc_client_secret_clear, oidc_token_auth_method, oidc_redirect_uri, oidc_scopes,
|
|||
|
|
oidc_auto_provision, oidc_admin_group, oidc_default_role,
|
|||
|
|
} = req.body;
|
|||
|
|
|
|||
|
|
// ── Legacy single/multi mode (unchanged behavior) ─────────────────────────
|
|||
|
|
if (auth_mode !== undefined) {
|
|||
|
|
if (!['multi', 'single'].includes(auth_mode))
|
|||
|
|
return res.status(400).json({ error: 'auth_mode must be "multi" or "single"' });
|
|||
|
|
if (auth_mode === 'single') {
|
|||
|
|
if (!default_user_id) return res.status(400).json({ error: 'default_user_id is required for single mode' });
|
|||
|
|
const u = getDb().prepare("SELECT id FROM users WHERE id=? AND role='user'").get(default_user_id);
|
|||
|
|
if (!u) return res.status(404).json({ error: 'User not found or not a regular user' });
|
|||
|
|
setSetting('default_user_id', default_user_id);
|
|||
|
|
}
|
|||
|
|
setSetting('auth_mode', auth_mode);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Auth method toggles ───────────────────────────────────────────────────
|
|||
|
|
const oidcConfigured = computeSubmittedOidcConfigured(req.body || {});
|
|||
|
|
const nextLocal = boolSetting(local_login_enabled, getSetting('local_login_enabled') !== 'false');
|
|||
|
|
const requestedOidc = oidc_login_enabled !== undefined ? oidc_login_enabled : oidc_enabled;
|
|||
|
|
const nextOidc = boolSetting(requestedOidc, getSetting('oidc_login_enabled') === 'true');
|
|||
|
|
const nextAdminGroup = oidc_admin_group !== undefined
|
|||
|
|
? trimOrEmpty(oidc_admin_group)
|
|||
|
|
: getAdminOidcSettings().oidc_admin_group;
|
|||
|
|
|
|||
|
|
// Lockout protection: cannot disable both login methods
|
|||
|
|
if (!nextLocal && !nextOidc) {
|
|||
|
|
return res.status(400).json({ error: 'Cannot disable all login methods. At least one must remain enabled.' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Lockout protection: cannot disable local login unless OIDC has a working admin path.
|
|||
|
|
if (!nextLocal && !oidcConfigured) {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
error: 'Cannot disable local login until authentik/OIDC is fully configured.',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
if (!nextLocal && !nextOidc) {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
error: 'Cannot disable local login without OIDC login enabled.',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
if (!nextLocal && !nextAdminGroup) {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
error: 'Cannot disable local login until an OIDC admin group is configured.',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cannot enable OIDC login if required provider settings are incomplete
|
|||
|
|
if (nextOidc && !oidcConfigured) {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
error: 'Cannot enable OIDC login until issuer URL, client ID, client secret, and redirect URI are configured.',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (local_login_enabled !== undefined) setSetting('local_login_enabled', nextLocal ? 'true' : 'false');
|
|||
|
|
if (oidc_login_enabled !== undefined) setSetting('oidc_login_enabled', nextOidc ? 'true' : 'false');
|
|||
|
|
|
|||
|
|
// OIDC provider settings. Client secret is write-only from the Admin API.
|
|||
|
|
if (oidc_provider_name !== undefined) {
|
|||
|
|
const name = String(oidc_provider_name).slice(0, 100).trim();
|
|||
|
|
if (name) setSetting('oidc_provider_name', name);
|
|||
|
|
}
|
|||
|
|
if (oidc_issuer_url !== undefined) setSetting('oidc_issuer_url', trimOrEmpty(oidc_issuer_url).slice(0, 500));
|
|||
|
|
if (oidc_client_id !== undefined) setSetting('oidc_client_id', trimOrEmpty(oidc_client_id).slice(0, 500));
|
|||
|
|
if (oidc_token_auth_method !== undefined) {
|
|||
|
|
const method = oidc_token_auth_method === 'client_secret_post' ? 'client_secret_post' : 'client_secret_basic';
|
|||
|
|
setSetting('oidc_token_auth_method', method);
|
|||
|
|
}
|
|||
|
|
if (oidc_redirect_uri !== undefined) setSetting('oidc_redirect_uri', trimOrEmpty(oidc_redirect_uri).slice(0, 500));
|
|||
|
|
if (oidc_scopes !== undefined) {
|
|||
|
|
const scopes = trimOrEmpty(oidc_scopes).split(/\s+/).filter(Boolean).join(' ') || 'openid email profile groups';
|
|||
|
|
setSetting('oidc_scopes', scopes.slice(0, 500));
|
|||
|
|
}
|
|||
|
|
if (oidc_client_secret_clear === true) setSetting('oidc_client_secret', '');
|
|||
|
|
if (trimOrEmpty(oidc_client_secret)) {
|
|||
|
|
setSetting('oidc_client_secret', trimOrEmpty(oidc_client_secret).slice(0, 1000));
|
|||
|
|
}
|
|||
|
|
if (oidc_auto_provision !== undefined) setSetting('oidc_auto_provision', !!oidc_auto_provision ? 'true' : 'false');
|
|||
|
|
if (oidc_admin_group !== undefined) setSetting('oidc_admin_group', String(oidc_admin_group).slice(0, 200).trim());
|
|||
|
|
if (oidc_default_role !== undefined) {
|
|||
|
|
setSetting('oidc_default_role', 'user');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
oidc_issuer_url !== undefined ||
|
|||
|
|
oidc_client_id !== undefined ||
|
|||
|
|
oidc_client_secret !== undefined ||
|
|||
|
|
oidc_client_secret_clear === true ||
|
|||
|
|
oidc_token_auth_method !== undefined ||
|
|||
|
|
oidc_redirect_uri !== undefined
|
|||
|
|
) {
|
|||
|
|
invalidateClientCache();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
res.json({ success: true, ...buildAuthModeStatus() });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
module.exports = router;
|