diff --git a/dashboard/src/app/user/[id]/page.tsx b/dashboard/src/app/user/[id]/page.tsx index 7b21fde..6453ec5 100644 --- a/dashboard/src/app/user/[id]/page.tsx +++ b/dashboard/src/app/user/[id]/page.tsx @@ -1,15 +1,29 @@ import React from 'react'; import { getIdentityApi } from '@/ory/sdk/server'; import { ErrorDisplay } from '@/components/error'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { IdentityTraitForm } from '@/components/forms/IdentityTraitForm'; +import { KratosSchema } from '@/lib/forms/identity-form'; export default async function UserDetailsPage({ params }: { params: Promise<{ id: string }> }) { const identityId = (await params).id; - console.log('Loading identity', identityId); - const identityApi = await getIdentityApi(); - const identity = await identityApi.getIdentity({ id: identityId }) + const identity = await identityApi.getIdentity({ + id: identityId, + includeCredential: [ + 'code', + 'code_recovery', + 'link_recovery', + 'lookup_secret', + 'oidc', + 'passkey', + 'password', + 'totp', + 'webauthn', + ], + }) .then((response) => response.data) .catch(() => { console.log('Identity not found'); @@ -27,6 +41,10 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id message="The identity you are trying to see exists but has no identifiable address"/>; } + const identitySchema = await identityApi + .getIdentitySchema({ id: identity.schema_id }) + .then((response) => response.data as KratosSchema); + const address = identity.verifiable_addresses[0]; return ( @@ -35,6 +53,35 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id

{address.value}

{identity.id}

+
+ + + Traits + All identity properties specified in the identity schema + + + + + + + + Addresses + + + + + + Credentials + All authentication mechanisms registered with this identity + + + + + Sessions + See and manage all sessions of this identity + + +
); } diff --git a/dashboard/src/components/forms/IdentityTraitForm.tsx b/dashboard/src/components/forms/IdentityTraitForm.tsx new file mode 100644 index 0000000..0c91720 --- /dev/null +++ b/dashboard/src/components/forms/IdentityTraitForm.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { generateZodSchema, KratosSchema, KratosSchemaProperties } from '@/lib/forms/identity-form'; +import { useForm, UseFormReturn } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { toast } from 'sonner'; +import { Identity } from '@ory/client'; + +interface IdentityTraitFormProps { + schema: KratosSchema; + identity: Identity; +} + +function renderUiNodes(form: UseFormReturn, properties: KratosSchemaProperties, prefix?: string): any { + + let keyPrefix = prefix ? prefix + '.' : ''; + + return Object.entries(properties).map(([key, value]) => { + if (value.type === 'object') { + return renderUiNodes(form, value.properties!, key); + } else { + return ( + ( + + {value.title} + + + + + )} + /> + ); + } + }, + ); +} + +export function IdentityTraitForm({ schema, identity }: IdentityTraitFormProps) { + + const zodIdentitySchema = generateZodSchema(schema); + const form = useForm>({ + defaultValues: identity.traits, + resolver: zodResolver(zodIdentitySchema), + }); + + function onSubmit(values: z.infer) { + toast.message(JSON.stringify(values, null, 4)); + } + + return ( +
+ + { + renderUiNodes(form, schema.properties.traits.properties) + } +
+ + ); +} diff --git a/dashboard/src/lib/forms/identity-form.ts b/dashboard/src/lib/forms/identity-form.ts new file mode 100644 index 0000000..bed74aa --- /dev/null +++ b/dashboard/src/lib/forms/identity-form.ts @@ -0,0 +1,80 @@ +import { z } from 'zod'; + +// interface for a list of properties +export interface KratosSchemaProperties { + [key: string]: { + type: string; + format?: string; + title: string; + minLength?: number; + maxLength?: number; + minimum?: number; + maximum?: number; + required?: boolean; + description?: string; + properties?: KratosSchemaProperties + }; +} + +// interface for the kratos identity schema +export interface KratosSchema { + $id: string; + $schema: string; + title: string; + type: 'object'; + properties: { + traits: { + type: 'object'; + properties: KratosSchemaProperties; + required: string[]; + additionalProperties: boolean; + }; + }; +} + +export function generateZodSchema(properties: KratosSchemaProperties) { + + const zodSchema = z.object({}); + + for (const [key, value] of Object.entries(properties)) { + let zodType; + switch (value.type) { + case 'string': + zodType = z.string(); + if (value.format === 'email') { + zodType = z.string().email(); + } + if (value.minLength) { + zodType = zodType.min(value.minLength); + } + if (value.maxLength) { + zodType = zodType.max(value.maxLength); + } + break; + case 'number': + zodType = z.number(); + if (value.minimum) { + zodType = zodType.min(value.minimum); + } + if (value.maximum) { + zodType = zodType.max(value.maximum); + } + break; + case 'object': + const schemaCopy = structuredClone(schema); + schemaCopy.properties.traits.properties = value.properties!; + zodType = generateZodSchema(schemaCopy); + break; + default: + zodType = z.any(); + } + + if (!value.required) { + zodType = zodType.nullable(); + } + + zodSchema.extend({ [key]: zodType }); + } + + return zodSchema; +}