diff --git a/apps/backend/src/lib/external-db-sync.ts b/apps/backend/src/lib/external-db-sync.ts index d638911a86..20af7b5e46 100644 --- a/apps/backend/src/lib/external-db-sync.ts +++ b/apps/backend/src/lib/external-db-sync.ts @@ -126,7 +126,7 @@ export async function recordExternalDbSyncDeletion( if (target.tableName === "ProjectUser") { assertUuid(target.projectUserId, "projectUserId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -150,18 +150,13 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for ProjectUser, got ${insertedCount}.` - ); - } return; } if (target.tableName === "ContactChannel") { assertUuid(target.projectUserId, "projectUserId"); assertUuid(target.contactChannelId, "contactChannelId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -193,17 +188,12 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for ContactChannel, got ${insertedCount}.` - ); - } return; } if (target.tableName === "Team") { assertUuid(target.teamId, "teamId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -227,18 +217,13 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for Team, got ${insertedCount}.` - ); - } return; } if (target.tableName === "TeamMember") { assertUuid(target.projectUserId, "projectUserId"); assertUuid(target.teamId, "teamId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -263,17 +248,12 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for TeamMember, got ${insertedCount}.` - ); - } return; } if (target.tableName === "TeamMemberDirectPermission") { assertUuid(target.permissionDbId, "permissionDbId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -302,17 +282,12 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for TeamMemberDirectPermission, got ${insertedCount}.` - ); - } return; } if (target.tableName === "ProjectUserDirectPermission") { assertUuid(target.permissionDbId, "permissionDbId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -340,17 +315,12 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for ProjectUserDirectPermission, got ${insertedCount}.` - ); - } return; } if (target.tableName === "UserNotificationPreference") { assertUuid(target.notificationPreferenceId, "notificationPreferenceId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -377,17 +347,12 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for UserNotificationPreference, got ${insertedCount}.` - ); - } return; } if (target.tableName === "ProjectUserRefreshToken") { assertUuid(target.refreshTokenId, "refreshTokenId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -411,17 +376,12 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for ProjectUserRefreshToken, got ${insertedCount}.` - ); - } return; } if (target.tableName === "ProjectUserOAuthAccount") { assertUuid(target.oauthAccountId, "oauthAccountId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -445,11 +405,6 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for ProjectUserOAuthAccount, got ${insertedCount}.` - ); - } return; } @@ -458,7 +413,7 @@ export async function recordExternalDbSyncDeletion( assertNonEmptyString(target.verificationCodeProjectId, "verificationCodeProjectId"); assertNonEmptyString(target.verificationCodeBranchId, "verificationCodeBranchId"); assertUuid(target.verificationCodeId, "verificationCodeId"); - const insertedCount = await tx.$executeRaw(Prisma.sql` + await tx.$executeRaw(Prisma.sql` INSERT INTO "DeletedRow" ( "id", "tenancyId", @@ -487,11 +442,6 @@ export async function recordExternalDbSyncDeletion( FOR UPDATE OF "VerificationCode" `); - if (insertedCount !== 1) { - throw new StackAssertionError( - `Expected to insert 1 DeletedRow entry for VerificationCode_TEAM_INVITATION, got ${insertedCount}.` - ); - } return; } } diff --git a/apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/current/index.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/current/index.test.ts index 4d38c50046..7d5391db5b 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/current/index.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/current/index.test.ts @@ -1,5 +1,88 @@ import { it } from "../../../../../../../helpers"; -import { Auth, niceBackendFetch } from "../../../../../../backend-helpers"; +import { Auth, backendContext, niceBackendFetch } from "../../../../../../backend-helpers"; + +it("should not crash when signing out a session that was already deleted by a bulk operation", async ({ expect }) => { + // Reproduce: sign up, then admin-delete all refresh tokens (simulating a + // concurrent password change), then attempt sign-out with the stale access token. + // Before fix: 500 assertion error in recordExternalDbSyncDeletion. + // After fix: 401 REFRESH_TOKEN_NOT_FOUND_OR_EXPIRED. + const signUpRes = await Auth.Password.signUpWithEmail({ noWaitForEmail: true }); + const savedAuth = backendContext.value.userAuth ?? undefined; + + // Admin updates the user's password, which bulk-deletes all refresh tokens + await niceBackendFetch(`/api/v1/users/${signUpRes.userId}`, { + accessType: "admin", + method: "PATCH", + body: { password: "completely-new-password-12345" }, + }); + + // Try to sign out using the original access token (which still references the + // now-deleted refresh token). This should NOT throw a 500 assertion error. + const response = await niceBackendFetch("/api/v1/auth/sessions/current", { + method: "DELETE", + accessType: "client", + userAuth: savedAuth, + }); + expect(response.status).not.toBe(500); + expect(response).toMatchInlineSnapshot(` + NiceResponse { + "status": 401, + "body": { + "code": "REFRESH_TOKEN_NOT_FOUND_OR_EXPIRED", + "error": "Refresh token not found for this project, or the session has expired/been revoked.", + }, + "headers": Headers { + "x-stack-known-error": "REFRESH_TOKEN_NOT_FOUND_OR_EXPIRED", +