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'),
|
||||
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'),
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user