Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced new components for managing admin settings, including AdminListControls, AdminSettingsCard, and TechBrandSelectLabel. - Implemented dynamic loading for admin settings sections to optimize performance. - Enhanced the layout of various admin settings sections for better user experience. - Updated the AiAssistantSection to include LLM provider management and improved model selection. - Refactored authentication settings to streamline configuration and improve accessibility.
800 lines
31 KiB
TypeScript
800 lines
31 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo, useState } from "react"
|
|
import {
|
|
Copy,
|
|
Loader2,
|
|
Plus,
|
|
RefreshCw,
|
|
TestTube2,
|
|
Trash2,
|
|
} from "lucide-react"
|
|
import { guideForProvider } from "@/components/admin/settings/guides/identity-provider-guides"
|
|
import { TechBrandSelectLabel } from "@/components/admin/settings/tech-brand-select-label"
|
|
import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store"
|
|
import type {
|
|
IdentityProvider,
|
|
IdentityProviderType,
|
|
OAuthProviderPreset,
|
|
} from "@/lib/admin-settings/org-settings-types"
|
|
import {
|
|
useIdentityProviderRedirectURI,
|
|
useSyncIdentityProvider,
|
|
useTestIdentityProvider,
|
|
} from "@/lib/api/hooks/use-identity-provider-actions"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from "@/components/ui/sheet"
|
|
import { Switch } from "@/components/ui/switch"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { toast } from "sonner"
|
|
|
|
const OAUTH_PRESET_LABELS: Record<OAuthProviderPreset, string> = {
|
|
google: "Google",
|
|
github: "GitHub",
|
|
linkedin: "LinkedIn",
|
|
microsoft: "Microsoft",
|
|
custom: "Autre / custom",
|
|
}
|
|
|
|
function splitList(value: string): string[] {
|
|
return value
|
|
.split(/[\n,]/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function joinList(values: string[] | undefined): string {
|
|
return (values ?? []).join("\n")
|
|
}
|
|
|
|
function slugify(value: string): string {
|
|
return value
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
}
|
|
|
|
function emptyProvider(type: IdentityProviderType): IdentityProvider {
|
|
const id = crypto.randomUUID()
|
|
const base = {
|
|
id,
|
|
name: "",
|
|
slug: "",
|
|
type,
|
|
enabled: true,
|
|
sync_status: "pending" as const,
|
|
allowed_email_domains: [],
|
|
allowed_identities: [],
|
|
allowed_organizations: [],
|
|
default_groups: [],
|
|
}
|
|
if (type === "oauth") {
|
|
return {
|
|
...base,
|
|
oauth: {
|
|
provider: "google",
|
|
client_id: "",
|
|
client_secret: "",
|
|
scopes: "openid email profile",
|
|
},
|
|
}
|
|
}
|
|
if (type === "saml") {
|
|
return { ...base, saml: { metadata_url: "", entity_id: "", sso_url: "" } }
|
|
}
|
|
return {
|
|
...base,
|
|
ldap: {
|
|
server_uri: "",
|
|
bind_dn: "",
|
|
bind_password: "",
|
|
base_dn: "",
|
|
user_filter: "",
|
|
start_tls: true,
|
|
sync_users: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
function syncBadge(status: IdentityProvider["sync_status"]) {
|
|
switch (status) {
|
|
case "synced":
|
|
return <Badge variant="secondary">Synchronisé</Badge>
|
|
case "error":
|
|
return <Badge variant="destructive">Erreur sync</Badge>
|
|
default:
|
|
return <Badge variant="outline">En attente</Badge>
|
|
}
|
|
}
|
|
|
|
export function IdentityProvidersPanel({
|
|
onRegisterBeforeSave,
|
|
}: {
|
|
onRegisterBeforeSave?: (fn: (() => void) | null) => void
|
|
}) {
|
|
const identityProviders = useOrgSettingsStore((s) => s.identityProviders)
|
|
const setIdentityProviders = useOrgSettingsStore((s) => s.setIdentityProviders)
|
|
const meta = useOrgSettingsStore((s) => s.meta)
|
|
const [draft, setDraft] = useState(identityProviders)
|
|
const [addOpen, setAddOpen] = useState(false)
|
|
const [editIndex, setEditIndex] = useState<number | null>(null)
|
|
const [newType, setNewType] = useState<IdentityProviderType>("oauth")
|
|
const testMutation = useTestIdentityProvider()
|
|
const syncMutation = useSyncIdentityProvider()
|
|
const redirectMutation = useIdentityProviderRedirectURI()
|
|
|
|
useEffect(() => {
|
|
setDraft(identityProviders)
|
|
}, [identityProviders])
|
|
|
|
const redirectTemplate =
|
|
meta?.effective.identity_providers?.oauth_redirect_template ??
|
|
"http://localhost/auth/source/oauth/callback/{slug}/"
|
|
|
|
const editingProvider = editIndex != null ? draft.providers[editIndex] : null
|
|
|
|
const guide = useMemo(() => {
|
|
if (!editingProvider) return null
|
|
return guideForProvider(
|
|
editingProvider.type,
|
|
editingProvider.oauth?.provider
|
|
)
|
|
}, [editingProvider])
|
|
|
|
function updateProvider(index: number, patch: Partial<IdentityProvider>) {
|
|
setDraft((prev) => {
|
|
const providers = [...prev.providers]
|
|
providers[index] = { ...providers[index], ...patch }
|
|
return { ...prev, providers }
|
|
})
|
|
}
|
|
|
|
function addProvider(type: IdentityProviderType) {
|
|
const provider = emptyProvider(type)
|
|
setDraft((prev) => ({
|
|
...prev,
|
|
providers: [...prev.providers, provider],
|
|
}))
|
|
setAddOpen(false)
|
|
setEditIndex(draft.providers.length)
|
|
}
|
|
|
|
function removeProvider(index: number) {
|
|
setDraft((prev) => ({
|
|
...prev,
|
|
providers: prev.providers.filter((_, i) => i !== index),
|
|
}))
|
|
if (editIndex === index) setEditIndex(null)
|
|
}
|
|
|
|
function providerSecrets(provider: IdentityProvider): Record<string, { configured?: boolean }> {
|
|
const idpSecrets = meta?.secrets?.identity_providers
|
|
if (!idpSecrets || typeof idpSecrets !== "object") return {}
|
|
const entry = (idpSecrets as Record<string, Record<string, { configured?: boolean }>>)[
|
|
provider.id
|
|
]
|
|
return entry ?? {}
|
|
}
|
|
|
|
async function handleTest(provider: IdentityProvider) {
|
|
try {
|
|
await testMutation.mutateAsync(provider.id)
|
|
toast.success("Configuration valide")
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : "Test échoué")
|
|
}
|
|
}
|
|
|
|
async function handleSync(provider: IdentityProvider) {
|
|
try {
|
|
await syncMutation.mutateAsync(provider.id)
|
|
toast.success("Synchronisation Authentik lancée")
|
|
} catch {
|
|
toast.error("Synchronisation échouée")
|
|
}
|
|
}
|
|
|
|
async function copyRedirect(slug: string) {
|
|
try {
|
|
const res = await redirectMutation.mutateAsync(slug)
|
|
await navigator.clipboard.writeText(res.redirect_uri)
|
|
toast.success("URI de redirection copiée")
|
|
} catch {
|
|
const fallback = redirectTemplate.replace("{slug}", slug)
|
|
await navigator.clipboard.writeText(fallback)
|
|
toast.success("URI de redirection copiée")
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
onRegisterBeforeSave?.(() => setIdentityProviders(draft))
|
|
return () => onRegisterBeforeSave?.(null)
|
|
}, [draft, onRegisterBeforeSave, setIdentityProviders])
|
|
|
|
return (
|
|
<>
|
|
<label className="flex items-center justify-between gap-4 rounded-lg border p-4">
|
|
<div>
|
|
<p className="text-sm font-medium">Inscription self-service Authentik</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
Flow ulti-enrollment : autoriser la création de compte locale en parallèle du SSO entreprise.
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={draft.allow_self_enrollment}
|
|
onCheckedChange={(allow_self_enrollment) =>
|
|
setDraft((prev) => ({ ...prev, allow_self_enrollment }))
|
|
}
|
|
/>
|
|
</label>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label>Fournisseurs configurés</Label>
|
|
<Button variant="outline" size="sm" onClick={() => setAddOpen(true)}>
|
|
<Plus className="mr-2 size-4" />
|
|
Ajouter
|
|
</Button>
|
|
</div>
|
|
|
|
{draft.providers.length === 0 ? (
|
|
<p className="rounded-lg border border-dashed p-6 text-sm text-muted-foreground">
|
|
Aucun fournisseur. Ajoutez Google Workspace, Azure AD SAML, LDAP AD ou un OAuth custom.
|
|
</p>
|
|
) : (
|
|
<div className="grid gap-3">
|
|
{draft.providers.map((provider, index) => (
|
|
<div
|
|
key={provider.id}
|
|
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border p-4"
|
|
>
|
|
<div className="space-y-1">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm font-medium">
|
|
{provider.name || provider.slug || "Nouveau fournisseur"}
|
|
</span>
|
|
<Badge variant="outline">{provider.type.toUpperCase()}</Badge>
|
|
{syncBadge(provider.sync_status)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Slug : {provider.slug || "—"}
|
|
{provider.sync_error ? ` · ${provider.sync_error}` : ""}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Switch
|
|
checked={provider.enabled}
|
|
onCheckedChange={(enabled) => updateProvider(index, { enabled })}
|
|
/>
|
|
<Button variant="outline" size="sm" onClick={() => setEditIndex(index)}>
|
|
Modifier
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
disabled={testMutation.isPending}
|
|
onClick={() => handleTest(provider)}
|
|
>
|
|
<TestTube2 className="size-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
disabled={syncMutation.isPending}
|
|
onClick={() => handleSync(provider)}
|
|
>
|
|
{syncMutation.isPending ? (
|
|
<Loader2 className="size-4 animate-spin" />
|
|
) : (
|
|
<RefreshCw className="size-4" />
|
|
)}
|
|
</Button>
|
|
<Button variant="ghost" size="icon" onClick={() => removeProvider(index)}>
|
|
<Trash2 className="size-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Ajouter un fournisseur</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Type</Label>
|
|
<Select
|
|
value={newType}
|
|
onValueChange={(value) => setNewType(value as IdentityProviderType)}
|
|
>
|
|
<SelectTrigger className="mt-1 h-9">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="oauth">
|
|
<TechBrandSelectLabel brand="oauth">
|
|
OAuth (Google, GitHub, LinkedIn…)
|
|
</TechBrandSelectLabel>
|
|
</SelectItem>
|
|
<SelectItem value="saml">
|
|
<TechBrandSelectLabel brand="saml">
|
|
SAML (Azure AD, Okta…)
|
|
</TechBrandSelectLabel>
|
|
</SelectItem>
|
|
<SelectItem value="ldap">
|
|
<TechBrandSelectLabel brand="ldap">
|
|
LDAP / Active Directory
|
|
</TechBrandSelectLabel>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button onClick={() => addProvider(newType)}>Créer</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Sheet open={editIndex != null} onOpenChange={(open) => !open && setEditIndex(null)}>
|
|
<SheetContent className="flex w-full flex-col gap-0 overflow-y-auto p-0 sm:max-w-2xl">
|
|
{editingProvider && editIndex != null ? (
|
|
<>
|
|
<SheetHeader className="border-b px-6 py-5">
|
|
<SheetTitle>Configurer le fournisseur</SheetTitle>
|
|
</SheetHeader>
|
|
<div className="grid gap-6 px-6 py-6 lg:grid-cols-[1fr_240px]">
|
|
<div className="space-y-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<Label>Nom</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.name}
|
|
onChange={(e) => {
|
|
const name = e.target.value
|
|
updateProvider(editIndex, {
|
|
name,
|
|
slug: editingProvider.slug || slugify(name),
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Slug Authentik</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.slug}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, { slug: slugify(e.target.value) })
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{editingProvider.type === "oauth" ? (
|
|
<div className="space-y-4 rounded-lg border p-4">
|
|
<div>
|
|
<Label>Présélection</Label>
|
|
<Select
|
|
value={editingProvider.oauth?.provider ?? "google"}
|
|
onValueChange={(provider) =>
|
|
updateProvider(editIndex, {
|
|
oauth: {
|
|
...(editingProvider.oauth ?? {
|
|
client_id: "",
|
|
client_secret: "",
|
|
scopes: "openid email profile",
|
|
provider: "google" as OAuthProviderPreset,
|
|
}),
|
|
provider: provider as OAuthProviderPreset,
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="mt-1 h-9">
|
|
<SelectValue>
|
|
{editingProvider.oauth?.provider ? (
|
|
<TechBrandSelectLabel brand={editingProvider.oauth.provider}>
|
|
{OAUTH_PRESET_LABELS[editingProvider.oauth.provider]}
|
|
</TechBrandSelectLabel>
|
|
) : null}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="google">
|
|
<TechBrandSelectLabel brand="google">Google</TechBrandSelectLabel>
|
|
</SelectItem>
|
|
<SelectItem value="github">
|
|
<TechBrandSelectLabel brand="github">GitHub</TechBrandSelectLabel>
|
|
</SelectItem>
|
|
<SelectItem value="linkedin">
|
|
<TechBrandSelectLabel brand="linkedin">LinkedIn</TechBrandSelectLabel>
|
|
</SelectItem>
|
|
<SelectItem value="microsoft">
|
|
<TechBrandSelectLabel brand="microsoft">Microsoft</TechBrandSelectLabel>
|
|
</SelectItem>
|
|
<SelectItem value="custom">
|
|
<TechBrandSelectLabel brand="custom">Autre / custom</TechBrandSelectLabel>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label>Client ID</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.oauth?.client_id ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
oauth: { ...editingProvider.oauth!, client_id: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Client Secret</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
type="password"
|
|
placeholder={
|
|
providerSecrets(editingProvider).oauth_client_secret?.configured
|
|
? "Laisser vide pour conserver"
|
|
: "Secret OAuth"
|
|
}
|
|
value={editingProvider.oauth?.client_secret ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
oauth: {
|
|
...editingProvider.oauth!,
|
|
client_secret: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Scopes</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.oauth?.scopes ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
oauth: { ...editingProvider.oauth!, scopes: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
{editingProvider.oauth?.provider === "custom" ? (
|
|
<>
|
|
<div>
|
|
<Label>Authorization URL</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.oauth.authorization_url ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
oauth: {
|
|
...editingProvider.oauth!,
|
|
authorization_url: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Token URL</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.oauth.token_url ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
oauth: {
|
|
...editingProvider.oauth!,
|
|
token_url: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Profile URL</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.oauth.profile_url ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
oauth: {
|
|
...editingProvider.oauth!,
|
|
profile_url: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
<div>
|
|
<Label>URI de redirection</Label>
|
|
<div className="mt-1 flex gap-2">
|
|
<Input
|
|
className="h-9"
|
|
readOnly
|
|
value={redirectTemplate.replace(
|
|
"{slug}",
|
|
editingProvider.slug || "votre-slug"
|
|
)}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => copyRedirect(editingProvider.slug)}
|
|
>
|
|
<Copy className="size-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{editingProvider.type === "saml" ? (
|
|
<div className="space-y-4 rounded-lg border p-4">
|
|
<div>
|
|
<Label>Metadata URL</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.saml?.metadata_url ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
saml: { ...editingProvider.saml!, metadata_url: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Entity ID / Issuer</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.saml?.entity_id ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
saml: { ...editingProvider.saml!, entity_id: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>SSO URL</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.saml?.sso_url ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
saml: { ...editingProvider.saml!, sso_url: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Certificat signing (PEM)</Label>
|
|
<Textarea
|
|
className="mt-1 min-h-24 font-mono text-xs"
|
|
placeholder={
|
|
providerSecrets(editingProvider).saml_signing_cert?.configured
|
|
? "Laisser vide pour conserver"
|
|
: "-----BEGIN CERTIFICATE-----"
|
|
}
|
|
value={editingProvider.saml?.signing_cert ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
saml: {
|
|
...editingProvider.saml!,
|
|
signing_cert: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{editingProvider.type === "ldap" ? (
|
|
<div className="space-y-4 rounded-lg border p-4">
|
|
<div>
|
|
<Label>Server URI</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
placeholder="ldaps://ad.company.com:636"
|
|
value={editingProvider.ldap?.server_uri ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
ldap: { ...editingProvider.ldap!, server_uri: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Bind DN</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.ldap?.bind_dn ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
ldap: { ...editingProvider.ldap!, bind_dn: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Bind password</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
type="password"
|
|
placeholder={
|
|
providerSecrets(editingProvider).ldap_bind_password?.configured
|
|
? "Laisser vide pour conserver"
|
|
: "Mot de passe LDAP"
|
|
}
|
|
value={editingProvider.ldap?.bind_password ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
ldap: {
|
|
...editingProvider.ldap!,
|
|
bind_password: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Base DN</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={editingProvider.ldap?.base_dn ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
ldap: { ...editingProvider.ldap!, base_dn: e.target.value },
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Filtre utilisateur</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
placeholder="(sAMAccountName=%(user)s)"
|
|
value={editingProvider.ldap?.user_filter ?? ""}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
ldap: {
|
|
...editingProvider.ldap!,
|
|
user_filter: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<label className="flex items-center justify-between gap-4 rounded-lg border p-3">
|
|
<span className="text-sm">StartTLS</span>
|
|
<Switch
|
|
checked={editingProvider.ldap?.start_tls ?? true}
|
|
onCheckedChange={(start_tls) =>
|
|
updateProvider(editIndex, {
|
|
ldap: { ...editingProvider.ldap!, start_tls },
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="flex items-center justify-between gap-4 rounded-lg border p-3">
|
|
<span className="text-sm">Synchroniser les utilisateurs LDAP</span>
|
|
<Switch
|
|
checked={editingProvider.ldap?.sync_users ?? false}
|
|
onCheckedChange={(sync_users) =>
|
|
updateProvider(editIndex, {
|
|
ldap: { ...editingProvider.ldap!, sync_users },
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="space-y-3 rounded-lg border p-4">
|
|
<p className="text-sm font-medium">Restrictions d'accès</p>
|
|
<div>
|
|
<Label>Domaines email autorisés</Label>
|
|
<Textarea
|
|
className="mt-1 min-h-20"
|
|
placeholder="company.com subsidiary.fr"
|
|
value={joinList(editingProvider.allowed_email_domains)}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
allowed_email_domains: splitList(e.target.value),
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Identités autorisées (emails)</Label>
|
|
<Textarea
|
|
className="mt-1 min-h-20"
|
|
placeholder="admin@company.com"
|
|
value={joinList(editingProvider.allowed_identities)}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
allowed_identities: splitList(e.target.value),
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Organisations autorisées</Label>
|
|
<Textarea
|
|
className="mt-1 min-h-20"
|
|
placeholder="tenant-id Azure, domaine Google hd, org GitHub…"
|
|
value={joinList(editingProvider.allowed_organizations)}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
allowed_organizations: splitList(e.target.value),
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Groupes Authentik par défaut</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
placeholder="ulti-users, ulti-admins"
|
|
value={joinList(editingProvider.default_groups).replace(/\n/g, ", ")}
|
|
onChange={(e) =>
|
|
updateProvider(editIndex, {
|
|
default_groups: splitList(e.target.value.replace(/,/g, "\n")),
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{guide ? (
|
|
<aside className="rounded-lg border bg-muted/30 p-4">
|
|
<p className="text-sm font-medium">{guide.title}</p>
|
|
<ol className="mt-3 list-decimal space-y-2 pl-4 text-xs text-muted-foreground">
|
|
{guide.steps.map((step) => (
|
|
<li key={step}>{step}</li>
|
|
))}
|
|
</ol>
|
|
</aside>
|
|
) : null}
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</SheetContent>
|
|
</Sheet>
|
|
</>
|
|
)
|
|
}
|