Fix users.ts and schema for override
This commit is contained in:
@@ -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'),
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user