Skip to content

Commit bd93c1c

Browse files
github-actions[bot]Marfuenclaudetofikwest
authored
fix(app): prevent duplicate org creation during setup onboarding
* fix(app): prevent duplicate org creation during setup onboarding When users retry or refresh during the redirect after org creation, the action fires again and creates a duplicate org. Add server-side idempotency check that reuses an existing incomplete org with the same name owned by the user, plus a client-side guard against double-submission. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(app): complete partial setup when reusing existing org When reusing an existing incomplete org (idempotency path), ensure onboarding record and framework initialization exist. Handles the case where the original request failed after DB insert but before completing all setup steps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Mariano Fuentes <marfuen98@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Tofik Hasanov <72318342+tofikwest@users.noreply.github.com>
1 parent cfbeb28 commit bd93c1c

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,67 @@ export const createOrganizationMinimal = authActionClientWithoutOrg
4646
// Check if self-hosted
4747
const isSelfHosted = env.NEXT_PUBLIC_SELF_HOSTED === 'true';
4848

49+
// Idempotency: if the user already has a recently created org with the
50+
// same name that hasn't completed onboarding, reuse it instead of
51+
// creating a duplicate (protects against retry/refresh during redirect).
52+
const existingOrg = await db.organization.findFirst({
53+
where: {
54+
name: parsedInput.organizationName,
55+
onboardingCompleted: false,
56+
members: {
57+
some: {
58+
userId: session.user.id,
59+
role: 'owner',
60+
},
61+
},
62+
},
63+
orderBy: { createdAt: 'desc' },
64+
});
65+
66+
if (existingOrg) {
67+
// Ensure post-creation steps are completed in case the original
68+
// request failed partway through (after DB insert but before
69+
// onboarding record or framework initialization).
70+
const existingOnboarding = await db.onboarding.findUnique({
71+
where: { organizationId: existingOrg.id },
72+
});
73+
74+
if (!existingOnboarding) {
75+
await db.onboarding.create({
76+
data: {
77+
organizationId: existingOrg.id,
78+
triggerJobCompleted: false,
79+
},
80+
});
81+
}
82+
83+
if (parsedInput.frameworkIds && parsedInput.frameworkIds.length > 0) {
84+
const existingFrameworks = await db.frameworkInstance.findFirst({
85+
where: { organizationId: existingOrg.id },
86+
});
87+
88+
if (!existingFrameworks) {
89+
await initializeOrganization({
90+
frameworkIds: parsedInput.frameworkIds,
91+
organizationId: existingOrg.id,
92+
});
93+
}
94+
}
95+
96+
// Ensure this org is set as the active one
97+
await auth.api.setActiveOrganization({
98+
headers: await headers(),
99+
body: {
100+
organizationId: existingOrg.id,
101+
},
102+
});
103+
104+
return {
105+
success: true,
106+
organizationId: existingOrg.id,
107+
};
108+
}
109+
49110
// Resolve framework IDs to display names (e.g. "SOC 2", "ISO 27001")
50111
const frameworks = await db.frameworkEditorFramework.findMany({
51112
where: { id: { in: parsedInput.frameworkIds } },

apps/app/src/app/(app)/setup/hooks/useOnboardingForm.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ export function useOnboardingForm({
146146
});
147147

148148
const handleCreateOrganizationAction = (currentAnswers: Partial<CompanyDetails>) => {
149+
// Guard against duplicate submissions (retry/double-click)
150+
if (isOnboarding || isFinalizing) return;
151+
149152
// Only pass the first 3 fields to the minimal action
150153
createOrganizationAction.execute({
151154
frameworkIds: currentAnswers.frameworkIds || [],

0 commit comments

Comments
 (0)