fix: Password reset issues, ODIC admin auth not filling, and electron x64 build issues
This commit is contained in:
@@ -5,11 +5,15 @@ import { db } from "../db/index.js";
|
||||
import {
|
||||
users,
|
||||
sshData,
|
||||
sshCredentials,
|
||||
fileManagerRecent,
|
||||
fileManagerPinned,
|
||||
fileManagerShortcuts,
|
||||
dismissedAlerts,
|
||||
settings,
|
||||
sshCredentialUsage,
|
||||
recentActivity,
|
||||
snippets,
|
||||
} from "../db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import bcrypt from "bcryptjs";
|
||||
@@ -405,6 +409,35 @@ router.delete("/oidc-config", authenticateJWT, async (req, res) => {
|
||||
// Route: Get OIDC configuration (public - needed for login page)
|
||||
// GET /users/oidc-config
|
||||
router.get("/oidc-config", async (req, res) => {
|
||||
try {
|
||||
const row = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
||||
.get();
|
||||
if (!row) {
|
||||
return res.json(null);
|
||||
}
|
||||
|
||||
const config = JSON.parse((row as Record<string, unknown>).value as string);
|
||||
|
||||
// Only return public fields needed for login page
|
||||
const publicConfig = {
|
||||
client_id: config.client_id,
|
||||
issuer_url: config.issuer_url,
|
||||
authorization_url: config.authorization_url,
|
||||
scopes: config.scopes,
|
||||
};
|
||||
|
||||
return res.json(publicConfig);
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to get OIDC config", err);
|
||||
res.status(500).json({ error: "Failed to get OIDC config" });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Get OIDC configuration for Admin (admin only)
|
||||
// GET /users/oidc-config/admin
|
||||
router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const row = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
|
||||
@@ -415,43 +448,6 @@ router.get("/oidc-config", async (req, res) => {
|
||||
|
||||
let config = JSON.parse((row as Record<string, unknown>).value as string);
|
||||
|
||||
// Check if user is authenticated admin
|
||||
let isAuthenticatedAdmin = false;
|
||||
let userId: string | null = null;
|
||||
const authHeader = req.headers["authorization"];
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
const token = authHeader.split(" ")[1];
|
||||
const authManager = AuthManager.getInstance();
|
||||
const payload = await authManager.verifyJWTToken(token);
|
||||
|
||||
if (payload) {
|
||||
userId = payload.userId;
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
|
||||
if (user && user.length > 0 && user[0].is_admin) {
|
||||
isAuthenticatedAdmin = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For non-admin users, hide sensitive fields
|
||||
if (!isAuthenticatedAdmin) {
|
||||
// Remove all sensitive fields for public access
|
||||
delete config.client_secret;
|
||||
delete config.id;
|
||||
|
||||
// Only return public fields needed for login page
|
||||
const publicConfig = {
|
||||
client_id: config.client_id,
|
||||
issuer_url: config.issuer_url,
|
||||
authorization_url: config.authorization_url,
|
||||
scopes: config.scopes,
|
||||
};
|
||||
|
||||
return res.json(publicConfig);
|
||||
}
|
||||
|
||||
// For authenticated admins, decrypt sensitive fields
|
||||
if (config.client_secret?.startsWith("encrypted:")) {
|
||||
try {
|
||||
const adminDataKey = DataCrypto.getUserDataKey(userId);
|
||||
@@ -463,8 +459,6 @@ router.get("/oidc-config", async (req, res) => {
|
||||
adminDataKey,
|
||||
);
|
||||
} else {
|
||||
// Admin is authenticated but data key is not available
|
||||
// This can happen if they haven't unlocked their data yet
|
||||
config.client_secret = "[ENCRYPTED - PASSWORD REQUIRED]";
|
||||
}
|
||||
} catch (decryptError) {
|
||||
@@ -475,7 +469,6 @@ router.get("/oidc-config", async (req, res) => {
|
||||
config.client_secret = "[ENCRYPTED - DECRYPTION FAILED]";
|
||||
}
|
||||
} else if (config.client_secret?.startsWith("encoded:")) {
|
||||
// Decode for authenticated admins
|
||||
try {
|
||||
const decoded = Buffer.from(
|
||||
config.client_secret.substring(8),
|
||||
@@ -493,8 +486,8 @@ router.get("/oidc-config", async (req, res) => {
|
||||
|
||||
res.json(config);
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to get OIDC config", err);
|
||||
res.status(500).json({ error: "Failed to get OIDC config" });
|
||||
authLogger.error("Failed to get OIDC config for admin", err);
|
||||
res.status(500).json({ error: "Failed to get OIDC config for admin" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1399,63 +1392,131 @@ router.post("/complete-reset", async (req, res) => {
|
||||
const saltRounds = parseInt(process.env.SALT || "10", 10);
|
||||
const password_hash = await bcrypt.hash(newPassword, saltRounds);
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({ password_hash })
|
||||
.where(eq(users.username, username));
|
||||
// Check if user is logged in and data is unlocked
|
||||
let userIdFromJwt: string | null = null;
|
||||
const cookie = req.cookies?.jwt;
|
||||
let header: string | undefined;
|
||||
if (req.headers?.authorization?.startsWith("Bearer ")) {
|
||||
header = req.headers?.authorization?.split(" ")[1];
|
||||
}
|
||||
const token = cookie || header;
|
||||
|
||||
try {
|
||||
// Delete all encrypted data since we're creating a new DEK
|
||||
// The old DEK is lost, so old encrypted data becomes unreadable
|
||||
await db.delete(sshData).where(eq(sshData.userId, userId));
|
||||
await db
|
||||
.delete(fileManagerRecent)
|
||||
.where(eq(fileManagerRecent.userId, userId));
|
||||
await db
|
||||
.delete(fileManagerPinned)
|
||||
.where(eq(fileManagerPinned.userId, userId));
|
||||
await db
|
||||
.delete(fileManagerShortcuts)
|
||||
.where(eq(fileManagerShortcuts.userId, userId));
|
||||
await db
|
||||
.delete(dismissedAlerts)
|
||||
.where(eq(dismissedAlerts.userId, userId));
|
||||
if (token) {
|
||||
const payload = await authManager.verifyJWTToken(token);
|
||||
if (payload) {
|
||||
userIdFromJwt = payload.userId;
|
||||
}
|
||||
}
|
||||
|
||||
// Now setup new encryption with new DEK
|
||||
await authManager.registerUser(userId, newPassword);
|
||||
authManager.logoutUser(userId);
|
||||
if (userIdFromJwt === userId && authManager.isUserUnlocked(userId)) {
|
||||
// Logged-in user: preserve data
|
||||
try {
|
||||
const success = await authManager.resetUserPasswordWithPreservedDEK(
|
||||
userId,
|
||||
newPassword,
|
||||
);
|
||||
|
||||
// Clear TOTP settings
|
||||
if (!success) {
|
||||
throw new Error("Failed to re-encrypt user data with new password.");
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({ password_hash })
|
||||
.where(eq(users.id, userId));
|
||||
authManager.logoutUser(userId);
|
||||
authLogger.success(
|
||||
`Password reset (data preserved) for user: ${username}`,
|
||||
{
|
||||
operation: "password_reset_preserved",
|
||||
userId,
|
||||
username,
|
||||
},
|
||||
);
|
||||
} catch (encryptionError) {
|
||||
authLogger.error(
|
||||
"Failed to setup user data encryption after password reset",
|
||||
encryptionError,
|
||||
{
|
||||
operation: "password_reset_encryption_failed_preserved",
|
||||
userId,
|
||||
username,
|
||||
},
|
||||
);
|
||||
return res.status(500).json({
|
||||
error: "Password reset failed. Please contact administrator.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Logged-out user: data is lost
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
totp_enabled: false,
|
||||
totp_secret: null,
|
||||
totp_backup_codes: null,
|
||||
})
|
||||
.where(eq(users.id, userId));
|
||||
.set({ password_hash })
|
||||
.where(eq(users.username, username));
|
||||
|
||||
authLogger.warn(
|
||||
`Password reset completed for user: ${username}. All encrypted data has been deleted due to lost encryption key.`,
|
||||
{
|
||||
operation: "password_reset_data_deleted",
|
||||
userId,
|
||||
username,
|
||||
},
|
||||
);
|
||||
} catch (encryptionError) {
|
||||
authLogger.error(
|
||||
"Failed to setup user data encryption after password reset",
|
||||
encryptionError,
|
||||
{
|
||||
operation: "password_reset_encryption_failed",
|
||||
userId,
|
||||
username,
|
||||
},
|
||||
);
|
||||
return res.status(500).json({
|
||||
error: "Password reset failed. Please contact administrator.",
|
||||
});
|
||||
try {
|
||||
// Delete all encrypted data since we're creating a new DEK
|
||||
// The old DEK is lost, so old encrypted data becomes unreadable
|
||||
await db
|
||||
.delete(sshCredentialUsage)
|
||||
.where(eq(sshCredentialUsage.userId, userId));
|
||||
await db
|
||||
.delete(fileManagerRecent)
|
||||
.where(eq(fileManagerRecent.userId, userId));
|
||||
await db
|
||||
.delete(fileManagerPinned)
|
||||
.where(eq(fileManagerPinned.userId, userId));
|
||||
await db
|
||||
.delete(fileManagerShortcuts)
|
||||
.where(eq(fileManagerShortcuts.userId, userId));
|
||||
await db
|
||||
.delete(recentActivity)
|
||||
.where(eq(recentActivity.userId, userId));
|
||||
await db
|
||||
.delete(dismissedAlerts)
|
||||
.where(eq(dismissedAlerts.userId, userId));
|
||||
await db.delete(snippets).where(eq(snippets.userId, userId));
|
||||
await db.delete(sshData).where(eq(sshData.userId, userId));
|
||||
await db
|
||||
.delete(sshCredentials)
|
||||
.where(eq(sshCredentials.userId, userId));
|
||||
|
||||
// Now setup new encryption with new DEK
|
||||
await authManager.registerUser(userId, newPassword);
|
||||
authManager.logoutUser(userId);
|
||||
|
||||
// Clear TOTP settings
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
totp_enabled: false,
|
||||
totp_secret: null,
|
||||
totp_backup_codes: null,
|
||||
})
|
||||
.where(eq(users.id, userId));
|
||||
|
||||
authLogger.warn(
|
||||
`Password reset completed for user: ${username}. All encrypted data has been deleted due to lost encryption key.`,
|
||||
{
|
||||
operation: "password_reset_data_deleted",
|
||||
userId,
|
||||
username,
|
||||
},
|
||||
);
|
||||
} catch (encryptionError) {
|
||||
authLogger.error(
|
||||
"Failed to setup user data encryption after password reset",
|
||||
encryptionError,
|
||||
{
|
||||
operation: "password_reset_encryption_failed",
|
||||
userId,
|
||||
username,
|
||||
},
|
||||
);
|
||||
return res.status(500).json({
|
||||
error: "Password reset failed. Please contact administrator.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
authLogger.success(`Password successfully reset for user: ${username}`);
|
||||
@@ -1474,6 +1535,52 @@ router.post("/complete-reset", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/change-password", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { oldPassword, newPassword } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "User not authenticated" });
|
||||
}
|
||||
|
||||
if (!oldPassword || !newPassword) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Old and new passwords are required." });
|
||||
}
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0) {
|
||||
return res.status(404).json({ error: "User not found" });
|
||||
}
|
||||
|
||||
// Verify old password for login hash
|
||||
const isMatch = await bcrypt.compare(oldPassword, user[0].password_hash);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({ error: "Incorrect current password" });
|
||||
}
|
||||
|
||||
// Change encryption keys and login hash
|
||||
const success = await authManager.changeUserPassword(
|
||||
userId,
|
||||
oldPassword,
|
||||
newPassword,
|
||||
);
|
||||
if (!success) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Failed to update password and re-encrypt data." });
|
||||
}
|
||||
|
||||
const saltRounds = parseInt(process.env.SALT || "10", 10);
|
||||
const password_hash = await bcrypt.hash(newPassword, saltRounds);
|
||||
await db.update(users).set({ password_hash }).where(eq(users.id, userId));
|
||||
|
||||
authManager.logoutUser(userId); // Log out user for security
|
||||
|
||||
res.json({ message: "Password changed successfully. Please log in again." });
|
||||
});
|
||||
|
||||
// Route: List all users (admin only)
|
||||
// GET /users/list
|
||||
router.get("/list", authenticateJWT, async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user