From bff9cfdb9c7fa84325455f1f45a2a3a8a134270d Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 14 Feb 2026 14:09:07 +0100 Subject: [PATCH] refactor: convert async transactions to sync + .run() --- .../cli/commands/assign-organization.ts | 10 +-- app/server/cli/commands/change-username.ts | 6 +- app/server/cli/commands/disable-2fa.ts | 6 +- app/server/cli/commands/rekey-2fa.ts | 49 +++++++---- app/server/cli/commands/reset-password.ts | 15 ++-- .../auth-middlewares/convert-legacy-user.ts | 81 +++++++++++-------- app/server/lib/auth.ts | 10 +-- app/server/modules/backups/backups.service.ts | 17 ++-- 8 files changed, 110 insertions(+), 84 deletions(-) diff --git a/app/server/cli/commands/assign-organization.ts b/app/server/cli/commands/assign-organization.ts index ee32f323..57deab32 100644 --- a/app/server/cli/commands/assign-organization.ts +++ b/app/server/cli/commands/assign-organization.ts @@ -38,20 +38,20 @@ const assignUserToOrganization = async (userId: string, organizationId: string) const existingMembership = await db.query.member.findFirst({ where: { userId } }); - await db.transaction(async (tx) => { + db.transaction((tx) => { if (existingMembership) { - await tx.update(member).set({ organizationId }).where(eq(member.id, existingMembership.id)); + tx.update(member).set({ organizationId }).where(eq(member.id, existingMembership.id)).run(); } else { - await tx.insert(member).values({ + tx.insert(member).values({ id: Bun.randomUUIDv7(), organizationId, userId, role: "member", createdAt: new Date(), - }); + }).run(); } - await tx.delete(sessionsTable).where(eq(sessionsTable.userId, userId)); + tx.delete(sessionsTable).where(eq(sessionsTable.userId, userId)).run(); }); }; diff --git a/app/server/cli/commands/change-username.ts b/app/server/cli/commands/change-username.ts index 7e13bb61..d45246bb 100644 --- a/app/server/cli/commands/change-username.ts +++ b/app/server/cli/commands/change-username.ts @@ -30,9 +30,9 @@ const changeUsername = async (oldUsername: string, newUsername: string) => { ); } - await db.transaction(async (tx) => { - await tx.update(usersTable).set({ username: normalizedUsername }).where(eq(usersTable.id, user.id)); - await tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)); + db.transaction((tx) => { + tx.update(usersTable).set({ username: normalizedUsername }).where(eq(usersTable.id, user.id)).run(); + tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)).run(); }); }; diff --git a/app/server/cli/commands/disable-2fa.ts b/app/server/cli/commands/disable-2fa.ts index f4de9046..667e843f 100644 --- a/app/server/cli/commands/disable-2fa.ts +++ b/app/server/cli/commands/disable-2fa.ts @@ -20,9 +20,9 @@ const disable2FA = async (username: string) => { throw new Error(`User "${username}" does not have 2FA enabled`); } - await db.transaction(async (tx) => { - await tx.update(usersTable).set({ twoFactorEnabled: false }).where(eq(usersTable.id, user.id)); - await tx.delete(twoFactor).where(eq(twoFactor.userId, user.id)); + db.transaction((tx) => { + tx.update(usersTable).set({ twoFactorEnabled: false }).where(eq(usersTable.id, user.id)).run(); + tx.delete(twoFactor).where(eq(twoFactor.userId, user.id)).run(); }); }; diff --git a/app/server/cli/commands/rekey-2fa.ts b/app/server/cli/commands/rekey-2fa.ts index bd49aa67..618a06aa 100644 --- a/app/server/cli/commands/rekey-2fa.ts +++ b/app/server/cli/commands/rekey-2fa.ts @@ -47,36 +47,51 @@ const resolveLegacySecret = async (options: { legacySecret?: string; legacySecre const rekeyTwoFactor = async (legacySecret: string) => { const legacyAuthSecret = await deriveSecretFromBase(legacySecret, "better-auth"); const currentAuthSecret = await cryptoUtils.deriveSecret("better-auth"); + const records = await db.query.twoFactor.findMany({}); + const errors: Array<{ userId: string; error: string }> = []; + const updates: Array<{ id: string; userId: string; secret: string; backupCodes: string }> = []; - return db.transaction(async (tx) => { - const records = await tx.query.twoFactor.findMany({}); - const errors: Array<{ userId: string; error: string }> = []; - let updated = 0; + for (const record of records) { + try { + const decryptedSecret = await symmetricDecrypt({ key: legacyAuthSecret, data: record.secret }); + const decryptedBackupCodes = await symmetricDecrypt({ + key: legacyAuthSecret, + data: record.backupCodes, + }); - for (const record of records) { + updates.push({ + id: record.id, + userId: record.userId, + secret: await symmetricEncrypt({ key: currentAuthSecret, data: decryptedSecret }), + backupCodes: await symmetricEncrypt({ key: currentAuthSecret, data: decryptedBackupCodes }), + }); + } catch (error) { + errors.push({ userId: record.userId, error: toMessage(error) }); + } + } + + let updated = 0; + + db.transaction((tx) => { + for (const record of updates) { try { - const decryptedSecret = await symmetricDecrypt({ key: legacyAuthSecret, data: record.secret }); - const decryptedBackupCodes = await symmetricDecrypt({ - key: legacyAuthSecret, - data: record.backupCodes, - }); - - await tx + tx .update(twoFactor) .set({ - secret: await symmetricEncrypt({ key: currentAuthSecret, data: decryptedSecret }), - backupCodes: await symmetricEncrypt({ key: currentAuthSecret, data: decryptedBackupCodes }), + secret: record.secret, + backupCodes: record.backupCodes, }) - .where(eq(twoFactor.id, record.id)); + .where(eq(twoFactor.id, record.id)) + .run(); updated += 1; } catch (error) { errors.push({ userId: record.userId, error: toMessage(error) }); } } - - return { total: records.length, updated, errors }; }); + + return { total: records.length, updated, errors }; }; export const rekey2FACommand = new Command("rekey-2fa") diff --git a/app/server/cli/commands/reset-password.ts b/app/server/cli/commands/reset-password.ts index 0306f802..b86fbe00 100644 --- a/app/server/cli/commands/reset-password.ts +++ b/app/server/cli/commands/reset-password.ts @@ -18,19 +18,20 @@ const resetPassword = async (username: string, newPassword: string) => { } const newPasswordHash = await hashPassword(newPassword); + const legacyHash = user.passwordHash ? await Bun.password.hash(newPassword) : null; - await db.transaction(async (tx) => { - await tx + db.transaction((tx) => { + tx .update(account) .set({ password: newPasswordHash }) - .where(and(eq(account.userId, user.id), eq(account.providerId, "credential"))); + .where(and(eq(account.userId, user.id), eq(account.providerId, "credential"))) + .run(); - if (user.passwordHash) { - const legacyHash = await Bun.password.hash(newPassword); - await tx.update(usersTable).set({ passwordHash: legacyHash }).where(eq(usersTable.id, user.id)); + if (legacyHash) { + tx.update(usersTable).set({ passwordHash: legacyHash }).where(eq(usersTable.id, user.id)).run(); } - await tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)); + tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)).run(); }); }; diff --git a/app/server/lib/auth-middlewares/convert-legacy-user.ts b/app/server/lib/auth-middlewares/convert-legacy-user.ts index f3c2d236..d21fcf6c 100644 --- a/app/server/lib/auth-middlewares/convert-legacy-user.ts +++ b/app/server/lib/auth-middlewares/convert-legacy-user.ts @@ -27,20 +27,45 @@ export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) const isValid = await Bun.password.verify(body.password, legacyUser.passwordHash ?? ""); if (isValid) { - await db.transaction(async (tx) => { - const newUserId = crypto.randomUUID(); - const accountId = crypto.randomUUID(); + const newUserId = crypto.randomUUID(); + const accountId = crypto.randomUUID(); - const oldMembership = await tx.query.member.findFirst({ - where: { userId: legacyUser.id }, - with: { - organization: true, + const oldMembership = await db.query.member.findFirst({ + where: { userId: legacyUser.id }, + with: { + organization: true, + }, + }); + + const passwordHash = await hashPassword(body.password); + + let newOrganizationData: { + id: string; + name: string; + slug: string; + createdAt: Date; + metadata: { + resticPassword: string; + }; + } | null = null; + + if (!oldMembership?.organization) { + const resticPassword = cryptoUtils.generateResticPassword(); + newOrganizationData = { + id: Bun.randomUUIDv7(), + name: `${legacyUser.name}'s Workspace`, + slug: legacyUser.email.split("@")[0] + "-" + Math.random().toString(36).slice(-4), + createdAt: new Date(), + metadata: { + resticPassword: await cryptoUtils.sealSecret(resticPassword), }, - }); + }; + } - await tx.delete(usersTable).where(eq(usersTable.id, legacyUser.id)); + db.transaction((tx) => { + tx.delete(usersTable).where(eq(usersTable.id, legacyUser.id)).run(); - await tx.insert(usersTable).values({ + tx.insert(usersTable).values({ id: newUserId, username: legacyUser.username, email: legacyUser.email, @@ -48,51 +73,37 @@ export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) hasDownloadedResticPassword: legacyUser.hasDownloadedResticPassword, emailVerified: false, role: "admin", // In legacy system, the only user is an admin - }); + }).run(); - await tx.insert(account).values({ + tx.insert(account).values({ id: accountId, providerId: "credential", accountId: legacyUser.username, userId: newUserId, - password: await hashPassword(body.password), + password: passwordHash, createdAt: new Date(), - }); + }).run(); // Migrate organization membership to the new user // The old membership was cascade-deleted when the old user was deleted if (oldMembership?.organization) { - await tx.insert(member).values({ + tx.insert(member).values({ id: Bun.randomUUIDv7(), userId: newUserId, organizationId: oldMembership.organization.id, role: oldMembership.role, createdAt: new Date(), - }); - } else { - const orgId = Bun.randomUUIDv7(); - const slug = legacyUser.email.split("@")[0] + "-" + Math.random().toString(36).slice(-4); + }).run(); + } else if (newOrganizationData) { + tx.insert(organization).values(newOrganizationData).run(); - const resticPassword = cryptoUtils.generateResticPassword(); - const metadata = { - resticPassword: await cryptoUtils.sealSecret(resticPassword), - }; - - await tx.insert(organization).values({ - id: orgId, - name: `${legacyUser.name}'s Workspace`, - slug: slug, - createdAt: new Date(), - metadata, - }); - - await tx.insert(member).values({ + tx.insert(member).values({ id: Bun.randomUUIDv7(), userId: newUserId, - organizationId: orgId, + organizationId: newOrganizationData.id, role: "owner", createdAt: new Date(), - }); + }).run(); } }); } else { diff --git a/app/server/lib/auth.ts b/app/server/lib/auth.ts index b1680b54..414e7576 100644 --- a/app/server/lib/auth.ts +++ b/app/server/lib/auth.ts @@ -84,24 +84,24 @@ export const auth = betterAuth({ }; try { - await db.transaction(async (tx) => { + db.transaction((tx) => { const orgId = Bun.randomUUIDv7(); - await tx.insert(organizationTable).values({ + tx.insert(organizationTable).values({ name: `${user.name}'s Workspace`, slug: slug, id: orgId, createdAt: new Date(), metadata, - }); + }).run(); - await tx.insert(member).values({ + tx.insert(member).values({ id: Bun.randomUUIDv7(), userId: user.id, role: "owner", organizationId: orgId, createdAt: new Date(), - }); + }).run(); }); } catch { await db diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index 26a88707..901424bb 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -320,16 +320,15 @@ const reorderSchedules = async (scheduleIds: number[]) => { } } - await db.transaction(async (tx) => { + db.transaction((tx) => { const now = Date.now(); - await Promise.all( - scheduleIds.map((scheduleId, index) => - tx - .update(backupSchedulesTable) - .set({ sortOrder: index, updatedAt: now }) - .where(and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId))), - ), - ); + for (const [index, scheduleId] of scheduleIds.entries()) { + tx + .update(backupSchedulesTable) + .set({ sortOrder: index, updatedAt: now }) + .where(and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId))) + .run(); + } }); };