mirror of
https://codeberg.org/MarkusThielker/next-ory.git
synced 2025-04-19 00:51:18 +00:00
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 React from 'react';
|
||||||
import { getIdentityApi } from '@/ory/sdk/server';
|
import { getIdentityApi } from '@/ory/sdk/server';
|
||||||
import { ErrorDisplay } from '@/components/error';
|
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 }> }) {
|
export default async function UserDetailsPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
|
||||||
const identityId = (await params).id;
|
const identityId = (await params).id;
|
||||||
|
|
||||||
console.log('Loading identity', identityId);
|
|
||||||
|
|
||||||
const identityApi = await getIdentityApi();
|
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)
|
.then((response) => response.data)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log('Identity not found');
|
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"/>;
|
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];
|
const address = identity.verifiable_addresses[0];
|
||||||
|
|
||||||
return (
|
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-3xl font-bold leading-tight tracking-tight">{address.value}</p>
|
||||||
<p className="text-lg font-light">{identity.id}</p>
|
<p className="text-lg font-light">{identity.id}</p>
|
||||||
</div>
|
</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>
|
</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