From c9ebf95dd510206b4e2041e530bd935f87df289f Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 27 Dec 2024 13:53:13 +0100 Subject: [PATCH 1/8] NORY-34: rework app sidebar to support multiple groups --- dashboard/src/components/app-sidebar.tsx | 135 ++++++++++++++++++----- 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/dashboard/src/components/app-sidebar.tsx b/dashboard/src/components/app-sidebar.tsx index 7039c7f..ff21f4d 100644 --- a/dashboard/src/components/app-sidebar.tsx +++ b/dashboard/src/components/app-sidebar.tsx @@ -5,14 +5,13 @@ import { SidebarContent, SidebarFooter, SidebarGroup, - SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, + useSidebar, } from '@/components/ui/sidebar'; -import { AppWindow, Home, LogOut, Moon, Sun, Users } from 'lucide-react'; -import React from 'react'; +import { AppWindow, ChartLine, FileLock2, Home, LogOut, LucideIcon, Moon, Sun, Users } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -21,50 +20,126 @@ import { } from '@/components/ui/dropdown-menu'; import { useTheme } from 'next-themes'; import { LogoutLink } from '@/ory'; +import React from 'react'; import Link from 'next/link'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; + +interface SidebarGroup { + type: 'group'; + label: string; + items: SidebarItem[]; +} + +interface SidebarItem { + type: 'item'; + label: string; + path: string; + icon: LucideIcon; +} + +type SidebarContent = SidebarItem | SidebarGroup + +function renderSidebarMenuItem(item: SidebarItem, key: number, collapsed: boolean) { + return ( + + + + + + + {item.label} + + + + + + {item.label} + + + ); +} export function AppSidebar({ ...props }: React.ComponentProps) { const { setTheme } = useTheme(); - const items = [ + const { state } = useSidebar(); + + const items: SidebarContent[] = [ { - title: 'Home', - url: '/', - icon: Home, + label: 'Application', + type: 'group', + items: [ + { + type: 'item', + label: 'Home', + path: '/', + icon: Home, + }, + { + type: 'item', + label: 'Analytics', + path: '/analytics', + icon: ChartLine, + }, + ], }, { - title: 'Users', - url: '/user', - icon: Users, + label: 'Ory Kratos', + type: 'group', + items: [ + { + type: 'item', + label: 'Users', + path: '/user', + icon: Users, + }, + ], }, { - title: 'Applications', - url: '/application', - icon: AppWindow, + label: 'Ory Hydra', + type: 'group', + items: [ + { + type: 'item', + label: 'Clients', + path: '/client', + icon: AppWindow, + }, + ], + }, + { + label: 'Ory Keto', + type: 'group', + items: [ + { + type: 'item', + label: 'Relations', + path: '/relation', + icon: FileLock2, + }, + ], }, ]; return ( - - Application - - - {items.map((item) => ( - - - - - {item.title} - - - - ))} - - - + + {items.map((item, index) => { + switch (item.type) { + case 'item': + return renderSidebarMenuItem(item, index, state === 'collapsed'); + case 'group': + return ( + + {item.label} + {item.items.map((subItem, subIndex) => renderSidebarMenuItem(subItem, subIndex, state === 'collapsed'))} + + ); + } + })} + From 502098d8ef8e270fa1a3c35fb58cf3eb9a487103 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 27 Dec 2024 17:10:38 +0100 Subject: [PATCH 2/8] NORY-34: add missing placeholder pages --- dashboard/src/app/(inside)/analytics/page.tsx | 12 ++++++++++++ dashboard/src/app/(inside)/application/page.tsx | 3 --- dashboard/src/app/(inside)/client/page.tsx | 12 ++++++++++++ dashboard/src/app/(inside)/relation/page.tsx | 12 ++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 dashboard/src/app/(inside)/analytics/page.tsx delete mode 100644 dashboard/src/app/(inside)/application/page.tsx create mode 100644 dashboard/src/app/(inside)/client/page.tsx create mode 100644 dashboard/src/app/(inside)/relation/page.tsx diff --git a/dashboard/src/app/(inside)/analytics/page.tsx b/dashboard/src/app/(inside)/analytics/page.tsx new file mode 100644 index 0000000..e416cc9 --- /dev/null +++ b/dashboard/src/app/(inside)/analytics/page.tsx @@ -0,0 +1,12 @@ +export default async function AnalyticsPage() { + return ( +
+
+

Analytics

+

+ Part of milestone v0.3.0 +

+
+
+ ); +} diff --git a/dashboard/src/app/(inside)/application/page.tsx b/dashboard/src/app/(inside)/application/page.tsx deleted file mode 100644 index eda2025..0000000 --- a/dashboard/src/app/(inside)/application/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default async function ApplicationPage() { - return <>; -} diff --git a/dashboard/src/app/(inside)/client/page.tsx b/dashboard/src/app/(inside)/client/page.tsx new file mode 100644 index 0000000..db0117a --- /dev/null +++ b/dashboard/src/app/(inside)/client/page.tsx @@ -0,0 +1,12 @@ +export default async function ListClientPage() { + return ( +
+
+

OAuth2 Clients

+

+ Part of milestone v0.2.0 +

+
+
+ ); +} diff --git a/dashboard/src/app/(inside)/relation/page.tsx b/dashboard/src/app/(inside)/relation/page.tsx new file mode 100644 index 0000000..3ad6bef --- /dev/null +++ b/dashboard/src/app/(inside)/relation/page.tsx @@ -0,0 +1,12 @@ +export default async function ListRelationPage() { + return ( +
+
+

Relations

+

+ Part of milestone v0.2.0 +

+
+
+ ); +} From 6335036a0421f44ebef42391a6af47273d00996f Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 27 Dec 2024 17:17:46 +0100 Subject: [PATCH 3/8] NORY-34: show oauth2 clients in a data table --- .../src/app/(inside)/client/data-table.tsx | 105 ++++++++++++++++++ dashboard/src/app/(inside)/client/page.tsx | 37 +++++- 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 dashboard/src/app/(inside)/client/data-table.tsx diff --git a/dashboard/src/app/(inside)/client/data-table.tsx b/dashboard/src/app/(inside)/client/data-table.tsx new file mode 100644 index 0000000..2e347db --- /dev/null +++ b/dashboard/src/app/(inside)/client/data-table.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { ColumnDef } from '@tanstack/react-table'; +import { OAuth2Client } from '@ory/client'; +import { DataTable } from '@/components/ui/data-table'; +import { useEffect, useRef, useState } from 'react'; +import { Spinner } from '@/components/ui/spinner'; +import { FetchClientPageProps } from '@/app/(inside)/client/page'; + +interface ClientDataTableProps { + data: OAuth2Client[]; + pageSize: number; + pageToken: string | undefined; + fetchClientPage: (props: FetchClientPageProps) => Promise<{ + data: OAuth2Client[], + tokens: Map + }>; +} + +export function ClientDataTable( + { + data, + pageSize, + pageToken, + fetchClientPage, + }: ClientDataTableProps, +) { + + console.log('OAuth2 client', data); + + const columns: ColumnDef[] = [ + { + id: 'client_id', + accessorKey: 'client_id', + header: 'Client ID', + }, + { + id: 'client_name', + accessorKey: 'client_name', + header: 'Client Name', + }, + { + id: 'owner', + accessorKey: 'owner', + header: 'Owner', + }, + ]; + + 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]); + + // infinite scroll handling + const infiniteScrollSensor = useRef(null); + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + fetchMore(); + } + }, + { threshold: 0.5 }, // Adjust threshold as needed + ); + + if (infiniteScrollSensor.current) { + observer.observe(infiniteScrollSensor.current); + } + + return () => { + if (infiniteScrollSensor.current) { + observer.unobserve(infiniteScrollSensor.current); + } + }; + }, [items]); + + const fetchMore = async () => { + if (!nextToken) return; + + const response = await fetchClientPage({ + pageSize: pageSize, + pageToken: nextToken, + }); + + setItems([...items, ...response.data]); + setNextToken(response.tokens.get('next') ?? undefined); + }; + + return ( + <> + + { + nextToken && ( +
+ +
+ ) + } + + ); +} diff --git a/dashboard/src/app/(inside)/client/page.tsx b/dashboard/src/app/(inside)/client/page.tsx index db0117a..439fd3e 100644 --- a/dashboard/src/app/(inside)/client/page.tsx +++ b/dashboard/src/app/(inside)/client/page.tsx @@ -1,12 +1,47 @@ +import { getOAuth2Api } from '@/ory/sdk/server'; +import { parseTokens } from '@/app/(inside)/user/page'; +import { ClientDataTable } from '@/app/(inside)/client/data-table'; + +export interface FetchClientPageProps { + pageSize: number; + pageToken: string; +} + +async function fetchClientPage({ pageSize, pageToken }: FetchClientPageProps) { + 'use server'; + + const oAuth2Api = await getOAuth2Api(); + const response = await oAuth2Api.listOAuth2Clients({ + pageSize: pageSize, + pageToken: pageToken, + }); + + return { + data: response.data, + tokens: parseTokens(response.headers.link), + }; +} + export default async function ListClientPage() { + + let pageSize = 100; + let pageToken: string = '00000000-0000-0000-0000-000000000000'; + + const initialFetch = await fetchClientPage({ pageSize, pageToken }); + return (

OAuth2 Clients

- Part of milestone v0.2.0 + See and manage all OAuth2 clients registered with your Ory Hydra instance

+
); } From a24b05c01e40937241c51916a8b6200b8b8624b7 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 27 Dec 2024 17:18:26 +0100 Subject: [PATCH 4/8] NORY-34: update script for creating oauth client --- docker/ory-dev/hydra-create-client.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docker/ory-dev/hydra-create-client.sh b/docker/ory-dev/hydra-create-client.sh index 8b6b038..181920e 100644 --- a/docker/ory-dev/hydra-create-client.sh +++ b/docker/ory-dev/hydra-create-client.sh @@ -2,15 +2,21 @@ # Ory Hydra CLI and writes the client id and # client secret to the command line. -read -r -p "Did you modify the script according to your needs? (y/N)? " answer -if [ answer != "y" && anser != "Y" ]; then - exit 0 +# Check if the number of arguments is correct +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 fi +name=$1 +owner=$2 + # it is likely you will have to set different redirect-uris # depending on the application you are trying to connect. code_client=$(docker compose exec ory-hydra \ hydra create client \ + --name "$name" \ + --owner "$owner" \ --endpoint http://localhost:4445 \ --grant-type authorization_code,refresh_token \ --response-type code,id_token \ From 3d6928a8399898dc31336ba9703258bbedb0db29 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 27 Dec 2024 19:02:20 +0100 Subject: [PATCH 5/8] NORY-34: fix layout not cutting off input ring --- dashboard/src/app/(inside)/layout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/src/app/(inside)/layout.tsx b/dashboard/src/app/(inside)/layout.tsx index 5c8f890..d7f42e7 100644 --- a/dashboard/src/app/(inside)/layout.tsx +++ b/dashboard/src/app/(inside)/layout.tsx @@ -10,8 +10,8 @@ export default function InsideLayout({ children }: Readonly<{ children: React.Re return ( - -
+ +
{ @@ -27,7 +27,7 @@ export default function InsideLayout({ children }: Readonly<{ children: React.Re
-
+
{children}
From 4ebd10d699ba2e281a1f1c1257fe376e8fe8eb4e Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Mon, 6 Jan 2025 19:27:37 +0100 Subject: [PATCH 6/8] NORY-34: add parseTokens() function after rebase --- dashboard/src/app/(inside)/client/page.tsx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/dashboard/src/app/(inside)/client/page.tsx b/dashboard/src/app/(inside)/client/page.tsx index 439fd3e..0d3f5a3 100644 --- a/dashboard/src/app/(inside)/client/page.tsx +++ b/dashboard/src/app/(inside)/client/page.tsx @@ -1,5 +1,4 @@ import { getOAuth2Api } from '@/ory/sdk/server'; -import { parseTokens } from '@/app/(inside)/user/page'; import { ClientDataTable } from '@/app/(inside)/client/data-table'; export interface FetchClientPageProps { @@ -7,6 +6,26 @@ export interface FetchClientPageProps { pageToken: string; } +function parseTokens(link: string) { + + const parsed = link.split(',').map((it) => { + const startRel = it.lastIndexOf('rel="'); + const endRel = it.lastIndexOf('"'); + const rel = it.slice(startRel, endRel); + + const startToken = it.lastIndexOf('page_token='); + const endToken = it.lastIndexOf('&'); + const token = it.slice(startToken, endToken); + + return [rel, token]; + }); + + return new Map(parsed.map(obj => [ + obj[0].replace('rel="', ''), + obj[1].replace('page_token=', ''), + ])); +} + async function fetchClientPage({ pageSize, pageToken }: FetchClientPageProps) { 'use server'; From 5fb9170c81a99fed7ec1c8b6a80775841ea502fa Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Mon, 6 Jan 2025 19:29:39 +0100 Subject: [PATCH 7/8] NORY-34: fix milestone references on placeholder pages --- dashboard/src/app/(inside)/analytics/page.tsx | 2 +- dashboard/src/app/(inside)/relation/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/src/app/(inside)/analytics/page.tsx b/dashboard/src/app/(inside)/analytics/page.tsx index e416cc9..998087c 100644 --- a/dashboard/src/app/(inside)/analytics/page.tsx +++ b/dashboard/src/app/(inside)/analytics/page.tsx @@ -4,7 +4,7 @@ export default async function AnalyticsPage() {

Analytics

- Part of milestone v0.3.0 + Part of milestone v0.5.0

diff --git a/dashboard/src/app/(inside)/relation/page.tsx b/dashboard/src/app/(inside)/relation/page.tsx index 3ad6bef..e6754a9 100644 --- a/dashboard/src/app/(inside)/relation/page.tsx +++ b/dashboard/src/app/(inside)/relation/page.tsx @@ -4,7 +4,7 @@ export default async function ListRelationPage() {

Relations

- Part of milestone v0.2.0 + Part of milestone v0.4.0

From ea13802183f4cbea563508ebefc67a552f11c9d7 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 7 Jan 2025 19:13:53 +0100 Subject: [PATCH 8/8] NORY-34: fix serwist configuration --- authentication/src/app/service-worker.ts | 24 +++++++++++++++--------- dashboard/src/app/service-worker.ts | 24 +++++++++++++++--------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/authentication/src/app/service-worker.ts b/authentication/src/app/service-worker.ts index 0538ba3..b9741f4 100644 --- a/authentication/src/app/service-worker.ts +++ b/authentication/src/app/service-worker.ts @@ -1,18 +1,24 @@ -import type { PrecacheEntry } from '@serwist/precaching'; -import { installSerwist } from '@serwist/sw'; import { defaultCache } from '@serwist/next/worker'; +import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist'; +import { Serwist } from 'serwist'; -declare const self: ServiceWorkerGlobalScope & { - // Change this attribute's name to your `injectionPoint`. - // `injectionPoint` is an InjectManifest option. - // See https://serwist.pages.dev/docs/build/inject-manifest/configuring - __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; -}; +declare const self: ServiceWorkerGlobalScope; -installSerwist({ +declare global { + interface WorkerGlobalScope extends SerwistGlobalConfig { + // Change this attribute's name to your `injectionPoint`. + // `injectionPoint` is an InjectManifest option. + // See https://serwist.pages.dev/docs/build/configuring + __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; + } +} + +const serwist = new Serwist({ precacheEntries: self.__SW_MANIFEST, skipWaiting: true, clientsClaim: true, navigationPreload: true, runtimeCaching: defaultCache, }); + +serwist.addEventListeners(); diff --git a/dashboard/src/app/service-worker.ts b/dashboard/src/app/service-worker.ts index 0538ba3..b9741f4 100644 --- a/dashboard/src/app/service-worker.ts +++ b/dashboard/src/app/service-worker.ts @@ -1,18 +1,24 @@ -import type { PrecacheEntry } from '@serwist/precaching'; -import { installSerwist } from '@serwist/sw'; import { defaultCache } from '@serwist/next/worker'; +import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist'; +import { Serwist } from 'serwist'; -declare const self: ServiceWorkerGlobalScope & { - // Change this attribute's name to your `injectionPoint`. - // `injectionPoint` is an InjectManifest option. - // See https://serwist.pages.dev/docs/build/inject-manifest/configuring - __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; -}; +declare const self: ServiceWorkerGlobalScope; -installSerwist({ +declare global { + interface WorkerGlobalScope extends SerwistGlobalConfig { + // Change this attribute's name to your `injectionPoint`. + // `injectionPoint` is an InjectManifest option. + // See https://serwist.pages.dev/docs/build/configuring + __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; + } +} + +const serwist = new Serwist({ precacheEntries: self.__SW_MANIFEST, skipWaiting: true, clientsClaim: true, navigationPreload: true, runtimeCaching: defaultCache, }); + +serwist.addEventListeners();