NORY-22: dynamically display identity traits

This commit is contained in:
Markus Thielker 2024-12-13 19:36:12 +01:00
parent aef8e84048
commit 4736148219
No known key found for this signature in database
3 changed files with 196 additions and 3 deletions

View file

@ -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
<p className="text-3xl font-bold leading-tight tracking-tight">{address.value}</p>
<p className="text-lg font-light">{identity.id}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Traits</CardTitle>
<CardDescription>All identity properties specified in the identity schema</CardDescription>
</CardHeader>
<CardContent>
<IdentityTraitForm schema={identitySchema} identity={identity}/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Addresses</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>Credentials</CardTitle>
<CardDescription>All authentication mechanisms registered with this identity</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>Sessions</CardTitle>
<CardDescription>See and manage all sessions of this identity</CardDescription>
</CardHeader>
</Card>
</div>
</div>
);
}

View file

@ -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 (
<FormField
control={form.control}
name={keyPrefix + key}
key={key}
render={({ field }) => (
<FormItem>
<FormLabel>{value.title}</FormLabel>
<FormControl>
<Input placeholder={value.title} readOnly {...field} />
</FormControl>
</FormItem>
)}
/>
);
}
},
);
}
export function IdentityTraitForm({ schema, identity }: IdentityTraitFormProps) {
const zodIdentitySchema = generateZodSchema(schema);
const form = useForm<z.infer<typeof zodIdentitySchema>>({
defaultValues: identity.traits,
resolver: zodResolver(zodIdentitySchema),
});
function onSubmit(values: z.infer<typeof zodIdentitySchema>) {
toast.message(JSON.stringify(values, null, 4));
}
return (
<Form {...form}>
<form onSubmit={onSubmit} className="grid grid-cols-1 gap-4">
{
renderUiNodes(form, schema.properties.traits.properties)
}
</form>
</Form>
);
}

View file

@ -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;
}