From 66a415b0ddd2b0a5bbd24c9091a48afcdb4eac54 Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Wed, 2 Jul 2025 02:24:35 +0000 Subject: [PATCH] internal frontend implementation with keycloak authentication --- .gitlab-ci-template.yml | 2 + docker-compose.yml | 2 + frontend/.gitlab-ci.yml | 5 - internal_frontend/.gitlab-ci.yml | 16 + .../app/api/auth/[...nextauth]/route.ts | 5 + internal_frontend/app/apps/page.tsx | 7 + .../demo/kanzlei/bilanzbuchhalter/page.tsx | 7 + internal_frontend/app/demo/kanzlei/page.tsx | 7 + .../app/demo/kanzlei/rechtsanwalt/page.tsx | 7 + .../app/demo/kanzlei/steuer/page.tsx | 7 + internal_frontend/app/demo/settings/page.tsx | 10 + internal_frontend/app/globals.css | 124 +- internal_frontend/app/layout.tsx | 88 +- internal_frontend/components.json | 21 + internal_frontend/components/app-sidebar.tsx | 173 +++ .../components/dynamic-breadcrumb.tsx | 42 + internal_frontend/components/login-screen.tsx | 22 + .../components/theme-provider.tsx | 11 + .../components/ui/breadcrumb.tsx | 109 ++ internal_frontend/components/ui/button.tsx | 59 + internal_frontend/components/ui/card.tsx | 92 ++ .../components/ui/collapsible.tsx | 33 + .../components/ui/dropdown-menu.tsx | 257 ++++ internal_frontend/components/ui/input-otp.tsx | 77 ++ internal_frontend/components/ui/input.tsx | 21 + internal_frontend/components/ui/separator.tsx | 28 + internal_frontend/components/ui/sheet.tsx | 139 ++ internal_frontend/components/ui/sidebar.tsx | 726 +++++++++++ internal_frontend/components/ui/skeleton.tsx | 13 + internal_frontend/components/ui/tooltip.tsx | 61 + internal_frontend/hooks/use-mobile.ts | 19 + internal_frontend/lib/auth/authOptions.ts | 86 ++ internal_frontend/lib/breadcrumb-map.ts | 8 + internal_frontend/lib/utils.ts | 6 + internal_frontend/package-lock.json | 1153 ++++++++++++++++- internal_frontend/package.json | 25 +- internal_frontend/tsconfig.json | 22 +- internal_frontend/types/auth.d.ts | 16 + internal_frontend/utils/BreadcrumbUtils.ts | 28 + 39 files changed, 3475 insertions(+), 59 deletions(-) create mode 100644 internal_frontend/app/api/auth/[...nextauth]/route.ts create mode 100644 internal_frontend/app/apps/page.tsx create mode 100644 internal_frontend/app/demo/kanzlei/bilanzbuchhalter/page.tsx create mode 100644 internal_frontend/app/demo/kanzlei/page.tsx create mode 100644 internal_frontend/app/demo/kanzlei/rechtsanwalt/page.tsx create mode 100644 internal_frontend/app/demo/kanzlei/steuer/page.tsx create mode 100644 internal_frontend/app/demo/settings/page.tsx create mode 100644 internal_frontend/components.json create mode 100644 internal_frontend/components/app-sidebar.tsx create mode 100644 internal_frontend/components/dynamic-breadcrumb.tsx create mode 100644 internal_frontend/components/login-screen.tsx create mode 100644 internal_frontend/components/theme-provider.tsx create mode 100644 internal_frontend/components/ui/breadcrumb.tsx create mode 100644 internal_frontend/components/ui/button.tsx create mode 100644 internal_frontend/components/ui/card.tsx create mode 100644 internal_frontend/components/ui/collapsible.tsx create mode 100644 internal_frontend/components/ui/dropdown-menu.tsx create mode 100644 internal_frontend/components/ui/input-otp.tsx create mode 100644 internal_frontend/components/ui/input.tsx create mode 100644 internal_frontend/components/ui/separator.tsx create mode 100644 internal_frontend/components/ui/sheet.tsx create mode 100644 internal_frontend/components/ui/sidebar.tsx create mode 100644 internal_frontend/components/ui/skeleton.tsx create mode 100644 internal_frontend/components/ui/tooltip.tsx create mode 100644 internal_frontend/hooks/use-mobile.ts create mode 100644 internal_frontend/lib/auth/authOptions.ts create mode 100644 internal_frontend/lib/breadcrumb-map.ts create mode 100644 internal_frontend/lib/utils.ts create mode 100644 internal_frontend/types/auth.d.ts create mode 100644 internal_frontend/utils/BreadcrumbUtils.ts diff --git a/.gitlab-ci-template.yml b/.gitlab-ci-template.yml index 897448a..e77af38 100644 --- a/.gitlab-ci-template.yml +++ b/.gitlab-ci-template.yml @@ -137,11 +137,13 @@ sed -i "s|registry.boomlab.party/rheinsw/rheinsw-mono-repo/internal_frontend|registry.boomlab.party/rheinsw/rheinsw-mono-repo/internal_frontend@$(cat digest-internal_frontend.txt)|g" docker-compose.generated.yml echo "Copying docker-compose.generated.yml to $HOST:$REMOTE_ENV_PATH/docker-compose.yml" + # Ensure remote path exists before scp ssh -p "$PORT" "$DEPLOY_USER@$HOST" "mkdir -p $REMOTE_ENV_PATH" # Copy scp -P "$PORT" docker-compose.generated.yml "$DEPLOY_USER@$HOST:$REMOTE_ENV_PATH/docker-compose.yml" + scp -P "$PORT" internal_frontend/.env "$DEPLOY_USER@$HOST:$REMOTE_ENV_PATH/internal_frontend.env" echo "Deploying on $HOST" ssh -p "$PORT" "$DEPLOY_USER@$HOST" " diff --git a/docker-compose.yml b/docker-compose.yml index eec2cdb..fbb28e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,8 @@ services: internal_frontend: image: registry.boomlab.party/rheinsw/rheinsw-mono-repo/internal_frontend container_name: internal_frontend + env_file: + - ./internal_frontend.env ports: - "5101:3000" restart: on-failure diff --git a/frontend/.gitlab-ci.yml b/frontend/.gitlab-ci.yml index 44f54b9..c354540 100644 --- a/frontend/.gitlab-ci.yml +++ b/frontend/.gitlab-ci.yml @@ -10,11 +10,6 @@ build_frontend: script: - | cd frontend - echo "NEXT_PUBLIC_HCAPTCHA_SITE_KEY=$HCAPTCHA_SITE_KEY" > .env - echo "NEXT_PUBLIC_RECAPTCHA_SITE_KEY=$RECAPTCHA_SITE_KEY" >> .env - echo "HCAPTCHA_SECRET=$HCAPTCHA_SECRET" >> .env - echo "Contents of .env file:" - cat .env npm install npx next build artifacts: diff --git a/internal_frontend/.gitlab-ci.yml b/internal_frontend/.gitlab-ci.yml index 72ae43b..af8e29f 100644 --- a/internal_frontend/.gitlab-ci.yml +++ b/internal_frontend/.gitlab-ci.yml @@ -10,10 +10,26 @@ build_internal_frontend: script: - | cd internal_frontend + echo "# environment file for internal_frontend" > .env + if [ "$CI_COMMIT_REF_NAME" = "production" ]; then + echo "NEXTAUTH_URL=$NEXTAUTH_URL_PROD" >> .env + echo "NEXTAUTH_SECRET=$NEXTAUTH_SECRET_PROD" >> .env + echo "KEYCLOAK_CLIENT_ID=$KEYCLOAK_CLIENT_ID_PROD" >> .env + echo "KEYCLOAK_CLIENT_SECRET=$KEYCLOAK_CLIENT_SECRET_PROD" >> .env + else + echo "NEXTAUTH_URL=$NEXTAUTH_URL_TEST" >> .env + echo "NEXTAUTH_SECRET=$NEXTAUTH_SECRET_TEST" >> .env + echo "KEYCLOAK_CLIENT_ID=$KEYCLOAK_CLIENT_ID_TEST" >> .env + echo "KEYCLOAK_CLIENT_SECRET=$KEYCLOAK_CLIENT_SECRET_TEST" >> .env + fi + echo "KEYCLOAK_ISSUER=$KEYCLOAK_ISSUER" >> .env + echo "Contents of .env file:" + cat .env npm install npx next build artifacts: paths: + - internal_frontend/.env - internal_frontend/.next - internal_frontend/public - internal_frontend/package.json diff --git a/internal_frontend/app/api/auth/[...nextauth]/route.ts b/internal_frontend/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..fe6ebe0 --- /dev/null +++ b/internal_frontend/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,5 @@ +import NextAuth from "next-auth"; +import {authOptions} from "@/lib/auth/authOptions"; + +const handler = NextAuth(authOptions); +export {handler as GET, handler as POST}; diff --git a/internal_frontend/app/apps/page.tsx b/internal_frontend/app/apps/page.tsx new file mode 100644 index 0000000..a332282 --- /dev/null +++ b/internal_frontend/app/apps/page.tsx @@ -0,0 +1,7 @@ +export default function Home() { + return ( +
+ apps +
+ ); +} diff --git a/internal_frontend/app/demo/kanzlei/bilanzbuchhalter/page.tsx b/internal_frontend/app/demo/kanzlei/bilanzbuchhalter/page.tsx new file mode 100644 index 0000000..c47a85d --- /dev/null +++ b/internal_frontend/app/demo/kanzlei/bilanzbuchhalter/page.tsx @@ -0,0 +1,7 @@ +export default function Home() { + return ( +
+ Bilanzbuchhalter +
+ ); +} diff --git a/internal_frontend/app/demo/kanzlei/page.tsx b/internal_frontend/app/demo/kanzlei/page.tsx new file mode 100644 index 0000000..7d015cf --- /dev/null +++ b/internal_frontend/app/demo/kanzlei/page.tsx @@ -0,0 +1,7 @@ +export default function Home() { + return ( +
+ Kanzlei +
+ ); +} diff --git a/internal_frontend/app/demo/kanzlei/rechtsanwalt/page.tsx b/internal_frontend/app/demo/kanzlei/rechtsanwalt/page.tsx new file mode 100644 index 0000000..cfda556 --- /dev/null +++ b/internal_frontend/app/demo/kanzlei/rechtsanwalt/page.tsx @@ -0,0 +1,7 @@ +export default function Home() { + return ( +
+ Rechtsanwalt +
+ ); +} diff --git a/internal_frontend/app/demo/kanzlei/steuer/page.tsx b/internal_frontend/app/demo/kanzlei/steuer/page.tsx new file mode 100644 index 0000000..4ebdb39 --- /dev/null +++ b/internal_frontend/app/demo/kanzlei/steuer/page.tsx @@ -0,0 +1,7 @@ +export default function Home() { + return ( +
+ Steuer +
+ ); +} diff --git a/internal_frontend/app/demo/settings/page.tsx b/internal_frontend/app/demo/settings/page.tsx new file mode 100644 index 0000000..3e25776 --- /dev/null +++ b/internal_frontend/app/demo/settings/page.tsx @@ -0,0 +1,10 @@ +import {SidebarGroupLabel} from "@/components/ui/sidebar"; + +export default function Home() { + return ( +
+ Documents + Settings +
+ ); +} diff --git a/internal_frontend/app/globals.css b/internal_frontend/app/globals.css index a2dc41e..4fced5d 100644 --- a/internal_frontend/app/globals.css +++ b/internal_frontend/app/globals.css @@ -1,26 +1,122 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.13 0.028 261.692); + --card: oklch(1 0 0); + --card-foreground: oklch(0.13 0.028 261.692); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.13 0.028 261.692); + --primary: oklch(0.21 0.034 264.665); + --primary-foreground: oklch(0.985 0.002 247.839); + --secondary: oklch(0.967 0.003 264.542); + --secondary-foreground: oklch(0.21 0.034 264.665); + --muted: oklch(0.967 0.003 264.542); + --muted-foreground: oklch(0.551 0.027 264.364); + --accent: oklch(0.967 0.003 264.542); + --accent-foreground: oklch(0.21 0.034 264.665); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.928 0.006 264.531); + --input: oklch(0.928 0.006 264.531); + --ring: oklch(0.707 0.022 261.325); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0.002 247.839); + --sidebar-foreground: oklch(0.13 0.028 261.692); + --sidebar-primary: oklch(0.21 0.034 264.665); + --sidebar-primary-foreground: oklch(0.985 0.002 247.839); + --sidebar-accent: oklch(0.967 0.003 264.542); + --sidebar-accent-foreground: oklch(0.21 0.034 264.665); + --sidebar-border: oklch(0.928 0.006 264.531); + --sidebar-ring: oklch(0.707 0.022 261.325); +} + +.dark { + --background: oklch(0.13 0.028 261.692); + --foreground: oklch(0.985 0.002 247.839); + --card: oklch(0.21 0.034 264.665); + --card-foreground: oklch(0.985 0.002 247.839); + --popover: oklch(0.21 0.034 264.665); + --popover-foreground: oklch(0.985 0.002 247.839); + --primary: oklch(0.928 0.006 264.531); + --primary-foreground: oklch(0.21 0.034 264.665); + --secondary: oklch(0.278 0.033 256.848); + --secondary-foreground: oklch(0.985 0.002 247.839); + --muted: oklch(0.278 0.033 256.848); + --muted-foreground: oklch(0.707 0.022 261.325); + --accent: oklch(0.278 0.033 256.848); + --accent-foreground: oklch(0.985 0.002 247.839); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.034 264.665); + --sidebar-foreground: oklch(0.985 0.002 247.839); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0.002 247.839); + --sidebar-accent: oklch(0.278 0.033 256.848); + --sidebar-accent-foreground: oklch(0.985 0.002 247.839); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } } - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/internal_frontend/app/layout.tsx b/internal_frontend/app/layout.tsx index f7fa87e..0063488 100644 --- a/internal_frontend/app/layout.tsx +++ b/internal_frontend/app/layout.tsx @@ -1,34 +1,68 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import type {Metadata} from "next"; import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +import {ThemeProvider} from "@/components/theme-provider"; +import {SidebarInset, SidebarProvider, SidebarTrigger} from "@/components/ui/sidebar"; +import {AppSidebar} from "@/components/app-sidebar"; +import React from "react"; +import {Separator} from "@/components/ui/separator"; +import {DynamicBreadcrumb} from "@/components/dynamic-breadcrumb"; +import {getServerSession} from "next-auth"; +import LoginScreen from "@/components/login-screen"; +import {authOptions} from "@/lib/auth/authOptions"; export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Internal | Rhein Software", + description: "Internal Tools for Rhein Software Development", }; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; +export default async function RootLayout({ + children, + }: Readonly<{ + children: React.ReactNode; }>) { - return ( - - - {children} - - - ); + const session = await getServerSession(authOptions); + + return ( + + + + {session?.accessToken ? ( + + +
+ +
+
+ + + +
+
+
+ {children} +
+
+ ) : ( + + )} +
+ + + ); } diff --git a/internal_frontend/components.json b/internal_frontend/components.json new file mode 100644 index 0000000..8de3dce --- /dev/null +++ b/internal_frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "gray", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/internal_frontend/components/app-sidebar.tsx b/internal_frontend/components/app-sidebar.tsx new file mode 100644 index 0000000..279084a --- /dev/null +++ b/internal_frontend/components/app-sidebar.tsx @@ -0,0 +1,173 @@ +import { + AppWindowIcon, + ChevronUp, + ChevronRight, + Home, + Scale, + User2, + Settings +} from "lucide-react"; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubItem, + SidebarMenuSubButton, +} from "@/components/ui/sidebar"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import { + Collapsible, + CollapsibleTrigger, + CollapsibleContent, +} from "@/components/ui/collapsible"; + +const rheinItems = [ + { + title: "Dashboard", + url: "/", + icon: Home, + }, + { + title: "Apps", + url: "/apps", + icon: AppWindowIcon, + }, +]; + +export function AppSidebar() { + return ( + + + + {/* Rhein section */} + + Rhein Software Development + + + {rheinItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + {/* Demos section */} + + Demos + + + + + + + + + Demo Settings + + + + + + + + + + + + Kanzlei + + + + + + + + + + Steuer + + + + + Rechtsanwalt + + + + + Bilanzbuchhalter + + + + + + + + + + + {/* Footer with user dropdown */} + + + + + + + + Username + + + + + + Account + + + Billing + + + Sign out + + + + + + + + ); +} diff --git a/internal_frontend/components/dynamic-breadcrumb.tsx b/internal_frontend/components/dynamic-breadcrumb.tsx new file mode 100644 index 0000000..623bb49 --- /dev/null +++ b/internal_frontend/components/dynamic-breadcrumb.tsx @@ -0,0 +1,42 @@ +// components/dynamic-breadcrumb.tsx +'use client'; + +import {usePathname} from 'next/navigation'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import React from 'react'; +import {getBreadcrumbs} from "@/utils/BreadcrumbUtils"; + +export function DynamicBreadcrumb() { + const pathname = usePathname(); + const breadcrumbs = getBreadcrumbs(pathname); + + return ( + + + {breadcrumbs.map((breadcrumb, index) => ( + + + {breadcrumb.isCurrentPage ? ( + {breadcrumb.label} + ) : ( + + {breadcrumb.label} + + )} + + {index < breadcrumbs.length - 1 && ( + + )} + + ))} + + + ); +} \ No newline at end of file diff --git a/internal_frontend/components/login-screen.tsx b/internal_frontend/components/login-screen.tsx new file mode 100644 index 0000000..8467ae4 --- /dev/null +++ b/internal_frontend/components/login-screen.tsx @@ -0,0 +1,22 @@ +'use client'; + +import {useEffect} from "react"; +import {signIn} from "next-auth/react"; +import {Loader2} from "lucide-react"; // optional loading spinner +import {cn} from "@/lib/utils"; // optional: your className utility + +export default function LoginScreen() { + useEffect(() => { + // Immediately redirect to Keycloak + signIn("keycloak"); + }, []); + + return ( +
+
+ +

Leite zur Anmeldung weiter ...

+
+
+ ); +} diff --git a/internal_frontend/components/theme-provider.tsx b/internal_frontend/components/theme-provider.tsx new file mode 100644 index 0000000..307725e --- /dev/null +++ b/internal_frontend/components/theme-provider.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as React from "react" +import {ThemeProvider as NextThemesProvider} from "next-themes" + +export function ThemeProvider({ + children, + ...props + }: Readonly>) { + return {children} +} \ No newline at end of file diff --git a/internal_frontend/components/ui/breadcrumb.tsx b/internal_frontend/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..eb88f32 --- /dev/null +++ b/internal_frontend/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return