mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 07:35: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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user