Skip to content

Commit 990782a

Browse files
committed
handle anon toggle properly based on perms
1 parent 1a2ac28 commit 990782a

File tree

8 files changed

+128
-67
lines changed

8 files changed

+128
-67
lines changed

packages/web/src/app/[domain]/repos/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default async function ReposPage({ params: { domain } }: { params: { doma
77
const org = await getOrgFromDomain(domain);
88
if (!org) {
99
return <PageNotFound />
10-
}
10+
}
1111

1212
return (
1313
<div>
Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
import { getOrgFromDomain } from "@/data/org";
2-
import { getAnonymousAccessStatus } from "@/actions";
3-
import { hasEntitlement } from "@sourcebot/shared";
4-
import { isServiceError, getBaseUrl, createInviteLink } from "@/lib/utils";
5-
import { headers } from "next/headers";
62
import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings";
73

84
interface AccessPageProps {
@@ -17,29 +13,14 @@ export default async function AccessPage({ params: { domain } }: AccessPageProps
1713
throw new Error("Organization not found");
1814
}
1915

20-
// Get the current URL to construct the full invite link
21-
const headersList = headers();
22-
const baseUrl = getBaseUrl(headersList);
23-
const inviteLink = createInviteLink(baseUrl, org.inviteLinkId);
24-
25-
// Get anonymous access status
26-
const anonymousAccessEntitlement = hasEntitlement("anonymous-access");
27-
const anonymousAccessStatus = await getAnonymousAccessStatus(domain);
28-
const anonymousAccessEnabled = anonymousAccessEntitlement && !isServiceError(anonymousAccessStatus) && anonymousAccessStatus;
29-
3016
return (
3117
<div className="flex flex-col gap-6">
3218
<div>
3319
<h3 className="text-lg font-medium">Access Control</h3>
3420
<p className="text-sm text-muted-foreground">Configure how users can access your Sourcebot deployment.</p>
3521
</div>
3622

37-
<OrganizationAccessSettings
38-
anonymousAccessEnabled={anonymousAccessEnabled}
39-
memberApprovalRequired={org.memberApprovalRequired}
40-
inviteLinkEnabled={org.inviteLinkEnabled}
41-
inviteLink={inviteLink}
42-
/>
23+
<OrganizationAccessSettings />
4324
</div>
4425
)
4526
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@ export default async function SettingsLayout({
6464
href: `/${domain}/settings/billing`,
6565
}
6666
] : []),
67-
{
68-
title: "Access",
69-
href: `/${domain}/settings/access`,
70-
},
67+
...(userRoleInOrg === OrgRole.OWNER ? [
68+
{
69+
title: "Access",
70+
href: `/${domain}/settings/access`,
71+
}
72+
] : []),
7173
{
7274
title: (
7375
<div className="flex items-center gap-2">

packages/web/src/app/components/anonymousAccessToggle.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import { isServiceError } from "@/lib/utils"
88
import { useToast } from "@/components/hooks/use-toast"
99

1010
interface AnonymousAccessToggleProps {
11+
hasAnonymousAccessEntitlement: boolean;
1112
anonymousAccessEnabled: boolean
1213
onToggleChange?: (checked: boolean) => void
1314
}
1415

15-
export function AnonymousAccessToggle({ anonymousAccessEnabled, onToggleChange }: AnonymousAccessToggleProps) {
16+
export function AnonymousAccessToggle({ hasAnonymousAccessEntitlement, anonymousAccessEnabled, onToggleChange }: AnonymousAccessToggleProps) {
1617
const [enabled, setEnabled] = useState(anonymousAccessEnabled)
1718
const [isLoading, setIsLoading] = useState(false)
1819
const { toast } = useToast()
@@ -46,7 +47,7 @@ export function AnonymousAccessToggle({ anonymousAccessEnabled, onToggleChange }
4647
}
4748

4849
return (
49-
<div className="p-4 rounded-lg border border-[var(--border)] bg-[var(--card)]">
50+
<div className={`p-4 rounded-lg border border-[var(--border)] bg-[var(--card)] ${!hasAnonymousAccessEntitlement ? 'opacity-60' : ''}`}>
5051
<div className="flex items-start justify-between gap-4">
5152
<div className="flex-1 min-w-0">
5253
<h3 className="font-medium text-[var(--foreground)] mb-2">
@@ -64,13 +65,44 @@ export function AnonymousAccessToggle({ anonymousAccessEnabled, onToggleChange }
6465
Learn More
6566
</a>
6667
</p>
68+
{!hasAnonymousAccessEntitlement && (
69+
<div className="mt-3 p-3 rounded-md bg-[var(--muted)] border border-[var(--border)]">
70+
<p className="text-sm text-[var(--foreground)] leading-relaxed flex items-center gap-2">
71+
<svg
72+
className="w-4 h-4 flex-shrink-0 text-[var(--muted-foreground)]"
73+
fill="none"
74+
viewBox="0 0 24 24"
75+
stroke="currentColor"
76+
>
77+
<path
78+
strokeLinecap="round"
79+
strokeLinejoin="round"
80+
strokeWidth={2}
81+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
82+
/>
83+
</svg>
84+
<span>
85+
Your current plan doesn't allow for anonymous access. Please{" "}
86+
<a
87+
href="https://www.sourcebot.dev/contact"
88+
target="_blank"
89+
rel="noopener"
90+
className="font-medium text-[var(--primary)] hover:text-[var(--primary)]/80 underline underline-offset-2 transition-colors"
91+
>
92+
reach out
93+
</a>
94+
{" "}for assistance.
95+
</span>
96+
</p>
97+
</div>
98+
)}
6799
</div>
68100
</div>
69101
<div className="flex-shrink-0">
70102
<Switch
71103
checked={enabled}
72104
onCheckedChange={handleToggle}
73-
disabled={isLoading}
105+
disabled={isLoading || !hasAnonymousAccessEntitlement}
74106
/>
75107
</div>
76108
</div>
Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,40 @@
1-
"use client"
2-
3-
import { useState } from "react"
1+
import { createInviteLink, getBaseUrl } from "@/lib/utils"
42
import { AnonymousAccessToggle } from "./anonymousAccessToggle"
5-
import { MemberApprovalRequiredToggle } from "./memberApprovalRequiredToggle"
6-
import { InviteLinkToggle } from "./inviteLinkToggle"
7-
8-
interface OrganizationAccessSettingsProps {
9-
anonymousAccessEnabled: boolean
10-
memberApprovalRequired: boolean
11-
inviteLinkEnabled: boolean
12-
inviteLink: string | null
13-
}
3+
import { OrganizationAccessSettingsWrapper } from "./organizationAccessSettingsWrapper"
4+
import { getOrgFromDomain } from "@/data/org"
5+
import { getOrgMetadata } from "@/types"
6+
import { headers } from "next/headers"
7+
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
8+
import { hasEntitlement } from "@sourcebot/shared"
149

15-
export function OrganizationAccessSettings({
16-
anonymousAccessEnabled,
17-
memberApprovalRequired,
18-
inviteLinkEnabled,
19-
inviteLink
20-
}: OrganizationAccessSettingsProps) {
21-
const [showInviteLink, setShowInviteLink] = useState(memberApprovalRequired && !anonymousAccessEnabled)
22-
const handleMemberApprovalToggle = (checked: boolean) => {
23-
setShowInviteLink(checked)
10+
export async function OrganizationAccessSettings() {
11+
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
12+
if (!org) {
13+
return <div>Error loading organization</div>
2414
}
2515

16+
const metadata = getOrgMetadata(org);
17+
const anonymousAccessEnabled = metadata?.anonymousAccessEnabled ?? false;
18+
19+
const headersList = headers();
20+
const baseUrl = getBaseUrl(headersList);
21+
const inviteLink = createInviteLink(baseUrl, org.inviteLinkId)
22+
23+
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");
24+
2625
return (
2726
<div className="space-y-6">
2827
<AnonymousAccessToggle
28+
hasAnonymousAccessEntitlement={hasAnonymousAccessEntitlement}
2929
anonymousAccessEnabled={anonymousAccessEnabled}
3030
/>
3131

32-
<div className={`transition-all duration-300 ease-in-out overflow-hidden max-h-96 opacity-100`}>
33-
<MemberApprovalRequiredToggle
34-
memberApprovalRequired={memberApprovalRequired}
35-
onToggleChange={handleMemberApprovalToggle}
36-
/>
37-
</div>
38-
39-
<div className={`transition-all duration-300 ease-in-out overflow-hidden ${
40-
showInviteLink
41-
? 'max-h-96 opacity-100'
42-
: 'max-h-0 opacity-0 pointer-events-none'
43-
}`}>
44-
<InviteLinkToggle
45-
inviteLinkEnabled={inviteLinkEnabled}
46-
inviteLink={inviteLink}
47-
/>
48-
</div>
32+
<OrganizationAccessSettingsWrapper
33+
memberApprovalRequired={org.memberApprovalRequired}
34+
inviteLinkEnabled={org.inviteLinkEnabled}
35+
inviteLink={inviteLink}
36+
anonymousAccessEnabled={anonymousAccessEnabled}
37+
/>
4938
</div>
5039
)
5140
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import { MemberApprovalRequiredToggle } from "./memberApprovalRequiredToggle"
5+
import { InviteLinkToggle } from "./inviteLinkToggle"
6+
7+
interface OrganizationAccessSettingsWrapperProps {
8+
memberApprovalRequired: boolean
9+
inviteLinkEnabled: boolean
10+
inviteLink: string | null
11+
anonymousAccessEnabled: boolean
12+
}
13+
14+
export function OrganizationAccessSettingsWrapper({
15+
memberApprovalRequired,
16+
inviteLinkEnabled,
17+
inviteLink,
18+
anonymousAccessEnabled
19+
}: OrganizationAccessSettingsWrapperProps) {
20+
const [showInviteLink, setShowInviteLink] = useState(memberApprovalRequired && !anonymousAccessEnabled)
21+
22+
const handleMemberApprovalToggle = (checked: boolean) => {
23+
setShowInviteLink(checked)
24+
}
25+
26+
return (
27+
<>
28+
<div className={`transition-all duration-300 ease-in-out overflow-hidden max-h-96 opacity-100`}>
29+
<MemberApprovalRequiredToggle
30+
memberApprovalRequired={memberApprovalRequired}
31+
onToggleChange={handleMemberApprovalToggle}
32+
/>
33+
</div>
34+
35+
<div className={`transition-all duration-300 ease-in-out overflow-hidden ${
36+
showInviteLink
37+
? 'max-h-96 opacity-100'
38+
: 'max-h-0 opacity-0 pointer-events-none'
39+
}`}>
40+
<InviteLinkToggle
41+
inviteLinkEnabled={inviteLinkEnabled}
42+
inviteLink={inviteLink}
43+
/>
44+
</div>
45+
</>
46+
)
47+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export default async function Onboarding({ searchParams }: OnboardingProps) {
162162
subtitle: "Set up your organization's security settings.",
163163
component: (
164164
<div className="space-y-6">
165-
<OrganizationAccessSettings anonymousAccessEnabled={anonymousAccessEnabled} memberApprovalRequired={org.memberApprovalRequired} inviteLinkEnabled={org.inviteLinkEnabled} inviteLink={inviteLink} />
165+
<OrganizationAccessSettings />
166166
<Button asChild className="w-full h-11 bg-primary hover:bg-primary/90 text-primary-foreground transition-all duration-200 font-medium">
167167
<a href="/onboard?step=3">Continue →</a>
168168
</Button>

packages/web/src/initialize.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ServiceErrorException } from './lib/serviceError';
1010
import { SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants";
1111
import { createLogger } from "@sourcebot/logger";
1212
import { createGuestUser } from '@/lib/authUtils';
13+
import { getOrgFromDomain } from './data/org';
1314

1415
const logger = createLogger('web-initialize');
1516

@@ -178,6 +179,15 @@ const initSingleTenancy = async () => {
178179
if (isServiceError(res)) {
179180
throw new ServiceErrorException(res);
180181
}
182+
} else {
183+
// If anonymous access entitlement is not enabled, set the flag to false in the org on init
184+
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
185+
if (org) {
186+
await prisma.org.update({
187+
where: { id: org.id },
188+
data: { metadata: { anonymousAccessEnabled: false } },
189+
});
190+
}
181191
}
182192

183193
// Load any connections defined declaratively in the config file.

0 commit comments

Comments
 (0)