diff --git a/.gitignore b/.gitignore index 1f484ddfd..cb99aa51e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ node-compile-cache/ .envrc +debug.log + *.cpuprofile diff --git a/AGENTS.md b/AGENTS.md index e43ddf587..a5f2597ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ This file provides guidance to coding agents when working with code in this repo #### Extra commands These commands are usually already called by the user, but you can remind them to run it for you if they forgot to. -- **Build packages**: `pnpm build:packages` (you should never call this yourself) +- **Build packages**: NEVER DO THIS YOURSELF — ASK THE USER TO DO IT FOR YOU! - **Start dependencies**: `pnpm restart-deps` (resets & restarts Docker containers for DB, Inbucket, etc. Usually already called by the user) - **Run development**: Already called by the user in the background. You don't need to do this. This will also watch for changes and rebuild packages, codegen, etc. Do NOT call build:packages, dev, codegen, or anything like that yourself, as the dev is already running it. - **Run minimal dev**: `pnpm dev:basic` (only backend and dashboard for resource-limited systems) diff --git a/apps/backend/src/app/api/latest/team-invitations/[id]/accept/route.tsx b/apps/backend/src/app/api/latest/team-invitations/[id]/accept/route.tsx index a448d62e0..77499efd7 100644 --- a/apps/backend/src/app/api/latest/team-invitations/[id]/accept/route.tsx +++ b/apps/backend/src/app/api/latest/team-invitations/[id]/accept/route.tsx @@ -84,8 +84,26 @@ export const POST = createSmartRouteHandler({ throw new KnownErrors.VerificationCodeNotFound(); } + // Atomically mark the invitation as used before creating the membership. + // This uses globalPrismaClient (not a tenancy transaction), so it must happen + // outside retryTransaction to avoid being re-executed on retry after already committing. + const updated = await globalPrismaClient.verificationCode.updateMany({ + where: { + projectId: auth.tenancy.project.id, + branchId: auth.tenancy.branchId, + id: params.id, + usedAt: null, + }, + data: { + usedAt: new Date(), + }, + }); + + if (updated.count === 0) { + throw new KnownErrors.VerificationCodeNotFound(); + } + await retryTransaction(prisma, async (tx) => { - // Internal project payment checks (same as in the verification code handler) if (auth.tenancy.project.id === "internal") { const currentMemberCount = await tx.teamMember.count({ where: { @@ -123,23 +141,6 @@ export const POST = createSmartRouteHandler({ data: {}, }); } - - // Mark the invitation as used inside the transaction to prevent race conditions - const updated = await globalPrismaClient.verificationCode.updateMany({ - where: { - projectId: auth.tenancy.project.id, - branchId: auth.tenancy.branchId, - id: params.id, - usedAt: null, - }, - data: { - usedAt: new Date(), - }, - }); - - if (updated.count === 0) { - throw new KnownErrors.VerificationCodeNotFound(); - } }); return { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts index ddbd6126a..97b791f5e 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts @@ -849,11 +849,20 @@ it("should allow a restricted user to accept invitation after verifying email", it("can list invitations by user_id on the server", async ({ expect }) => { await Project.createAndSwitch(); - const { userId: inviter } = await Auth.fastSignUp(); + await Auth.fastSignUp(); const { teamId } = await Team.create(); const receiveMailbox = createMailbox(); - await Team.sendInvitation(receiveMailbox.emailAddress, teamId); + backendContext.set({ userAuth: null }); + await niceBackendFetch("/api/v1/team-invitations/send-code", { + method: "POST", + accessType: "server", + body: { + email: receiveMailbox.emailAddress, + team_id: teamId, + callback_url: "http://localhost:12345/some-callback-url", + }, + }); // Create a new user with the invited email as a verified contact channel const { userId: invitedUserId } = await Auth.fastSignUp({ @@ -866,34 +875,29 @@ it("can list invitations by user_id on the server", async ({ expect }) => { accessType: "server", method: "GET", }); - expect(listResponse).toMatchInlineSnapshot(` - NiceResponse { - "status": 200, - "body": { - "is_paginated": false, - "items": [ - { - "expires_at_millis": , - "id": "", - "recipient_email": "${receiveMailbox.emailAddress}", - "team_display_name": "New Team", - "team_id": "", - }, - ], - }, - "headers": Headers {