Fix users.ts and schema for override

This commit is contained in:
LukeGus
2025-09-03 13:09:53 -05:00
parent 81b85c04a3
commit 190470022a
2 changed files with 74 additions and 34 deletions

View File

@@ -17,7 +17,7 @@ export const users = sqliteTable('users', {
identifier_path: text('identifier_path'), identifier_path: text('identifier_path'),
name_path: text('name_path'), name_path: text('name_path'),
scopes: text().default("openid email profile"), scopes: text().default("openid email profile"),
totp_secret: text('totp_secret'), totp_secret: text('totp_secret'),
totp_enabled: integer('totp_enabled', {mode: 'boolean'}).notNull().default(false), totp_enabled: integer('totp_enabled', {mode: 'boolean'}).notNull().default(false),
totp_backup_codes: text('totp_backup_codes'), totp_backup_codes: text('totp_backup_codes'),

View File

@@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
import {db} from '../db/index.js'; import {db} from '../db/index.js';
import {users, settings} from '../db/schema.js'; import {users, settings, sshData, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts} from '../db/schema.js';
import {eq, and} from 'drizzle-orm'; import {eq, and} from 'drizzle-orm';
import chalk from 'chalk'; import chalk from 'chalk';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
@@ -234,6 +234,7 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
issuer_url, issuer_url,
authorization_url, authorization_url,
token_url, token_url,
userinfo_url,
identifier_path, identifier_path,
name_path, name_path,
scopes scopes
@@ -252,6 +253,7 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
issuer_url, issuer_url,
authorization_url, authorization_url,
token_url, token_url,
userinfo_url: userinfo_url || '',
identifier_path, identifier_path,
name_path, name_path,
scopes: scopes || 'openid email profile' scopes: scopes || 'openid email profile'
@@ -374,14 +376,38 @@ router.get('/oidc/callback', async (req, res) => {
const tokenData = await tokenResponse.json() as any; const tokenData = await tokenResponse.json() as any;
let userInfo: any = null; let userInfo: any = null;
const userInfoUrls = []; let userInfoUrls: string[] = [];
const normalizedIssuerUrl = config.issuer_url.endsWith('/') ? config.issuer_url.slice(0, -1) : config.issuer_url; const normalizedIssuerUrl = config.issuer_url.endsWith('/') ? config.issuer_url.slice(0, -1) : config.issuer_url;
const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, ''); const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, '');
userInfoUrls.push(`${baseUrl}/userinfo/`); try {
userInfoUrls.push(`${normalizedIssuerUrl}/userinfo/`); const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
userInfoUrls.push(`${normalizedIssuerUrl}/userinfo`); const discoveryResponse = await fetch(discoveryUrl);
if (discoveryResponse.ok) {
const discovery = await discoveryResponse.json() as any;
if (discovery.userinfo_endpoint) {
userInfoUrls.push(discovery.userinfo_endpoint);
}
}
} catch (discoveryError) {
logger.error(`OIDC discovery failed: ${discoveryError}`);
}
if (config.userinfo_url) {
userInfoUrls.unshift(config.userinfo_url);
}
userInfoUrls.push(
`${baseUrl}/userinfo/`,
`${baseUrl}/userinfo`,
`${normalizedIssuerUrl}/userinfo/`,
`${normalizedIssuerUrl}/userinfo`,
`${baseUrl}/oauth2/userinfo/`,
`${baseUrl}/oauth2/userinfo`,
`${normalizedIssuerUrl}/oauth2/userinfo/`,
`${normalizedIssuerUrl}/oauth2/userinfo`
);
if (tokenData.id_token) { if (tokenData.id_token) {
try { try {
@@ -414,8 +440,11 @@ router.get('/oidc/callback', async (req, res) => {
if (userInfoResponse.ok) { if (userInfoResponse.ok) {
userInfo = await userInfoResponse.json(); userInfo = await userInfoResponse.json();
break; break;
} else {
logger.error(`Userinfo endpoint ${userInfoUrl} failed with status: ${userInfoResponse.status}`);
} }
} catch (error) { } catch (error) {
logger.error(`Userinfo endpoint ${userInfoUrl} failed:`, error);
continue; continue;
} }
} }
@@ -423,6 +452,10 @@ router.get('/oidc/callback', async (req, res) => {
if (!userInfo) { if (!userInfo) {
logger.error('Failed to get user information from all sources'); logger.error('Failed to get user information from all sources');
logger.error(`Tried userinfo URLs: ${userInfoUrls.join(', ')}`);
logger.error(`Token data keys: ${Object.keys(tokenData).join(', ')}`);
logger.error(`Has id_token: ${!!tokenData.id_token}`);
logger.error(`Has access_token: ${!!tokenData.access_token}`);
return res.status(400).json({error: 'Failed to get user information'}); return res.status(400).json({error: 'Failed to get user information'});
} }
@@ -431,17 +464,17 @@ router.get('/oidc/callback', async (req, res) => {
return path.split('.').reduce((current, key) => current?.[key], obj); return path.split('.').reduce((current, key) => current?.[key], obj);
}; };
const identifier = getNestedValue(userInfo, config.identifier_path) || const identifier = getNestedValue(userInfo, config.identifier_path) ||
userInfo[config.identifier_path] || userInfo[config.identifier_path] ||
userInfo.sub || userInfo.sub ||
userInfo.email || userInfo.email ||
userInfo.preferred_username; userInfo.preferred_username;
const name = getNestedValue(userInfo, config.name_path) || const name = getNestedValue(userInfo, config.name_path) ||
userInfo[config.name_path] || userInfo[config.name_path] ||
userInfo.name || userInfo.name ||
userInfo.given_name || userInfo.given_name ||
identifier; identifier;
if (!identifier) { if (!identifier) {
logger.error(`Identifier not found at path: ${config.identifier_path}`); logger.error(`Identifier not found at path: ${config.identifier_path}`);
@@ -974,7 +1007,7 @@ router.post('/totp/verify-login', async (req, res) => {
} }
const jwtSecret = process.env.JWT_SECRET || 'secret'; const jwtSecret = process.env.JWT_SECRET || 'secret';
try { try {
const decoded = jwt.verify(temp_token, jwtSecret) as any; const decoded = jwt.verify(temp_token, jwtSecret) as any;
if (!decoded.pending_totp) { if (!decoded.pending_totp) {
@@ -987,7 +1020,7 @@ router.post('/totp/verify-login', async (req, res) => {
} }
const userRecord = user[0]; const userRecord = user[0];
if (!userRecord.totp_enabled || !userRecord.totp_secret) { if (!userRecord.totp_enabled || !userRecord.totp_secret) {
return res.status(400).json({error: 'TOTP not enabled for this user'}); return res.status(400).json({error: 'TOTP not enabled for this user'});
} }
@@ -1002,11 +1035,11 @@ router.post('/totp/verify-login', async (req, res) => {
if (!verified) { if (!verified) {
const backupCodes = userRecord.totp_backup_codes ? JSON.parse(userRecord.totp_backup_codes) : []; const backupCodes = userRecord.totp_backup_codes ? JSON.parse(userRecord.totp_backup_codes) : [];
const backupIndex = backupCodes.indexOf(totp_code); const backupIndex = backupCodes.indexOf(totp_code);
if (backupIndex === -1) { if (backupIndex === -1) {
return res.status(401).json({error: 'Invalid TOTP code'}); return res.status(401).json({error: 'Invalid TOTP code'});
} }
backupCodes.splice(backupIndex, 1); backupCodes.splice(backupIndex, 1);
await db.update(users) await db.update(users)
.set({totp_backup_codes: JSON.stringify(backupCodes)}) .set({totp_backup_codes: JSON.stringify(backupCodes)})
@@ -1033,7 +1066,7 @@ router.post('/totp/verify-login', async (req, res) => {
// POST /users/totp/setup // POST /users/totp/setup
router.post('/totp/setup', authenticateJWT, async (req, res) => { router.post('/totp/setup', authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as any).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0) { if (!user || user.length === 0) {
@@ -1041,7 +1074,7 @@ router.post('/totp/setup', authenticateJWT, async (req, res) => {
} }
const userRecord = user[0]; const userRecord = user[0];
if (userRecord.totp_enabled) { if (userRecord.totp_enabled) {
return res.status(400).json({error: 'TOTP is already enabled'}); return res.status(400).json({error: 'TOTP is already enabled'});
} }
@@ -1085,7 +1118,7 @@ router.post('/totp/enable', authenticateJWT, async (req, res) => {
} }
const userRecord = user[0]; const userRecord = user[0];
if (userRecord.totp_enabled) { if (userRecord.totp_enabled) {
return res.status(400).json({error: 'TOTP is already enabled'}); return res.status(400).json({error: 'TOTP is already enabled'});
} }
@@ -1105,7 +1138,7 @@ router.post('/totp/enable', authenticateJWT, async (req, res) => {
return res.status(401).json({error: 'Invalid TOTP code'}); return res.status(401).json({error: 'Invalid TOTP code'});
} }
const backupCodes = Array.from({length: 8}, () => const backupCodes = Array.from({length: 8}, () =>
Math.random().toString(36).substring(2, 10).toUpperCase() Math.random().toString(36).substring(2, 10).toUpperCase()
); );
@@ -1144,7 +1177,7 @@ router.post('/totp/disable', authenticateJWT, async (req, res) => {
} }
const userRecord = user[0]; const userRecord = user[0];
if (!userRecord.totp_enabled) { if (!userRecord.totp_enabled) {
return res.status(400).json({error: 'TOTP is not enabled'}); return res.status(400).json({error: 'TOTP is not enabled'});
} }
@@ -1202,7 +1235,7 @@ router.post('/totp/backup-codes', authenticateJWT, async (req, res) => {
} }
const userRecord = user[0]; const userRecord = user[0];
if (!userRecord.totp_enabled) { if (!userRecord.totp_enabled) {
return res.status(400).json({error: 'TOTP is not enabled'}); return res.status(400).json({error: 'TOTP is not enabled'});
} }
@@ -1227,7 +1260,7 @@ router.post('/totp/backup-codes', authenticateJWT, async (req, res) => {
return res.status(400).json({error: 'Authentication required'}); return res.status(400).json({error: 'Authentication required'});
} }
const backupCodes = Array.from({length: 8}, () => const backupCodes = Array.from({length: 8}, () =>
Math.random().toString(36).substring(2, 10).toUpperCase() Math.random().toString(36).substring(2, 10).toUpperCase()
); );
@@ -1278,12 +1311,19 @@ router.delete('/delete-user', authenticateJWT, async (req, res) => {
const targetUserId = targetUser[0].id; const targetUserId = targetUser[0].id;
try { try {
db.$client.prepare('DELETE FROM file_manager_recent WHERE user_id = ?').run(targetUserId); await db.delete(fileManagerRecent).where(eq(fileManagerRecent.userId, targetUserId));
db.$client.prepare('DELETE FROM file_manager_pinned WHERE user_id = ?').run(targetUserId); await db.delete(fileManagerPinned).where(eq(fileManagerPinned.userId, targetUserId));
db.$client.prepare('DELETE FROM file_manager_shortcuts WHERE user_id = ?').run(targetUserId); await db.delete(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, targetUserId));
db.$client.prepare('DELETE FROM ssh_data WHERE user_id = ?').run(targetUserId);
await db.delete(dismissedAlerts).where(eq(dismissedAlerts.userId, targetUserId));
await db.delete(sshData).where(eq(sshData.userId, targetUserId));
// Note: All user-related data has been deleted above
// The tables config_editor_* and shared_hosts don't exist in the current schema
} catch (cleanupError) { } catch (cleanupError) {
logger.error(`Cleanup failed for user ${username}:`, cleanupError); logger.error(`Cleanup failed for user ${username}:`, cleanupError);
throw cleanupError;
} }
await db.delete(users).where(eq(users.id, targetUserId)); await db.delete(users).where(eq(users.id, targetUserId));