Ctrl K
ComunidadGuías…

src/features/applications/management/application-federated-credentials.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { DestructiveConfirmation } from '@/components/destructive-confirmation'
import { MutationError } from '@/features/management/dialogs'
import {
  Badge,
  Button,
  createManagementFederatedCredentialRequestSchema,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
  Field,
  type FormEvent,
  type ManagementFederatedCredentialResponse,
  MoreHorizontal,
  Plus,
  Save,
  SelectInput,
  Sheet,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  Table,
  TableBody,
  TableCell,
  TableEmptyRow,
  TableHead,
  TableHeader,
  TableRow,
  TextArea,
  TextInput,
  tt,
  type updateManagementFederatedCredentialRequestSchema,
  useMutation,
  useQuery,
  useQueryClient,
  useState,
} from '@/features/management/shared'
import { parseForm } from '@/features/management/utils'
import { consoleQueryKeys } from '@/lib/api/console-query-keys'
import {
  createFederatedCredential,
  deleteFederatedCredential,
  listApiResources,
  listFederatedCredentials,
  updateFederatedCredential,
} from '@/lib/api/management'

type JwkRecord = Record<string, unknown>

type KeyMaterial = { jwksUrl?: string; publicKeys?: JwkRecord[] }

export function parseKeyMaterial(jwksUrl: string, publicKeysText: string): KeyMaterial {
  const trimmedUrl = jwksUrl.trim()
  const trimmedKeys = publicKeysText.trim()
  if (trimmedUrl && trimmedKeys) {
    throw new Error('Provide either a JWKS URL or inline public keys, not both.')
  }
  if (trimmedUrl) return { jwksUrl: trimmedUrl }
  if (!trimmedKeys) {
    throw new Error('A federated credential requires either a JWKS URL or one or more public keys.')
  }
  let parsed: unknown
  try {
    parsed = JSON.parse(trimmedKeys)
  } catch {
    throw new Error('Public keys must be a valid JWK or JWK Set in JSON format.')
  }
  if (parsed && typeof parsed === 'object' && 'keys' in parsed && Array.isArray((parsed as { keys: unknown }).keys)) {
    return { publicKeys: (parsed as { keys: JwkRecord[] }).keys }
  }
  if (Array.isArray(parsed)) return { publicKeys: parsed as JwkRecord[] }
  if (parsed && typeof parsed === 'object') return { publicKeys: [parsed as JwkRecord] }
  throw new Error('Public keys must be a valid JWK or JWK Set in JSON format.')
}

export function parseFederatedCredentialForm(form: FormData): {
  material: KeyMaterial
  base: { name: string; issuer: string; subject: string; audienceResourceId: string }
} {
  return {
    material: parseKeyMaterial(String(form.get('jwksUrl') ?? ''), String(form.get('publicKeys') ?? '')),
    base: {
      name: String(form.get('name') ?? ''),
      issuer: String(form.get('issuer') ?? ''),
      subject: String(form.get('subject') ?? ''),
      audienceResourceId: String(form.get('audienceResourceId') ?? ''),
    },
  }
}

export function ApplicationFederatedCredentialsPanel({ applicationId }: { applicationId: string }) {
  const queryClient = useQueryClient()
  const queryKey = consoleQueryKeys.federatedCredentials(applicationId)
  const credentialsQuery = useQuery({
    queryKey,
    queryFn: () => listFederatedCredentials(applicationId),
  })
  const resourcesQuery = useQuery({
    queryKey: consoleQueryKeys.apiResources,
    queryFn: () => listApiResources(),
  })
  const resources = resourcesQuery.data?.items ?? []
  const invalidate = () => queryClient.invalidateQueries({ queryKey })
  const [createOpen, setCreateOpen] = useState(false)
  const [deleteCredential, setDeleteCredential] = useState<ManagementFederatedCredentialResponse | null>(null)
  const createMutation = useMutation({
    mutationFn: (input: ReturnType<typeof parseForm<typeof createManagementFederatedCredentialRequestSchema>>) =>
      createFederatedCredential(applicationId, input),
    onSuccess: async () => {
      await invalidate()
      setCreateOpen(false)
    },
  })
  const updateMutation = useMutation({
    mutationFn: ({
      id,
      input,
    }: {
      id: string
      input: ReturnType<typeof parseForm<typeof updateManagementFederatedCredentialRequestSchema>>
    }) => updateFederatedCredential(applicationId, id, input),
    onSuccess: async () => {
      await invalidate()
      setDeleteCredential(null)
    },
  })
  const deleteMutation = useMutation({
    mutationFn: (id: string) => deleteFederatedCredential(applicationId, id),
    onSuccess: async () => {
      await invalidate()
      setDeleteCredential(null)
    },
  })
  const credentials = credentialsQuery.data?.items ?? []

  return (
    <section className="detailSection mt-7">
      <header>
        <div>
          <h2>{tt('Federated credentials')}</h2>
          <p>{tt('Trusted workload issuer and subject pairs that can exchange tokens without a client secret.')}</p>
        </div>
        <Button onClick={() => setCreateOpen(true)} variant="outline">
          <Plus />
          {tt('Add credential')}
        </Button>
      </header>
      <div className="overflow-hidden rounded-xl border mt-4">
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>{tt('Credential')}</TableHead>
              <TableHead>{tt('Audience')}</TableHead>
              <TableHead>{tt('Key material')}</TableHead>
              <TableHead>{tt('Status')}</TableHead>
              <TableHead />
            </TableRow>
          </TableHeader>
          <TableBody>
            {credentials.length ? (
              credentials.map((credential) => (
                <FederatedCredentialRow
                  credential={credential}
                  key={credential.id}
                  onDelete={() => setDeleteCredential(credential)}
                  onToggle={() => updateMutation.mutate({ id: credential.id, input: { enabled: !credential.enabled } })}
                  pending={updateMutation.isPending || deleteMutation.isPending}
                  resources={resources}
                />
              ))
            ) : (
              <TableEmptyRow
                colSpan={5}
                description={tt('Add a trusted workload identity only when this client needs token exchange.')}
                title={tt('No federated credentials')}
              />
            )}
          </TableBody>
        </Table>
      </div>
      <MutationError error={updateMutation.error ?? deleteMutation.error} />
      <Sheet onOpenChange={setCreateOpen} open={createOpen}>
        <SheetContent className="h-full overflow-hidden sm:max-w-xl">
          <SheetHeader className="shrink-0">
            <SheetTitle>{tt('Add federated credential')}</SheetTitle>
            <SheetDescription>
              {tt('Trust an external workload identity for client authentication and token exchange.')}
            </SheetDescription>
          </SheetHeader>
          <div className="min-h-0 flex-1 overflow-y-auto px-4 py-5">
            <FederatedCredentialForm
              error={createMutation.error}
              pending={createMutation.isPending}
              resources={resources}
              onSubmit={(material, base) =>
                createMutation.mutate(
                  parseForm(createManagementFederatedCredentialRequestSchema, { ...base, ...material }),
                )
              }
            />
          </div>
          <SheetFooter className="shrink-0">
            <Button onClick={() => setCreateOpen(false)} variant="outline">
              {tt('Cancel')}
            </Button>
          </SheetFooter>
        </SheetContent>
      </Sheet>
      <DestructiveConfirmation
        confirmLabel={deleteMutation.isPending ? tt('Deleting…') : tt('Delete credential')}
        description={tt('The matching workload can no longer exchange its identity token through this application.')}
        error={<MutationError error={deleteMutation.error} />}
        onClose={() => setDeleteCredential(null)}
        onConfirm={() => {
          if (deleteCredential) deleteMutation.mutate(deleteCredential.id)
        }}
        open={deleteCredential !== null}
        pending={deleteMutation.isPending}
        title={tt('Delete federated credential?')}
      />
    </section>
  )
}

function FederatedCredentialForm({
  error,
  pending,
  resources,
  onSubmit,
}: {
  error: unknown
  pending: boolean
  resources: Array<{ id: string; name: string; identifier: string }>
  onSubmit: (
    material: KeyMaterial,
    base: { name: string; issuer: string; subject: string; audienceResourceId: string },
  ) => void
}) {
  const [validationError, setValidationError] = useState<unknown>(null)

  return (
    <form
      className="formStack"
      onSubmit={(event: FormEvent<HTMLFormElement>) => {
        event.preventDefault()
        setValidationError(null)
        try {
          const { material, base } = parseFederatedCredentialForm(new FormData(event.currentTarget))
          onSubmit(material, base)
        } catch (caught) {
          setValidationError(caught)
        }
      }}
    >
      <Field label={tt('Name')}>
        <TextInput name="name" required />
      </Field>
      <Field label={tt('Issuer')} help={tt('Logical issuer identity, an opaque string. Not dereferenced as a URL.')}>
        <TextInput name="issuer" required />
      </Field>
      <Field label={tt('Subject')} help={tt('Exact subject, or a prefix ending in * to match a range.')}>
        <TextInput name="subject" required />
      </Field>
      <Field label={tt('Audience')} help={tt('The API resource this credential may request tokens for.')}>
        <SelectInput name="audienceResourceId" required defaultValue="">
          <option disabled value="">
            {tt('Select an API resource')}
          </option>
          {resources.map((resource) => (
            <option key={resource.id} value={resource.id}>
              {resource.name} ({resource.identifier})
            </option>
          ))}
        </SelectInput>
      </Field>
      <Field label={tt('JWKS URL')} help={tt('Public keys are fetched from this URL.')}>
        <TextInput name="jwksUrl" placeholder="https://issuer.example.com/.well-known/jwks.json" type="url" />
      </Field>
      <Field
        label={tt('Public keys')}
        help={tt('Or paste a JWK or JWK Set as JSON. Provide a JWKS URL or public keys, not both.')}
      >
        <TextArea name="publicKeys" rows={6} placeholder='{ "kty": "RSA", "n": "...", "e": "AQAB" }' />
      </Field>
      <Button disabled={pending} type="submit">
        <Save data-icon="inline-start" />
        {tt('Add credential')}
      </Button>
      <MutationError error={validationError ?? error} />
    </form>
  )
}

function FederatedCredentialRow({
  credential,
  resources,
  pending,
  onToggle,
  onDelete,
}: {
  credential: ManagementFederatedCredentialResponse
  resources: Array<{ id: string; name: string }>
  pending: boolean
  onToggle: () => void
  onDelete: () => void
}) {
  const audience = resources.find((resource) => resource.id === credential.audienceResourceId)
  const keyMaterial = credential.jwksUrl
    ? credential.jwksUrl
    : `${credential.publicKeys?.length ?? 0} ${tt('public key(s)')}`
  return (
    <TableRow>
      <TableCell>
        <span className="font-medium">{credential.name}</span>
        <span className="block font-mono text-xs text-muted-foreground">
          {credential.issuer} · {credential.subject}
        </span>
      </TableCell>
      <TableCell>{audience?.name ?? credential.audienceResourceId}</TableCell>
      <TableCell className="max-w-56 truncate font-mono text-xs">{keyMaterial}</TableCell>
      <TableCell>
        <Badge variant={credential.enabled ? 'secondary' : 'outline'}>
          {credential.enabled ? tt('Enabled') : tt('Disabled')}
        </Badge>
      </TableCell>
      <TableCell className="text-right">
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button aria-label={tt('Credential actions')} disabled={pending} size="icon" variant="ghost">
              <MoreHorizontal />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuItem onSelect={onToggle}>{credential.enabled ? tt('Disable') : tt('Enable')}</DropdownMenuItem>
            <DropdownMenuItem onSelect={onDelete} variant="destructive">
              {tt('Delete')}
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </TableCell>
    </TableRow>
  )
}