mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: move sso and ldap logins into plugins (C2)
This commit is contained in:
1 parent
5c18435105
commit
925da4a661
223 files changed
+25778
-6820
No files matched your search
@@ -57,12 +57,6 @@ import {
|
||||
uploadManualSslCertificate,
|
||||
type AcmeSettings,
|
||||
} from "@/api/acme-ssl-api";
|
||||
import {
|
||||
getAdminSSOProviders,
|
||||
updateSSOProvider,
|
||||
deleteSSOProvider,
|
||||
} from "@/api/sso-provider-api";
|
||||
import type { SSOProvider } from "@/types/index";
|
||||
import {
|
||||
type ApiKey,
|
||||
type CreatedApiKey,
|
||||
@@ -85,10 +79,8 @@ import {
|
||||
AdminDatabaseSection,
|
||||
AdminGeneralSettingsSection,
|
||||
AdminHostDefaultsSection,
|
||||
AdminSSOSection,
|
||||
AdminSSLSection,
|
||||
} from "./AdminSettingsSections";
|
||||
import { SSOProviderDialog } from "./SSOProviderDialog";
|
||||
import { AdminApiKeysSection } from "./AdminApiKeysSection";
|
||||
import { AdminAuditLogSection } from "./AdminAuditLogSection";
|
||||
import {
|
||||
@@ -168,15 +160,11 @@ export function AdminSettingsPanel({
|
||||
const [brandingSaving, setBrandingSaving] = useState(false);
|
||||
const { applyBranding } = useBranding();
|
||||
|
||||
// SSO / auto-provision state
|
||||
// External login state
|
||||
const [oidcAutoProvision, setOidcAutoProvision] = useState(false);
|
||||
const [secondFactorAfterExternalLogin, setSecondFactorAfterExternalLogin] =
|
||||
useState(false);
|
||||
const [oidcSilentLoginDefault, setOidcSilentLoginDefault] = useState(false);
|
||||
const [ssoProviders, setSsoProviders] = useState<SSOProvider[]>([]);
|
||||
const [ssoDialogOpen, setSsoDialogOpen] = useState(false);
|
||||
const [ssoDialogProvider, setSsoDialogProvider] =
|
||||
useState<SSOProvider | null>(null);
|
||||
|
||||
// Create user dialog
|
||||
const [createUserOpen, setCreateUserOpen] = useState(false);
|
||||
@@ -259,7 +247,6 @@ export function AdminSettingsPanel({
|
||||
loadRoles();
|
||||
loadApiKeys();
|
||||
loadGeneralSettings();
|
||||
loadSSOProviders();
|
||||
}, []);
|
||||
|
||||
// Debounced so typing in the search box does not fire a request per keystroke.
|
||||
@@ -414,15 +401,6 @@ export function AdminSettingsPanel({
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async function loadSSOProviders() {
|
||||
try {
|
||||
const providers = await getAdminSSOProviders();
|
||||
setSsoProviders(providers);
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(id: AdminSection) {
|
||||
setOpenSections((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -627,43 +605,6 @@ export function AdminSettingsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddProvider() {
|
||||
setSsoDialogProvider(null);
|
||||
setSsoDialogOpen(true);
|
||||
}
|
||||
|
||||
function handleEditProvider(provider: SSOProvider) {
|
||||
setSsoDialogProvider(provider);
|
||||
setSsoDialogOpen(true);
|
||||
}
|
||||
|
||||
async function handleDeleteProvider(id: number) {
|
||||
if (!window.confirm(t("admin.ssoDeleteConfirm"))) return;
|
||||
try {
|
||||
await deleteSSOProvider(id);
|
||||
setSsoProviders((prev) => prev.filter((p) => p.id !== id));
|
||||
toast.success(t("common.deleted"));
|
||||
} catch (e) {
|
||||
toast.error(apiErrorMessage(e, t("common.deleteFailed")));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleProviderEnabled(id: number, enabled: boolean) {
|
||||
const provider = ssoProviders.find((p) => p.id === id);
|
||||
if (!provider) return;
|
||||
setSsoProviders((prev) =>
|
||||
prev.map((p) => (p.id === id ? { ...p, enabled } : p)),
|
||||
);
|
||||
try {
|
||||
await updateSSOProvider(id, { enabled });
|
||||
} catch (e) {
|
||||
setSsoProviders((prev) =>
|
||||
prev.map((p) => (p.id === id ? { ...p, enabled: !enabled } : p)),
|
||||
);
|
||||
toast.error(apiErrorMessage(e, t("common.saveFailed")));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveAcmeSettings() {
|
||||
try {
|
||||
const payload: Parameters<typeof updateAcmeSslSettings>[0] = {
|
||||
@@ -736,18 +677,6 @@ export function AdminSettingsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
function handleProviderSaved(saved: SSOProvider) {
|
||||
setSsoProviders((prev) => {
|
||||
const idx = prev.findIndex((p) => p.id === saved.id);
|
||||
if (idx >= 0) {
|
||||
const next = [...prev];
|
||||
next[idx] = saved;
|
||||
return next;
|
||||
}
|
||||
return [...prev, saved];
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCreateUser() {
|
||||
if (!newUsername.trim() || !newPassword.trim()) {
|
||||
toast.error(t("admin.createUserRequired"));
|
||||
@@ -1043,23 +972,6 @@ export function AdminSettingsPanel({
|
||||
handleSaveLogLevel={handleSaveLogLevel}
|
||||
/>
|
||||
|
||||
<AdminSSOSection
|
||||
open={openSections.has("sso")}
|
||||
onToggle={() => toggle("sso")}
|
||||
providers={ssoProviders}
|
||||
onAddProvider={handleAddProvider}
|
||||
onEditProvider={handleEditProvider}
|
||||
onDeleteProvider={handleDeleteProvider}
|
||||
onToggleEnabled={handleToggleProviderEnabled}
|
||||
/>
|
||||
|
||||
<SSOProviderDialog
|
||||
open={ssoDialogOpen}
|
||||
onOpenChange={setSsoDialogOpen}
|
||||
provider={ssoDialogProvider}
|
||||
onSaved={handleProviderSaved}
|
||||
/>
|
||||
|
||||
<AdminUsersSection
|
||||
open={openSections.has("users")}
|
||||
onToggle={() => toggle("users")}
|
||||
|
||||
@@ -5,19 +5,8 @@ import { Input } from "@/components/input";
|
||||
import { PasswordInput } from "@/components/password-input";
|
||||
import { SettingRow } from "@/components/section-card";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import {
|
||||
Database,
|
||||
Lock,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Server,
|
||||
Settings,
|
||||
Shield,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Database, Lock, RefreshCw, Server, Settings } from "lucide-react";
|
||||
import { AccordionSection, AdminToggle } from "./AdminSettingsShared";
|
||||
import type { SSOProvider, SSOProviderType } from "@/types/index";
|
||||
import type { HostDefaults } from "@/api/settings-api";
|
||||
import type { AcmeSettings, AcmeChallengeType } from "@/api/acme-ssl-api";
|
||||
|
||||
@@ -355,268 +344,6 @@ export function AdminGeneralSettingsSection({
|
||||
);
|
||||
}
|
||||
|
||||
type OidcSettingsSectionProps = {
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
oidcClientId: string;
|
||||
setOidcClientId: Dispatch<SetStateAction<string>>;
|
||||
oidcClientSecret: string;
|
||||
setOidcClientSecret: Dispatch<SetStateAction<string>>;
|
||||
oidcAuthUrl: string;
|
||||
setOidcAuthUrl: Dispatch<SetStateAction<string>>;
|
||||
oidcIssuerUrl: string;
|
||||
setOidcIssuerUrl: Dispatch<SetStateAction<string>>;
|
||||
oidcTokenUrl: string;
|
||||
setOidcTokenUrl: Dispatch<SetStateAction<string>>;
|
||||
oidcUserIdentifier: string;
|
||||
setOidcUserIdentifier: Dispatch<SetStateAction<string>>;
|
||||
oidcDisplayName: string;
|
||||
setOidcDisplayName: Dispatch<SetStateAction<string>>;
|
||||
oidcScopes: string;
|
||||
setOidcScopes: Dispatch<SetStateAction<string>>;
|
||||
oidcUserinfoUrl: string;
|
||||
setOidcUserinfoUrl: Dispatch<SetStateAction<string>>;
|
||||
oidcAllowedUsers: string;
|
||||
setOidcAllowedUsers: Dispatch<SetStateAction<string>>;
|
||||
oidcAdminGroup: string;
|
||||
setOidcAdminGroup: Dispatch<SetStateAction<string>>;
|
||||
oidcGroupClaim: string;
|
||||
setOidcGroupClaim: Dispatch<SetStateAction<string>>;
|
||||
oidcSaving: boolean;
|
||||
handleRemoveOidc: () => void;
|
||||
handleSaveOidc: () => void;
|
||||
};
|
||||
|
||||
export function AdminOidcSettingsSection({
|
||||
open,
|
||||
onToggle,
|
||||
oidcClientId,
|
||||
setOidcClientId,
|
||||
oidcClientSecret,
|
||||
setOidcClientSecret,
|
||||
oidcAuthUrl,
|
||||
setOidcAuthUrl,
|
||||
oidcIssuerUrl,
|
||||
setOidcIssuerUrl,
|
||||
oidcTokenUrl,
|
||||
setOidcTokenUrl,
|
||||
oidcUserIdentifier,
|
||||
setOidcUserIdentifier,
|
||||
oidcDisplayName,
|
||||
setOidcDisplayName,
|
||||
oidcScopes,
|
||||
setOidcScopes,
|
||||
oidcUserinfoUrl,
|
||||
setOidcUserinfoUrl,
|
||||
oidcAllowedUsers,
|
||||
setOidcAllowedUsers,
|
||||
oidcAdminGroup,
|
||||
setOidcAdminGroup,
|
||||
oidcGroupClaim,
|
||||
setOidcGroupClaim,
|
||||
oidcSaving,
|
||||
handleRemoveOidc,
|
||||
handleSaveOidc,
|
||||
}: OidcSettingsSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<AccordionSection
|
||||
label={t("admin.sectionOidc")}
|
||||
icon={<Shield className="size-3.5" />}
|
||||
open={open}
|
||||
onToggle={onToggle}
|
||||
>
|
||||
<div className="flex flex-col gap-3 pt-3">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.oidcDescription").split("*")[0]}
|
||||
<span className="text-accent-brand">*</span>
|
||||
{t("admin.oidcDescription").split("*")[1]}{" "}
|
||||
<a
|
||||
href="https://docs.termix.site/features/authentication/oidc"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent-brand hover:underline"
|
||||
>
|
||||
{t("admin.oidcDocsLink")}
|
||||
</a>
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcClientId")}{" "}
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={oidcClientId}
|
||||
onChange={(e) => setOidcClientId(e.target.value)}
|
||||
placeholder="your-client-id"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcClientSecret")}{" "}
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={oidcClientSecret}
|
||||
onChange={(e) => setOidcClientSecret(e.target.value)}
|
||||
placeholder="your-client-secret"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcAuthUrl")}{" "}
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={oidcAuthUrl}
|
||||
onChange={(e) => setOidcAuthUrl(e.target.value)}
|
||||
placeholder="https://provider/oauth2/auth"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcIssuerUrl")}{" "}
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={oidcIssuerUrl}
|
||||
onChange={(e) => setOidcIssuerUrl(e.target.value)}
|
||||
placeholder="https://provider"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcTokenUrl")}{" "}
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={oidcTokenUrl}
|
||||
onChange={(e) => setOidcTokenUrl(e.target.value)}
|
||||
placeholder="https://provider/oauth2/token"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcUserIdentifier")}{" "}
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={oidcUserIdentifier}
|
||||
onChange={(e) => setOidcUserIdentifier(e.target.value)}
|
||||
placeholder="sub"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcDisplayName")}{" "}
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={oidcDisplayName}
|
||||
onChange={(e) => setOidcDisplayName(e.target.value)}
|
||||
placeholder="name"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcScopes")} <span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={oidcScopes}
|
||||
onChange={(e) => setOidcScopes(e.target.value)}
|
||||
placeholder="openid email profile"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcUserinfoUrl")}
|
||||
</label>
|
||||
<Input
|
||||
value={oidcUserinfoUrl}
|
||||
onChange={(e) => setOidcUserinfoUrl(e.target.value)}
|
||||
placeholder="https://provider/oauth2/userinfo"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcAllowedUsers")}
|
||||
</label>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.oidcAllowedUsersDesc")}
|
||||
</span>
|
||||
<textarea
|
||||
value={oidcAllowedUsers}
|
||||
onChange={(e) => setOidcAllowedUsers(e.target.value)}
|
||||
placeholder={"user@example.com\nanother@example.com"}
|
||||
rows={3}
|
||||
className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcAdminGroup")}
|
||||
</label>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.oidcAdminGroupDesc")}
|
||||
</span>
|
||||
<input
|
||||
value={oidcAdminGroup}
|
||||
onChange={(e) => setOidcAdminGroup(e.target.value)}
|
||||
placeholder="admin"
|
||||
className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.oidcGroupClaim")}
|
||||
</label>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.oidcGroupClaimDesc")}
|
||||
</span>
|
||||
<input
|
||||
value={oidcGroupClaim}
|
||||
onChange={(e) => setOidcGroupClaim(e.target.value)}
|
||||
placeholder="groups"
|
||||
className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={handleRemoveOidc}
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
{t("admin.removeOidc")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
||||
onClick={handleSaveOidc}
|
||||
disabled={oidcSaving}
|
||||
>
|
||||
<RefreshCw className="size-3" />
|
||||
{oidcSaving ? t("admin.saving") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionSection>
|
||||
);
|
||||
}
|
||||
|
||||
type DatabaseSectionProps = {
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
@@ -708,115 +435,6 @@ export function AdminDatabaseSection({
|
||||
);
|
||||
}
|
||||
|
||||
const SSO_TYPE_LABELS: Record<SSOProviderType, string> = {
|
||||
oidc: "OIDC",
|
||||
ldap: "LDAP",
|
||||
github: "GitHub",
|
||||
google: "Google",
|
||||
};
|
||||
|
||||
type AdminSSOSectionProps = {
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
providers: SSOProvider[];
|
||||
onAddProvider: () => void;
|
||||
onEditProvider: (provider: SSOProvider) => void;
|
||||
onDeleteProvider: (id: number) => void;
|
||||
onToggleEnabled: (id: number, enabled: boolean) => void;
|
||||
};
|
||||
|
||||
export function AdminSSOSection({
|
||||
open,
|
||||
onToggle,
|
||||
providers,
|
||||
onAddProvider,
|
||||
onEditProvider,
|
||||
onDeleteProvider,
|
||||
onToggleEnabled,
|
||||
}: AdminSSOSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<AccordionSection
|
||||
label={t("admin.sectionSso")}
|
||||
icon={<Shield className="size-3.5" />}
|
||||
open={open}
|
||||
onToggle={onToggle}
|
||||
>
|
||||
<div className="flex flex-col gap-3 pt-3">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
<a
|
||||
href="https://docs.termix.site/features/authentication/sso-providers"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent-brand hover:underline"
|
||||
>
|
||||
{t("admin.ssoDocsLink")}
|
||||
</a>
|
||||
</span>
|
||||
{providers.length === 0 ? (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.ssoNoProviders")}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{providers.map((provider) => (
|
||||
<div
|
||||
key={provider.id}
|
||||
className="flex items-center gap-2 p-2 border border-border bg-background"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-medium truncate">
|
||||
{provider.name}
|
||||
</span>
|
||||
<span className="text-[9px] px-1 py-0.5 bg-muted text-muted-foreground font-mono uppercase">
|
||||
{SSO_TYPE_LABELS[provider.type] ?? provider.type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<AdminToggle
|
||||
on={provider.enabled}
|
||||
onToggle={() =>
|
||||
onToggleEnabled(provider.id, !provider.enabled)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => onEditProvider(provider)}
|
||||
title={t("admin.ssoEditProvider")}
|
||||
>
|
||||
<Pencil className="size-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => onDeleteProvider(provider.id)}
|
||||
title={t("admin.ssoDeleteProvider")}
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="self-start text-xs border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
||||
onClick={onAddProvider}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
{t("admin.ssoAddProvider")}
|
||||
</Button>
|
||||
</div>
|
||||
</AccordionSection>
|
||||
);
|
||||
}
|
||||
|
||||
type AdminHostDefaultsSectionProps = {
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
|
||||
@@ -1,726 +0,0 @@
|
||||
import { useState, useEffect, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/button";
|
||||
import { Input } from "@/components/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/dialog";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type LDAPProviderConfig,
|
||||
type OIDCProviderConfig,
|
||||
type SSOProvider,
|
||||
type SSOProviderType,
|
||||
} from "@/types/index";
|
||||
import { createSSOProvider, updateSSOProvider } from "@/api/sso-provider-api";
|
||||
import { Select2 } from "@/components/select2";
|
||||
|
||||
type ApiErrorLike = {
|
||||
response?: { data?: { error?: string } };
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function apiErrorMsg(error: unknown, fallback: string) {
|
||||
const err = error as ApiErrorLike;
|
||||
return err.response?.data?.error || err.message || fallback;
|
||||
}
|
||||
|
||||
const PROVIDER_TYPE_OPTIONS: { value: SSOProviderType; label: string }[] = [
|
||||
{ value: "oidc", label: "OIDC" },
|
||||
{ value: "github", label: "GitHub" },
|
||||
{ value: "google", label: "Google" },
|
||||
{ value: "ldap", label: "LDAP" },
|
||||
];
|
||||
|
||||
const githubDefaults = {
|
||||
authorization_url: "https://github.com/login/oauth/authorize",
|
||||
token_url: "https://github.com/login/oauth/access_token",
|
||||
issuer_url: "https://token.actions.githubusercontent.com",
|
||||
userinfo_url: "https://api.github.com/user",
|
||||
scopes: "read:user user:email",
|
||||
identifier_path: "id",
|
||||
name_path: "name",
|
||||
};
|
||||
|
||||
const googleDefaults = {
|
||||
authorization_url: "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
token_url: "https://oauth2.googleapis.com/token",
|
||||
issuer_url: "https://accounts.google.com",
|
||||
userinfo_url: "https://openidconnect.googleapis.com/v1/userinfo",
|
||||
scopes: "openid email profile",
|
||||
identifier_path: "sub",
|
||||
name_path: "name",
|
||||
};
|
||||
|
||||
type SSOProviderDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
provider: SSOProvider | null;
|
||||
onSaved: (provider: SSOProvider) => void;
|
||||
};
|
||||
|
||||
function emptyOidc(): OIDCProviderConfig {
|
||||
return {
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
issuer_url: "",
|
||||
authorization_url: "",
|
||||
token_url: "",
|
||||
userinfo_url: "",
|
||||
identifier_path: "sub",
|
||||
name_path: "name",
|
||||
scopes: "openid email profile",
|
||||
allowed_users: "",
|
||||
admin_group: "",
|
||||
group_claim: "",
|
||||
ca_cert: "",
|
||||
};
|
||||
}
|
||||
|
||||
function emptyLdap(): Required<LDAPProviderConfig> {
|
||||
return {
|
||||
host: "",
|
||||
port: 389,
|
||||
useTLS: false,
|
||||
bindDN: "",
|
||||
bindPassword: "",
|
||||
userSearchBase: "",
|
||||
userSearchFilter: "(uid={{username}})",
|
||||
usernameAttribute: "uid",
|
||||
displayNameAttribute: "cn",
|
||||
groupSearchBase: "",
|
||||
adminGroup: "",
|
||||
allowedUsers: "",
|
||||
};
|
||||
}
|
||||
|
||||
type OIDCFields = {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
issuer_url: string;
|
||||
authorization_url: string;
|
||||
token_url: string;
|
||||
userinfo_url: string;
|
||||
identifier_path: string;
|
||||
name_path: string;
|
||||
scopes: string;
|
||||
allowed_users: string;
|
||||
admin_group: string;
|
||||
group_claim: string;
|
||||
ca_cert: string;
|
||||
};
|
||||
|
||||
type LDAPFields = {
|
||||
host: string;
|
||||
port: string;
|
||||
useTLS: boolean;
|
||||
bindDN: string;
|
||||
bindPassword: string;
|
||||
userSearchBase: string;
|
||||
userSearchFilter: string;
|
||||
usernameAttribute: string;
|
||||
displayNameAttribute: string;
|
||||
groupSearchBase: string;
|
||||
adminGroup: string;
|
||||
allowedUsers: string;
|
||||
};
|
||||
|
||||
export function SSOProviderDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
provider,
|
||||
onSaved,
|
||||
}: SSOProviderDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = provider != null;
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [type, setType] = useState<SSOProviderType>("oidc");
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [oidc, setOidc] = useState<OIDCFields>(
|
||||
() => ({ ...emptyOidc() }) as OIDCFields,
|
||||
);
|
||||
const [ldap, setLdap] = useState<LDAPFields>(() => {
|
||||
const d = emptyLdap();
|
||||
return { ...d, port: String(d.port) };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (provider) {
|
||||
setName(provider.name);
|
||||
setType(provider.type);
|
||||
setEnabled(provider.enabled);
|
||||
let config: Record<string, unknown> = {};
|
||||
if (typeof provider.config === "string") {
|
||||
try {
|
||||
config = JSON.parse(provider.config);
|
||||
} catch {
|
||||
/* */
|
||||
}
|
||||
} else if (provider.config && typeof provider.config === "object") {
|
||||
config = provider.config as Record<string, unknown>;
|
||||
}
|
||||
if (provider.type === "ldap") {
|
||||
const d = emptyLdap();
|
||||
setLdap({
|
||||
host: (config.host as string) ?? d.host,
|
||||
port: String((config.port as number) ?? d.port),
|
||||
useTLS: (config.useTLS as boolean) ?? d.useTLS,
|
||||
bindDN: (config.bindDN as string) ?? d.bindDN,
|
||||
bindPassword: (config.bindPassword as string) ?? "",
|
||||
userSearchBase: (config.userSearchBase as string) ?? d.userSearchBase,
|
||||
userSearchFilter:
|
||||
(config.userSearchFilter as string) ?? d.userSearchFilter,
|
||||
usernameAttribute:
|
||||
(config.usernameAttribute as string) ?? d.usernameAttribute,
|
||||
displayNameAttribute:
|
||||
(config.displayNameAttribute as string) ?? d.displayNameAttribute,
|
||||
groupSearchBase: (config.groupSearchBase as string) ?? "",
|
||||
adminGroup: (config.adminGroup as string) ?? "",
|
||||
allowedUsers: (config.allowedUsers as string) ?? "",
|
||||
});
|
||||
} else {
|
||||
const d = emptyOidc();
|
||||
setOidc({
|
||||
client_id: (config.client_id as string) ?? d.client_id,
|
||||
client_secret: (config.client_secret as string) ?? "",
|
||||
issuer_url: (config.issuer_url as string) ?? d.issuer_url,
|
||||
authorization_url:
|
||||
(config.authorization_url as string) ?? d.authorization_url,
|
||||
token_url: (config.token_url as string) ?? d.token_url,
|
||||
userinfo_url: (config.userinfo_url as string) ?? d.userinfo_url,
|
||||
identifier_path:
|
||||
(config.identifier_path as string) ?? d.identifier_path,
|
||||
name_path: (config.name_path as string) ?? d.name_path,
|
||||
scopes: (config.scopes as string) ?? d.scopes,
|
||||
allowed_users: (config.allowed_users as string) ?? d.allowed_users,
|
||||
admin_group: (config.admin_group as string) ?? d.admin_group,
|
||||
group_claim: (config.group_claim as string) ?? "",
|
||||
ca_cert: (config.ca_cert as string) ?? "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setName("");
|
||||
setType("oidc");
|
||||
setEnabled(true);
|
||||
setOidc({ ...emptyOidc() } as OIDCFields);
|
||||
const d = emptyLdap();
|
||||
setLdap({ ...d, port: String(d.port) });
|
||||
}
|
||||
}, [open, provider]);
|
||||
|
||||
function setOidcField<K extends keyof OIDCFields>(
|
||||
field: K,
|
||||
value: OIDCFields[K],
|
||||
) {
|
||||
setOidc((prev) => ({ ...prev, [field]: value }));
|
||||
}
|
||||
|
||||
function setLdapField<K extends keyof LDAPFields>(
|
||||
field: K,
|
||||
value: LDAPFields[K],
|
||||
) {
|
||||
setLdap((prev) => ({ ...prev, [field]: value }));
|
||||
}
|
||||
|
||||
function buildConfig(): Record<string, unknown> {
|
||||
if (type === "ldap") {
|
||||
return {
|
||||
host: ldap.host,
|
||||
port: parseInt(ldap.port, 10) || 389,
|
||||
useTLS: ldap.useTLS,
|
||||
bindDN: ldap.bindDN,
|
||||
bindPassword: ldap.bindPassword,
|
||||
userSearchBase: ldap.userSearchBase,
|
||||
userSearchFilter: ldap.userSearchFilter,
|
||||
usernameAttribute: ldap.usernameAttribute || "uid",
|
||||
displayNameAttribute: ldap.displayNameAttribute || "cn",
|
||||
groupSearchBase: ldap.groupSearchBase || undefined,
|
||||
adminGroup: ldap.adminGroup || undefined,
|
||||
allowedUsers: ldap.allowedUsers || undefined,
|
||||
};
|
||||
}
|
||||
const providerDefaults =
|
||||
type === "google"
|
||||
? googleDefaults
|
||||
: type === "github"
|
||||
? githubDefaults
|
||||
: null;
|
||||
return {
|
||||
client_id: oidc.client_id,
|
||||
client_secret: oidc.client_secret,
|
||||
issuer_url: oidc.issuer_url || providerDefaults?.issuer_url || "",
|
||||
authorization_url:
|
||||
oidc.authorization_url || providerDefaults?.authorization_url || "",
|
||||
token_url: oidc.token_url || providerDefaults?.token_url || "",
|
||||
userinfo_url:
|
||||
oidc.userinfo_url || providerDefaults?.userinfo_url || undefined,
|
||||
identifier_path:
|
||||
oidc.identifier_path || providerDefaults?.identifier_path || "sub",
|
||||
name_path: oidc.name_path || providerDefaults?.name_path || "name",
|
||||
scopes: oidc.scopes || providerDefaults?.scopes || "openid email profile",
|
||||
allowed_users: oidc.allowed_users || undefined,
|
||||
admin_group: oidc.admin_group || undefined,
|
||||
group_claim: oidc.group_claim || undefined,
|
||||
ca_cert: oidc.ca_cert || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!name.trim()) {
|
||||
toast.error(
|
||||
t("admin.ssoProviderName") + " " + t("common.required").toLowerCase(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const data = {
|
||||
name: name.trim(),
|
||||
type,
|
||||
enabled,
|
||||
displayOrder: 0,
|
||||
config: buildConfig(),
|
||||
};
|
||||
let saved: SSOProvider;
|
||||
if (isEdit && provider) {
|
||||
saved = await updateSSOProvider(provider.id, data);
|
||||
} else {
|
||||
saved = await createSSOProvider(data);
|
||||
}
|
||||
toast.success(t("common.saved"));
|
||||
onSaved(saved);
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
toast.error(apiErrorMsg(err, t("common.saveFailed")));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const isGithubOrGoogle = type === "github" || type === "google";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEdit ? t("admin.ssoEditProvider") : t("admin.ssoAddProvider")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3 pt-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.ssoProviderName")}{" "}
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My SSO Provider"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isEdit && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.ssoProviderType")}
|
||||
</label>
|
||||
<Select2
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as SSOProviderType)}
|
||||
className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
{PROVIDER_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.ssoEnabled")}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEnabled((v) => !v)}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center border-2 transition-colors ${enabled ? "bg-accent-brand border-accent-brand" : "bg-muted border-border"}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none inline-block h-3 w-3 bg-background shadow-sm transition-transform ${enabled ? "translate-x-4" : "translate-x-0.5"}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-3">
|
||||
{type === "ldap" ? (
|
||||
<LDAPConfigFields ldap={ldap} setLdapField={setLdapField} t={t} />
|
||||
) : (
|
||||
<OIDCConfigFields
|
||||
oidc={oidc}
|
||||
setOidcField={setOidcField}
|
||||
type={type}
|
||||
simplified={isGithubOrGoogle}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? t("admin.saving") : t("admin.ssoSaveProvider")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
required,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{label}
|
||||
{required && <span className="text-accent-brand ml-1">*</span>}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OIDCConfigFields({
|
||||
oidc,
|
||||
setOidcField,
|
||||
type,
|
||||
simplified,
|
||||
t,
|
||||
}: {
|
||||
oidc: OIDCFields;
|
||||
setOidcField: <K extends keyof OIDCFields>(k: K, v: OIDCFields[K]) => void;
|
||||
type: SSOProviderType;
|
||||
simplified: boolean;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const docsHref = simplified
|
||||
? "https://docs.termix.site/features/authentication/github-google"
|
||||
: "https://docs.termix.site/features/authentication/oidc";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
<a
|
||||
href={docsHref}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent-brand hover:underline"
|
||||
>
|
||||
{t("admin.ssoProviderDocsLink")}
|
||||
</a>
|
||||
</span>
|
||||
<Field label={t("admin.oidcClientId")} required>
|
||||
<Input
|
||||
value={oidc.client_id}
|
||||
onChange={(e) => setOidcField("client_id", e.target.value)}
|
||||
placeholder="your-client-id"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcClientSecret")} required>
|
||||
<Input
|
||||
type="password"
|
||||
value={oidc.client_secret}
|
||||
onChange={(e) => setOidcField("client_secret", e.target.value)}
|
||||
placeholder="your-client-secret"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
{simplified ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{type === "github"
|
||||
? `Authorization URL: ${githubDefaults.authorization_url}`
|
||||
: `Authorization URL: ${googleDefaults.authorization_url}`}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Field label={t("admin.oidcIssuerUrl")} required>
|
||||
<Input
|
||||
value={oidc.issuer_url}
|
||||
onChange={(e) => setOidcField("issuer_url", e.target.value)}
|
||||
placeholder="https://provider"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcAuthUrl")} required>
|
||||
<Input
|
||||
value={oidc.authorization_url}
|
||||
onChange={(e) =>
|
||||
setOidcField("authorization_url", e.target.value)
|
||||
}
|
||||
placeholder="https://provider/oauth2/auth"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcTokenUrl")} required>
|
||||
<Input
|
||||
value={oidc.token_url}
|
||||
onChange={(e) => setOidcField("token_url", e.target.value)}
|
||||
placeholder="https://provider/oauth2/token"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcUserIdentifier")} required>
|
||||
<Input
|
||||
value={oidc.identifier_path}
|
||||
onChange={(e) => setOidcField("identifier_path", e.target.value)}
|
||||
placeholder="sub"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcDisplayName")} required>
|
||||
<Input
|
||||
value={oidc.name_path}
|
||||
onChange={(e) => setOidcField("name_path", e.target.value)}
|
||||
placeholder="name"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcScopes")} required>
|
||||
<Input
|
||||
value={oidc.scopes}
|
||||
onChange={(e) => setOidcField("scopes", e.target.value)}
|
||||
placeholder="openid email profile"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcUserinfoUrl")}>
|
||||
<Input
|
||||
value={oidc.userinfo_url}
|
||||
onChange={(e) => setOidcField("userinfo_url", e.target.value)}
|
||||
placeholder="https://provider/oauth2/userinfo"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcGroupClaim")}>
|
||||
<Input
|
||||
value={oidc.group_claim}
|
||||
onChange={(e) => setOidcField("group_claim", e.target.value)}
|
||||
placeholder="groups"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<Field label={t("admin.oidcAllowedUsers")}>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.oidcAllowedUsersDesc")}
|
||||
</span>
|
||||
<textarea
|
||||
value={oidc.allowed_users}
|
||||
onChange={(e) => setOidcField("allowed_users", e.target.value)}
|
||||
placeholder={"user@example.com\nanother@example.com"}
|
||||
rows={3}
|
||||
className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcAdminGroup")}>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.oidcAdminGroupDesc")}
|
||||
</span>
|
||||
<Input
|
||||
value={oidc.admin_group}
|
||||
onChange={(e) => setOidcField("admin_group", e.target.value)}
|
||||
placeholder="admin"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.oidcCaCert")}>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.oidcCaCertDesc")}
|
||||
</span>
|
||||
<textarea
|
||||
value={oidc.ca_cert}
|
||||
onChange={(e) => setOidcField("ca_cert", e.target.value)}
|
||||
placeholder={
|
||||
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
}
|
||||
rows={4}
|
||||
className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LDAPConfigFields({
|
||||
ldap,
|
||||
setLdapField,
|
||||
t,
|
||||
}: {
|
||||
ldap: LDAPFields;
|
||||
setLdapField: <K extends keyof LDAPFields>(k: K, v: LDAPFields[K]) => void;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
<a
|
||||
href="https://docs.termix.site/features/authentication/ldap"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent-brand hover:underline"
|
||||
>
|
||||
{t("admin.ssoProviderDocsLink")}
|
||||
</a>
|
||||
</span>
|
||||
<Field label={t("admin.ldapHost")} required>
|
||||
<Input
|
||||
value={ldap.host}
|
||||
onChange={(e) => setLdapField("host", e.target.value)}
|
||||
placeholder="ldap.example.com"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Field label={t("admin.ldapPort")} required>
|
||||
<Input
|
||||
value={ldap.port}
|
||||
onChange={(e) => setLdapField("port", e.target.value)}
|
||||
placeholder="389"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex items-end pb-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLdapField("useTLS", !ldap.useTLS)}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center border-2 transition-colors ${ldap.useTLS ? "bg-accent-brand border-accent-brand" : "bg-muted border-border"}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none inline-block h-3 w-3 bg-background shadow-sm transition-transform ${ldap.useTLS ? "translate-x-4" : "translate-x-0.5"}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.ldapUseTls")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Field label={t("admin.ldapBindDn")} required>
|
||||
<Input
|
||||
value={ldap.bindDN}
|
||||
onChange={(e) => setLdapField("bindDN", e.target.value)}
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.ldapBindPassword")} required>
|
||||
<Input
|
||||
type="password"
|
||||
value={ldap.bindPassword}
|
||||
onChange={(e) => setLdapField("bindPassword", e.target.value)}
|
||||
placeholder="bind password"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.ldapUserSearchBase")} required>
|
||||
<Input
|
||||
value={ldap.userSearchBase}
|
||||
onChange={(e) => setLdapField("userSearchBase", e.target.value)}
|
||||
placeholder="ou=users,dc=example,dc=com"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.ldapUserSearchFilter")} required>
|
||||
<Input
|
||||
value={ldap.userSearchFilter}
|
||||
onChange={(e) => setLdapField("userSearchFilter", e.target.value)}
|
||||
placeholder="(uid={{username}})"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.ldapUsernameAttr")} required>
|
||||
<Input
|
||||
value={ldap.usernameAttribute}
|
||||
onChange={(e) => setLdapField("usernameAttribute", e.target.value)}
|
||||
placeholder="uid"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.ldapDisplayNameAttr")} required>
|
||||
<Input
|
||||
value={ldap.displayNameAttribute}
|
||||
onChange={(e) => setLdapField("displayNameAttribute", e.target.value)}
|
||||
placeholder="cn"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<div className="border-t border-border pt-3 flex flex-col gap-3">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.ldapGroupSearchBase")}
|
||||
</span>
|
||||
<Field label={t("admin.ldapGroupSearchBase")}>
|
||||
<Input
|
||||
value={ldap.groupSearchBase}
|
||||
onChange={(e) => setLdapField("groupSearchBase", e.target.value)}
|
||||
placeholder="ou=groups,dc=example,dc=com"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.ldapAdminGroup")}>
|
||||
<Input
|
||||
value={ldap.adminGroup}
|
||||
onChange={(e) => setLdapField("adminGroup", e.target.value)}
|
||||
placeholder="cn=admins,ou=groups,dc=example,dc=com"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("admin.ldapAllowedUsers")}>
|
||||
<Input
|
||||
value={ldap.allowedUsers}
|
||||
onChange={(e) => setLdapField("allowedUsers", e.target.value)}
|
||||
placeholder="user1,user2,@domain.com"
|
||||
className="text-xs"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user