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 },
) =>