mirror of
https://github.com/rustfs/console.git
synced 2026-08-28 19:47:21 +08:00
405 lines
15 KiB
TypeScript
405 lines
15 KiB
TypeScript
"use client"
|
|
|
|
import { useTranslation } from "react-i18next"
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Switch } from "@/components/ui/switch"
|
|
import type {
|
|
OidcConfigSource,
|
|
OidcProviderFormErrors,
|
|
OidcProviderFormValues,
|
|
ValidateOidcConfigResponse,
|
|
} from "@/types/oidc"
|
|
|
|
interface OidcFormProps {
|
|
values: OidcProviderFormValues
|
|
errors: OidcProviderFormErrors
|
|
source?: OidcConfigSource
|
|
isCreateMode: boolean
|
|
isReadOnly: boolean
|
|
secretConfigured: boolean
|
|
restartRequired: boolean
|
|
readOnlyDescription?: string
|
|
isSaving: boolean
|
|
isValidating: boolean
|
|
validateResult: ValidateOidcConfigResponse | null
|
|
onChange: <K extends keyof OidcProviderFormValues>(field: K, value: OidcProviderFormValues[K]) => void
|
|
onSave: () => void
|
|
onValidate: () => void
|
|
onDelete: () => void
|
|
}
|
|
|
|
function sourceLabel(source: OidcConfigSource | undefined, t: (key: string) => string) {
|
|
if (!source) return null
|
|
return source === "env" ? t("Environment Managed") : t("Persisted Configuration")
|
|
}
|
|
|
|
export function OidcForm({
|
|
values,
|
|
errors,
|
|
source,
|
|
isCreateMode,
|
|
isReadOnly,
|
|
secretConfigured,
|
|
restartRequired,
|
|
readOnlyDescription,
|
|
isSaving,
|
|
isValidating,
|
|
validateResult,
|
|
onChange,
|
|
onSave,
|
|
onValidate,
|
|
onDelete,
|
|
}: OidcFormProps) {
|
|
const { t } = useTranslation()
|
|
const showRedirectUri = !values.redirect_uri_dynamic
|
|
|
|
return (
|
|
<div className="space-y-4 rounded-md border p-4 md:p-6">
|
|
<div className="flex flex-col gap-3 border-b pb-4 md:flex-row md:items-start md:justify-between">
|
|
<div className="space-y-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<h2 className="text-lg font-semibold">
|
|
{isCreateMode
|
|
? t("Add Provider")
|
|
: values.display_name.trim() || values.provider_id || t("OIDC Provider")}
|
|
</h2>
|
|
{sourceLabel(source, t) ? <Badge variant="outline">{sourceLabel(source, t)}</Badge> : null}
|
|
{!isCreateMode ? (
|
|
<Badge variant={values.enabled ? "secondary" : "outline"}>
|
|
{values.enabled ? t("Enabled") : t("Disabled")}
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{isReadOnly
|
|
? (readOnlyDescription ?? t("Environment-managed providers are read-only"))
|
|
: t("Changes will take effect after RustFS restarts")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{isCreateMode && !isReadOnly ? (
|
|
<Button type="button" variant="outline" onClick={onValidate} disabled={isValidating || isSaving}>
|
|
{isValidating ? t("Validating...") : t("Validate")}
|
|
</Button>
|
|
) : null}
|
|
{!isReadOnly ? (
|
|
<Button type="button" onClick={onSave} disabled={isSaving || isValidating}>
|
|
{isSaving ? t("Saving...") : t("Save")}
|
|
</Button>
|
|
) : null}
|
|
{!isCreateMode && !isReadOnly ? (
|
|
<Button type="button" variant="outline" onClick={onDelete} disabled={isSaving || isValidating}>
|
|
{t("Delete")}
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
{restartRequired ? (
|
|
<Alert>
|
|
<AlertTitle>{t("Restart Required")}</AlertTitle>
|
|
<AlertDescription>{t("Changes will take effect after RustFS restarts")}</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
|
|
{validateResult ? (
|
|
<Alert variant={validateResult.valid ? "default" : "destructive"}>
|
|
<AlertTitle>
|
|
{validateResult.valid
|
|
? t("OIDC configuration validated successfully")
|
|
: t("Failed to validate OIDC configuration")}
|
|
</AlertTitle>
|
|
<AlertDescription className="space-y-1">
|
|
<p>{validateResult.message}</p>
|
|
{validateResult.issuer ? (
|
|
<p>
|
|
<span className="font-medium">{t("Issuer")}:</span> {validateResult.issuer}
|
|
</p>
|
|
) : null}
|
|
{validateResult.authorization_endpoint ? (
|
|
<p>
|
|
<span className="font-medium">{t("Authorization Endpoint")}:</span>{" "}
|
|
{validateResult.authorization_endpoint}
|
|
</p>
|
|
) : null}
|
|
{validateResult.token_endpoint ? (
|
|
<p>
|
|
<span className="font-medium">{t("Token Endpoint")}:</span> {validateResult.token_endpoint}
|
|
</p>
|
|
) : null}
|
|
</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel htmlFor="provider_id">{t("Provider ID")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="provider_id"
|
|
value={values.provider_id}
|
|
onChange={(event) => onChange("provider_id", event.target.value)}
|
|
placeholder={t("Provider ID")}
|
|
disabled={!isCreateMode || isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
<FieldDescription>{t("Only letters, numbers, underscores, and hyphens are allowed.")}</FieldDescription>
|
|
<FieldError>{errors.provider_id}</FieldError>
|
|
</Field>
|
|
|
|
<Field orientation="responsive" className="items-start gap-3 rounded-md border p-3">
|
|
<FieldLabel htmlFor="enabled">{t("Enabled")}</FieldLabel>
|
|
<FieldContent className="gap-2">
|
|
<div className="flex items-center gap-3">
|
|
<Switch
|
|
id="enabled"
|
|
checked={values.enabled}
|
|
onCheckedChange={(checked) => onChange("enabled", checked)}
|
|
disabled={isReadOnly}
|
|
/>
|
|
<span className="text-xs text-muted-foreground">{values.enabled ? t("Enabled") : t("Disabled")}</span>
|
|
</div>
|
|
</FieldContent>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="display_name">{t("Display Name")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="display_name"
|
|
value={values.display_name}
|
|
onChange={(event) => onChange("display_name", event.target.value)}
|
|
placeholder={t("Display Name")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="config_url">{t("Configuration URL")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="config_url"
|
|
value={values.config_url}
|
|
onChange={(event) => onChange("config_url", event.target.value)}
|
|
placeholder="https://idp.example.com/.well-known/openid-configuration"
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
<FieldError>{errors.config_url}</FieldError>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="client_id">{t("Client ID")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="client_id"
|
|
value={values.client_id}
|
|
onChange={(event) => onChange("client_id", event.target.value)}
|
|
placeholder={t("Client ID")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
<FieldError>{errors.client_id}</FieldError>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="client_secret">{t("Client Secret")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="client_secret"
|
|
type="password"
|
|
value={values.client_secret}
|
|
onChange={(event) => onChange("client_secret", event.target.value)}
|
|
placeholder={isCreateMode ? t("Client Secret") : t("Leave empty to keep current secret")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
<FieldDescription>
|
|
{isCreateMode
|
|
? t("Client secret is required when creating a provider.")
|
|
: secretConfigured
|
|
? t("Secret Configured")
|
|
: t("No Secret Configured")}
|
|
</FieldDescription>
|
|
{!isCreateMode ? <FieldDescription>{t("Leave empty to keep current secret")}</FieldDescription> : null}
|
|
<FieldError>{errors.client_secret}</FieldError>
|
|
</Field>
|
|
|
|
<Field className="md:col-span-2">
|
|
<FieldLabel htmlFor="scopes">{t("Scopes")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="scopes"
|
|
value={values.scopes}
|
|
onChange={(event) => onChange("scopes", event.target.value)}
|
|
placeholder="openid,profile,email"
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
<FieldDescription>{t("Use comma-separated scopes. The openid scope is required.")}</FieldDescription>
|
|
<FieldError>{errors.scopes}</FieldError>
|
|
</Field>
|
|
|
|
<Field className="md:col-span-2">
|
|
<FieldLabel htmlFor="other_audiences">{t("Other Audiences")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="other_audiences"
|
|
value={values.other_audiences}
|
|
onChange={(event) => onChange("other_audiences", event.target.value)}
|
|
placeholder=""
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
<FieldDescription>{t("Comma-separated audience IDs.")}</FieldDescription>
|
|
<FieldError>{errors.other_audiences}</FieldError>
|
|
</Field>
|
|
|
|
<Field orientation="responsive" className="items-start gap-3 rounded-md border p-3">
|
|
<FieldLabel htmlFor="redirect_uri_dynamic">{t("Use Dynamic Redirect URI")}</FieldLabel>
|
|
<FieldContent className="gap-2">
|
|
<div className="flex items-center gap-3">
|
|
<Switch
|
|
id="redirect_uri_dynamic"
|
|
checked={values.redirect_uri_dynamic}
|
|
onCheckedChange={(checked) => onChange("redirect_uri_dynamic", checked)}
|
|
disabled={isReadOnly}
|
|
/>
|
|
<span className="text-xs text-muted-foreground">
|
|
{values.redirect_uri_dynamic ? t("Enabled") : t("Disabled")}
|
|
</span>
|
|
</div>
|
|
<FieldDescription>
|
|
{values.redirect_uri_dynamic
|
|
? t("Redirect URI will be resolved dynamically at runtime.")
|
|
: t("Provide an absolute callback URL when dynamic redirect is disabled.")}
|
|
</FieldDescription>
|
|
</FieldContent>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="redirect_uri">{t("Redirect URI")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="redirect_uri"
|
|
value={values.redirect_uri}
|
|
onChange={(event) => onChange("redirect_uri", event.target.value)}
|
|
placeholder="https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default"
|
|
disabled={isReadOnly || !showRedirectUri}
|
|
/>
|
|
</FieldContent>
|
|
<FieldDescription>
|
|
{showRedirectUri
|
|
? t("Must be an absolute callback URL.")
|
|
: t("Dynamic redirect URI is enabled, so this field is optional.")}
|
|
</FieldDescription>
|
|
<FieldError>{errors.redirect_uri}</FieldError>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="claim_name">{t("Claim Name")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="claim_name"
|
|
value={values.claim_name}
|
|
onChange={(event) => onChange("claim_name", event.target.value)}
|
|
placeholder={t("Claim Name")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="claim_prefix">{t("Claim Prefix")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="claim_prefix"
|
|
value={values.claim_prefix}
|
|
onChange={(event) => onChange("claim_prefix", event.target.value)}
|
|
placeholder={t("Claim Prefix")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="role_policy">{t("Role Policy")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="role_policy"
|
|
value={values.role_policy}
|
|
onChange={(event) => onChange("role_policy", event.target.value)}
|
|
placeholder={t("Role Policy")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
<FieldDescription>
|
|
{t("Optional. Leave empty to let the backend apply its default role mapping.")}
|
|
</FieldDescription>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="groups_claim">{t("Groups Claim")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="groups_claim"
|
|
value={values.groups_claim}
|
|
onChange={(event) => onChange("groups_claim", event.target.value)}
|
|
placeholder={t("Groups Claim")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="roles_claim">{t("Roles Claim")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="roles_claim"
|
|
value={values.roles_claim}
|
|
onChange={(event) => onChange("roles_claim", event.target.value)}
|
|
placeholder="roles"
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
<FieldDescription>
|
|
{t("Optional. Separate claim for role values (e.g. roles). Leave empty to use groups claim only.")}
|
|
</FieldDescription>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="email_claim">{t("Email Claim")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="email_claim"
|
|
value={values.email_claim}
|
|
onChange={(event) => onChange("email_claim", event.target.value)}
|
|
placeholder={t("Email Claim")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="username_claim">{t("Username Claim")}</FieldLabel>
|
|
<FieldContent>
|
|
<Input
|
|
id="username_claim"
|
|
value={values.username_claim}
|
|
onChange={(event) => onChange("username_claim", event.target.value)}
|
|
placeholder={t("Username Claim")}
|
|
disabled={isReadOnly}
|
|
/>
|
|
</FieldContent>
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|