mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 00:47:52 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user