NORY-14: add dashboard page (#19)

This commit is contained in:
Markus Thielker 2024-12-02 00:45:03 +01:00 committed by GitHub
commit 5f52cb0515
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 378 additions and 67 deletions

View file

@ -2,11 +2,11 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import getHydra from '@/ory/sdk/hydra';
import { OAuth2ConsentRequest, OAuth2RedirectTo } from '@ory/client';
import ConsentForm from '@/components/consentForm';
import { redirect } from 'next/navigation';
import { toast } from 'sonner';
import { getOAuth2Api } from '@/ory/sdk/server';
export default async function Consent(props: { searchParams: Promise<{ consent_challenge: string }> }) {
@ -18,7 +18,7 @@ export default async function Consent(props: { searchParams: Promise<{ consent_c
const onAccept = async (challenge: string, scopes: string[], remember: boolean) => {
'use server';
const hydra = await getHydra();
const hydra = await getOAuth2Api();
const response = await hydra
.acceptOAuth2ConsentRequest({
consentChallenge: challenge,
@ -43,7 +43,7 @@ export default async function Consent(props: { searchParams: Promise<{ consent_c
const onReject = async (challenge: string) => {
'use server';
const hydra = await getHydra();
const hydra = await getOAuth2Api();
const response: OAuth2RedirectTo | void = await hydra
.rejectOAuth2ConsentRequest({
consentChallenge: challenge,
@ -64,7 +64,7 @@ export default async function Consent(props: { searchParams: Promise<{ consent_c
return;
}
const hydra = await getHydra();
const hydra = await getOAuth2Api();
await hydra
.getOAuth2ConsentRequest({ consentChallenge })
.then(({ data }) => {

View file

@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { FlowError } from '@ory/client';
import { AxiosError } from 'axios';
import { kratos } from '@/ory/sdk/kratos';
import { kratos } from '@/ory';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Button } from '@/components/ui/button';

View file

@ -2,10 +2,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Flow, HandleError, LogoutLink } from '@/ory';
import { Flow, HandleError, kratos, LogoutLink } from '@/ory';
import Link from 'next/link';
import { LoginFlow, UpdateLoginFlowBody } from '@ory/client';
import { kratos } from '@/ory/sdk/kratos';
import { Button } from '@/components/ui/button';
import { useRouter, useSearchParams } from 'next/navigation';
import Image from 'next/image';

View file

@ -2,10 +2,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Flow, HandleError } from '@/ory';
import { Flow, HandleError, kratos } from '@/ory';
import { RecoveryFlow, UpdateRecoveryFlowBody } from '@ory/client';
import { AxiosError } from 'axios';
import { kratos } from '@/ory/sdk/kratos';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Button } from '@/components/ui/button';

View file

@ -2,11 +2,10 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Flow, HandleError } from '@/ory';
import { Flow, HandleError, kratos } from '@/ory';
import Link from 'next/link';
import { RegistrationFlow, UpdateRegistrationFlowBody } from '@ory/client';
import { AxiosError } from 'axios';
import { kratos } from '@/ory/sdk/kratos';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';

View file

@ -2,10 +2,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Flow, HandleError } from '@/ory';
import { Flow, HandleError, kratos } from '@/ory';
import { UpdateVerificationFlowBody, VerificationFlow } from '@ory/client';
import { AxiosError } from 'axios';
import { kratos } from '@/ory/sdk/kratos';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Button } from '@/components/ui/button';

View file

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { getFrontendApi } from '@/ory/sdk/hydra';
import { getFrontendApi } from 'ory/sdk/server';
import { cookies } from 'next/headers';
export async function middleware(request: NextRequest) {

View file

@ -3,7 +3,7 @@
import { AxiosError } from 'axios';
import React, { DependencyList, useEffect, useState } from 'react';
import { kratos } from './sdk/kratos';
import { kratos } from './sdk/client';
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
export const HandleError = (

View file

@ -1,3 +1,3 @@
export * from './hooks';
export * from './ui';
export * from './sdk/kratos';
export * from './sdk/client';

View file

@ -1,15 +0,0 @@
'use server';
import { Configuration, OAuth2Api } from '@ory/client';
// implemented as a function because of 'use server'
export default async function getHydra() {
return new OAuth2Api(new Configuration(
new Configuration({
basePath: process.env.ORY_HYDRA_ADMIN_URL,
baseOptions: {
withCredentials: true,
},
}),
));
}

View file

@ -0,0 +1,39 @@
'use server';
import { Configuration, FrontendApi, OAuth2Api } from '@ory/client';
// ####################################################################################
// OAuth2 API
// ####################################################################################
const oAuth2Api = new OAuth2Api(new Configuration(
{
basePath: process.env.ORY_HYDRA_ADMIN_URL,
baseOptions: {
withCredentials: true,
},
},
));
export async function getOAuth2Api() {
return oAuth2Api;
}
// ####################################################################################
// Frontend API
// ####################################################################################
const frontendApi = new FrontendApi(
new Configuration({
basePath: process.env.NEXT_PUBLIC_ORY_KRATOS_URL,
baseOptions: {
withCredentials: true,
},
}),
);
export async function getFrontendApi() {
return frontendApi;
}

View file

@ -5,8 +5,10 @@ import { cn } from '@/lib/utils';
import { Toaster } from '@/components/ui/sonner';
import React from 'react';
import { ThemeProvider } from 'next-themes';
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';
import { Separator } from '@/components/ui/separator';
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from '@/components/ui/breadcrumb';
const inter = Inter({ subsets: ['latin'] });
@ -48,23 +50,38 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
/>
</head>
<body className={cn(inter.className)}>
<main>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<SidebarProvider>
<AppSidebar/>
<SidebarTrigger className="m-2"/>
<main className="w-full min-h-screen">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<SidebarProvider>
<AppSidebar className="mx-1"/>
<SidebarInset className="p-2">
<header className="flex h-16 shrink-0 items-center gap-2 px-4">
<SidebarTrigger className="-ml-1 p-1"/>
<Separator orientation="vertical" className="mr-2 h-4"/>
{
// TODO: implement dynamic Breadcrumbs
}
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="/">
Ory Dashboard
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<div className="flex flex-col p-4 pt-0">
{children}
<Toaster/>
</main>
</SidebarProvider>
</ThemeProvider>
</main>
</div>
</SidebarInset>
</SidebarProvider>
</ThemeProvider>
</body>
</html>
);

View file

@ -1,3 +1,81 @@
import { getHydraMetadataApi, getKratosMetadataApi } from '@/ory/sdk/server';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
export default async function RootPage() {
return <></>;
const kratosMetadataApi = await getKratosMetadataApi();
const kratosVersion = await kratosMetadataApi
.getVersion()
.then(res => res.data.version)
.catch(() => '');
const kratosStatusData = await fetch(process.env.ORY_KRATOS_ADMIN_URL + '/health/alive');
const kratosStatus = await kratosStatusData.json() as { status: string };
const kratosDBStatusData = await fetch(process.env.ORY_KRATOS_ADMIN_URL + '/health/ready');
const kratosDBStatus = await kratosDBStatusData.json() as { status: string };
const hydraMetadataApi = await getHydraMetadataApi();
const hydraVersion = await hydraMetadataApi
.getVersion()
.then(res => res.data.version)
.catch(() => '');
const hydraStatusData = await fetch(process.env.ORY_KRATOS_ADMIN_URL + '/health/alive');
const hydraStatus = await hydraStatusData.json() as { status: string };
const hydraDBStatusData = await fetch(process.env.ORY_KRATOS_ADMIN_URL + '/health/ready');
const hydraDBStatus = await hydraDBStatusData.json() as { status: string };
return (
<div className="flex flex-col space-y-4">
<div>
<p className="text-3xl font-bold leading-tight tracking-tight">Software Stack</p>
<p className="text-lg font-light">See the list of all applications in your stack</p>
</div>
<div className="flex flex-row space-x-4">
<Card className="flex-1">
<CardHeader>
<CardTitle>
Ory Kratos
</CardTitle>
<CardDescription>
Version {kratosVersion}
</CardDescription>
</CardHeader>
<CardContent className="space-x-1">
<Badge variant={kratosStatus.status === 'ok' ? 'success' : 'destructive'}>
Kratos {kratosStatus.status.toUpperCase()}
</Badge>
<Badge variant={kratosStatus.status === 'ok' ? 'success' : 'destructive'}>
Database {kratosDBStatus.status.toUpperCase()}
</Badge>
</CardContent>
</Card>
<Card className="flex-1">
<CardHeader>
<CardTitle>
Ory Hydra
</CardTitle>
<CardDescription>
Version {hydraVersion}
</CardDescription>
</CardHeader>
<CardContent className="space-x-1">
<Badge variant={kratosStatus.status === 'ok' ? 'success' : 'destructive'}>
Hydra {hydraStatus.status.toUpperCase()}
</Badge>
<Badge variant={kratosStatus.status === 'ok' ? 'success' : 'destructive'}>
Database {hydraDBStatus.status.toUpperCase()}
</Badge>
</CardContent>
</Card>
<div className="flex-1"></div>
<div className="flex-1"></div>
</div>
</div>
);
}

View file

@ -45,7 +45,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
];
return (
<Sidebar variant="floating" {...props} collapsible="icon">
<Sidebar variant="inset" collapsible="icon" {...props}>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>

View file

@ -14,6 +14,8 @@ const badgeVariants = cva(
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
success:
'border-transparent bg-emerald-600 text-white hover:bg-emerald-600/80',
outline: 'text-foreground',
},
},

View file

@ -0,0 +1,115 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils';
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<'nav'> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = 'Breadcrumb';
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<'ol'>
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
className,
)}
{...props}
/>
));
BreadcrumbList.displayName = 'BreadcrumbList';
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<'li'>
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
));
BreadcrumbItem.displayName = 'BreadcrumbItem';
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<'a'> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
className={cn('transition-colors hover:text-foreground', className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = 'BreadcrumbLink';
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<'span'>
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn('font-normal text-foreground', className)}
{...props}
/>
));
BreadcrumbPage.displayName = 'BreadcrumbPage';
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<'li'>) => (
<li
role="presentation"
aria-hidden="true"
className={cn('[&>svg]:w-3.5 [&>svg]:h-3.5', className)}
{...props}
>
{children ?? <ChevronRight/>}
</li>
);
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
role="presentation"
aria-hidden="true"
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4"/>
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View file

@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import { getFrontendApi } from '@/ory/sdk/hydra';
import { cookies } from 'next/headers';
import { getFrontendApi } from '@/ory/sdk/server';
export async function middleware() {

View file

@ -3,7 +3,7 @@
import { AxiosError } from 'axios';
import { DependencyList, useEffect, useState } from 'react';
import { kratos } from './sdk/kratos';
import { kratos } from './sdk/client';
// Returns a function which will log the user out
export function LogoutLink(deps?: DependencyList) {

View file

@ -1,2 +1,2 @@
export * from './hooks';
export * from './sdk/kratos';
export * from './sdk/client';

View file

@ -1,15 +0,0 @@
'use server';
import { Configuration, OAuth2Api } from '@ory/client';
// implemented as a function because of 'use server'
export default async function getHydra() {
return new OAuth2Api(new Configuration(
new Configuration({
basePath: process.env.ORY_HYDRA_ADMIN_URL,
baseOptions: {
withCredentials: true,
},
}),
));
}

View file

@ -0,0 +1,94 @@
'use server';
import { Configuration, FrontendApi, IdentityApi, MetadataApi, OAuth2Api } from '@ory/client';
// ####################################################################################
// OAuth2 API
// ####################################################################################
const oAuth2Api = new OAuth2Api(new Configuration(
{
basePath: process.env.ORY_HYDRA_ADMIN_URL,
baseOptions: {
withCredentials: true,
},
},
));
export async function getOAuth2Api() {
return oAuth2Api;
}
// ####################################################################################
// Hydra Metadata API
// ####################################################################################
const hydraMetadataApi = new MetadataApi(new Configuration(
{
basePath: process.env.ORY_HYDRA_ADMIN_URL,
baseOptions: {
withCredentials: true,
},
},
));
export async function getHydraMetadataApi() {
return hydraMetadataApi;
}
// ####################################################################################
// Frontend API
// ####################################################################################
const frontendApi = new FrontendApi(
new Configuration({
basePath: process.env.NEXT_PUBLIC_ORY_KRATOS_URL,
baseOptions: {
withCredentials: true,
},
}),
);
export async function getFrontendApi() {
return frontendApi;
}
// ####################################################################################
// Identity API
// ####################################################################################
const identityApi = new IdentityApi(new Configuration(
{
basePath: process.env.ORY_KRATOS_ADMIN_URL,
baseOptions: {
withCredentials: true,
},
},
));
export async function getIdentityApi() {
return identityApi;
}
// ####################################################################################
// Kratos Metadata API
// ####################################################################################
const kratosMetadataApi = new MetadataApi(
new Configuration(
{
basePath: process.env.ORY_KRATOS_ADMIN_URL,
baseOptions: {
withCredentials: true,
},
},
));
export async function getKratosMetadataApi() {
return kratosMetadataApi;
}