NORY-22: dynamically display identity traits
This commit is contained in:
parent
aef8e84048
commit
4736148219
3 changed files with 196 additions and 3 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
66
dashboard/src/components/forms/IdentityTraitForm.tsx
Normal file
66
dashboard/src/components/forms/IdentityTraitForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
80
dashboard/src/lib/forms/identity-form.ts
Normal file
80
dashboard/src/lib/forms/identity-form.ts
Normal 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;
|
||||
}
|
Loading…
Add table
Reference in a new issue