From 436e5c8765f9ee6c61fa5e1626c5307be29d0512 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 8 Dec 2024 17:26:00 +0100 Subject: [PATCH 01/11] NORY-22: fix toasts not showing --- dashboard/src/app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx index 94cc7a8..8fda46d 100644 --- a/dashboard/src/app/layout.tsx +++ b/dashboard/src/app/layout.tsx @@ -79,8 +79,8 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac {children} + - From 7299a8e206959b1243359a8169e9189fa1ae64da Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 8 Dec 2024 17:26:11 +0100 Subject: [PATCH 02/11] NORY-22: create error component --- dashboard/src/components/error.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 dashboard/src/components/error.tsx diff --git a/dashboard/src/components/error.tsx b/dashboard/src/components/error.tsx new file mode 100644 index 0000000..e264180 --- /dev/null +++ b/dashboard/src/components/error.tsx @@ -0,0 +1,13 @@ +interface ErrorDisplayProps { + title: string; + message: string; +} + +export async function ErrorDisplay({ title, message }: ErrorDisplayProps) { + return ( +
+

{title}

+

{message}

+
+ ); +} \ No newline at end of file From aef8e840481ad80c0fe26b5239354310bea7f6e8 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Mon, 9 Dec 2024 01:01:43 +0100 Subject: [PATCH 03/11] NORY-22: add quick actions to identity page --- dashboard/src/app/user/[id]/page.tsx | 40 +++ dashboard/src/app/user/[id]/sessions/page.tsx | 32 +++ dashboard/src/app/user/action.ts | 66 +++++ dashboard/src/app/user/data-table.tsx | 236 ++++++++++++++++-- dashboard/src/app/user/page.tsx | 2 +- dashboard/src/components/ui/spinner.tsx | 2 +- 6 files changed, 360 insertions(+), 18 deletions(-) create mode 100644 dashboard/src/app/user/[id]/page.tsx create mode 100644 dashboard/src/app/user/[id]/sessions/page.tsx create mode 100644 dashboard/src/app/user/action.ts diff --git a/dashboard/src/app/user/[id]/page.tsx b/dashboard/src/app/user/[id]/page.tsx new file mode 100644 index 0000000..7b21fde --- /dev/null +++ b/dashboard/src/app/user/[id]/page.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { getIdentityApi } from '@/ory/sdk/server'; +import { ErrorDisplay } from '@/components/error'; + +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 }) + .then((response) => response.data) + .catch(() => { + console.log('Identity not found'); + }); + + if (!identity) { + return ; + } + + if (!identity.verifiable_addresses || !identity.verifiable_addresses[0]) { + return ; + } + + const address = identity.verifiable_addresses[0]; + + return ( +
+
+

{address.value}

+

{identity.id}

+
+
+ ); +} diff --git a/dashboard/src/app/user/[id]/sessions/page.tsx b/dashboard/src/app/user/[id]/sessions/page.tsx new file mode 100644 index 0000000..2f1ad70 --- /dev/null +++ b/dashboard/src/app/user/[id]/sessions/page.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { getIdentityApi } from '@/ory/sdk/server'; +import { ErrorDisplay } from '@/components/error'; + +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 sessions = await identityApi.listIdentitySessions({ id: identityId }) + .then((response) => response.data) + .catch(() => { + console.log('Identity not found'); + }); + + if (!sessions) { + return ; + } + + return ( +
+
+

Sessions

+

These are all active sessions of the identity

+
+
+ ); +} diff --git a/dashboard/src/app/user/action.ts b/dashboard/src/app/user/action.ts new file mode 100644 index 0000000..4c393cc --- /dev/null +++ b/dashboard/src/app/user/action.ts @@ -0,0 +1,66 @@ +'use server'; + +import { getIdentityApi } from '@/ory/sdk/server'; +import { revalidatePath } from 'next/cache'; + +interface IdentityIdProps { + id: string; +} + +export async function deleteIdentitySessions({ id }: IdentityIdProps) { + + const identityApi = await getIdentityApi(); + const { data } = await identityApi.deleteIdentitySessions({ id }); + + console.log('Deleted identity\'s sessions', data); + + return data; +} + +export async function blockIdentity({ id }: IdentityIdProps) { + + const identityApi = await getIdentityApi(); + const { data } = await identityApi.patchIdentity({ + id, + jsonPatch: [ + { + op: 'replace', + path: '/state', + value: 'inactive', + }, + ], + }); + + console.log('Blocked identity', data); + + revalidatePath('/user'); +} + +export async function unblockIdentity({ id }: IdentityIdProps) { + + const identityApi = await getIdentityApi(); + const { data } = await identityApi.patchIdentity({ + id, + jsonPatch: [ + { + op: 'replace', + path: '/state', + value: 'active', + }, + ], + }); + + console.log('Unblocked identity', data); + + revalidatePath('/user'); +} + +export async function deleteIdentity({ id }: IdentityIdProps) { + + const identityApi = await getIdentityApi(); + const { data } = await identityApi.deleteIdentity({ id }); + + console.log('Deleted identity', data); + + revalidatePath('/user'); +} diff --git a/dashboard/src/app/user/data-table.tsx b/dashboard/src/app/user/data-table.tsx index 49cdefb..8501043 100644 --- a/dashboard/src/app/user/data-table.tsx +++ b/dashboard/src/app/user/data-table.tsx @@ -3,11 +3,33 @@ import { ColumnDef } from '@tanstack/react-table'; import { Identity } from '@ory/client'; import { DataTable } from '@/components/ui/data-table'; -import { CircleCheck, CircleX } from 'lucide-react'; +import { CircleCheck, CircleX, Copy, MoreHorizontal, Trash, UserCheck, UserMinus, UserX } from 'lucide-react'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; -import { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { FetchIdentityPageProps } from '@/app/user/page'; import { Spinner } from '@/components/ui/spinner'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; +import { toast } from 'sonner'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { blockIdentity, deleteIdentity, deleteIdentitySessions, unblockIdentity } from '@/app/user/action'; interface IdentityDataTableProps { data: Identity[]; @@ -85,31 +107,98 @@ export function IdentityDataTable({ data, pageSize, pageToken, query, fetchIdent } }, }, + { + id: 'actions', + cell: ({ row }) => { + const identity = row.original; + return ( + + + + + + Actions + { + navigator.clipboard.writeText(identity.id); + toast.success('Copied to clipboard'); + }} + > + + Copy identity ID + + + + View identity + + + View sessions + + + { + setCurrentIdentity(identity); + setIdentitySessionVisible(true); + }} + className="flex items-center space-x-2 text-red-500"> + + Delete sessions + + { + identity.state === 'active' && + { + setCurrentIdentity(identity); + setBlockIdentityVisible(true); + }} + className="flex items-center space-x-2 text-red-500"> + + Block identity + + } + { + identity.state === 'inactive' && + { + setCurrentIdentity(identity); + setUnblockIdentityVisible(true); + }} + className="flex items-center space-x-2 text-red-500"> + + Unblock identity + + } + { + setCurrentIdentity(identity); + setDeleteIdentityVisible(true); + }} + className="flex items-center space-x-2 text-red-500"> + + Delete identity + + + + ); + }, + }, ]; const [items, setItems] = useState(data); const [nextToken, setNextToken] = useState(pageToken); + // react on changes from ssr (query params) useEffect(() => { setItems(data); setNextToken(pageToken); }, [data, pageSize, pageToken, query]); - const fetchMore = async () => { - if (!nextToken) return; - - const response = await fetchIdentityPage({ - pageSize: pageSize, - pageToken: nextToken, - query: query, - }); - - setItems([...items, ...response.data]); - setNextToken(response.tokens.get('next') ?? undefined); - }; - + // infinite scroll handling const infiniteScrollSensor = useRef(null); - useEffect(() => { const observer = new IntersectionObserver( (entries) => { @@ -131,9 +220,124 @@ export function IdentityDataTable({ data, pageSize, pageToken, query, fetchIdent }; }, [items]); + const fetchMore = async () => { + if (!nextToken) return; + + const response = await fetchIdentityPage({ + pageSize: pageSize, + pageToken: nextToken, + query: query, + }); + + setItems([...items, ...response.data]); + setNextToken(response.tokens.get('next') ?? undefined); + }; + + // quick actions + const [currentIdentity, setCurrentIdentity] = useState(undefined); + const [identitySessionVisible, setIdentitySessionVisible] = useState(false); + const [blockIdentityVisible, setBlockIdentityVisible] = useState(false); + const [unblockIdentityVisible, setUnblockIdentityVisible] = useState(false); + const [deleteIdentityVisible, setDeleteIdentityVisible] = useState(false); + return ( <> + { + currentIdentity && ( + <> + {/* delete sessions dialog */} + setIdentitySessionVisible(open)}> + + + Delete sessions + + Are you sure you want to delete this identity's sessions? + {JSON.stringify(currentIdentity.traits, null, 4)} + + + + deleteIdentitySessions({ id: currentIdentity.id })}> + Invalidate sessions + + + Cancel + + + + + + {/* block identity dialog */} + setBlockIdentityVisible(open)}> + + + Block identity + + Are you sure you want to block this identity? + {JSON.stringify(currentIdentity.traits, null, 4)} + + + + blockIdentity({ id: currentIdentity.id })}> + Block identity + + + Cancel + + + + + + {/* unblock identity dialog */} + setUnblockIdentityVisible(open)}> + + + Unblock identity + + Are you sure you want to unblock this identity? + {JSON.stringify(currentIdentity.traits, null, 4)} + + + + unblockIdentity({ id: currentIdentity.id })}> + Unblock identity + + + Cancel + + + + + + {/* delete identity dialog */} + setDeleteIdentityVisible(open)}> + + + Delete identity + + Are you sure you want to delete this identity? + This action can not be undone! + {JSON.stringify(currentIdentity.traits, null, 4)} + + + + deleteIdentity({ id: currentIdentity.id })}> + Delete identity + + + Cancel + + + + + + ) + } { nextToken && (
diff --git a/dashboard/src/app/user/page.tsx b/dashboard/src/app/user/page.tsx index b6bfa01..6fe3108 100644 --- a/dashboard/src/app/user/page.tsx +++ b/dashboard/src/app/user/page.tsx @@ -70,7 +70,7 @@ export default async function UserPage(

- + }, + { className, ref }: { className?: string, ref?: RefObject }, ) => Date: Fri, 13 Dec 2024 19:36:12 +0100 Subject: [PATCH 04/11] NORY-22: dynamically display identity traits --- dashboard/src/app/user/[id]/page.tsx | 53 +++++++++++- .../components/forms/IdentityTraitForm.tsx | 66 +++++++++++++++ dashboard/src/lib/forms/identity-form.ts | 80 +++++++++++++++++++ 3 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 dashboard/src/components/forms/IdentityTraitForm.tsx create mode 100644 dashboard/src/lib/forms/identity-form.ts 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; +} From 206a40baedcef06dc97f654e67f8b10435596e5b Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 13 Dec 2024 20:20:11 +0100 Subject: [PATCH 05/11] NORY-22: add missing UI for boolean traits --- .../src/components/forms/IdentityTraitForm.tsx | 16 ++++++++++++++++ dashboard/src/lib/forms/identity-form.ts | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/dashboard/src/components/forms/IdentityTraitForm.tsx b/dashboard/src/components/forms/IdentityTraitForm.tsx index 0c91720..8f863d8 100644 --- a/dashboard/src/components/forms/IdentityTraitForm.tsx +++ b/dashboard/src/components/forms/IdentityTraitForm.tsx @@ -8,6 +8,7 @@ import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { toast } from 'sonner'; import { Identity } from '@ory/client'; +import { Checkbox } from '@/components/ui/checkbox'; interface IdentityTraitFormProps { schema: KratosSchema; @@ -21,6 +22,21 @@ function renderUiNodes(form: UseFormReturn, properties: KratosSchemaProperties, return Object.entries(properties).map(([key, value]) => { if (value.type === 'object') { return renderUiNodes(form, value.properties!, key); + } else if (value.type === 'boolean') { + return ( + ( + + + {value.title} + + )} + /> + ); } else { return ( Date: Sat, 14 Dec 2024 02:18:05 +0100 Subject: [PATCH 06/11] NORY-22: list all linked credentials --- dashboard/src/app/user/[id]/page.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/dashboard/src/app/user/[id]/page.tsx b/dashboard/src/app/user/[id]/page.tsx index 6453ec5..8c7b31d 100644 --- a/dashboard/src/app/user/[id]/page.tsx +++ b/dashboard/src/app/user/[id]/page.tsx @@ -4,6 +4,7 @@ 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'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; export default async function UserDetailsPage({ params }: { params: Promise<{ id: string }> }) { @@ -74,6 +75,28 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id Credentials All authentication mechanisms registered with this identity + + + + + Type + Value + + + + { + Object.entries(identity.credentials).map(([key, value]) => { + return ( + + {key} + {value.identifiers[0]} + + ); + }) + } + +
+
From 9faba3dc29b95cffc2c87c0b901c773db713afd9 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sat, 14 Dec 2024 03:41:14 +0100 Subject: [PATCH 07/11] NORY-22: remove identity/session page --- dashboard/src/app/user/[id]/sessions/page.tsx | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 dashboard/src/app/user/[id]/sessions/page.tsx diff --git a/dashboard/src/app/user/[id]/sessions/page.tsx b/dashboard/src/app/user/[id]/sessions/page.tsx deleted file mode 100644 index 2f1ad70..0000000 --- a/dashboard/src/app/user/[id]/sessions/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { getIdentityApi } from '@/ory/sdk/server'; -import { ErrorDisplay } from '@/components/error'; - -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 sessions = await identityApi.listIdentitySessions({ id: identityId }) - .then((response) => response.data) - .catch(() => { - console.log('Identity not found'); - }); - - if (!sessions) { - return ; - } - - return ( -
-
-

Sessions

-

These are all active sessions of the identity

-
-
- ); -} From 63affda5fa083127824e3c2afc8c092d5f04b0d8 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sat, 14 Dec 2024 03:41:43 +0100 Subject: [PATCH 08/11] NORY-22: fix typing problems --- dashboard/src/app/user/[id]/page.tsx | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/dashboard/src/app/user/[id]/page.tsx b/dashboard/src/app/user/[id]/page.tsx index 8c7b31d..c77dc4d 100644 --- a/dashboard/src/app/user/[id]/page.tsx +++ b/dashboard/src/app/user/[id]/page.tsx @@ -11,21 +11,11 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id const identityId = (await params).id; const identityApi = await getIdentityApi(); - const identity = await identityApi.getIdentity({ - id: identityId, - includeCredential: [ - 'code', - 'code_recovery', - 'link_recovery', - 'lookup_secret', - 'oidc', - 'passkey', - 'password', - 'totp', - 'webauthn', - ], - }) - .then((response) => response.data) + const identity = await identityApi.getIdentity({ id: identityId }) + .then((response) => { + console.log('identity', response.data); + return response.data; + }) .catch(() => { console.log('Identity not found'); }); @@ -85,11 +75,11 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id { - Object.entries(identity.credentials).map(([key, value]) => { + Object.entries(identity.credentials!).map(([key, value]) => { return ( {key} - {value.identifiers[0]} + {value.identifiers![0]} ); }) From 26a25f9d7deb3fcd37fda94d79915bbe73e844ab Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sat, 14 Dec 2024 13:17:00 +0100 Subject: [PATCH 09/11] NORY-22: list all identity sessions --- dashboard/src/app/user/[id]/page.tsx | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/dashboard/src/app/user/[id]/page.tsx b/dashboard/src/app/user/[id]/page.tsx index c77dc4d..3e2b908 100644 --- a/dashboard/src/app/user/[id]/page.tsx +++ b/dashboard/src/app/user/[id]/page.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { IdentityTraitForm } from '@/components/forms/IdentityTraitForm'; import { KratosSchema } from '@/lib/forms/identity-form'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { UAParser } from 'ua-parser-js'; export default async function UserDetailsPage({ params }: { params: Promise<{ id: string }> }) { @@ -20,6 +21,15 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id console.log('Identity not found'); }); + const sessions = await identityApi.listIdentitySessions({ id: identityId }) + .then((response) => { + console.log('sessions', response.data); + return response.data; + }) + .catch(() => { + console.log('No sessions found'); + }); + if (!identity) { return Sessions See and manage all sessions of this identity
+ + + + + OS + Browser + Active since + + + + { + sessions ? + sessions.map((session) => { + + const device = session.devices![0]; + const parser = new UAParser(device.user_agent); + const result = parser.getResult(); + + return ( + + + {result.os.name} + {result.os.version} + + + {result.browser.name} + {result.browser.version} + + + {new Date(session.authenticated_at!).toLocaleString()} + + + ); + }) + : + + } + +
+
From e1b1bd82e580d7a6870784e98c043eedb4bae776 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sat, 14 Dec 2024 18:43:56 +0100 Subject: [PATCH 10/11] NORY-22: update identity actions --- dashboard/src/app/user/data-table.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dashboard/src/app/user/data-table.tsx b/dashboard/src/app/user/data-table.tsx index 8501043..5819331 100644 --- a/dashboard/src/app/user/data-table.tsx +++ b/dashboard/src/app/user/data-table.tsx @@ -3,7 +3,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { Identity } from '@ory/client'; import { DataTable } from '@/components/ui/data-table'; -import { CircleCheck, CircleX, Copy, MoreHorizontal, Trash, UserCheck, UserMinus, UserX } from 'lucide-react'; +import { CircleCheck, CircleX, Copy, MoreHorizontal, Trash, UserCheck, UserMinus, UserPen, UserX } from 'lucide-react'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; import React, { useEffect, useRef, useState } from 'react'; import { FetchIdentityPageProps } from '@/app/user/page'; @@ -133,10 +133,10 @@ export function IdentityDataTable({ data, pageSize, pageToken, query, fetchIdent - View identity - - - View sessions + + + View identity + Date: Sat, 14 Dec 2024 22:36:06 +0100 Subject: [PATCH 11/11] NORY-22: list all identity addresses --- dashboard/src/app/user/[id]/page.tsx | 113 ++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 3 deletions(-) diff --git a/dashboard/src/app/user/[id]/page.tsx b/dashboard/src/app/user/[id]/page.tsx index 3e2b908..44806a3 100644 --- a/dashboard/src/app/user/[id]/page.tsx +++ b/dashboard/src/app/user/[id]/page.tsx @@ -6,6 +6,71 @@ import { IdentityTraitForm } from '@/components/forms/IdentityTraitForm'; import { KratosSchema } from '@/lib/forms/identity-form'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { UAParser } from 'ua-parser-js'; +import { RecoveryIdentityAddress, VerifiableIdentityAddress } from '@ory/client'; +import { Badge } from '@/components/ui/badge'; +import { Check, X } from 'lucide-react'; + +interface MergedAddress { + recovery_id?: string; + verifiable_id?: string; + verified?: boolean; + verified_at?: string; + value: string; + via: string; +} + +function mergeAddresses( + recovery: RecoveryIdentityAddress[], + verifiable: VerifiableIdentityAddress[], +): MergedAddress[] { + + const merged = [...recovery, ...verifiable]; + return merged.reduce((acc: MergedAddress[], curr: any) => { + + const existingValue = + acc.find(item => item.value && curr.value && item.value === curr.value); + + if (!existingValue) { + + let newEntry: MergedAddress; + if (curr.status) { + + // status property exists only in verifiable addresses + // expecting verifiable address + newEntry = { + verifiable_id: curr.id, + verified: curr.verified, + verified_at: curr.verified_at, + value: curr.value, + via: curr.via, + } as MergedAddress; + + } else { + + // expecting recovery address + newEntry = { + recovery_id: curr.id, + value: curr.value, + via: curr.via, + } as MergedAddress; + } + + acc.push(newEntry); + + } else { + + const additionalValues = { + recovery_id: existingValue.recovery_id, + verifiable_id: curr.id, + verified: curr.verified, + verified_at: curr.verified_at, + }; + + Object.assign(existingValue, additionalValues); + } + return acc; + }, []); +} export default async function UserDetailsPage({ params }: { params: Promise<{ id: string }> }) { @@ -46,12 +111,15 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id .getIdentitySchema({ id: identity.schema_id }) .then((response) => response.data as KratosSchema); - const address = identity.verifiable_addresses[0]; + const addresses = mergeAddresses( + identity.recovery_addresses ?? [], + identity.verifiable_addresses ?? [], + ); return (
-

{address.value}

+

{addresses[0].value}

{identity.id}

@@ -67,8 +135,47 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id Addresses - + All linked addresses for verification and recovery + + + + + + Type + Value + + + + { + addresses.map((address) => { + return ( + + {address.value} + {address.via} + + {address.verifiable_id && + + Verifiable + { + address.verified ? + + : + + } + + } + {address.recovery_id && + Recovery + } + + + ); + }) + } + +
+