mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-03 14:18:02 +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 { Badge } from '@/components/ui/badge';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { ProGate } from './ProGate';
|
import { TeamProGate } from './TeamProGate';
|
||||||
import { Shield, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
import { Shield, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||||
|
|
||||||
interface SSOProviderConfig {
|
interface SSOProviderConfig {
|
||||||
@@ -320,7 +320,7 @@ export function SSOSection() {
|
|||||||
try {
|
try {
|
||||||
const res = await apiFetch('/sso/config');
|
const res = await apiFetch('/sso/config');
|
||||||
if (res.ok) setConfigs(await res.json());
|
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
|
// 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;
|
const getConfig = (provider: string) => configs.find(c => c.provider === provider) || null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProGate featureName="SSO Authentication">
|
<TeamProGate featureName="SSO Authentication">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold tracking-tight flex items-center gap-2">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</ProGate>
|
</TeamProGate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -998,7 +998,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|||||||
{!isRemote && isAdmin && (
|
{!isRemote && isAdmin && (
|
||||||
<NavButton section="users" icon={<Users className="w-4 h-4 mr-2" />} label="Users" />
|
<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 section="sso" icon={<Shield className="w-4 h-4 mr-2" />} label="SSO" />
|
||||||
)}
|
)}
|
||||||
<NavButton
|
<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