diff --git a/README.md b/README.md
index 1b3b83f..6ccce62 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,15 @@ npm install
npm run dev
```
+Inside another terminal session we can start the dashboard UI using npm:
+
+```bash
+cd dashboard
+cp .env.example .env
+npm install
+npm run dev
+```
+
## Deployment
*soon.*
diff --git a/dashboard/.env.example b/dashboard/.env.example
new file mode 100644
index 0000000..0890e33
--- /dev/null
+++ b/dashboard/.env.example
@@ -0,0 +1,8 @@
+
+ORY_KRATOS_ADMIN_URL=http://localhost:4434
+ORY_HYDRA_ADMIN_URL=http://localhost:4445
+
+NEXT_PUBLIC_ORY_KRATOS_URL=http://localhost:4433
+
+NEXT_PUBLIC_AUTHENTICATION_NODE_URL=http://localhost:3000
+NEXT_PUBLIC_DASHBOARD_NODE_URL=http://localhost:4000
diff --git a/dashboard/.eslintrc.json b/dashboard/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/dashboard/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/dashboard/.gitignore b/dashboard/.gitignore
new file mode 100644
index 0000000..16fb5e1
--- /dev/null
+++ b/dashboard/.gitignore
@@ -0,0 +1,40 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# serwist
+public/sw.js
diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile
new file mode 100644
index 0000000..fdd1557
--- /dev/null
+++ b/dashboard/Dockerfile
@@ -0,0 +1,49 @@
+FROM node:21-alpine AS base
+
+# Install dependencies only when needed
+FROM base AS deps
+
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+COPY package.json package-lock.json* ./
+RUN npm ci
+
+
+# Rebuild the source code only when needed
+FROM base AS builder
+
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+ENV NEXT_TELEMETRY_DISABLED 1
+RUN npm run build
+
+
+# Production image, copy all the files and run next
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV production
+ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+COPY --from=builder /app/public ./public
+
+RUN mkdir .next
+RUN chown nextjs:nodejs .next
+
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT 3000
+ENV HOSTNAME "0.0.0.0"
+
+CMD ["node", "server.js"]
diff --git a/dashboard/README.md b/dashboard/README.md
new file mode 100644
index 0000000..1d8021d
--- /dev/null
+++ b/dashboard/README.md
@@ -0,0 +1,9 @@
+# Next-Ory - Dashboard
+
+This directory contains a NextJS 15 (app router) UI Node, implementing the admin dashboard to the ORY Kratos instance.
+
+## Stack
+
+- [NextJS](https://nextjs.org/)
+- [TailwindCSS](https://tailwindcss.com/)
+- [shadcn/ui](https://ui.shadcn.com/)
diff --git a/dashboard/bun.lockb b/dashboard/bun.lockb
new file mode 100755
index 0000000..6d3fd3c
Binary files /dev/null and b/dashboard/bun.lockb differ
diff --git a/dashboard/components.json b/dashboard/components.json
new file mode 100644
index 0000000..8ecaf9e
--- /dev/null
+++ b/dashboard/components.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
diff --git a/dashboard/next.config.mjs b/dashboard/next.config.mjs
new file mode 100644
index 0000000..7bfcddb
--- /dev/null
+++ b/dashboard/next.config.mjs
@@ -0,0 +1,13 @@
+import withSerwistInit from '@serwist/next';
+
+const withSerwist = withSerwistInit({
+ swSrc: 'src/app/service-worker.ts',
+ swDest: 'public/sw.js',
+});
+
+export default withSerwist({
+ output: 'standalone',
+ env: {
+ appVersion: process.env.npm_package_version,
+ },
+});
diff --git a/dashboard/package.json b/dashboard/package.json
new file mode 100644
index 0000000..c16c4cb
--- /dev/null
+++ b/dashboard/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "next-base-dashboard",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev -p 4000",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^3.3.4",
+ "@ory/client": "^1.9.0",
+ "@ory/integrations": "^1.1.5",
+ "@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-checkbox": "^1.0.4",
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-scroll-area": "^1.0.5",
+ "@radix-ui/react-separator": "^1.0.3",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-tabs": "^1.1.1",
+ "@serwist/next": "^9.0.0-preview.21",
+ "@serwist/precaching": "^9.0.0-preview.21",
+ "@serwist/sw": "^9.0.0-preview.21",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.0",
+ "lucide-react": "^0.460.0",
+ "next": "15.0.3",
+ "next-themes": "^0.4.3",
+ "oslo": "^1.1.3",
+ "react": "19.0.0-rc-66855b96-20241106",
+ "react-dom": "19.0.0-rc-66855b96-20241106",
+ "react-hook-form": "^7.51.0",
+ "sharp": "^0.33.4",
+ "sonner": "^1.4.3",
+ "tailwind-merge": "^2.2.1",
+ "tailwindcss-animate": "^1.0.7",
+ "ua-parser-js": "^2.0.0",
+ "usehooks-ts": "^3.1.0",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@types/node": "^22.9.3",
+ "@types/react": "npm:types-react@19.0.0-rc.1",
+ "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
+ "autoprefixer": "^10.0.1",
+ "eslint": "^8",
+ "eslint-config-next": "15.0.3",
+ "postcss": "^8",
+ "tailwindcss": "^3.3.0",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.4.2"
+ },
+ "overrides": {
+ "@types/react": "npm:types-react@19.0.0-rc.1",
+ "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
+ }
+}
diff --git a/dashboard/postcss.config.js b/dashboard/postcss.config.js
new file mode 100644
index 0000000..67cdf1a
--- /dev/null
+++ b/dashboard/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/dashboard/public/favicon.png b/dashboard/public/favicon.png
new file mode 100644
index 0000000..fdace3b
Binary files /dev/null and b/dashboard/public/favicon.png differ
diff --git a/dashboard/public/icon-128.png b/dashboard/public/icon-128.png
new file mode 100644
index 0000000..0f5c25b
Binary files /dev/null and b/dashboard/public/icon-128.png differ
diff --git a/dashboard/public/icon-144.png b/dashboard/public/icon-144.png
new file mode 100644
index 0000000..eb96684
Binary files /dev/null and b/dashboard/public/icon-144.png differ
diff --git a/dashboard/public/icon-192.png b/dashboard/public/icon-192.png
new file mode 100644
index 0000000..ba8cc78
Binary files /dev/null and b/dashboard/public/icon-192.png differ
diff --git a/dashboard/public/icon-512.png b/dashboard/public/icon-512.png
new file mode 100644
index 0000000..70de619
Binary files /dev/null and b/dashboard/public/icon-512.png differ
diff --git a/dashboard/public/icon-72.png b/dashboard/public/icon-72.png
new file mode 100644
index 0000000..48b11f6
Binary files /dev/null and b/dashboard/public/icon-72.png differ
diff --git a/dashboard/public/manifest.json b/dashboard/public/manifest.json
new file mode 100644
index 0000000..97abfd0
--- /dev/null
+++ b/dashboard/public/manifest.json
@@ -0,0 +1,37 @@
+{
+ "name": "Next Base",
+ "short_name": "Next Base",
+ "description": "The starting point for your next project",
+ "icons": [
+ {
+ "src": "icon-72.png",
+ "sizes": "72x72",
+ "type": "image/png"
+ },
+ {
+ "src": "icon-128.png",
+ "sizes": "128x128",
+ "type": "image/png"
+ },
+ {
+ "src": "icon-144.png",
+ "sizes": "144x144",
+ "type": "image/png"
+ },
+ {
+ "src": "icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#0B0908",
+ "background_color": "#0B0908",
+ "start_url": "/",
+ "display": "standalone",
+ "orientation": "portrait"
+}
\ No newline at end of file
diff --git a/dashboard/public/mt-logo-orange.png b/dashboard/public/mt-logo-orange.png
new file mode 100644
index 0000000..57239fc
Binary files /dev/null and b/dashboard/public/mt-logo-orange.png differ
diff --git a/dashboard/src/app/globals.css b/dashboard/src/app/globals.css
new file mode 100644
index 0000000..8827f47
--- /dev/null
+++ b/dashboard/src/app/globals.css
@@ -0,0 +1,62 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 20 14.3% 4.1%;
+ --card: 0 0% 100%;
+ --card-foreground: 20 14.3% 4.1%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 20 14.3% 4.1%;
+ --primary: 24.6 95% 53.1%;
+ --primary-foreground: 60 9.1% 97.8%;
+ --secondary: 60 4.8% 95.9%;
+ --secondary-foreground: 24 9.8% 10%;
+ --muted: 60 4.8% 95.9%;
+ --muted-foreground: 25 5.3% 44.7%;
+ --accent: 60 4.8% 95.9%;
+ --accent-foreground: 24 9.8% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 20 5.9% 90%;
+ --input: 20 5.9% 90%;
+ --ring: 24.6 95% 53.1%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 20 14.3% 4.1%;
+ --foreground: 60 9.1% 97.8%;
+ --card: 20 14.3% 4.1%;
+ --card-foreground: 60 9.1% 97.8%;
+ --popover: 20 14.3% 4.1%;
+ --popover-foreground: 60 9.1% 97.8%;
+ --primary: 20.5 90.2% 48.2%;
+ --primary-foreground: 60 9.1% 97.8%;
+ --secondary: 12 6.5% 15.1%;
+ --secondary-foreground: 60 9.1% 97.8%;
+ --muted: 12 6.5% 15.1%;
+ --muted-foreground: 24 5.4% 63.9%;
+ --accent: 12 6.5% 15.1%;
+ --accent-foreground: 60 9.1% 97.8%;
+ --destructive: 0 72.2% 50.6%;
+ --destructive-foreground: 60 9.1% 97.8%;
+ --border: 12 6.5% 15.1%;
+ --input: 12 6.5% 15.1%;
+ --ring: 20.5 90.2% 48.2%;
+ }
+}
+
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+}
\ No newline at end of file
diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx
new file mode 100644
index 0000000..9b81615
--- /dev/null
+++ b/dashboard/src/app/layout.tsx
@@ -0,0 +1,63 @@
+import type { Viewport } from 'next';
+import { Inter } from 'next/font/google';
+import './globals.css';
+import { cn } from '@/lib/utils';
+import { Toaster } from '@/components/ui/sonner';
+import React from 'react';
+import { ThemeProvider } from 'next-themes';
+
+const inter = Inter({ subsets: ['latin'] });
+
+const APP_NAME = 'Next Ory';
+const APP_DEFAULT_TITLE = 'Next Ory';
+const APP_TITLE_TEMPLATE = `%s | ${APP_DEFAULT_TITLE}`;
+const APP_DESCRIPTION = 'Get started with ORY authentication quickly and easily.';
+
+export const metadata = {
+ applicationName: APP_NAME,
+ title: {
+ default: APP_DEFAULT_TITLE,
+ template: APP_TITLE_TEMPLATE,
+ },
+ description: APP_DESCRIPTION,
+ appleWebApp: {
+ capable: true,
+ statusBarStyle: 'default',
+ title: APP_DEFAULT_TITLE,
+ },
+ formatDetection: {
+ telephone: false,
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: '#0B0908',
+ width: 'device-width',
+};
+
+export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/dashboard/src/app/page.tsx b/dashboard/src/app/page.tsx
new file mode 100644
index 0000000..8fbe383
--- /dev/null
+++ b/dashboard/src/app/page.tsx
@@ -0,0 +1,53 @@
+'use client';
+
+import React, { useCallback, useEffect, useState } from 'react';
+import { kratos, LogoutLink } from '@/ory';
+import { useRouter } from 'next/navigation';
+import { ThemeToggle } from '@/components/themeToggle';
+import { Session } from '@ory/client';
+import { LogOut } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+
+export default function Page() {
+
+ const router = useRouter();
+
+ const [session, setSession] = useState();
+ const loadSession = useCallback(async () => {
+ console.log(kratos.toSession());
+ return kratos.toSession();
+ }, [router]);
+
+ useEffect(() => {
+
+ if (session) {
+ return;
+ }
+
+ loadSession()
+ .then((response) => {
+ console.log(response.data);
+ response.data && setSession(response.data);
+ })
+ .catch(() => {
+ const authentication_url = process.env.NEXT_PUBLIC_AUTHENTICATION_NODE_URL;
+ const dashboard_url = process.env.NEXT_PUBLIC_AUTHENTICATION_NODE_URL;
+ authentication_url && dashboard_url &&
+ router.push(authentication_url + '/flow/login?return_to=' + dashboard_url);
+ });
+
+ }, [router, session]);
+
+ const onLogout = LogoutLink();
+
+ return (
+
+ );
+}
diff --git a/dashboard/src/app/service-worker.ts b/dashboard/src/app/service-worker.ts
new file mode 100644
index 0000000..0538ba3
--- /dev/null
+++ b/dashboard/src/app/service-worker.ts
@@ -0,0 +1,18 @@
+import type { PrecacheEntry } from '@serwist/precaching';
+import { installSerwist } from '@serwist/sw';
+import { defaultCache } from '@serwist/next/worker';
+
+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({
+ precacheEntries: self.__SW_MANIFEST,
+ skipWaiting: true,
+ clientsClaim: true,
+ navigationPreload: true,
+ runtimeCaching: defaultCache,
+});
diff --git a/dashboard/src/components/themeProvider.tsx b/dashboard/src/components/themeProvider.tsx
new file mode 100644
index 0000000..af4029a
--- /dev/null
+++ b/dashboard/src/components/themeProvider.tsx
@@ -0,0 +1,8 @@
+'use client';
+
+import * as React from 'react';
+import { ThemeProvider as NextThemesProvider, ThemeProviderProps } from 'next-themes';
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children};
+}
diff --git a/dashboard/src/components/themeToggle.tsx b/dashboard/src/components/themeToggle.tsx
new file mode 100644
index 0000000..d8e00ae
--- /dev/null
+++ b/dashboard/src/components/themeToggle.tsx
@@ -0,0 +1,43 @@
+'use client';
+
+import * as React from 'react';
+import { Moon, Sun } from 'lucide-react';
+import { useTheme } from 'next-themes';
+
+import { Button } from '@/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+
+export function ThemeToggle() {
+
+ const { setTheme } = useTheme();
+
+ return (
+
+
+
+
+
+ setTheme('light')}>
+ Light
+
+ setTheme('dark')}>
+ Dark
+
+ setTheme('system')}>
+ System
+
+
+
+ );
+}
diff --git a/dashboard/src/components/ui/alert-dialog.tsx b/dashboard/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..d65d9d1
--- /dev/null
+++ b/dashboard/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+'use client';
+
+import * as React from 'react';
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
+
+import { cn } from '@/lib/utils';
+import { buttonVariants } from '@/components/ui/button';
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+ }: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = 'AlertDialogHeader';
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+ }: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = 'AlertDialogFooter';
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/dashboard/src/components/ui/alert.tsx b/dashboard/src/components/ui/alert.tsx
new file mode 100644
index 0000000..0d93e7c
--- /dev/null
+++ b/dashboard/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const alertVariants = cva(
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
+ {
+ variants: {
+ variant: {
+ default: 'bg-background text-foreground',
+ destructive:
+ 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = 'Alert';
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = 'AlertTitle';
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = 'AlertDescription';
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/dashboard/src/components/ui/badge.tsx b/dashboard/src/components/ui/badge.tsx
new file mode 100644
index 0000000..a59a355
--- /dev/null
+++ b/dashboard/src/components/ui/badge.tsx
@@ -0,0 +1,37 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ destructive:
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ outline: 'text-foreground',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {
+}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/dashboard/src/components/ui/button.tsx b/dashboard/src/components/ui/button.tsx
new file mode 100644
index 0000000..0ae7431
--- /dev/null
+++ b/dashboard/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ outline:
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-10 px-4 py-2',
+ sm: 'h-9 rounded-md px-3',
+ lg: 'h-11 rounded-md px-8',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+ return (
+
+ );
+ },
+);
+Button.displayName = 'Button';
+
+export { Button, buttonVariants };
diff --git a/dashboard/src/components/ui/card.tsx b/dashboard/src/components/ui/card.tsx
new file mode 100644
index 0000000..3c69472
--- /dev/null
+++ b/dashboard/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = 'CardFooter';
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/dashboard/src/components/ui/checkbox.tsx b/dashboard/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..96c99d1
--- /dev/null
+++ b/dashboard/src/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import * as React from 'react';
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import { Check } from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export { Checkbox };
diff --git a/dashboard/src/components/ui/dropdown-menu.tsx b/dashboard/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..f50a704
--- /dev/null
+++ b/dashboard/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,200 @@
+'use client';
+
+import * as React from 'react';
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { Check, ChevronRight, Circle } from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+}
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+}
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+}
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+ }: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/dashboard/src/components/ui/form.tsx b/dashboard/src/components/ui/form.tsx
new file mode 100644
index 0000000..6560780
--- /dev/null
+++ b/dashboard/src/components/ui/form.tsx
@@ -0,0 +1,169 @@
+import * as React from 'react';
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { Slot } from '@radix-ui/react-slot';
+import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form';
+
+import { cn } from '@/lib/utils';
+import { Label } from '@/components/ui/label';
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue,
+);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+ }: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
+
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ if (!fieldContext) {
+ throw new Error('useFormField should be used within ');
+ }
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ };
+};
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue,
+);
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+});
+FormItem.displayName = 'FormItem';
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField();
+
+ return (
+
+ );
+});
+FormLabel.displayName = 'FormLabel';
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
+
+ return (
+
+ );
+});
+FormControl.displayName = 'FormControl';
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+});
+FormDescription.displayName = 'FormDescription';
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+});
+FormMessage.displayName = 'FormMessage';
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+};
diff --git a/dashboard/src/components/ui/input.tsx b/dashboard/src/components/ui/input.tsx
new file mode 100644
index 0000000..58f31dc
--- /dev/null
+++ b/dashboard/src/components/ui/input.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+export interface InputProps
+ extends React.InputHTMLAttributes {
+}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/dashboard/src/components/ui/label.tsx b/dashboard/src/components/ui/label.tsx
new file mode 100644
index 0000000..0754e9f
--- /dev/null
+++ b/dashboard/src/components/ui/label.tsx
@@ -0,0 +1,26 @@
+'use client';
+
+import * as React from 'react';
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const labelVariants = cva(
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/dashboard/src/components/ui/scroll-area.tsx b/dashboard/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..23fb95b
--- /dev/null
+++ b/dashboard/src/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+'use client';
+
+import * as React from 'react';
+import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
+
+import { cn } from '@/lib/utils';
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'vertical', ...props }, ref) => (
+
+
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/dashboard/src/components/ui/separator.tsx b/dashboard/src/components/ui/separator.tsx
new file mode 100644
index 0000000..0988b83
--- /dev/null
+++ b/dashboard/src/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import * as React from 'react';
+import * as SeparatorPrimitive from '@radix-ui/react-separator';
+
+import { cn } from '@/lib/utils';
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = 'horizontal', decorative = true, ...props },
+ ref,
+ ) => (
+
+ ),
+);
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/dashboard/src/components/ui/skeleton.tsx b/dashboard/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..b6600a7
--- /dev/null
+++ b/dashboard/src/components/ui/skeleton.tsx
@@ -0,0 +1,16 @@
+import { cn } from '@/lib/utils';
+import React from 'react';
+
+function Skeleton({
+ className,
+ ...props
+ }: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/dashboard/src/components/ui/sonner.tsx b/dashboard/src/components/ui/sonner.tsx
new file mode 100644
index 0000000..48ffc11
--- /dev/null
+++ b/dashboard/src/components/ui/sonner.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import { useTheme } from 'next-themes';
+import { Toaster as Sonner } from 'sonner';
+import React from 'react';
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = 'system' } = useTheme();
+
+ return (
+
+ );
+};
+
+export { Toaster };
diff --git a/dashboard/src/components/ui/tabs.tsx b/dashboard/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..3f8cb28
--- /dev/null
+++ b/dashboard/src/components/ui/tabs.tsx
@@ -0,0 +1,55 @@
+'use client';
+
+import * as React from 'react';
+import * as TabsPrimitive from '@radix-ui/react-tabs';
+
+import { cn } from '@/lib/utils';
+
+const Tabs = TabsPrimitive.Root;
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/dashboard/src/lib/utils.ts b/dashboard/src/lib/utils.ts
new file mode 100644
index 0000000..dd53ea8
--- /dev/null
+++ b/dashboard/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/dashboard/src/ory/hooks.tsx b/dashboard/src/ory/hooks.tsx
new file mode 100644
index 0000000..fa4797f
--- /dev/null
+++ b/dashboard/src/ory/hooks.tsx
@@ -0,0 +1,43 @@
+'use client';
+
+import { AxiosError } from 'axios';
+import { DependencyList, useEffect, useState } from 'react';
+
+import { kratos } from './sdk/kratos';
+
+// Returns a function which will log the user out
+export function LogoutLink(deps?: DependencyList) {
+
+ const [logoutToken, setLogoutToken] = useState('');
+
+ useEffect(() => {
+ kratos
+ .createBrowserLogoutFlow()
+ .then(({ data }) => {
+ setLogoutToken(data.logout_token);
+ })
+ .catch((err: AxiosError) => {
+ switch (err.response?.status) {
+ case 401:
+ // do nothing, the user is not logged in
+ return;
+ }
+
+ // Something else happened!
+ return Promise.reject(err);
+ });
+ }, deps);
+
+ return () => {
+ if (logoutToken) {
+
+ const url = process.env.NEXT_PUBLIC_AUTHENTICATION_NODE_URL +
+ '/flow/login?return_to=' +
+ process.env.NEXT_PUBLIC_DASHBOARD_NODE_URL;
+
+ kratos
+ .updateLogoutFlow({ token: logoutToken })
+ .then(() => window.location.href = url);
+ }
+ };
+}
diff --git a/dashboard/src/ory/index.ts b/dashboard/src/ory/index.ts
new file mode 100644
index 0000000..c6f14a3
--- /dev/null
+++ b/dashboard/src/ory/index.ts
@@ -0,0 +1,2 @@
+export * from './hooks';
+export * from './sdk/kratos';
diff --git a/dashboard/src/ory/sdk/hydra/index.ts b/dashboard/src/ory/sdk/hydra/index.ts
new file mode 100644
index 0000000..335cd85
--- /dev/null
+++ b/dashboard/src/ory/sdk/hydra/index.ts
@@ -0,0 +1,15 @@
+'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,
+ },
+ }),
+ ));
+}
\ No newline at end of file
diff --git a/dashboard/src/ory/sdk/kratos/index.ts b/dashboard/src/ory/sdk/kratos/index.ts
new file mode 100644
index 0000000..e40e331
--- /dev/null
+++ b/dashboard/src/ory/sdk/kratos/index.ts
@@ -0,0 +1,14 @@
+'use client';
+
+import { Configuration, FrontendApi } from '@ory/client';
+
+const kratos = new FrontendApi(
+ new Configuration({
+ basePath: process.env.NEXT_PUBLIC_ORY_KRATOS_URL,
+ baseOptions: {
+ withCredentials: true,
+ },
+ }),
+);
+
+export { kratos };
diff --git a/dashboard/tailwind.config.ts b/dashboard/tailwind.config.ts
new file mode 100644
index 0000000..57a411d
--- /dev/null
+++ b/dashboard/tailwind.config.ts
@@ -0,0 +1,85 @@
+import type { Config } from 'tailwindcss';
+
+const config = {
+ darkMode: ['class'],
+ content: [
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}',
+ ],
+ prefix: '',
+ theme: {
+ container: {
+ center: true,
+ padding: '2rem',
+ screens: {
+ '2xl': '1400px',
+ },
+ },
+ extend: {
+ colors: {
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))',
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))',
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: { height: '0' },
+ to: { height: 'var(--radix-accordion-content-height)' },
+ },
+ 'accordion-up': {
+ from: { height: 'var(--radix-accordion-content-height)' },
+ to: { height: '0' },
+ },
+ fadeIn: {
+ '0%': { opacity: '0' },
+ '100%': { opacity: '1' },
+ },
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ 'fadeIn': 'fadeIn 0.25s ease-in-out',
+ },
+ },
+ },
+ plugins: [require('tailwindcss-animate')],
+} satisfies Config;
+
+export default config;
diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json
new file mode 100644
index 0000000..6578253
--- /dev/null
+++ b/dashboard/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext",
+ "webworker"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ },
+ "types": [
+ "@serwist/next/typings"
+ ],
+ "target": "ES2017"
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/docker/ory-dev/ory/kratos/kratos.yaml b/docker/ory-dev/ory/kratos/kratos.yaml
index bd51dcc..0913a29 100644
--- a/docker/ory-dev/ory/kratos/kratos.yaml
+++ b/docker/ory-dev/ory/kratos/kratos.yaml
@@ -27,6 +27,7 @@ serve:
enabled: true
allowed_origins:
- http://localhost:3000
+ - http://localhost:4000
admin:
base_url: http://localhost:4434
@@ -47,6 +48,7 @@ selfservice:
default_browser_return_url: http://localhost:3000
allowed_return_urls:
- http://localhost:3000
+ - http://localhost:4000
methods:
password: