2026-05-03 19:51:57 -05:00
const express = require ( 'express' ) ;
const router = express . Router ( ) ;
2026-05-10 10:44:39 -05:00
const { getDb , getSetting , setSetting , rollbackMigration } = require ( '../db/database' ) ;
2026-05-03 19:51:57 -05:00
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' ) ;
2026-05-09 13:03:36 -05:00
const { backupOperationLimiter } = require ( '../middleware/rateLimiter' ) ;
2026-05-03 19:51:57 -05:00
// 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 ) => {
2026-05-04 23:34:24 -05:00
const count = getDb ( ) . prepare ( 'SELECT COUNT(*) AS n FROM users WHERE id != ?' ) . get ( req . user . id ) . n ;
2026-05-03 19:51:57 -05:00
res . json ( { has _users : count > 0 } ) ;
} ) ;
// GET /api/admin/users
router . get ( '/users' , ( req , res ) => {
res . json (
getDb ( ) . prepare (
2026-05-04 23:34:24 -05:00
'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users ORDER BY is_default_admin DESC, role DESC, username ASC'
2026-05-03 19:51:57 -05:00
) . all ( )
) ;
} ) ;
// POST /api/admin/backups
2026-05-09 13:03:36 -05:00
router . post ( '/backups' , backupOperationLimiter , async ( req , res ) => {
2026-05-03 19:51:57 -05:00
try {
const backup = await createBackup ( ) ;
res . status ( 201 ) . json ( backup ) ;
} catch ( err ) {
sendError ( res , err ) ;
}
} ) ;
// GET /api/admin/backups
2026-05-09 13:03:36 -05:00
router . get ( '/backups' , backupOperationLimiter , ( req , res ) => {
2026-05-03 19:51:57 -05:00
try {
res . json ( { backups : listBackups ( ) } ) ;
} catch ( err ) {
sendError ( res , err ) ;
}
} ) ;
// GET /api/admin/backups/settings
2026-05-09 13:03:36 -05:00
router . get ( '/backups/settings' , backupOperationLimiter , ( req , res ) => {
2026-05-03 19:51:57 -05:00
try {
res . json ( getScheduleStatus ( ) ) ;
} catch ( err ) {
sendError ( res , err ) ;
}
} ) ;
// PUT /api/admin/backups/settings
2026-05-09 13:03:36 -05:00
router . put ( '/backups/settings' , backupOperationLimiter , ( req , res ) => {
2026-05-03 19:51:57 -05:00
try {
res . json ( saveBackupScheduleSettings ( req . body ) ) ;
} catch ( err ) {
sendError ( res , err ) ;
}
} ) ;
// POST /api/admin/backups/run-scheduled-now
2026-05-09 13:03:36 -05:00
router . post ( '/backups/run-scheduled-now' , backupOperationLimiter , async ( req , res ) => {
2026-05-03 19:51:57 -05:00
try {
res . status ( 201 ) . json ( await runScheduledBackupNow ( ) ) ;
} catch ( err ) {
sendError ( res , err ) ;
}
} ) ;
// POST /api/admin/backups/import
router . post (
'/backups/import' ,
2026-05-09 13:03:36 -05:00
backupOperationLimiter ,
2026-05-03 19:51:57 -05:00
express . raw ( {
type : [ 'application/octet-stream' , 'application/x-sqlite3' , 'application/vnd.sqlite3' ] ,
limit : '100mb' ,
} ) ,
async ( req , res ) => {
try {
2026-05-09 13:03:36 -05:00
// Extract expected checksum from request headers or query
const expectedChecksum = req . headers [ 'x-checksum-sha256' ] || req . query . checksum ;
const backup = await importBackupBuffer ( req . body , {
expectedChecksum : expectedChecksum ? String ( expectedChecksum ) . trim ( ) : undefined ,
} ) ;
2026-05-03 19:51:57 -05:00
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
2026-05-09 13:03:36 -05:00
router . post ( '/backups/:id/restore' , backupOperationLimiter , async ( req , res ) => {
2026-05-03 19:51:57 -05:00
try {
res . json ( await restoreBackup ( req . params . id ) ) ;
} catch ( err ) {
sendError ( res , err ) ;
}
} ) ;
// DELETE /api/admin/backups/:id
2026-05-09 13:03:36 -05:00
router . delete ( '/backups/:id' , backupOperationLimiter , ( req , res ) => {
2026-05-03 19:51:57 -05:00
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 (
2026-05-04 23:34:24 -05:00
db . prepare ( 'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users WHERE id = ?' )
2026-05-03 19:51:57 -05:00
. 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' } ) ;
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 } ) ;
} ) ;
2026-05-10 00:03:12 -05:00
// Import audit service
const { logAudit } = require ( '../services/auditService' ) ;
2026-05-03 19:51:57 -05:00
// 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.' } ) ;
}
}
2026-05-09 13:03:36 -05:00
// SECURITY FIX (2026-05-08): Delete all sessions for the target user when role changes.
// This forces re-authentication with the new role, preventing session hijacking
// from being used to bypass privilege checks.
db . prepare ( 'DELETE FROM sessions WHERE user_id = ?' ) . run ( targetId ) ;
2026-05-10 00:03:12 -05:00
const previousRole = user . role ;
2026-05-03 19:51:57 -05:00
db . prepare ( "UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?" )
. run ( role , targetId ) ;
2026-05-10 00:03:12 -05:00
logAudit ( { user _id : req . user . id , action : 'role.change' , entity _type : 'user' , entity _id : targetId , details : { old _role : previousRole , new _role : role } , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
2026-05-03 19:51:57 -05:00
const updated = db . prepare (
2026-05-04 23:34:24 -05:00
'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users WHERE id = ?'
2026-05-03 19:51:57 -05:00
) . get ( targetId ) ;
res . json ( updated ) ;
} ) ;
2026-05-04 23:34:24 -05:00
// PUT /api/admin/users/:id/active
router . put ( '/users/:id/active' , ( req , res ) => {
const active = req . body ? . active ? 1 : 0 ;
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 deactivate your own account.' } ) ;
}
db . prepare ( "UPDATE users SET active = ?, updated_at = datetime('now') WHERE id = ?" ) . run ( active , targetId ) ;
if ( ! active ) db . prepare ( 'DELETE FROM sessions WHERE user_id = ?' ) . run ( targetId ) ;
res . json ( db . prepare (
'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users WHERE id = ?'
) . get ( targetId ) ) ;
} ) ;
2026-05-03 19:51:57 -05:00
// 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' } ) ;
2026-05-04 23:34:24 -05:00
if ( req . user ? . id === user . id ) return res . status ( 400 ) . json ( { error : 'You cannot delete your own account.' } ) ;
const deleteUser = db . transaction ( ( ) => {
db . prepare ( 'DELETE FROM import_sessions WHERE user_id = ?' ) . run ( user . id ) ;
db . prepare ( 'DELETE FROM import_history WHERE user_id = ?' ) . run ( user . id ) ;
db . prepare ( 'DELETE FROM sessions WHERE user_id = ?' ) . run ( user . id ) ;
db . prepare ( 'DELETE FROM users WHERE id = ?' ) . run ( user . id ) ;
} ) ;
deleteUser ( ) ;
res . json ( { success : true , deleted _user _id : user . id } ) ;
2026-05-03 19:51:57 -05:00
} ) ;
// ── 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 ) ;
}
} ) ;
2026-05-09 13:03:36 -05:00
2026-05-03 19:51:57 -05:00
// 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.
2026-05-09 13:03:36 -05:00
router . post ( '/cleanup/run' , backupOperationLimiter , async ( req , res ) => {
2026-05-03 19:51:57 -05:00
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 ( ) } ) ;
} ) ;
2026-05-10 10:44:39 -05:00
// ── Migration Rollback ────────────────────────────────────────────────────────
router . post ( '/migrations/rollback' , async ( req , res ) => {
const { version } = req . body ;
if ( ! version ) {
return res . status ( 400 ) . json ( { error : 'Version is required' } ) ;
}
try {
const result = rollbackMigration ( version ) ;
logAudit ( {
user _id : req . user . id ,
action : 'migration.rollback' ,
entity _type : 'migration' ,
entity _id : null ,
details : { version , performed _by : req . user . username } ,
ip _address : req . ip ,
user _agent : req . get ( 'user-agent' )
} ) ;
res . json ( { success : true , ... result } ) ;
} catch ( err ) {
logAudit ( {
user _id : req . user . id ,
action : 'migration.rollback.failure' ,
entity _type : 'migration' ,
entity _id : null ,
details : { version , error : err . message , performed _by : req . user . username } ,
ip _address : req . ip ,
user _agent : req . get ( 'user-agent' )
} ) ;
if ( err . code === 'NOT_APPLIED' ) {
return res . status ( 404 ) . json ( { error : err . message } ) ;
}
if ( err . code === 'ROLLBACK_NOT_SUPPORTED' ) {
return res . status ( 422 ) . json ( { error : err . message } ) ;
}
res . status ( 500 ) . json ( { error : 'Rollback failed' , details : err . message } ) ;
}
} ) ;
2026-05-03 19:51:57 -05:00
module . exports = router ;