Skip to content

Commit 04edbed

Browse files
committed
handle join when max capacity case
1 parent abe959b commit 04edbed

File tree

8 files changed

+85
-50
lines changed

8 files changed

+85
-50
lines changed

packages/web/src/actions.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { env } from "@/env.mjs";
44
import { ErrorCode } from "@/lib/errorCodes";
5-
import { notAuthenticated, notFound, secretAlreadyExists, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError";
5+
import { notAuthenticated, notFound, orgNotFound, secretAlreadyExists, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError";
66
import { CodeHostType, isServiceError } from "@/lib/utils";
77
import { prisma } from "@/prisma";
88
import { render } from "@react-email/components";
@@ -1671,13 +1671,19 @@ export const createAccountRequest = async (userId: string, domain: string) => se
16711671
}
16721672
});
16731673

1674-
export const getMemberApprovalRequired = async (domain: string): Promise<boolean | ServiceError> => sew(async () =>
1675-
withAuth(async (userId) =>
1676-
withOrgMembership(userId, domain, async ({ org }) => {
1677-
return org.memberApprovalRequired;
1678-
}, /* minRequiredRole = */ OrgRole.OWNER)
1679-
)
1680-
);
1674+
export const getMemberApprovalRequired = async (domain: string): Promise<boolean | ServiceError> => sew(async () => {
1675+
const org = await prisma.org.findUnique({
1676+
where: {
1677+
domain,
1678+
},
1679+
});
1680+
1681+
if (!org) {
1682+
return orgNotFound();
1683+
}
1684+
1685+
return org.memberApprovalRequired;
1686+
});
16811687

16821688
export const setMemberApprovalRequired = async (domain: string, required: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () =>
16831689
withAuth(async (userId) =>

packages/web/src/app/[domain]/layout.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { hasEntitlement } from "@sourcebot/shared";
1919
import { getPublicAccessStatus } from "@/ee/features/publicAccess/publicAccess";
2020
import { env } from "@/env.mjs";
2121
import { GcpIapAuth } from "./components/gcpIapAuth";
22+
import { getMemberApprovalRequired } from "@/actions";
23+
import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
24+
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
2225

2326
interface LayoutProps {
2427
children: React.ReactNode,
@@ -53,25 +56,39 @@ export default async function Layout({
5356
orgId: org.id,
5457
userId: session.user.id
5558
}
56-
},
59+
},
5760
include: {
5861
user: true
5962
}
6063
});
6164

65+
// There's two reasons why a user might not be a member of an org:
66+
// 1. The org doesn't require member approval, but the org was at max capacity when the user registered. In this case, we show them
67+
// the join organization card to allow them to join the org if seat capacity is freed up. This card handles checking if the org has available seats.
68+
// 2. The org requires member approval, and they haven't been approved yet. In this case, we allow them to submit a request to join the org.
6269
if (!membership) {
70+
const memberApprovalRequired = await getMemberApprovalRequired(domain);
71+
if (!memberApprovalRequired) {
72+
return (
73+
<div className="min-h-screen flex items-center justify-center p-6">
74+
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
75+
<JoinOrganizationCard />
76+
</div>
77+
)
78+
} else {
6379
const hasPendingApproval = await prisma.accountRequest.findFirst({
6480
where: {
6581
orgId: org.id,
6682
requestedById: session.user.id
6783
}
6884
});
69-
85+
7086
if (hasPendingApproval) {
7187
return <PendingApprovalCard />
7288
} else {
7389
return <SubmitJoinRequest domain={domain} />
7490
}
91+
}
7592
}
7693
}
7794

packages/web/src/app/invite/components/joinOrganizationButton.tsx renamed to packages/web/src/app/components/joinOrganizationButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { useRouter } from "next/navigation";
55
import { useToast } from "@/components/hooks/use-toast";
66
import { useState } from "react";
77
import { Loader2 } from "lucide-react";
8-
import { joinOrganization } from "../actions";
8+
import { joinOrganization } from "../invite/actions";
99
import { isServiceError } from "@/lib/utils";
1010
import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID } from "@/lib/constants";
1111

12-
export function JoinOrganizationButton({ inviteLinkId }: { inviteLinkId: string }) {
12+
export function JoinOrganizationButton({ inviteLinkId }: { inviteLinkId?: string }) {
1313
const [isLoading, setIsLoading] = useState(false);
1414
const router = useRouter();
1515
const { toast } = useToast();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
2+
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
3+
import { JoinOrganizationButton } from "./joinOrganizationButton";
4+
5+
export function JoinOrganizationCard({ inviteLinkId }: { inviteLinkId?: string }) {
6+
return (
7+
<div className="min-h-screen bg-gradient-to-br from-[var(--background)] to-[var(--accent)]/30 flex items-center justify-center p-6">
8+
<Card className="w-full max-w-md">
9+
<CardHeader className="text-center">
10+
<SourcebotLogo className="h-12 mb-4 mx-auto" size="large" />
11+
</CardHeader>
12+
<CardContent className="space-y-6">
13+
<div className="text-center space-y-4">
14+
<p className="text-[var(--muted-foreground)] text-[15px] leading-6">
15+
Welcome to Sourcebot! Click the button below to join this organization.
16+
</p>
17+
</div>
18+
<JoinOrganizationButton inviteLinkId={inviteLinkId} />
19+
</CardContent>
20+
</Card>
21+
</div>
22+
);
23+
}

packages/web/src/app/invite/actions.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { prisma } from "@/prisma";
99
import { StatusCodes } from "http-status-codes";
1010
import { ErrorCode } from "@/lib/errorCodes";
1111

12-
export const joinOrganization = (orgId: number, inviteLinkId: string) => sew(async () =>
12+
export const joinOrganization = (orgId: number, inviteLinkId?: string) => sew(async () =>
1313
withAuth(async (userId) => {
1414
const org = await prisma.org.findUnique({
1515
where: {
@@ -21,20 +21,23 @@ export const joinOrganization = (orgId: number, inviteLinkId: string) => sew(asy
2121
return orgNotFound();
2222
}
2323

24-
if (!org.inviteLinkEnabled) {
25-
return {
26-
statusCode: StatusCodes.BAD_REQUEST,
27-
errorCode: ErrorCode.INVITE_LINK_NOT_ENABLED,
28-
message: "Invite link is not enabled.",
29-
} satisfies ServiceError;
30-
}
24+
// If member approval is required we must be using a valid invite link
25+
if (org.memberApprovalRequired) {
26+
if (!org.inviteLinkEnabled) {
27+
return {
28+
statusCode: StatusCodes.BAD_REQUEST,
29+
errorCode: ErrorCode.INVITE_LINK_NOT_ENABLED,
30+
message: "Invite link is not enabled.",
31+
} satisfies ServiceError;
32+
}
3133

32-
if (org.inviteLinkId !== inviteLinkId) {
33-
return {
34-
statusCode: StatusCodes.BAD_REQUEST,
35-
errorCode: ErrorCode.INVALID_INVITE_LINK,
36-
message: "Invalid invite link.",
37-
} satisfies ServiceError;
34+
if (org.inviteLinkId !== inviteLinkId) {
35+
return {
36+
statusCode: StatusCodes.BAD_REQUEST,
37+
errorCode: ErrorCode.INVALID_INVITE_LINK,
38+
message: "Invalid invite link.",
39+
} satisfies ServiceError;
40+
}
3841
}
3942

4043
const addUserToOrgRes = await addUserToOrganization(userId, org.id);

packages/web/src/app/invite/page.tsx

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { notFound, redirect } from "next/navigation";
66
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
77
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
88
import { AuthMethodSelector } from "@/app/components/authMethodSelector";
9-
import { JoinOrganizationButton } from "@/app/invite/components/joinOrganizationButton"
109
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
1110
import { getAuthProviders } from "@/lib/authProviders";
11+
import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
1212

1313
interface InvitePageProps {
1414
searchParams: {
@@ -51,7 +51,7 @@ export default async function InvitePage({ searchParams }: InvitePageProps) {
5151
return (
5252
<div className="min-h-screen flex items-center justify-center p-6">
5353
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
54-
<JoinInvitationCard inviteLinkId={inviteLinkId} />
54+
<JoinOrganizationCard inviteLinkId={inviteLinkId} />
5555
</div>
5656
);
5757
}
@@ -84,23 +84,3 @@ function WelcomeCard({ inviteLinkId, providers }: { inviteLinkId: string; provid
8484
</div>
8585
);
8686
}
87-
88-
function JoinInvitationCard({ inviteLinkId }: { inviteLinkId: string }) {
89-
return (
90-
<div className="min-h-screen bg-gradient-to-br from-[var(--background)] to-[var(--accent)]/30 flex items-center justify-center p-6">
91-
<Card className="w-full max-w-md">
92-
<CardHeader className="text-center">
93-
<SourcebotLogo className="h-12 mb-4 mx-auto" size="large" />
94-
</CardHeader>
95-
<CardContent className="space-y-6">
96-
<div className="text-center space-y-4">
97-
<p className="text-[var(--muted-foreground)] text-[15px] leading-6">
98-
Welcome to Sourcebot! Click the button below to join this organization.
99-
</p>
100-
</div>
101-
<JoinOrganizationButton inviteLinkId={inviteLinkId} />
102-
</CardContent>
103-
</Card>
104-
</div>
105-
);
106-
}

packages/web/src/app/onboard/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export default async function Onboarding({ searchParams }: OnboardingProps) {
141141
title: "You're All Set!",
142142
subtitle: (
143143
<>
144-
Your Sourcebot deployment is ready. Check out these resources to get started.
144+
Your Sourcebot deployment is ready. Check out these resources to learn how to get the most out of Sourcebot.
145145
<div className="text-center space-y-4">
146146
<div className="w-16 h-16 mx-auto bg-primary rounded-full flex items-center justify-center">
147147
<svg className="w-8 h-8 text-primary-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">

packages/web/src/lib/authUtils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,13 @@ export const onCreateUser = async ({ user }: { user: AuthJsUser }) => {
106106
type: "org"
107107
}
108108
});
109-
} else if (!defaultOrg.memberApprovalRequired) { // Else, we add the user to org if approval isn't required
109+
} else if (!defaultOrg.memberApprovalRequired) {
110+
const hasAvailability = await orgHasAvailability(defaultOrg.domain);
111+
if (!hasAvailability) {
112+
logger.warn(`onCreateUser: org ${SINGLE_TENANT_ORG_ID} has reached max capacity. User ${user.id} was not added to the org.`);
113+
return;
114+
}
115+
110116
await prisma.userToOrg.create({
111117
data: {
112118
userId: user.id,

0 commit comments

Comments
 (0)