refactor: convert async transactions to sync + .run()

This commit is contained in:
Nicolas Meienberger 2026-02-14 14:09:07 +01:00
parent 79141198a9
commit bff9cfdb9c
8 changed files with 110 additions and 84 deletions

View file

@ -38,20 +38,20 @@ const assignUserToOrganization = async (userId: string, organizationId: string)
const existingMembership = await db.query.member.findFirst({ where: { userId } }); const existingMembership = await db.query.member.findFirst({ where: { userId } });
await db.transaction(async (tx) => { db.transaction((tx) => {
if (existingMembership) { 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 { } else {
await tx.insert(member).values({ tx.insert(member).values({
id: Bun.randomUUIDv7(), id: Bun.randomUUIDv7(),
organizationId, organizationId,
userId, userId,
role: "member", role: "member",
createdAt: new Date(), createdAt: new Date(),
}); }).run();
} }
await tx.delete(sessionsTable).where(eq(sessionsTable.userId, userId)); tx.delete(sessionsTable).where(eq(sessionsTable.userId, userId)).run();
}); });
}; };

View file

@ -30,9 +30,9 @@ const changeUsername = async (oldUsername: string, newUsername: string) => {
); );
} }
await db.transaction(async (tx) => { db.transaction((tx) => {
await tx.update(usersTable).set({ username: normalizedUsername }).where(eq(usersTable.id, user.id)); tx.update(usersTable).set({ username: normalizedUsername }).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();
}); });
}; };

View file

@ -20,9 +20,9 @@ const disable2FA = async (username: string) => {
throw new Error(`User "${username}" does not have 2FA enabled`); throw new Error(`User "${username}" does not have 2FA enabled`);
} }
await db.transaction(async (tx) => { db.transaction((tx) => {
await tx.update(usersTable).set({ twoFactorEnabled: false }).where(eq(usersTable.id, user.id)); tx.update(usersTable).set({ twoFactorEnabled: false }).where(eq(usersTable.id, user.id)).run();
await tx.delete(twoFactor).where(eq(twoFactor.userId, user.id)); tx.delete(twoFactor).where(eq(twoFactor.userId, user.id)).run();
}); });
}; };

View file

@ -47,36 +47,51 @@ const resolveLegacySecret = async (options: { legacySecret?: string; legacySecre
const rekeyTwoFactor = async (legacySecret: string) => { const rekeyTwoFactor = async (legacySecret: string) => {
const legacyAuthSecret = await deriveSecretFromBase(legacySecret, "better-auth"); const legacyAuthSecret = await deriveSecretFromBase(legacySecret, "better-auth");
const currentAuthSecret = await cryptoUtils.deriveSecret("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) => { for (const record of records) {
const records = await tx.query.twoFactor.findMany({}); try {
const errors: Array<{ userId: string; error: string }> = []; const decryptedSecret = await symmetricDecrypt({ key: legacyAuthSecret, data: record.secret });
let updated = 0; 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 { try {
const decryptedSecret = await symmetricDecrypt({ key: legacyAuthSecret, data: record.secret }); tx
const decryptedBackupCodes = await symmetricDecrypt({
key: legacyAuthSecret,
data: record.backupCodes,
});
await tx
.update(twoFactor) .update(twoFactor)
.set({ .set({
secret: await symmetricEncrypt({ key: currentAuthSecret, data: decryptedSecret }), secret: record.secret,
backupCodes: await symmetricEncrypt({ key: currentAuthSecret, data: decryptedBackupCodes }), backupCodes: record.backupCodes,
}) })
.where(eq(twoFactor.id, record.id)); .where(eq(twoFactor.id, record.id))
.run();
updated += 1; updated += 1;
} catch (error) { } catch (error) {
errors.push({ userId: record.userId, error: toMessage(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") export const rekey2FACommand = new Command("rekey-2fa")

View file

@ -18,19 +18,20 @@ const resetPassword = async (username: string, newPassword: string) => {
} }
const newPasswordHash = await hashPassword(newPassword); const newPasswordHash = await hashPassword(newPassword);
const legacyHash = user.passwordHash ? await Bun.password.hash(newPassword) : null;
await db.transaction(async (tx) => { db.transaction((tx) => {
await tx tx
.update(account) .update(account)
.set({ password: newPasswordHash }) .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) { if (legacyHash) {
const legacyHash = await Bun.password.hash(newPassword); tx.update(usersTable).set({ passwordHash: legacyHash }).where(eq(usersTable.id, user.id)).run();
await tx.update(usersTable).set({ passwordHash: legacyHash }).where(eq(usersTable.id, user.id));
} }
await tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)); tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)).run();
}); });
}; };

View file

@ -27,20 +27,45 @@ export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext)
const isValid = await Bun.password.verify(body.password, legacyUser.passwordHash ?? ""); const isValid = await Bun.password.verify(body.password, legacyUser.passwordHash ?? "");
if (isValid) { if (isValid) {
await db.transaction(async (tx) => { const newUserId = crypto.randomUUID();
const newUserId = crypto.randomUUID(); const accountId = crypto.randomUUID();
const accountId = crypto.randomUUID();
const oldMembership = await tx.query.member.findFirst({ const oldMembership = await db.query.member.findFirst({
where: { userId: legacyUser.id }, where: { userId: legacyUser.id },
with: { with: {
organization: true, 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, id: newUserId,
username: legacyUser.username, username: legacyUser.username,
email: legacyUser.email, email: legacyUser.email,
@ -48,51 +73,37 @@ export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext)
hasDownloadedResticPassword: legacyUser.hasDownloadedResticPassword, hasDownloadedResticPassword: legacyUser.hasDownloadedResticPassword,
emailVerified: false, emailVerified: false,
role: "admin", // In legacy system, the only user is an admin role: "admin", // In legacy system, the only user is an admin
}); }).run();
await tx.insert(account).values({ tx.insert(account).values({
id: accountId, id: accountId,
providerId: "credential", providerId: "credential",
accountId: legacyUser.username, accountId: legacyUser.username,
userId: newUserId, userId: newUserId,
password: await hashPassword(body.password), password: passwordHash,
createdAt: new Date(), createdAt: new Date(),
}); }).run();
// Migrate organization membership to the new user // Migrate organization membership to the new user
// The old membership was cascade-deleted when the old user was deleted // The old membership was cascade-deleted when the old user was deleted
if (oldMembership?.organization) { if (oldMembership?.organization) {
await tx.insert(member).values({ tx.insert(member).values({
id: Bun.randomUUIDv7(), id: Bun.randomUUIDv7(),
userId: newUserId, userId: newUserId,
organizationId: oldMembership.organization.id, organizationId: oldMembership.organization.id,
role: oldMembership.role, role: oldMembership.role,
createdAt: new Date(), createdAt: new Date(),
}); }).run();
} else { } else if (newOrganizationData) {
const orgId = Bun.randomUUIDv7(); tx.insert(organization).values(newOrganizationData).run();
const slug = legacyUser.email.split("@")[0] + "-" + Math.random().toString(36).slice(-4);
const resticPassword = cryptoUtils.generateResticPassword(); tx.insert(member).values({
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({
id: Bun.randomUUIDv7(), id: Bun.randomUUIDv7(),
userId: newUserId, userId: newUserId,
organizationId: orgId, organizationId: newOrganizationData.id,
role: "owner", role: "owner",
createdAt: new Date(), createdAt: new Date(),
}); }).run();
} }
}); });
} else { } else {

View file

@ -84,24 +84,24 @@ export const auth = betterAuth({
}; };
try { try {
await db.transaction(async (tx) => { db.transaction((tx) => {
const orgId = Bun.randomUUIDv7(); const orgId = Bun.randomUUIDv7();
await tx.insert(organizationTable).values({ tx.insert(organizationTable).values({
name: `${user.name}'s Workspace`, name: `${user.name}'s Workspace`,
slug: slug, slug: slug,
id: orgId, id: orgId,
createdAt: new Date(), createdAt: new Date(),
metadata, metadata,
}); }).run();
await tx.insert(member).values({ tx.insert(member).values({
id: Bun.randomUUIDv7(), id: Bun.randomUUIDv7(),
userId: user.id, userId: user.id,
role: "owner", role: "owner",
organizationId: orgId, organizationId: orgId,
createdAt: new Date(), createdAt: new Date(),
}); }).run();
}); });
} catch { } catch {
await db await db

View file

@ -320,16 +320,15 @@ const reorderSchedules = async (scheduleIds: number[]) => {
} }
} }
await db.transaction(async (tx) => { db.transaction((tx) => {
const now = Date.now(); const now = Date.now();
await Promise.all( for (const [index, scheduleId] of scheduleIds.entries()) {
scheduleIds.map((scheduleId, index) => tx
tx .update(backupSchedulesTable)
.update(backupSchedulesTable) .set({ sortOrder: index, updatedAt: now })
.set({ sortOrder: index, updatedAt: now }) .where(and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)))
.where(and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId))), .run();
), }
);
}); });
}; };