refactor: convert async transactions to sync + .run()
This commit is contained in:
parent
79141198a9
commit
bff9cfdb9c
8 changed files with 110 additions and 84 deletions
|
|
@ -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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
),
|
}
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue