mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 12:48:10 +00:00
fix(sso): harden SSO with role sync, security fixes, design compliance, and test coverage (#564)
Rate-limit OIDC callback route and clear state cookie on all paths. Validate GitHub API responses and add server-side config validation. Sync SSO user roles on every login respecting seat limits. Add LDAP connection timeout, bind DN warning, and multiple entry logging. Replace Select with Combobox and apply card design tokens in SSOSection. Expose OIDC scopes field in settings UI. Fix hardcoded colors to use design system tokens. Add standard and diagnostic logging throughout SSO flow. Add tests for role sync, seat limits, LDAP escaping, and config validation. Remove unused login-form.tsx template. Update docs and screenshots for SSO feature.
This commit is contained in:
@@ -159,7 +159,7 @@ export function Login({
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-sm text-red-500 text-center">
|
||||
<div className="text-sm text-destructive text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
@@ -12,6 +12,11 @@ import { CapabilityGate } from './CapabilityGate';
|
||||
import { TierBadge } from './TierBadge';
|
||||
import { Shield, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ value: 'viewer', label: 'Viewer' },
|
||||
{ value: 'admin', label: 'Admin' },
|
||||
];
|
||||
|
||||
interface SSOProviderConfig {
|
||||
provider: string;
|
||||
enabled: boolean;
|
||||
@@ -120,7 +125,7 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-border rounded-lg">
|
||||
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card text-card-foreground shadow-card-bevel transition-colors hover:border-t-card-border-hover">
|
||||
<div
|
||||
className="flex items-center justify-between p-4 cursor-pointer hover:bg-muted/30 transition-colors"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
@@ -204,16 +209,12 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs text-muted-foreground">Default Role</Label>
|
||||
<Select
|
||||
<Combobox
|
||||
options={ROLE_OPTIONS}
|
||||
value={config.ldapDefaultRole || 'viewer'}
|
||||
onValueChange={v => update('ldapDefaultRole', v)}
|
||||
>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="viewer">Viewer</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
placeholder="Select role"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -273,18 +274,27 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs text-muted-foreground">Default Role</Label>
|
||||
<Select
|
||||
value={config.oidcDefaultRole || 'viewer'}
|
||||
onValueChange={v => update('oidcDefaultRole', v)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="viewer">Viewer</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs text-muted-foreground">Scopes</Label>
|
||||
<Input
|
||||
placeholder="openid email profile"
|
||||
value={config.oidcScopes || ''}
|
||||
onChange={e => update('oidcScopes', e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Space-separated list of OAuth scopes. Leave blank for default.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs text-muted-foreground">Default Role</Label>
|
||||
<Combobox
|
||||
options={ROLE_OPTIONS}
|
||||
value={config.oidcDefaultRole || 'viewer'}
|
||||
onValueChange={v => update('oidcDefaultRole', v)}
|
||||
placeholder="Select role"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -300,11 +310,11 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
|
||||
{testResult && (
|
||||
testResult.success
|
||||
? <CheckCircle className="w-4 h-4 text-success" />
|
||||
: <XCircle className="w-4 h-4 text-red-500" />
|
||||
: <XCircle className="w-4 h-4 text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
{initialConfig && (
|
||||
<Button size="sm" variant="ghost" className="text-red-500 hover:text-red-400" onClick={handleDelete}>
|
||||
<Button size="sm" variant="ghost" className="text-destructive/60 hover:bg-destructive hover:text-destructive-foreground" onClick={handleDelete}>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Login with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user