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 } });
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();
});
};

View file

@ -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();
});
};

View file

@ -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();
});
};

View file

@ -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")

View file

@ -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();
});
};

View file

@ -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 {

View file

@ -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

View file

@ -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();
}
});
};