Skip to content

Commit f81c32d

Browse files
authored
Merge pull request #948 from trycompai/claudio/stripe
[dev] [claudfuen] claudio/stripe
2 parents 6b088ca + 139167f commit f81c32d

45 files changed

Lines changed: 742 additions & 760 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/app/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"languine": "^3.1.4",
5858
"marked": "^15.0.11",
5959
"motion": "^12.9.2",
60-
"next": "15.4.0-canary.83",
60+
"next": "15.4.0-canary.85",
6161
"next-international": "^1.3.1",
6262
"next-intl": "^3.26.5",
6363
"next-safe-action": "^8.0.3",
@@ -94,7 +94,7 @@
9494
"@types/d3": "^7.4.3",
9595
"@types/node": "^22.13.2",
9696
"eslint": "^9",
97-
"eslint-config-next": "15.4.0-canary.83",
97+
"eslint-config-next": "15.4.0-canary.85",
9898
"fleetctl": "^4.68.1",
9999
"postcss": "^8.5.4",
100100
"react": "^19.1.0",

apps/app/src/actions/organization/create-organization-action.ts renamed to apps/app/src/actions/organization/initialize-organization-action.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import { organizationSchema } from '../schema';
1010
import { createStripeCustomer } from './lib/create-stripe-customer';
1111
import { initializeOrganization } from './lib/initialize-organization';
1212

13-
export const createOrganizationAction = authActionClient
13+
export const initializeOrganizationAction = authActionClient
1414
.inputSchema(organizationSchema)
1515
.metadata({
16-
name: 'create-organization',
16+
name: 'initialize-organization',
1717
track: {
18-
event: 'create-organization',
18+
event: 'initialize-organization',
1919
channel: 'server',
2020
},
2121
})

apps/app/src/actions/safe-action.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,89 @@ export const authWithOrgAccessClient = authActionClient.use(async ({ next, clien
253253
},
254254
});
255255
});
256+
257+
// New action client that requires auth but not an active organization
258+
export const authActionClientWithoutOrg = actionClientWithMeta
259+
.use(async ({ next, clientInput }) => {
260+
const response = await auth.api.getSession({
261+
headers: await headers(),
262+
});
263+
264+
const { session, user } = response ?? {};
265+
266+
if (!session) {
267+
throw new Error('Unauthorized');
268+
}
269+
270+
const result = await next({
271+
ctx: {
272+
user: user,
273+
session: session,
274+
},
275+
});
276+
277+
if (process.env.NODE_ENV === 'development') {
278+
logger('Input ->', JSON.stringify(clientInput, null, 2));
279+
logger('Result ->', JSON.stringify(result.data, null, 2));
280+
281+
// Also log validation errors if they exist
282+
if (result.validationErrors) {
283+
logger('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2));
284+
}
285+
286+
return result;
287+
}
288+
289+
return result;
290+
})
291+
.use(async ({ next, metadata }) => {
292+
const headersList = await headers();
293+
let remaining: number | undefined;
294+
295+
if (ratelimit) {
296+
const { success, remaining: rateLimitRemaining } = await ratelimit.limit(
297+
`${headersList.get('x-forwarded-for')}-${metadata.name}`,
298+
);
299+
300+
if (!success) {
301+
throw new Error('Too many requests');
302+
}
303+
304+
remaining = rateLimitRemaining;
305+
}
306+
307+
return next({
308+
ctx: {
309+
ip: headersList.get('x-forwarded-for'),
310+
userAgent: headersList.get('user-agent'),
311+
ratelimit: {
312+
remaining: remaining ?? 0,
313+
},
314+
},
315+
});
316+
})
317+
.use(async ({ next, metadata, ctx }) => {
318+
const session = await auth.api.getSession({
319+
headers: await headers(),
320+
});
321+
322+
if (!session) {
323+
throw new Error('Unauthorized');
324+
}
325+
326+
if (metadata.track) {
327+
track(session.user.id, metadata.track.event, {
328+
channel: metadata.track.channel,
329+
email: session.user.email,
330+
name: session.user.name,
331+
// organizationId is optional here since there might not be one
332+
organizationId: session.session.activeOrganizationId || undefined,
333+
});
334+
}
335+
336+
return next({
337+
ctx: {
338+
user: session.user,
339+
},
340+
});
341+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { auth } from '@/utils/auth';
2+
import { db } from '@comp/db';
3+
import { headers } from 'next/headers';
4+
import { notFound, redirect } from 'next/navigation';
5+
import { AcceptInvite } from '../../setup/components/accept-invite';
6+
7+
interface InvitePageProps {
8+
params: Promise<{ code: string }>;
9+
}
10+
11+
export default async function InvitePage({ params }: InvitePageProps) {
12+
const { code } = await params;
13+
const session = await auth.api.getSession({
14+
headers: await headers(),
15+
});
16+
17+
if (!session) {
18+
// Redirect to auth with the invite code
19+
return redirect(`/auth?inviteCode=${code}`);
20+
}
21+
22+
// Check if this invitation exists and is valid for this user
23+
const invitation = await db.invitation.findFirst({
24+
where: {
25+
id: code,
26+
email: session.user.email,
27+
status: 'pending',
28+
},
29+
include: {
30+
organization: {
31+
select: {
32+
name: true,
33+
},
34+
},
35+
},
36+
});
37+
38+
if (!invitation) {
39+
// Either invitation doesn't exist, already accepted, or not for this user
40+
notFound();
41+
}
42+
43+
return (
44+
<div className="flex min-h-screen items-center justify-center p-4">
45+
<AcceptInvite
46+
inviteCode={invitation.id}
47+
organizationName={invitation.organization.name || ''}
48+
/>
49+
</div>
50+
);
51+
}

apps/app/src/app/(app)/no-access/page.tsx

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,6 @@ export default async function NoAccess() {
3030
},
3131
});
3232

33-
const frameworks = await db.frameworkEditorFramework.findMany({
34-
select: {
35-
id: true,
36-
name: true,
37-
description: true,
38-
version: true,
39-
visible: true,
40-
},
41-
});
42-
4333
return (
4434
<div className="bg-foreground/05 flex h-screen flex-col items-center justify-center gap-4">
4535
<h1 className="text-2xl font-bold">Access Denied</h1>
@@ -54,11 +44,7 @@ export default async function NoAccess() {
5444
<p>Please select another organization or contact your organization administrator.</p>
5545
</div>
5646
<div>
57-
<OrganizationSwitcher
58-
organizations={organizations}
59-
organization={currentOrg}
60-
frameworks={frameworks}
61-
/>
47+
<OrganizationSwitcher organizations={organizations} organization={currentOrg} />
6248
</div>
6349
</div>
6450
);
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use server';
2+
3+
import { createStripeCustomer } from '@/actions/organization/lib/create-stripe-customer';
4+
import { initializeOrganization } from '@/actions/organization/lib/initialize-organization';
5+
import { authActionClientWithoutOrg } from '@/actions/safe-action';
6+
import { auth } from '@/utils/auth';
7+
import { db } from '@comp/db';
8+
import { revalidatePath } from 'next/cache';
9+
import { headers } from 'next/headers';
10+
import { z } from 'zod';
11+
12+
const createOrganizationMinimalSchema = z.object({
13+
organizationName: z.string().min(1, 'Organization name is required'),
14+
website: z.string().url('Please enter a valid URL'),
15+
frameworkIds: z.array(z.string()).default([]),
16+
});
17+
18+
export const createOrganizationMinimal = authActionClientWithoutOrg
19+
.inputSchema(createOrganizationMinimalSchema)
20+
.metadata({
21+
name: 'create-organization-minimal',
22+
track: {
23+
event: 'create-organization-minimal',
24+
channel: 'server',
25+
},
26+
})
27+
.action(async ({ parsedInput, ctx }) => {
28+
try {
29+
const session = await auth.api.getSession({
30+
headers: await headers(),
31+
});
32+
33+
if (!session) {
34+
return {
35+
success: false,
36+
error: 'Not authorized.',
37+
};
38+
}
39+
40+
// Create a new organization directly in the database
41+
const randomSuffix = Math.floor(100000 + Math.random() * 900000).toString();
42+
43+
const newOrg = await db.organization.create({
44+
data: {
45+
name: parsedInput.organizationName,
46+
website: parsedInput.website,
47+
members: {
48+
create: {
49+
userId: session.user.id,
50+
role: 'owner',
51+
},
52+
},
53+
},
54+
});
55+
56+
const orgId = newOrg.id;
57+
58+
// Create onboarding record for new org (mark as completed since they're skipping)
59+
await db.onboarding.create({
60+
data: {
61+
organizationId: orgId,
62+
completed: true,
63+
},
64+
});
65+
66+
// Create Stripe customer for new org
67+
const stripeCustomerId = await createStripeCustomer({
68+
name: parsedInput.organizationName,
69+
email: session.user.email,
70+
organizationId: orgId,
71+
});
72+
73+
if (stripeCustomerId) {
74+
await db.organization.update({
75+
where: { id: orgId },
76+
data: { stripeCustomerId },
77+
});
78+
}
79+
80+
// Initialize frameworks if provided
81+
if (parsedInput.frameworkIds && parsedInput.frameworkIds.length > 0) {
82+
await initializeOrganization({
83+
frameworkIds: parsedInput.frameworkIds,
84+
organizationId: orgId,
85+
});
86+
}
87+
88+
// Set new org as active
89+
await auth.api.setActiveOrganization({
90+
headers: await headers(),
91+
body: {
92+
organizationId: orgId,
93+
},
94+
});
95+
96+
const userOrgs = await db.member.findMany({
97+
where: {
98+
userId: session.user.id,
99+
},
100+
select: {
101+
organizationId: true,
102+
},
103+
});
104+
105+
for (const org of userOrgs) {
106+
revalidatePath(`/${org.organizationId}`);
107+
}
108+
109+
revalidatePath('/');
110+
revalidatePath(`/${orgId}`);
111+
revalidatePath('/setup');
112+
113+
return {
114+
success: true,
115+
organizationId: orgId,
116+
};
117+
} catch (error) {
118+
console.error('Error during organization setup:', error);
119+
120+
// Return the actual error message for debugging
121+
if (error instanceof Error) {
122+
return {
123+
success: false,
124+
error: error.message,
125+
};
126+
}
127+
128+
return {
129+
success: false,
130+
error: 'Failed to setup organization',
131+
};
132+
}
133+
});

0 commit comments

Comments
 (0)