Skip to content

Commit db4a9cf

Browse files
further wip
1 parent 9c5f28e commit db4a9cf

5 files changed

Lines changed: 143 additions & 4 deletions

File tree

packages/web/src/app/(app)/@sidebar/components/upgradeBadge.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client"
2+
13
import { Badge } from "@/components/ui/badge"
24
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
35
import { OFFERINGS_DOCS_LINK } from "@/lib/constants"
@@ -9,7 +11,7 @@ export const UpgradeBadge = () => {
911
<Tooltip>
1012
<TooltipTrigger asChild>
1113
<Badge
12-
className="bg-purple-500/20 text-purple-400 border-purple-500/30 text-[10px] px-1.5 py-0 rounded-md leading-normal tracking-wide"
14+
className="bg-purple-500/20 text-purple-400 border-purple-500/30 text-[10px] px-1.5 py-0 rounded-md leading-normal tracking-wide select-none"
1315
>
1416
Pro
1517
</Badge>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Image from "next/image";
2+
import { Badge } from "@/components/ui/badge";
3+
import { SettingsCard } from "@/app/(app)/settings/components/settingsCard";
4+
import { getAuthProviderInfo, cn } from "@/lib/utils";
5+
import { IdentityProvider } from "@/auth";
6+
7+
interface IdentityProviderSettingsCardProps {
8+
provider: IdentityProvider;
9+
}
10+
11+
export function IdentityProviderSettingsCard({ provider }: IdentityProviderSettingsCardProps) {
12+
const providerInfo = getAuthProviderInfo(provider.type);
13+
const name = provider.displayName ?? providerInfo.displayName;
14+
15+
return (
16+
<SettingsCard>
17+
<div className="flex items-center justify-between gap-4">
18+
<div className="flex items-center gap-4 min-w-0">
19+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted flex-shrink-0">
20+
{providerInfo.icon && (
21+
<Image
22+
src={providerInfo.icon.src}
23+
alt={name}
24+
className={cn("w-5 h-5", providerInfo.icon.className)}
25+
/>
26+
)}
27+
</div>
28+
<div className="min-w-0">
29+
<p className="font-medium text-sm truncate">{name}</p>
30+
{provider.issuerUrl && (
31+
<p className="text-xs text-muted-foreground truncate">{provider.issuerUrl}</p>
32+
)}
33+
</div>
34+
</div>
35+
<Badge className="flex-shrink-0">Configured</Badge>
36+
</div>
37+
</SettingsCard>
38+
);
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import { Sparkles } from "lucide-react"
5+
import { Button } from "@/components/ui/button"
6+
import { SettingsCard } from "@/app/(app)/settings/components/settingsCard"
7+
import { UpsellDialog } from "@/features/billing/upsellDialog"
8+
9+
export function IdentityProviderUpsellCard() {
10+
const [isUpsellDialogOpen, setIsUpsellDialogOpen] = useState(false)
11+
12+
return (
13+
<>
14+
<SettingsCard>
15+
<div className="flex items-center justify-between gap-4">
16+
<div className="flex items-center gap-4 min-w-0">
17+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted flex-shrink-0">
18+
<Sparkles className="h-4 w-4 text-muted-foreground" />
19+
</div>
20+
<div className="min-w-0">
21+
<p className="font-medium text-sm">Single sign-on is a paid feature</p>
22+
<p className="text-xs text-muted-foreground">Upgrade to let users authenticate with providers like GitHub, Google, and Okta.</p>
23+
</div>
24+
</div>
25+
<Button className="flex-shrink-0" onClick={() => setIsUpsellDialogOpen(true)}>
26+
Upgrade
27+
</Button>
28+
</div>
29+
</SettingsCard>
30+
31+
<UpsellDialog
32+
open={isUpsellDialogOpen}
33+
onOpenChange={setIsUpsellDialogOpen}
34+
source="sso_settings"
35+
returnPath="/settings/security"
36+
/>
37+
</>
38+
)
39+
}

packages/web/src/app/(app)/settings/security/page.tsx

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,25 @@ import { InviteLinkEnabledSettingsCard } from "./components/inviteLinkEnabledSet
33
import { MemberApprovalRequiredSettingsCard } from "./components/memberApprovalRequiredSettingsCard";
44
import { CredentialsLoginEnabledSettingsCard } from "./components/credentialsLoginEnabledSettingsCard";
55
import { EmailCodeLoginEnabledSettingsCard } from "./components/emailCodeLoginEnabledSettingsCard";
6-
import { isAnonymousAccessEnabled } from "@/lib/entitlements";
6+
import { IdentityProviderSettingsCard } from "./components/identityProviderSettingsCard";
7+
import { IdentityProviderUpsellCard } from "./components/identityProviderUpsellCard";
8+
import { UpgradeBadge } from "@/app/(app)/@sidebar/components/upgradeBadge";
9+
import { getProviders, IdentityProvider } from "@/auth";
10+
import { hasEntitlement, isAnonymousAccessEnabled } from "@/lib/entitlements";
711
import { createInviteLink } from "@/lib/utils";
812
import { authenticatedPage } from "@/middleware/authenticatedPage";
913
import { OrgRole } from "@sourcebot/db";
1014
import { env, getSMTPConnectionURL, isCredentialsLoginEnabled, isEmailCodeLoginEnabled, isMemberApprovalRequired } from "@sourcebot/shared";
1115
import { SettingsCardGroup } from "../components/settingsCard";
16+
import { Alert, AlertDescription } from "@/components/ui/alert";
17+
import { Info } from "lucide-react";
1218

1319
export default authenticatedPage(async ({ org }) => {
1420
const anonymousAccessEnabled = await isAnonymousAccessEnabled();
1521
const inviteLink = createInviteLink(env.AUTH_URL, org.inviteLinkId);
22+
const hasSSOEntitlement = await hasEntitlement("sso");
23+
const identityProviders = await getConfiguredIdentityProviders();
24+
1625

1726
return (
1827
<div className="flex flex-col gap-6">
@@ -25,7 +34,7 @@ export default authenticatedPage(async ({ org }) => {
2534
href="https://docs.sourcebot.dev/docs/configuration/auth/access-settings"
2635
target="_blank"
2736
rel="noopener"
28-
className="underline text-primary hover:text-primary/80 transition-colors"
37+
className="text-link hover:underline transition-colors"
2938
>
3039
Learn more
3140
</a>
@@ -45,7 +54,7 @@ export default authenticatedPage(async ({ org }) => {
4554
/>
4655
</SettingsCardGroup>
4756

48-
<p className="text-md font-medium">Authentication methods</p>
57+
<p className="text-md font-medium">Email login</p>
4958

5059
<SettingsCardGroup>
5160
<CredentialsLoginEnabledSettingsCard
@@ -56,10 +65,59 @@ export default authenticatedPage(async ({ org }) => {
5665
isEmailServiceConfigured={!!getSMTPConnectionURL() && !!env.EMAIL_FROM_ADDRESS}
5766
/>
5867
</SettingsCardGroup>
68+
69+
<div>
70+
<div className="flex items-center gap-2">
71+
<p className="text-md font-medium">Single Sign-On</p>
72+
{!hasSSOEntitlement && <UpgradeBadge />}
73+
</div>
74+
<p className="text-sm text-muted-foreground">Let users sign in with an external identity provider such as GitHub, Google, or Okta. Providers are managed in your config file.{" "}
75+
<a
76+
href="https://docs.sourcebot.dev/docs/configuration/idp"
77+
target="_blank"
78+
rel="noopener"
79+
className="text-link hover:underline transition-colors"
80+
>
81+
Learn more
82+
</a>
83+
</p>
84+
</div>
85+
86+
{!hasSSOEntitlement ? (
87+
<IdentityProviderUpsellCard />
88+
) : identityProviders.length > 0 ? (
89+
<SettingsCardGroup>
90+
{identityProviders.map((provider) => (
91+
<IdentityProviderSettingsCard key={provider.id} provider={provider} />
92+
))}
93+
</SettingsCardGroup>
94+
) : (
95+
<Alert className="items-center p-4">
96+
<Info className="w-4 h-4 text-muted-foreground" />
97+
<AlertDescription>
98+
No identity providers are configured. Add them in your config file.{" "}
99+
<a
100+
href="https://docs.sourcebot.dev/docs/configuration/idp"
101+
target="_blank"
102+
rel="noopener"
103+
className="!text-link !no-underline hover:!underline"
104+
>
105+
Learn more
106+
</a>
107+
</AlertDescription>
108+
</Alert>
109+
)}
59110
</div>
60111
</div>
61112
)
62113
}, {
63114
minRole: OrgRole.OWNER,
64115
redirectTo: '/settings',
65116
});
117+
118+
const getConfiguredIdentityProviders = async (): Promise<IdentityProvider[]> => {
119+
const providers = await getProviders();
120+
return providers.filter((provider) =>
121+
provider.purpose === "sso" && !["credentials", "nodemailer"].includes(provider.type)
122+
);
123+
}

packages/web/src/lib/posthogEvents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type UpsellSource =
99
'onboard' |
1010
'license_settings' |
1111
'mcp_settings' |
12+
'sso_settings' |
1213
'chat_connectors';
1314

1415
export type SourcebotWebClientSource = 'sourcebot-web-client';

0 commit comments

Comments
 (0)