mirror of
https://codeberg.org/MarkusThielker/next-ory.git
synced 2025-04-10 11:58:41 +00:00
NORY-34: add OAuth2 client page (#51)
This commit is contained in:
commit
38488d71e8
10 changed files with 342 additions and 57 deletions
|
@ -1,18 +1,24 @@
|
||||||
import type { PrecacheEntry } from '@serwist/precaching';
|
|
||||||
import { installSerwist } from '@serwist/sw';
|
|
||||||
import { defaultCache } from '@serwist/next/worker';
|
import { defaultCache } from '@serwist/next/worker';
|
||||||
|
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist';
|
||||||
|
import { Serwist } from 'serwist';
|
||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope & {
|
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
precacheEntries: self.__SW_MANIFEST,
|
||||||
skipWaiting: true,
|
skipWaiting: true,
|
||||||
clientsClaim: true,
|
clientsClaim: true,
|
||||||
navigationPreload: true,
|
navigationPreload: true,
|
||||||
runtimeCaching: defaultCache,
|
runtimeCaching: defaultCache,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
serwist.addEventListeners();
|
||||||
|
|
12
dashboard/src/app/(inside)/analytics/page.tsx
Normal file
12
dashboard/src/app/(inside)/analytics/page.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export default async function AnalyticsPage() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-3xl font-bold leading-tight tracking-tight">Analytics</p>
|
||||||
|
<p className="text-lg font-light">
|
||||||
|
Part of milestone v0.5.0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
export default async function ApplicationPage() {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
105
dashboard/src/app/(inside)/client/data-table.tsx
Normal file
105
dashboard/src/app/(inside)/client/data-table.tsx
Normal file
|
@ -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<string, string>
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClientDataTable(
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
pageSize,
|
||||||
|
pageToken,
|
||||||
|
fetchClientPage,
|
||||||
|
}: ClientDataTableProps,
|
||||||
|
) {
|
||||||
|
|
||||||
|
console.log('OAuth2 client', data);
|
||||||
|
|
||||||
|
const columns: ColumnDef<OAuth2Client>[] = [
|
||||||
|
{
|
||||||
|
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<OAuth2Client[]>(data);
|
||||||
|
const [nextToken, setNextToken] = useState<string | undefined>(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 (
|
||||||
|
<>
|
||||||
|
<DataTable columns={columns} data={items}/>
|
||||||
|
{
|
||||||
|
nextToken && (
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
<Spinner ref={infiniteScrollSensor} className="h-10"/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
66
dashboard/src/app/(inside)/client/page.tsx
Normal file
66
dashboard/src/app/(inside)/client/page.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { getOAuth2Api } from '@/ory/sdk/server';
|
||||||
|
import { ClientDataTable } from '@/app/(inside)/client/data-table';
|
||||||
|
|
||||||
|
export interface FetchClientPageProps {
|
||||||
|
pageSize: number;
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-3xl font-bold leading-tight tracking-tight">OAuth2 Clients</p>
|
||||||
|
<p className="text-lg font-light">
|
||||||
|
See and manage all OAuth2 clients registered with your Ory Hydra instance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ClientDataTable
|
||||||
|
data={initialFetch.data}
|
||||||
|
pageSize={pageSize}
|
||||||
|
pageToken={initialFetch.tokens.get('next')}
|
||||||
|
fetchClientPage={fetchClientPage}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,8 +10,8 @@ export default function InsideLayout({ children }: Readonly<{ children: React.Re
|
||||||
return (
|
return (
|
||||||
<SidebarProvider className="max-h-screen min-h-screen">
|
<SidebarProvider className="max-h-screen min-h-screen">
|
||||||
<AppSidebar className="mx-1"/>
|
<AppSidebar className="mx-1"/>
|
||||||
<SidebarInset className="overflow-hidden p-6 space-y-6">
|
<SidebarInset className="overflow-hidden space-y-6">
|
||||||
<header className="flex h-4 items-center gap-2">
|
<header className="flex items-center px-6 pt-6 gap-2">
|
||||||
<SidebarTrigger className="-ml-1 p-1"/>
|
<SidebarTrigger className="-ml-1 p-1"/>
|
||||||
<Separator orientation="vertical" className="mr-2 h-4"/>
|
<Separator orientation="vertical" className="mr-2 h-4"/>
|
||||||
{
|
{
|
||||||
|
@ -27,7 +27,7 @@ export default function InsideLayout({ children }: Readonly<{ children: React.Re
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</header>
|
</header>
|
||||||
<div className="flex-1 overflow-scroll">
|
<div className="flex-1 px-6 pb-6 overflow-scroll">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|
12
dashboard/src/app/(inside)/relation/page.tsx
Normal file
12
dashboard/src/app/(inside)/relation/page.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export default async function ListRelationPage() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-3xl font-bold leading-tight tracking-tight">Relations</p>
|
||||||
|
<p className="text-lg font-light">
|
||||||
|
Part of milestone v0.4.0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,18 +1,24 @@
|
||||||
import type { PrecacheEntry } from '@serwist/precaching';
|
|
||||||
import { installSerwist } from '@serwist/sw';
|
|
||||||
import { defaultCache } from '@serwist/next/worker';
|
import { defaultCache } from '@serwist/next/worker';
|
||||||
|
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist';
|
||||||
|
import { Serwist } from 'serwist';
|
||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope & {
|
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
precacheEntries: self.__SW_MANIFEST,
|
||||||
skipWaiting: true,
|
skipWaiting: true,
|
||||||
clientsClaim: true,
|
clientsClaim: true,
|
||||||
navigationPreload: true,
|
navigationPreload: true,
|
||||||
runtimeCaching: defaultCache,
|
runtimeCaching: defaultCache,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
serwist.addEventListeners();
|
||||||
|
|
|
@ -5,14 +5,13 @@ import {
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { AppWindow, Home, LogOut, Moon, Sun, Users } from 'lucide-react';
|
import { AppWindow, ChartLine, FileLock2, Home, LogOut, LucideIcon, Moon, Sun, Users } from 'lucide-react';
|
||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
@ -21,50 +20,126 @@ import {
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { LogoutLink } from '@/ory';
|
import { LogoutLink } from '@/ory';
|
||||||
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
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 (
|
||||||
|
<Tooltip key={key} delayDuration={500}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<Link href={item.path}>
|
||||||
|
<item.icon/>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" className={collapsed ? '' : 'hidden'}>
|
||||||
|
{item.label}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
|
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
const items = [
|
const { state } = useSidebar();
|
||||||
|
|
||||||
|
const items: SidebarContent[] = [
|
||||||
{
|
{
|
||||||
title: 'Home',
|
label: 'Application',
|
||||||
url: '/',
|
type: 'group',
|
||||||
icon: Home,
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Home',
|
||||||
|
path: '/',
|
||||||
|
icon: Home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Analytics',
|
||||||
|
path: '/analytics',
|
||||||
|
icon: ChartLine,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Users',
|
label: 'Ory Kratos',
|
||||||
url: '/user',
|
type: 'group',
|
||||||
icon: Users,
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Users',
|
||||||
|
path: '/user',
|
||||||
|
icon: Users,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Applications',
|
label: 'Ory Hydra',
|
||||||
url: '/application',
|
type: 'group',
|
||||||
icon: AppWindow,
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Clients',
|
||||||
|
path: '/client',
|
||||||
|
icon: AppWindow,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ory Keto',
|
||||||
|
type: 'group',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Relations',
|
||||||
|
path: '/relation',
|
||||||
|
icon: FileLock2,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar variant="inset" collapsible="icon" {...props}>
|
<Sidebar variant="inset" collapsible="icon" {...props}>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarMenu>
|
||||||
<SidebarGroupLabel>Application</SidebarGroupLabel>
|
{items.map((item, index) => {
|
||||||
<SidebarGroupContent>
|
switch (item.type) {
|
||||||
<SidebarMenu>
|
case 'item':
|
||||||
{items.map((item) => (
|
return renderSidebarMenuItem(item, index, state === 'collapsed');
|
||||||
<SidebarMenuItem key={item.title}>
|
case 'group':
|
||||||
<SidebarMenuButton asChild>
|
return (
|
||||||
<Link href={item.url}>
|
<SidebarGroup key={index}>
|
||||||
<item.icon/>
|
<SidebarGroupLabel>{item.label}</SidebarGroupLabel>
|
||||||
<span>{item.title}</span>
|
{item.items.map((subItem, subIndex) => renderSidebarMenuItem(subItem, subIndex, state === 'collapsed'))}
|
||||||
</Link>
|
</SidebarGroup>
|
||||||
</SidebarMenuButton>
|
);
|
||||||
</SidebarMenuItem>
|
}
|
||||||
))}
|
})}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
|
|
|
@ -2,15 +2,21 @@
|
||||||
# Ory Hydra CLI and writes the client id and
|
# Ory Hydra CLI and writes the client id and
|
||||||
# client secret to the command line.
|
# client secret to the command line.
|
||||||
|
|
||||||
read -r -p "Did you modify the script according to your needs? (y/N)? " answer
|
# Check if the number of arguments is correct
|
||||||
if [ answer != "y" && anser != "Y" ]; then
|
if [ $# -ne 2 ]; then
|
||||||
exit 0
|
echo "Usage: $0 <name> <owner>"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
name=$1
|
||||||
|
owner=$2
|
||||||
|
|
||||||
# it is likely you will have to set different redirect-uris
|
# it is likely you will have to set different redirect-uris
|
||||||
# depending on the application you are trying to connect.
|
# depending on the application you are trying to connect.
|
||||||
code_client=$(docker compose exec ory-hydra \
|
code_client=$(docker compose exec ory-hydra \
|
||||||
hydra create client \
|
hydra create client \
|
||||||
|
--name "$name" \
|
||||||
|
--owner "$owner" \
|
||||||
--endpoint http://localhost:4445 \
|
--endpoint http://localhost:4445 \
|
||||||
--grant-type authorization_code,refresh_token \
|
--grant-type authorization_code,refresh_token \
|
||||||
--response-type code,id_token \
|
--response-type code,id_token \
|
||||||
|
|
Loading…
Add table
Reference in a new issue