From 190470022aa6cfa8fb2ab5b17413ca37f8cb298d Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 3 Sep 2025 13:09:53 -0500 Subject: [PATCH] Fix users.ts and schema for override --- src/backend/database/db/schema.ts | 2 +- src/backend/database/routes/users.ts | 106 ++++++++++++++++++--------- 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 81300eea..759efbdf 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -17,7 +17,7 @@ export const users = sqliteTable('users', { identifier_path: text('identifier_path'), name_path: text('name_path'), scopes: text().default("openid email profile"), - + totp_secret: text('totp_secret'), totp_enabled: integer('totp_enabled', {mode: 'boolean'}).notNull().default(false), totp_backup_codes: text('totp_backup_codes'), diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index fe979b0c..a3935edf 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1,6 +1,6 @@ import express from 'express'; 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 chalk from 'chalk'; import bcrypt from 'bcryptjs'; @@ -234,6 +234,7 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => { issuer_url, authorization_url, token_url, + userinfo_url, identifier_path, name_path, scopes @@ -252,6 +253,7 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => { issuer_url, authorization_url, token_url, + userinfo_url: userinfo_url || '', identifier_path, name_path, scopes: scopes || 'openid email profile' @@ -374,14 +376,38 @@ router.get('/oidc/callback', async (req, res) => { const tokenData = await tokenResponse.json() as any; 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 baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, ''); - - userInfoUrls.push(`${baseUrl}/userinfo/`); - userInfoUrls.push(`${normalizedIssuerUrl}/userinfo/`); - userInfoUrls.push(`${normalizedIssuerUrl}/userinfo`); + + try { + const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; + 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) { try { @@ -414,8 +440,11 @@ router.get('/oidc/callback', async (req, res) => { if (userInfoResponse.ok) { userInfo = await userInfoResponse.json(); break; + } else { + logger.error(`Userinfo endpoint ${userInfoUrl} failed with status: ${userInfoResponse.status}`); } } catch (error) { + logger.error(`Userinfo endpoint ${userInfoUrl} failed:`, error); continue; } } @@ -423,6 +452,10 @@ router.get('/oidc/callback', async (req, res) => { if (!userInfo) { 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'}); } @@ -431,17 +464,17 @@ router.get('/oidc/callback', async (req, res) => { return path.split('.').reduce((current, key) => current?.[key], obj); }; - const identifier = getNestedValue(userInfo, config.identifier_path) || - userInfo[config.identifier_path] || - userInfo.sub || - userInfo.email || - userInfo.preferred_username; - - const name = getNestedValue(userInfo, config.name_path) || - userInfo[config.name_path] || - userInfo.name || - userInfo.given_name || - identifier; + const identifier = getNestedValue(userInfo, config.identifier_path) || + userInfo[config.identifier_path] || + userInfo.sub || + userInfo.email || + userInfo.preferred_username; + + const name = getNestedValue(userInfo, config.name_path) || + userInfo[config.name_path] || + userInfo.name || + userInfo.given_name || + identifier; if (!identifier) { 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'; - + try { const decoded = jwt.verify(temp_token, jwtSecret) as any; if (!decoded.pending_totp) { @@ -987,7 +1020,7 @@ router.post('/totp/verify-login', async (req, res) => { } const userRecord = user[0]; - + if (!userRecord.totp_enabled || !userRecord.totp_secret) { 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) { const backupCodes = userRecord.totp_backup_codes ? JSON.parse(userRecord.totp_backup_codes) : []; const backupIndex = backupCodes.indexOf(totp_code); - + if (backupIndex === -1) { return res.status(401).json({error: 'Invalid TOTP code'}); } - + backupCodes.splice(backupIndex, 1); await db.update(users) .set({totp_backup_codes: JSON.stringify(backupCodes)}) @@ -1033,7 +1066,7 @@ router.post('/totp/verify-login', async (req, res) => { // POST /users/totp/setup router.post('/totp/setup', authenticateJWT, async (req, res) => { const userId = (req as any).userId; - + try { const user = await db.select().from(users).where(eq(users.id, userId)); if (!user || user.length === 0) { @@ -1041,7 +1074,7 @@ router.post('/totp/setup', authenticateJWT, async (req, res) => { } const userRecord = user[0]; - + if (userRecord.totp_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]; - + if (userRecord.totp_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'}); } - const backupCodes = Array.from({length: 8}, () => + const backupCodes = Array.from({length: 8}, () => Math.random().toString(36).substring(2, 10).toUpperCase() ); @@ -1144,7 +1177,7 @@ router.post('/totp/disable', authenticateJWT, async (req, res) => { } const userRecord = user[0]; - + if (!userRecord.totp_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]; - + if (!userRecord.totp_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'}); } - const backupCodes = Array.from({length: 8}, () => + const backupCodes = Array.from({length: 8}, () => 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; try { - db.$client.prepare('DELETE FROM file_manager_recent WHERE user_id = ?').run(targetUserId); - db.$client.prepare('DELETE FROM file_manager_pinned WHERE user_id = ?').run(targetUserId); - db.$client.prepare('DELETE FROM file_manager_shortcuts WHERE user_id = ?').run(targetUserId); - db.$client.prepare('DELETE FROM ssh_data WHERE user_id = ?').run(targetUserId); + await db.delete(fileManagerRecent).where(eq(fileManagerRecent.userId, targetUserId)); + await db.delete(fileManagerPinned).where(eq(fileManagerPinned.userId, targetUserId)); + await db.delete(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, 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) { logger.error(`Cleanup failed for user ${username}:`, cleanupError); + throw cleanupError; } await db.delete(users).where(eq(users.id, targetUserId));