mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-17 05:43:36 +00:00
feat(auth): non-destructive password resets and admin reset endpoint
Password resets no longer destroy user data: the DEK is system-wrapped, so forgot-password and admin resets are just a hash update plus session revoke. The wipe branch survives only for accounts that never logged in since the encryption upgrade and now requires explicit confirmDataWipe (surfaced as a 409 DATA_WIPE_REQUIRED; the reset UI asks for confirmation). Adds POST /users/admin/reset-password and removes the dead re-encryption paths.
This commit is contained in:
@@ -483,4 +483,127 @@ export function registerUserAdminRoutes(
|
||||
res.status(500).json({ error: "Failed to create user" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /users/admin/reset-password:
|
||||
* post:
|
||||
* summary: Reset a user's password (admin only)
|
||||
* description: >
|
||||
* Resets another user's password. Data is preserved for users whose
|
||||
* encryption key has been migrated to the system wrap. Users who never
|
||||
* logged in since the encryption upgrade require confirmDataWipe,
|
||||
* which deletes their encrypted data.
|
||||
* tags:
|
||||
* - Users
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - newPassword
|
||||
* properties:
|
||||
* userId:
|
||||
* type: string
|
||||
* username:
|
||||
* type: string
|
||||
* newPassword:
|
||||
* type: string
|
||||
* confirmDataWipe:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Password reset; dataWiped indicates whether encrypted data was deleted.
|
||||
* 400:
|
||||
* description: Missing or invalid parameters.
|
||||
* 403:
|
||||
* description: Admin access required.
|
||||
* 404:
|
||||
* description: User not found.
|
||||
* 409:
|
||||
* description: Reset would wipe the user's data and confirmDataWipe was not set.
|
||||
* 500:
|
||||
* description: Failed to reset password.
|
||||
*/
|
||||
router.post("/admin/reset-password", authenticateJWT, async (req, res) => {
|
||||
const adminId = (req as AuthenticatedRequest).userId;
|
||||
const { userId: targetUserId, username, newPassword } = req.body;
|
||||
const resolvedUserId = isNonEmptyString(targetUserId)
|
||||
? targetUserId.trim()
|
||||
: null;
|
||||
const resolvedUsername = isNonEmptyString(username)
|
||||
? username.trim()
|
||||
: null;
|
||||
|
||||
if (!resolvedUserId && !resolvedUsername) {
|
||||
return res.status(400).json({ error: "User ID or username is required" });
|
||||
}
|
||||
if (!isNonEmptyString(newPassword)) {
|
||||
return res.status(400).json({ error: "New password is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const userRepository = createCurrentUserRepository();
|
||||
const adminUser = await userRepository.findById(adminId);
|
||||
if (!adminUser?.isAdmin) {
|
||||
return res.status(403).json({ error: "Admin access required" });
|
||||
}
|
||||
|
||||
const targetUser = await getUserByPreferredIdentifier(
|
||||
userRepository,
|
||||
resolvedUserId,
|
||||
resolvedUsername,
|
||||
);
|
||||
if (!targetUser) {
|
||||
return res.status(404).json({ error: "User not found" });
|
||||
}
|
||||
if (targetUser.isOidc && !targetUser.passwordHash) {
|
||||
return res.status(400).json({
|
||||
error: "This user authenticates through an external provider",
|
||||
});
|
||||
}
|
||||
|
||||
const { resetUserPassword } =
|
||||
await import("./user-password-reset-routes.js");
|
||||
const outcome = await resetUserPassword(AuthManager.getInstance(), {
|
||||
userId: targetUser.id,
|
||||
username: targetUser.username,
|
||||
newPassword,
|
||||
confirmDataWipe: req.body?.confirmDataWipe === true,
|
||||
});
|
||||
|
||||
if (outcome.status === "wipe_confirmation_required") {
|
||||
return res.status(409).json({
|
||||
error:
|
||||
"This user has not logged in since the encryption upgrade, so their data cannot be recovered. Set confirmDataWipe to reset anyway and delete their hosts, credentials and snippets.",
|
||||
code: "DATA_WIPE_REQUIRED",
|
||||
});
|
||||
}
|
||||
|
||||
const { ipAddress, userAgent } = getRequestMeta(req);
|
||||
await logAudit({
|
||||
userId: adminId,
|
||||
username: adminUser.username ?? adminId,
|
||||
action: "admin_reset_password",
|
||||
resourceType: "user",
|
||||
resourceId: targetUser.id,
|
||||
resourceName: targetUser.username,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
success: true,
|
||||
});
|
||||
|
||||
await DatabaseSaveTrigger.forceSave("admin_password_reset");
|
||||
|
||||
res.json({
|
||||
message: "Password reset",
|
||||
dataWiped: outcome.dataWiped,
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to reset user password", err);
|
||||
res.status(500).json({ error: "Failed to reset password" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AuthManager } from "../../utils/auth-manager.js";
|
||||
|
||||
const calls = vi.hoisted(() => ({
|
||||
userUpdates: [] as Array<[string, Record<string, unknown>]>,
|
||||
deletedFor: [] as string[],
|
||||
rotatedFor: [] as string[],
|
||||
legacyWrapsDeletedFor: [] as string[],
|
||||
}));
|
||||
|
||||
function deletingRepo(label: string) {
|
||||
return () => ({
|
||||
deleteByUserId: async (userId: string) => {
|
||||
calls.deletedFor.push(`${label}:${userId}`);
|
||||
return 0;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
vi.mock("../repositories/factory.js", () => ({
|
||||
createCurrentUserRepository: () => ({
|
||||
update: async (userId: string, update: Record<string, unknown>) => {
|
||||
calls.userUpdates.push([userId, update]);
|
||||
return { id: userId };
|
||||
},
|
||||
}),
|
||||
createCurrentSettingsRepository: () => ({}),
|
||||
createCurrentSshCredentialUsageRepository: deletingRepo("usage"),
|
||||
createCurrentFileManagerBookmarkRepository: deletingRepo("bookmarks"),
|
||||
createCurrentRecentActivityRepository: deletingRepo("activity"),
|
||||
createCurrentDismissedAlertRepository: deletingRepo("alerts"),
|
||||
createCurrentSnippetRepository: deletingRepo("snippets"),
|
||||
createCurrentHostRepository: deletingRepo("hosts"),
|
||||
createCurrentCredentialRepository: deletingRepo("credentials"),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/user-keys.js", () => ({
|
||||
UserKeyManager: {
|
||||
getInstance: () => ({
|
||||
rotateUserDEK: async (userId: string) => {
|
||||
calls.rotatedFor.push(userId);
|
||||
return Buffer.alloc(32);
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/crypto-migration/dek-migration.js", () => ({
|
||||
deleteLegacyWraps: async (userId: string) => {
|
||||
calls.legacyWrapsDeletedFor.push(userId);
|
||||
},
|
||||
}));
|
||||
|
||||
import { resetUserPassword } from "./user-password-reset-routes.js";
|
||||
|
||||
function fakeAuthManager(unlocked: boolean): AuthManager {
|
||||
return {
|
||||
isUserUnlocked: () => unlocked,
|
||||
logoutUser: vi.fn(async () => {}),
|
||||
} as unknown as AuthManager;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
calls.userUpdates = [];
|
||||
calls.deletedFor = [];
|
||||
calls.rotatedFor = [];
|
||||
calls.legacyWrapsDeletedFor = [];
|
||||
});
|
||||
|
||||
describe("resetUserPassword", () => {
|
||||
it("preserves data for users with a server-wrapped key", async () => {
|
||||
const outcome = await resetUserPassword(fakeAuthManager(true), {
|
||||
userId: "user-1",
|
||||
username: "alice",
|
||||
newPassword: "new-password",
|
||||
confirmDataWipe: false,
|
||||
});
|
||||
|
||||
expect(outcome).toEqual({ status: "reset", dataWiped: false });
|
||||
expect(calls.userUpdates).toHaveLength(1);
|
||||
expect(calls.userUpdates[0][1]).toHaveProperty("passwordHash");
|
||||
expect(calls.deletedFor).toEqual([]);
|
||||
expect(calls.rotatedFor).toEqual([]);
|
||||
});
|
||||
|
||||
it("requires explicit consent before wiping an unmigrated user", async () => {
|
||||
const outcome = await resetUserPassword(fakeAuthManager(false), {
|
||||
userId: "user-1",
|
||||
username: "alice",
|
||||
newPassword: "new-password",
|
||||
confirmDataWipe: false,
|
||||
});
|
||||
|
||||
expect(outcome).toEqual({ status: "wipe_confirmation_required" });
|
||||
expect(calls.userUpdates).toEqual([]);
|
||||
expect(calls.deletedFor).toEqual([]);
|
||||
});
|
||||
|
||||
it("wipes data and rotates the key when consent is given", async () => {
|
||||
const outcome = await resetUserPassword(fakeAuthManager(false), {
|
||||
userId: "user-1",
|
||||
username: "alice",
|
||||
newPassword: "new-password",
|
||||
confirmDataWipe: true,
|
||||
});
|
||||
|
||||
expect(outcome).toEqual({ status: "reset", dataWiped: true });
|
||||
expect(calls.deletedFor).toEqual([
|
||||
"usage:user-1",
|
||||
"bookmarks:user-1",
|
||||
"activity:user-1",
|
||||
"alerts:user-1",
|
||||
"snippets:user-1",
|
||||
"hosts:user-1",
|
||||
"credentials:user-1",
|
||||
]);
|
||||
expect(calls.rotatedFor).toEqual(["user-1"]);
|
||||
expect(calls.legacyWrapsDeletedFor).toEqual(["user-1"]);
|
||||
expect(
|
||||
calls.userUpdates.some(([, update]) => update.totpEnabled === false),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,82 @@ function isNonEmptyString(val: unknown): val is string {
|
||||
return typeof val === "string" && val.trim().length > 0;
|
||||
}
|
||||
|
||||
export type PasswordResetOutcome =
|
||||
| { status: "reset"; dataWiped: false }
|
||||
| { status: "reset"; dataWiped: true }
|
||||
| { status: "wipe_confirmation_required" };
|
||||
|
||||
// Resets a user's password. The DEK is wrapped by the system key, so for any
|
||||
// user migrated to the v3 wrap this is just a hash update and their data
|
||||
// survives. Users who never logged in after the encryption upgrade still have
|
||||
// a password-wrapped DEK that a reset cannot recover; their encrypted data
|
||||
// must be wiped, which callers have to confirm explicitly.
|
||||
export async function resetUserPassword(
|
||||
authManager: AuthManager,
|
||||
options: {
|
||||
userId: string;
|
||||
username: string;
|
||||
newPassword: string;
|
||||
confirmDataWipe: boolean;
|
||||
},
|
||||
): Promise<PasswordResetOutcome> {
|
||||
const { userId, username, newPassword, confirmDataWipe } = options;
|
||||
const passwordHash = await bcrypt.hash(newPassword, 10);
|
||||
const userRepository = createCurrentUserRepository();
|
||||
|
||||
if (authManager.isUserUnlocked(userId)) {
|
||||
await userRepository.update(userId, { passwordHash });
|
||||
await authManager.logoutUser(userId);
|
||||
|
||||
authLogger.success(
|
||||
`Password reset (data preserved) for user: ${username}`,
|
||||
{
|
||||
operation: "password_reset_preserved",
|
||||
userId,
|
||||
username,
|
||||
},
|
||||
);
|
||||
return { status: "reset", dataWiped: false };
|
||||
}
|
||||
|
||||
if (!confirmDataWipe) {
|
||||
return { status: "wipe_confirmation_required" };
|
||||
}
|
||||
|
||||
await userRepository.update(userId, { passwordHash });
|
||||
|
||||
await createCurrentSshCredentialUsageRepository().deleteByUserId(userId);
|
||||
await createCurrentFileManagerBookmarkRepository().deleteByUserId(userId);
|
||||
await createCurrentRecentActivityRepository().deleteByUserId(userId);
|
||||
await createCurrentDismissedAlertRepository().deleteByUserId(userId);
|
||||
await createCurrentSnippetRepository().deleteByUserId(userId);
|
||||
await createCurrentHostRepository().deleteByUserId(userId);
|
||||
await createCurrentCredentialRepository().deleteByUserId(userId);
|
||||
|
||||
const { UserKeyManager } = await import("../../utils/user-keys.js");
|
||||
await UserKeyManager.getInstance().rotateUserDEK(userId);
|
||||
const { deleteLegacyWraps } =
|
||||
await import("../../utils/crypto-migration/dek-migration.js");
|
||||
await deleteLegacyWraps(userId);
|
||||
await authManager.logoutUser(userId);
|
||||
|
||||
await userRepository.update(userId, {
|
||||
totpEnabled: false,
|
||||
totpSecret: null,
|
||||
totpBackupCodes: null,
|
||||
});
|
||||
|
||||
authLogger.warn(
|
||||
`Password reset completed for user: ${username}. All encrypted data has been deleted because the old key was unrecoverable.`,
|
||||
{
|
||||
operation: "password_reset_data_deleted",
|
||||
userId,
|
||||
username,
|
||||
},
|
||||
);
|
||||
return { status: "reset", dataWiped: true };
|
||||
}
|
||||
|
||||
export function registerUserPasswordResetRoutes(
|
||||
router: Router,
|
||||
{ authManager }: UserPasswordResetRoutesDeps,
|
||||
@@ -332,111 +408,19 @@ export function registerUserPasswordResetRoutes(
|
||||
}
|
||||
const userId = user.id;
|
||||
|
||||
const password_hash = await bcrypt.hash(newPassword, 10);
|
||||
const outcome = await resetUserPassword(authManager, {
|
||||
userId,
|
||||
username,
|
||||
newPassword,
|
||||
confirmDataWipe: req.body?.confirmDataWipe === true,
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
if (token) {
|
||||
const payload = await authManager.verifyJWTToken(token);
|
||||
if (payload) {
|
||||
userIdFromJwt = payload.userId;
|
||||
}
|
||||
}
|
||||
|
||||
if (userIdFromJwt === userId) {
|
||||
try {
|
||||
const success = await authManager.resetUserPasswordWithPreservedDEK(
|
||||
userId,
|
||||
newPassword,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
throw new Error(
|
||||
"Failed to re-encrypt user data with new password.",
|
||||
);
|
||||
}
|
||||
|
||||
await createCurrentUserRepository().update(userId, {
|
||||
passwordHash: password_hash,
|
||||
});
|
||||
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 {
|
||||
await createCurrentUserRepository().update(userId, {
|
||||
passwordHash: password_hash,
|
||||
if (outcome.status === "wipe_confirmation_required") {
|
||||
return res.status(409).json({
|
||||
error:
|
||||
"This account has not logged in since the encryption upgrade, so its stored data cannot be recovered without the old password. Resetting will permanently delete its hosts, credentials and snippets.",
|
||||
code: "DATA_WIPE_REQUIRED",
|
||||
});
|
||||
|
||||
try {
|
||||
await createCurrentSshCredentialUsageRepository().deleteByUserId(
|
||||
userId,
|
||||
);
|
||||
await createCurrentFileManagerBookmarkRepository().deleteByUserId(
|
||||
userId,
|
||||
);
|
||||
await createCurrentRecentActivityRepository().deleteByUserId(userId);
|
||||
await createCurrentDismissedAlertRepository().deleteByUserId(userId);
|
||||
await createCurrentSnippetRepository().deleteByUserId(userId);
|
||||
await createCurrentHostRepository().deleteByUserId(userId);
|
||||
await createCurrentCredentialRepository().deleteByUserId(userId);
|
||||
|
||||
await authManager.registerUser(userId, newPassword);
|
||||
authManager.logoutUser(userId);
|
||||
|
||||
await createCurrentUserRepository().update(userId, {
|
||||
totpEnabled: false,
|
||||
totpSecret: null,
|
||||
totpBackupCodes: null,
|
||||
});
|
||||
|
||||
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}`);
|
||||
@@ -445,7 +429,10 @@ export function registerUserPasswordResetRoutes(
|
||||
await settingsRepository.delete(`reset_code_${username}`);
|
||||
await settingsRepository.delete(`temp_reset_token_${username}`);
|
||||
|
||||
res.json({ message: "Password has been successfully reset" });
|
||||
res.json({
|
||||
message: "Password has been successfully reset",
|
||||
dataWiped: outcome.dataWiped,
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to complete password reset", err);
|
||||
res.status(500).json({ error: "Failed to complete password reset" });
|
||||
|
||||
@@ -892,13 +892,6 @@ class AuthManager {
|
||||
return this.userKeys.tryGetUserDEK(userId) !== null;
|
||||
}
|
||||
|
||||
async resetUserPasswordWithPreservedDEK(
|
||||
userId: string,
|
||||
_newPassword?: string,
|
||||
): Promise<boolean> {
|
||||
return this.userKeys.tryGetUserDEK(userId) !== null;
|
||||
}
|
||||
|
||||
async isTrustedDevice(
|
||||
userId: string,
|
||||
deviceFingerprint: string,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
RawSqliteUserEncryptionMigrationStore,
|
||||
type LegacyDatabaseInstance,
|
||||
type UserEncryptionMigrationStore,
|
||||
type UserEncryptionMigrationRecord,
|
||||
} from "./user-encryption-migration-store.js";
|
||||
|
||||
class DataCrypto {
|
||||
@@ -238,180 +237,6 @@ class DataCrypto {
|
||||
return this.userKeys.tryGetUserDEK(userId);
|
||||
}
|
||||
|
||||
static async reencryptUserDataAfterPasswordReset(
|
||||
userId: string,
|
||||
newUserDataKey: Buffer,
|
||||
db: LegacyDatabaseInstance,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
reencryptedTables: string[];
|
||||
reencryptedFieldsCount: number;
|
||||
errors: string[];
|
||||
}> {
|
||||
const result = {
|
||||
success: false,
|
||||
reencryptedTables: [] as string[],
|
||||
reencryptedFieldsCount: 0,
|
||||
errors: [] as string[],
|
||||
};
|
||||
|
||||
try {
|
||||
const store = new RawSqliteUserEncryptionMigrationStore(db);
|
||||
type PasswordResetTable = Parameters<
|
||||
UserEncryptionMigrationStore["updatePasswordResetFields"]
|
||||
>[0];
|
||||
const tablesToReencrypt: Array<{
|
||||
table: PasswordResetTable;
|
||||
fields: string[];
|
||||
}> = [
|
||||
{
|
||||
table: "ssh_data",
|
||||
fields: [
|
||||
"password",
|
||||
"key",
|
||||
"key_password",
|
||||
"sudo_password",
|
||||
"autostart_password",
|
||||
"autostart_key",
|
||||
"autostart_key_password",
|
||||
],
|
||||
},
|
||||
{
|
||||
table: "ssh_credentials",
|
||||
fields: [
|
||||
"password",
|
||||
"key",
|
||||
"private_key",
|
||||
"public_key",
|
||||
"key_password",
|
||||
],
|
||||
},
|
||||
{
|
||||
table: "users",
|
||||
fields: [
|
||||
"client_secret",
|
||||
"totp_secret",
|
||||
"totp_backup_codes",
|
||||
"oidc_identifier",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const { table, fields } of tablesToReencrypt) {
|
||||
try {
|
||||
const records =
|
||||
table === "ssh_data"
|
||||
? store.listHostRecords(userId)
|
||||
: table === "ssh_credentials"
|
||||
? store.listCredentialRecords(userId)
|
||||
: [store.getUserRecord(userId)].filter(
|
||||
(record): record is UserEncryptionMigrationRecord =>
|
||||
Boolean(record),
|
||||
);
|
||||
|
||||
for (const record of records) {
|
||||
const recordId = record.id.toString();
|
||||
const updatedRecord: UserEncryptionMigrationRecord = { ...record };
|
||||
let needsUpdate = false;
|
||||
|
||||
for (const fieldName of fields) {
|
||||
const fieldValue = record[fieldName];
|
||||
|
||||
if (
|
||||
fieldValue &&
|
||||
typeof fieldValue === "string" &&
|
||||
fieldValue.trim() !== ""
|
||||
) {
|
||||
try {
|
||||
const reencryptedValue = FieldCrypto.encryptField(
|
||||
fieldValue,
|
||||
newUserDataKey,
|
||||
recordId,
|
||||
fieldName,
|
||||
);
|
||||
|
||||
updatedRecord[fieldName] = reencryptedValue;
|
||||
needsUpdate = true;
|
||||
result.reencryptedFieldsCount++;
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to re-encrypt ${fieldName} for ${table} record ${recordId}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
||||
result.errors.push(errorMsg);
|
||||
databaseLogger.warn(
|
||||
"Field re-encryption failed during password reset",
|
||||
{
|
||||
operation: "password_reset_reencrypt_failed",
|
||||
userId,
|
||||
table,
|
||||
recordId,
|
||||
fieldName,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
const updateFields = fields.filter(
|
||||
(field) => updatedRecord[field] !== record[field],
|
||||
);
|
||||
if (updateFields.length > 0) {
|
||||
store.updatePasswordResetFields(
|
||||
table,
|
||||
record.id,
|
||||
updateFields,
|
||||
updatedRecord,
|
||||
);
|
||||
|
||||
if (!result.reencryptedTables.includes(table)) {
|
||||
result.reencryptedTables.push(table);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (tableError) {
|
||||
const errorMsg = `Failed to re-encrypt table ${table}: ${tableError instanceof Error ? tableError.message : "Unknown error"}`;
|
||||
result.errors.push(errorMsg);
|
||||
databaseLogger.error(
|
||||
"Table re-encryption failed during password reset",
|
||||
tableError,
|
||||
{
|
||||
operation: "password_reset_table_reencrypt_failed",
|
||||
userId,
|
||||
table,
|
||||
error:
|
||||
tableError instanceof Error
|
||||
? tableError.message
|
||||
: "Unknown error",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result.success = result.errors.length === 0;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
databaseLogger.error(
|
||||
"User data re-encryption failed after password reset",
|
||||
error,
|
||||
{
|
||||
operation: "password_reset_reencrypt_failed",
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
);
|
||||
|
||||
result.errors.push(
|
||||
`Critical error during re-encryption: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static validateUserAccess(userId: string): Buffer {
|
||||
return this.userKeys.getUserDEK(userId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user