fix: gate SSO and Audit behind Team Pro license tier (#213)

SSO settings tab and content were visible to Personal Pro users despite
being a Team Pro-only feature. Added TeamProGate component that checks
both isPro and variant === 'team', and hid the SSO nav button for
non-Team Pro users. Audit was already correctly gated in EditorLayout.
This commit is contained in:
Anso
2026-03-28 03:56:42 -04:00
committed by GitHub
parent a9865f705b
commit 8d48b0abff
3 changed files with 78 additions and 5 deletions
+4 -4
View File
@@ -7,7 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { apiFetch } from '@/lib/api';
import { ProGate } from './ProGate';
import { TeamProGate } from './TeamProGate';
import { Shield, Loader2, CheckCircle, XCircle } from 'lucide-react';
interface SSOProviderConfig {
@@ -320,7 +320,7 @@ export function SSOSection() {
try {
const res = await apiFetch('/sso/config');
if (res.ok) setConfigs(await res.json());
} catch { /* ignore - ProGate will handle non-pro */ }
} catch { /* ignore - TeamProGate will handle non-pro */ }
};
// eslint-disable-next-line react-hooks/set-state-in-effect
@@ -329,7 +329,7 @@ export function SSOSection() {
const getConfig = (provider: string) => configs.find(c => c.provider === provider) || null;
return (
<ProGate featureName="SSO Authentication">
<TeamProGate featureName="SSO Authentication">
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold tracking-tight flex items-center gap-2">
@@ -360,6 +360,6 @@ export function SSOSection() {
<p>For OIDC providers, set the OAuth callback URL to: <code className="bg-muted px-1 rounded">{'https://<your-sencho-url>/api/auth/sso/oidc/<provider>/callback'}</code></p>
</div>
</div>
</ProGate>
</TeamProGate>
);
}
+1 -1
View File
@@ -998,7 +998,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
{!isRemote && isAdmin && (
<NavButton section="users" icon={<Users className="w-4 h-4 mr-2" />} label="Users" />
)}
{!isRemote && isAdmin && (
{!isRemote && isAdmin && isPro && license?.variant === 'team' && (
<NavButton section="sso" icon={<Shield className="w-4 h-4 mr-2" />} label="SSO" />
)}
<NavButton
+73
View File
@@ -0,0 +1,73 @@
import { type ReactNode, useState } from 'react';
import { Crown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useLicense } from '@/context/LicenseContext';
interface TeamProGateProps {
children: ReactNode;
featureName?: string;
}
const DISMISS_KEY = 'sencho-team-upgrade-prompt-dismissed';
const DISMISS_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
function isDismissedFromStorage(): boolean {
const dismissedAt = localStorage.getItem(DISMISS_KEY);
return !!dismissedAt && Date.now() - parseInt(dismissedAt, 10) < DISMISS_DURATION_MS;
}
export function TeamProGate({ children, featureName = 'This feature' }: TeamProGateProps) {
const { isPro, license } = useLicense();
const [dismissed, setDismissed] = useState(isDismissedFromStorage);
if (isPro && license?.variant === 'team') return <>{children}</>;
if (dismissed) {
return (
<div className="relative">
<div className="opacity-40 pointer-events-none select-none blur-[2px]">
{children}
</div>
<div className="absolute inset-0 flex items-start justify-center pt-8">
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-muted/80 border border-border text-muted-foreground text-xs">
<Crown className="w-3 h-3" />
Upgrade to Team Pro to unlock
</div>
</div>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center h-full gap-6 p-8">
<div className="flex items-center justify-center w-16 h-16 rounded-2xl bg-muted/50 border border-border">
<Crown className="w-8 h-8 text-muted-foreground" />
</div>
<div className="text-center max-w-md">
<h3 className="text-lg font-semibold mb-2">{featureName} requires Sencho Team Pro</h3>
<p className="text-sm text-muted-foreground">
Unlock team features like SSO authentication, audit logging, and unlimited user accounts with a Sencho Team Pro license.
</p>
</div>
<div className="flex gap-3">
<Button
variant="outline"
size="sm"
onClick={() => {
localStorage.setItem(DISMISS_KEY, Date.now().toString());
setDismissed(true);
}}
>
Dismiss
</Button>
<Button
size="sm"
onClick={() => window.open('https://sencho.io/pricing', '_blank')}
>
<Crown className="w-4 h-4 mr-2" />
Get Team Pro
</Button>
</div>
</div>
);
}