Files
teatea-pension/modules/settings/components/GlobalSettingsClient.tsx

254 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { FormEvent, useState } from "react";
import { Building2, Globe2, KeyRound, Save, ShieldCheck, UserPlus } from "lucide-react";
import { useRouter } from "next/navigation";
import { Badge } from "@/components/ui/badge";
import { Button, LinkButton } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import type { ApiResult } from "@/modules/core/server/api";
import type { SystemSettings } from "@/modules/core/types";
type GlobalSettingsClientProps = {
organizationSettingsHref: string;
settings: SystemSettings;
};
type SettingsTab = "registration" | "invitation" | "oidc";
const tabs: Array<{ label: string; value: SettingsTab }> = [
{ label: "注册入口", value: "registration" },
{ label: "邀请注册", value: "invitation" },
{ label: "OIDC 默认配置", value: "oidc" },
];
export function GlobalSettingsClient({
organizationSettingsHref,
settings,
}: GlobalSettingsClientProps): React.ReactElement {
const router = useRouter();
const [activeTab, setActiveTab] = useState<SettingsTab>("registration");
const [registrationEnabled, setRegistrationEnabled] = useState(settings.registrationEnabled);
const [oidcEnabled, setOidcEnabled] = useState(settings.oidcEnabled);
const [oidcAutoProvision, setOidcAutoProvision] = useState(settings.oidcAutoProvision);
const [message, setMessage] = useState("");
const [isPending, setIsPending] = useState(false);
async function handleSubmit(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
setMessage("");
setIsPending(true);
const formData = new FormData(event.currentTarget);
const response = await fetch("/api/settings/global", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
registrationEnabled,
oidcEnabled,
oidcIssuerUrl: String(formData.get("oidcIssuerUrl") ?? ""),
oidcClientId: String(formData.get("oidcClientId") ?? ""),
oidcClientSecret: String(formData.get("oidcClientSecret") ?? ""),
oidcScopes: String(formData.get("oidcScopes") ?? ""),
oidcRedirectUri: String(formData.get("oidcRedirectUri") ?? ""),
oidcAvatarClaim: String(formData.get("oidcAvatarClaim") ?? ""),
oidcAutoProvision,
}),
});
const result = (await response.json()) as ApiResult<{ settings: SystemSettings }>;
setIsPending(false);
if (!result.success) {
setMessage(result.reason);
return;
}
setMessage(result.reason);
router.refresh();
}
return (
<form className="grid gap-4" onSubmit={handleSubmit}>
<div className="flex flex-wrap gap-2" role="tablist" aria-label="全局配置">
{tabs.map((tab) => (
<Button
aria-selected={activeTab === tab.value}
key={tab.value}
onClick={() => setActiveTab(tab.value)}
role="tab"
type="button"
variant={activeTab === tab.value ? "default" : "outline"}
>
{tab.label}
</Button>
))}
</div>
<Card>
<div hidden={activeTab !== "registration"} role="tabpanel">
<CardHeader>
<div className="flex items-center gap-2">
<Globe2 className="size-5 text-primary" aria-hidden="true" />
<CardTitle></CardTitle>
</div>
<CardDescription> OIDC </CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="rounded-md border p-3 text-sm">
<Checkbox
checked={registrationEnabled}
label={
<span className="min-w-0">
<span className="block font-medium"></span>
<span className="block text-xs text-muted-foreground">使</span>
</span>
}
name="registrationEnabled"
onCheckedChange={setRegistrationEnabled}
/>
</div>
<div className="rounded-md border p-3 text-sm">
<Checkbox
checked={oidcEnabled}
label={
<span className="min-w-0">
<span className="block font-medium"> OIDC </span>
<span className="block text-xs text-muted-foreground"> OIDC </span>
</span>
}
name="oidcEnabled"
onCheckedChange={setOidcEnabled}
/>
</div>
<div className="rounded-md border p-3 text-sm">
<Checkbox
checked={oidcAutoProvision}
label={
<span className="min-w-0">
<span className="block font-medium">OIDC </span>
<span className="block text-xs text-muted-foreground"> OIDC </span>
</span>
}
name="oidcAutoProvision"
onCheckedChange={setOidcAutoProvision}
/>
</div>
</CardContent>
</div>
<div hidden={activeTab !== "invitation"} role="tabpanel">
<CardHeader>
<div className="flex items-center gap-2">
<KeyRound className="size-5 text-primary" aria-hidden="true" />
<CardTitle></CardTitle>
</div>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-3 lg:grid-cols-3">
<section className="rounded-md border p-3">
<div className="flex items-center gap-2">
<Globe2 className="size-4 text-primary" aria-hidden="true" />
<h3 className="text-sm font-semibold"></h3>
</div>
<p className="mt-2 text-sm text-muted-foreground">
{registrationEnabled
? "访客可以从注册页创建账号,并选择申请加入开放注册的机构。"
: "普通注册入口关闭;用户需要通过邀请链接或管理员创建账号。"}
</p>
</section>
<section className="rounded-md border p-3">
<div className="flex items-center gap-2">
<UserPlus className="size-4 text-primary" aria-hidden="true" />
<h3 className="text-sm font-semibold"></h3>
</div>
<p className="mt-2 text-sm text-muted-foreground">
`/register?invite=...`使
</p>
</section>
<section className="rounded-md border p-3">
<div className="flex items-center gap-2">
<Building2 className="size-4 text-primary" aria-hidden="true" />
<h3 className="text-sm font-semibold"></h3>
</div>
<p className="mt-2 text-sm text-muted-foreground">
</p>
</section>
</div>
<div className="flex flex-col gap-3 rounded-md border bg-secondary/25 p-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 text-sm">
<p className="font-medium"></p>
<p className="mt-1 text-muted-foreground">使</p>
</div>
<LinkButton href={organizationSettingsHref} variant="outline">
<UserPlus className="size-4" aria-hidden="true" />
</LinkButton>
</div>
</CardContent>
</div>
<div hidden={activeTab !== "oidc"} role="tabpanel">
<CardHeader>
<div className="flex items-center gap-2">
<ShieldCheck className="size-5 text-primary" aria-hidden="true" />
<CardTitle>OIDC </CardTitle>
</div>
<CardDescription> OIDC issuerclientscope claim</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-3 md:grid-cols-2">
<Input
defaultValue={settings.oidcIssuerUrl}
label="Issuer URL"
name="oidcIssuerUrl"
placeholder="https://idp.example.com"
/>
<Input defaultValue={settings.oidcClientId} label="Client ID" name="oidcClientId" />
</div>
<Input
label="Client Secret"
name="oidcClientSecret"
placeholder={settings.oidcHasClientSecret ? "已配置,留空则不修改" : "未配置"}
type="password"
/>
<div className="grid gap-3 md:grid-cols-2">
<Input defaultValue={settings.oidcScopes} label="Scopes" name="oidcScopes" placeholder="openid profile email" />
<Input
defaultValue={settings.oidcRedirectUri}
label="Redirect URI"
name="oidcRedirectUri"
placeholder="/api/auth/oidc/callback"
/>
</div>
<Input defaultValue={settings.oidcAvatarClaim} label="Avatar Claim" name="oidcAvatarClaim" placeholder="picture" />
</CardContent>
</div>
</Card>
{message ? (
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
{message}
</p>
) : null}
<div className="flex items-center justify-between gap-3">
<Badge variant={oidcEnabled ? "success" : "secondary"}>
{oidcEnabled ? "OIDC 已启用" : "OIDC 未启用"}
</Badge>
<Button disabled={isPending} type="submit">
<Save className="size-4" aria-hidden="true" />
</Button>
</div>
</form>
);
}