fix: address automated code review feedback

- Fix migration UPDATE statements to only run when admin exists (prevents errors on fresh installs)
- Add password re-encryption logic to preserve existing encrypted data when changing password
- Remove unused imports and add mapRowToUser helper to eliminate code duplication
- Fix ValidationError import path
This commit is contained in:
Somoru 2025-10-21 12:12:35 +05:30
parent 99c7659abe
commit 1bf9a858eb
3 changed files with 57 additions and 39 deletions

View File

@ -13,10 +13,8 @@
import sql from "../services/sql.js"; import sql from "../services/sql.js";
import optionService from "../services/options.js"; import optionService from "../services/options.js";
import { randomSecureToken } from "../services/utils.js"; import { randomSecureToken, toBase64 } from "../services/utils.js";
import passwordEncryptionService from "../services/encryption/password_encryption.js";
import myScryptService from "../services/encryption/my_scrypt.js"; import myScryptService from "../services/encryption/my_scrypt.js";
import { toBase64 } from "../services/utils.js";
export default async () => { export default async () => {
console.log("Starting multi-user support migration (v234)..."); console.log("Starting multi-user support migration (v234)...");
@ -191,16 +189,16 @@ export default async () => {
`, [adminUserId, 'role_admin', now]); `, [adminUserId, 'role_admin', now]);
console.log(`Created default admin user with ID: ${adminUserId}`); console.log(`Created default admin user with ID: ${adminUserId}`);
// 8. Associate all existing data with the admin user (only if admin was created)
sql.execute(`UPDATE notes SET userId = ? WHERE userId IS NULL`, [adminUserId]);
sql.execute(`UPDATE branches SET userId = ? WHERE userId IS NULL`, [adminUserId]);
sql.execute(`UPDATE etapi_tokens SET userId = ? WHERE userId IS NULL`, [adminUserId]);
sql.execute(`UPDATE recent_notes SET userId = ? WHERE userId IS NULL`, [adminUserId]);
} }
} else { } else {
console.log("No existing password found, admin user will need to be created on first login"); console.log("No existing password found, admin user will need to be created on first login");
} }
// 8. Associate all existing data with the admin user
sql.execute(`UPDATE notes SET userId = ? WHERE userId IS NULL`, [adminUserId]);
sql.execute(`UPDATE branches SET userId = ? WHERE userId IS NULL`, [adminUserId]);
sql.execute(`UPDATE etapi_tokens SET userId = ? WHERE userId IS NULL`, [adminUserId]);
sql.execute(`UPDATE recent_notes SET userId = ? WHERE userId IS NULL`, [adminUserId]);
console.log("Multi-user support migration completed successfully!"); console.log("Multi-user support migration completed successfully!");
}; };

View File

@ -5,9 +5,9 @@
* All endpoints require authentication and most require admin privileges. * All endpoints require authentication and most require admin privileges.
*/ */
import userManagement from "../../services/user_management.js"; import { Request } from "express";
import type { Request, Response } from "express";
import ValidationError from "../../errors/validation_error.js"; import ValidationError from "../../errors/validation_error.js";
import userManagement from "../../services/user_management.js";
/** /**
* Get list of all users * Get list of all users

View File

@ -34,6 +34,7 @@ export interface UserCreateData {
export interface UserUpdateData { export interface UserUpdateData {
email?: string; email?: string;
password?: string; password?: string;
oldPassword?: string; // Required when changing password to decrypt existing data
isActive?: boolean; isActive?: boolean;
isAdmin?: boolean; isAdmin?: boolean;
} }
@ -108,15 +109,9 @@ function createUser(userData: UserCreateData): User {
} }
/** /**
* Get user by ID * Helper function to map database row to User object
*/ */
function getUserById(userId: string): User | null { function mapRowToUser(user: any): User {
const user = sql.getRow(`
SELECT * FROM users WHERE userId = ?
`, [userId]) as any;
if (!user) return null;
return { return {
userId: user.userId, userId: user.userId,
username: user.username, username: user.username,
@ -132,6 +127,17 @@ function getUserById(userId: string): User | null {
}; };
} }
/**
* Get user by ID
*/
function getUserById(userId: string): User | null {
const user = sql.getRow(`
SELECT * FROM users WHERE userId = ?
`, [userId]) as any;
return user ? mapRowToUser(user) : null;
}
/** /**
* Get user by username * Get user by username
*/ */
@ -140,21 +146,7 @@ function getUserByUsername(username: string): User | null {
SELECT * FROM users WHERE username = ? COLLATE NOCASE SELECT * FROM users WHERE username = ? COLLATE NOCASE
`, [username]) as any; `, [username]) as any;
if (!user) return null; return user ? mapRowToUser(user) : null;
return {
userId: user.userId,
username: user.username,
email: user.email,
passwordHash: user.passwordHash,
passwordSalt: user.passwordSalt,
derivedKeySalt: user.derivedKeySalt,
encryptedDataKey: user.encryptedDataKey,
isActive: Boolean(user.isActive),
isAdmin: Boolean(user.isAdmin),
utcDateCreated: user.utcDateCreated,
utcDateModified: user.utcDateModified
};
} }
/** /**
@ -173,16 +165,44 @@ function updateUser(userId: string, updates: UserUpdateData): User | null {
values.push(updates.email || null); values.push(updates.email || null);
} }
if (updates.password !== undefined) { if (updates.password !== undefined && updates.oldPassword !== undefined) {
// Validate that user has existing encrypted data
if (!user.derivedKeySalt || !user.encryptedDataKey) {
throw new Error("Cannot change password: user has no encrypted data");
}
// First, decrypt the existing dataKey with the old password
const oldPasswordDerivedKey = crypto.scryptSync(
updates.oldPassword,
user.derivedKeySalt,
32,
{ N: 16384, r: 8, p: 1 }
);
const dataKey = dataEncryptionService.decrypt(
oldPasswordDerivedKey,
user.encryptedDataKey
);
if (!dataKey) {
throw new Error("Cannot change password: failed to decrypt existing data key with old password");
}
// Generate new password hash // Generate new password hash
const passwordSalt = randomSecureToken(32); const passwordSalt = randomSecureToken(32);
const derivedKeySalt = randomSecureToken(32); const derivedKeySalt = randomSecureToken(32);
const passwordHash = hashPassword(updates.password, passwordSalt); const passwordHash = hashPassword(updates.password, passwordSalt);
// Re-encrypt data key with new password // Re-encrypt the same dataKey with new password
const dataKey = randomSecureToken(16); const passwordDerivedKey = crypto.scryptSync(
const passwordDerivedKey = crypto.scryptSync(updates.password, derivedKeySalt, 32, { N: 16384, r: 8, p: 1 }); updates.password,
const encryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, Buffer.from(dataKey)); derivedKeySalt,
32,
{ N: 16384, r: 8, p: 1 }
);
const encryptedDataKey = dataEncryptionService.encrypt(
passwordDerivedKey,
dataKey
);
updateParts.push('passwordHash = ?', 'passwordSalt = ?', 'derivedKeySalt = ?', 'encryptedDataKey = ?'); updateParts.push('passwordHash = ?', 'passwordSalt = ?', 'derivedKeySalt = ?', 'encryptedDataKey = ?');
values.push(passwordHash, passwordSalt, derivedKeySalt, encryptedDataKey); values.push(passwordHash, passwordSalt, derivedKeySalt, encryptedDataKey);