From eb44360cdac3f67a59c29360f5839b84b71a97cf Mon Sep 17 00:00:00 2001 From: f-s-h Date: Thu, 11 Jun 2026 22:06:52 +0200 Subject: [PATCH 01/19] Implemented local ollama support --- services/py-genai-helper/app.py | 12 ++++++++++++ services/py-genai-helper/rag.py | 19 +++++++++++++------ services/py-genai-helper/service.py | 4 ++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/services/py-genai-helper/app.py b/services/py-genai-helper/app.py index f8ccd5a..f0fe683 100644 --- a/services/py-genai-helper/app.py +++ b/services/py-genai-helper/app.py @@ -27,3 +27,15 @@ def rag_response(): response = generate_rag_response(question) return {"response": response}, 200 + +@app.route("/rag-response-local", methods=["POST"]) +def rag_response(): + # Get the json of the object. force=True ignores the stated MimeType + data = request.get_json(force=True) or {} + question = data.get("question") + + if not question: + return {"error": "Missing required field: 'question'"}, 400 + + response = generate_rag_response(question, local=True) + return {"response": response}, 200 \ No newline at end of file diff --git a/services/py-genai-helper/rag.py b/services/py-genai-helper/rag.py index 903d516..712b34f 100644 --- a/services/py-genai-helper/rag.py +++ b/services/py-genai-helper/rag.py @@ -5,17 +5,16 @@ from langchain_community.document_loaders import PyPDFLoader from langchain_community.vectorstores import FAISS from langchain_core.tools import create_retriever_tool +from langchain_ollama import ChatOllama, OllamaEmbeddings from langchain_openai import OpenAIEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter load_dotenv() -embeddings = OpenAIEmbeddings(model="text-embedding-3-large") - _FILE_STORAGE = Path(__file__).parent / "file-storage" -def _load_pdfs() -> FAISS | None: +def _load_pdfs(embeddings) -> FAISS | None: pdf_files = list(_FILE_STORAGE.glob("*.pdf")) if not pdf_files: return None @@ -29,10 +28,18 @@ def _load_pdfs() -> FAISS | None: return FAISS.from_documents(docs, embedding=embeddings) -vector_store = _load_pdfs() +_local_vector_store = _load_pdfs(OllamaEmbeddings(model="nomic-embed-text")) +_remote_vector_store = _load_pdfs(OpenAIEmbeddings(model="text-embedding-3-large")) + +def get_rag_agent(local: bool): + if local: + vector_store = _local_vector_store + model = ChatOllama(model="llama3.2") + else: + vector_store = _remote_vector_store + model = "gpt-4.1-mini" -def get_rag_agent(): if vector_store is None: raise RuntimeError("No PDFs found in file-storage/") @@ -45,7 +52,7 @@ def get_rag_agent(): ) rag_agent = create_agent( - model="gpt-4.1-mini", + model=model, tools=[retriever_tool], system_prompt=( "You are a helpful assistant." diff --git a/services/py-genai-helper/service.py b/services/py-genai-helper/service.py index 187b16f..410f17b 100644 --- a/services/py-genai-helper/service.py +++ b/services/py-genai-helper/service.py @@ -22,7 +22,7 @@ def hello(): return response["messages"][-1].content -def generate_rag_response(question): - rag_agent = get_rag_agent() +def generate_rag_response(question, local=False): + rag_agent = get_rag_agent(local) response = rag_agent.invoke({"messages": [{"role": "user", "content": question}]}) return response["messages"][-1].content From cdc5ec2d51a551a9e46c7f81336e366047f6aa39 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 02/19] chore: add tanstack query, zustand, react-hook-form, and zod deps --- web-client/package.json | 10 +++- web-client/pnpm-lock.yaml | 99 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/web-client/package.json b/web-client/package.json index 1358ef7..db0c719 100644 --- a/web-client/package.json +++ b/web-client/package.json @@ -22,8 +22,11 @@ "dependencies": { "@fontsource/bebas-neue": "^5.2.7", "@fontsource/poppins": "^5.2.7", + "@hookform/resolvers": "^5.4.0", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-query": "^5", + "@tanstack/react-query-devtools": "^5.101.0", "axios": "^1.16.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -32,13 +35,16 @@ "radix-ui": "^1.4.3", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-hook-form": "^7.77.0", "react-router-dom": "^7.15.1", "rolldown": "^1.0.2", "rollup": "^4.60.4", - "tslib": "^2.8.1", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.0", - "tw-animate-css": "^1.4.0" + "tslib": "^2.8.1", + "tw-animate-css": "^1.4.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/web-client/pnpm-lock.yaml b/web-client/pnpm-lock.yaml index d681a79..74fdb89 100644 --- a/web-client/pnpm-lock.yaml +++ b/web-client/pnpm-lock.yaml @@ -14,12 +14,21 @@ importers: '@fontsource/poppins': specifier: ^5.2.7 version: 5.2.7 + '@hookform/resolvers': + specifier: ^5.4.0 + version: 5.4.0(react-hook-form@7.77.0(react@19.2.6)) '@radix-ui/react-slot': specifier: ^1.2.4 version: 1.2.4(@types/react@19.2.14)(react@19.2.6) '@tailwindcss/vite': specifier: ^4.3.0 version: 4.3.0(vite@8.0.13(@types/node@24.12.4)(jiti@2.7.0)) + '@tanstack/react-query': + specifier: ^5 + version: 5.101.0(react@19.2.6) + '@tanstack/react-query-devtools': + specifier: ^5.101.0 + version: 5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6) axios: specifier: ^1.16.1 version: 1.16.1 @@ -44,6 +53,9 @@ importers: react-dom: specifier: ^19.2.6 version: 19.2.6(react@19.2.6) + react-hook-form: + specifier: ^7.77.0 + version: 7.77.0(react@19.2.6) react-router-dom: specifier: ^7.15.1 version: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -65,6 +77,12 @@ importers: tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + zod: + specifier: ^4.4.3 + version: 4.4.3 + zustand: + specifier: ^5.0.14 + version: 5.0.14(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) devDependencies: '@eslint/js': specifier: ^10.0.1 @@ -561,6 +579,11 @@ packages: peerDependencies: hono: ^4 + '@hookform/resolvers@5.4.0': + resolution: {integrity: sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1728,6 +1751,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@tailwindcss/node@4.3.0': resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} @@ -1818,6 +1844,23 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/query-core@5.101.0': + resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} + + '@tanstack/query-devtools@5.101.0': + resolution: {integrity: sha512-MVqw17k08RQtGGLEL654+dX/btbX9p/8WjkznO//zusLTMaObxi3Q+MoFwGVkC9K3tqjn8qrrNhJevXx4fJTeQ==} + + '@tanstack/react-query-devtools@5.101.0': + resolution: {integrity: sha512-cpZA0+WqKXwrwMfiWZEGGF6QrIWVQFbhBtxqDF5sQsAfrFf47HIE6fiPbQU3wyAUEN2+7UNqLCQe7oG6m3f93w==} + peerDependencies: + '@tanstack/react-query': ^5.101.0 + react: ^18 || ^19 + + '@tanstack/react-query@5.101.0': + resolution: {integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==} + peerDependencies: + react: ^18 || ^19 + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -3354,6 +3397,12 @@ packages: peerDependencies: react: ^19.2.6 + react-hook-form@7.77.0: + resolution: {integrity: sha512-Sslh9YDYc0GDlWT/lxasnIduNo4v3yyvqRGvmGKUre5AFjDs/HV9/OafHGD8d+sB2yoL4UIL9L8X9i0WlZZebg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -4032,6 +4081,24 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@ampproject/remapping@2.3.0': @@ -4482,6 +4549,11 @@ snapshots: dependencies: hono: 4.12.23 + '@hookform/resolvers@5.4.0(react-hook-form@7.77.0(react@19.2.6))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.77.0(react@19.2.6) + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -5587,6 +5659,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/utils@0.3.0': {} + '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 @@ -5655,6 +5729,21 @@ snapshots: tailwindcss: 4.3.0 vite: 8.0.13(@types/node@24.12.4)(jiti@2.7.0) + '@tanstack/query-core@5.101.0': {} + + '@tanstack/query-devtools@5.101.0': {} + + '@tanstack/react-query-devtools@5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/query-devtools': 5.101.0 + '@tanstack/react-query': 5.101.0(react@19.2.6) + react: 19.2.6 + + '@tanstack/react-query@5.101.0(react@19.2.6)': + dependencies: + '@tanstack/query-core': 5.101.0 + react: 19.2.6 + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -7229,6 +7318,10 @@ snapshots: react: 19.2.6 scheduler: 0.27.0 + react-hook-form@7.77.0(react@19.2.6): + dependencies: + react: 19.2.6 + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.6): dependencies: react: 19.2.6 @@ -7904,3 +7997,9 @@ snapshots: zod@3.25.76: {} zod@4.4.3: {} + + zustand@5.0.14(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) From d741c648b7b7a4b34e2201f004425b107582d01d Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 03/19] feat: add app shell layout and migrate to route-object router --- web-client/src/App.tsx | 9 +- web-client/src/app/layout/AppShell.tsx | 79 ++++++++ web-client/src/app/router/AppRouter.tsx | 241 ------------------------ web-client/src/app/router/routes.tsx | 26 +++ web-client/src/main.tsx | 29 ++- 5 files changed, 134 insertions(+), 250 deletions(-) create mode 100644 web-client/src/app/layout/AppShell.tsx delete mode 100644 web-client/src/app/router/AppRouter.tsx create mode 100644 web-client/src/app/router/routes.tsx diff --git a/web-client/src/App.tsx b/web-client/src/App.tsx index 872168e..5b18815 100644 --- a/web-client/src/App.tsx +++ b/web-client/src/App.tsx @@ -1,7 +1,6 @@ -import { AppRouter } from '@/app/router/AppRouter' +import { RouterProvider } from 'react-router-dom' +import { router } from '@/app/router/routes' -function App() { - return +export default function App() { + return } - -export default App diff --git a/web-client/src/app/layout/AppShell.tsx b/web-client/src/app/layout/AppShell.tsx new file mode 100644 index 0000000..cd4ba32 --- /dev/null +++ b/web-client/src/app/layout/AppShell.tsx @@ -0,0 +1,79 @@ +import { NavLink, Outlet } from 'react-router-dom' +import { LayoutGrid } from 'lucide-react' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { ThemeToggle } from '@/app/theme/ThemeToggle' +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarProvider, + SidebarTrigger, +} from '@/components/ui/sidebar' + +const NAV_ITEMS = [ + { to: '/members', label: 'Members' }, + { to: '/sport-events', label: 'Sport Events' }, + { to: '/payments', label: 'Payments' }, + { to: '/letters', label: 'Letters' }, + { to: '/organization', label: 'Organization' }, + { to: '/feedback', label: 'Feedback' }, + { to: '/helper', label: 'GenAI Helper' }, +] + +export function AppShell() { + return ( + + + +
+ + Sports Club Platform +
+
+

+ Team Devoops +

+
+
+ + + + {NAV_ITEMS.map(({ to, label }) => ( + + + {({ isActive }) => ( + + {label} + + )} + + + ))} + + + + +
+ +
+
+
+ + +
+ +
+
+ +
+
+ + +
+ ) +} diff --git a/web-client/src/app/router/AppRouter.tsx b/web-client/src/app/router/AppRouter.tsx deleted file mode 100644 index f7047d0..0000000 --- a/web-client/src/app/router/AppRouter.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { useEffect, useState } from 'react' -import { NavLink, Navigate, Route, Routes } from 'react-router-dom' -import { ArrowRight, LayoutGrid, Orbit, Sparkles } from 'lucide-react' -import { getEventsHello } from '@/features/events/api' -import { getFeedbackHello } from '@/features/feedback/api' -import { getLettersHello } from '@/features/letters/api' -import { getMembersHello, getMembersAdminHello } from '@/features/members/api' -import { getOrganizationHello } from '@/features/organization/api' -import { getPaymentsHello } from '@/features/payments/api' -import { ThemeToggle } from '@/app/theme/ThemeToggle' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card' -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarInset, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarProvider, - SidebarTrigger, -} from '@/components/ui/sidebar' - - -type ServicePlaceholderPageProps = { - title: string - loadMessage: () => Promise -} - -function ServicePlaceholderPage({ title, loadMessage }: ServicePlaceholderPageProps) { - const [message, setMessage] = useState(null) - const [error, setError] = useState(null) - const [loading, setLoading] = useState(true) - - useEffect(() => { - let isMounted = true - - loadMessage() - .then((response) => { - if (isMounted) { - setMessage(response) - setError(null) - } - }) - .catch((err: unknown) => { - if (isMounted) { - setMessage(null) - setError(err instanceof Error ? err.message : 'Unknown error') - } - }) - .finally(() => { - if (isMounted) setLoading(false) - }) - - return () => { - isMounted = false - } - }, [loadMessage]) - - return ( -
- - -
- - Connected service -
-
- - {title} - - - The navigation and design system are wired up. This page is still a service - placeholder, but it now lives inside the new Sera-inspired shell and shadcn - components. - -
-
- -
-
-

- Endpoint status -

-
- {loading &&

Loading hello endpoint response...

} - {message &&

{message}

} - {error &&

Failed to load response: {error}

} -
-
-
-
-

- What changed -

-

- DaisyUI primitives are out, semantic tokens and reusable UI building blocks are in. -

-
-
- - Ready for real feature screens using the same component foundation. -
-
-
-
-
- - - - Migration Notes - - The shell is now driven by the Sera theme variables and shadcn components. - - - -
-

- Tokens -

-

Light and dark mode both resolve through CSS variables instead of DaisyUI themes.

-
-
-

- Components -

-

Navigation, cards, and actions use shadcn-style primitives with shared variants.

-
-
-

- Next build step -

-

Feature pages can now expand from this foundation without carrying DaisyUI utility debt forward.

-
-
-
-
- ) -} - -const NAV_ITEMS = [ - { to: '/members', label: 'Members' }, - { to: '/events', label: 'Events' }, - { to: '/payments', label: 'Payments' }, - { to: '/letters', label: 'Letters' }, - { to: '/organization', label: 'Organization' }, - { to: '/feedback', label: 'Feedback' }, -] - -export function AppRouter() { - return ( - - - -
- - Sports Club Platform -
-
-

- Team Devoops -

-
-
- - - - {NAV_ITEMS.map(({ to, label }) => ( - - - {({ isActive }) => ( - - {label} - - )} - - - ))} - - - - -
-
- Active shell - -
- -
-
-
- - -
- -
-
- - } /> - - - - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - -
-
-
- ) -} diff --git a/web-client/src/app/router/routes.tsx b/web-client/src/app/router/routes.tsx new file mode 100644 index 0000000..7899155 --- /dev/null +++ b/web-client/src/app/router/routes.tsx @@ -0,0 +1,26 @@ +import { createBrowserRouter, Navigate } from 'react-router-dom' +import { AppShell } from '@/app/layout/AppShell' +import { MembersPage } from '@/features/members' +import { SportEventsPage } from '@/features/sport-events' +import { PaymentsPage } from '@/features/payments' +import { LettersPage } from '@/features/letters' +import { OrganizationPage } from '@/features/organization' +import { FeedbackPage } from '@/features/feedback' +import { HelperPage } from '@/features/helper' + +export const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { index: true, element: }, + { path: 'members', element: }, + { path: 'sport-events', element: }, + { path: 'payments', element: }, + { path: 'letters', element: }, + { path: 'organization', element: }, + { path: 'feedback', element: }, + { path: 'helper', element: }, + ], + }, +]) diff --git a/web-client/src/main.tsx b/web-client/src/main.tsx index a129fd4..c786e9f 100644 --- a/web-client/src/main.tsx +++ b/web-client/src/main.tsx @@ -1,10 +1,31 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { BrowserRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { AxiosError } from 'axios' import keycloak from '@/lib/keycloak' +import { ThemeProvider } from '@/app/theme/ThemeProvider' import '@/index.css' import App from './App.tsx' -import { ThemeProvider } from './app/theme/ThemeProvider' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + retry: (failureCount, error) => { + if ( + error instanceof AxiosError && + error.response && + error.response.status >= 400 && + error.response.status < 500 + ) { + return false + } + return failureCount < 2 + }, + }, + }, +}) keycloak.init({ onLoad: 'login-required', pkceMethod: 'S256' }).then((authenticated) => { if (!authenticated) { @@ -15,9 +36,9 @@ keycloak.init({ onLoad: 'login-required', pkceMethod: 'S256' }).then((authentica createRoot(document.getElementById('root')!).render( - + - + , ) From bd5b48812896c93ffb2b124eb9031596dadd2cd0 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 04/19] feat: add shared types bridge, form lib, zustand store, and ui primitives --- web-client/src/components/ui/ErrorMessage.tsx | 10 ++++++ .../src/components/ui/LoadingSpinner.tsx | 7 +++++ web-client/src/lib/forms.ts | 3 ++ web-client/src/store/ui.ts | 11 +++++++ web-client/src/types.ts | 31 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 web-client/src/components/ui/ErrorMessage.tsx create mode 100644 web-client/src/components/ui/LoadingSpinner.tsx create mode 100644 web-client/src/lib/forms.ts create mode 100644 web-client/src/store/ui.ts create mode 100644 web-client/src/types.ts diff --git a/web-client/src/components/ui/ErrorMessage.tsx b/web-client/src/components/ui/ErrorMessage.tsx new file mode 100644 index 0000000..faf987e --- /dev/null +++ b/web-client/src/components/ui/ErrorMessage.tsx @@ -0,0 +1,10 @@ +import { AlertCircle } from 'lucide-react' + +export function ErrorMessage({ message }: { message: string }) { + return ( +
+ + {message} +
+ ) +} diff --git a/web-client/src/components/ui/LoadingSpinner.tsx b/web-client/src/components/ui/LoadingSpinner.tsx new file mode 100644 index 0000000..0bb0c4a --- /dev/null +++ b/web-client/src/components/ui/LoadingSpinner.tsx @@ -0,0 +1,7 @@ +export function LoadingSpinner() { + return ( +
+
+
+ ) +} diff --git a/web-client/src/lib/forms.ts b/web-client/src/lib/forms.ts new file mode 100644 index 0000000..edefd82 --- /dev/null +++ b/web-client/src/lib/forms.ts @@ -0,0 +1,3 @@ +export { useForm } from 'react-hook-form' +export { zodResolver } from '@hookform/resolvers/zod' +export { z } from 'zod' diff --git a/web-client/src/store/ui.ts b/web-client/src/store/ui.ts new file mode 100644 index 0000000..8337041 --- /dev/null +++ b/web-client/src/store/ui.ts @@ -0,0 +1,11 @@ +import { create } from 'zustand' + +interface UIState { + activeTab: string | null + setActiveTab: (tab: string | null) => void +} + +export const useUIStore = create((set) => ({ + activeTab: null, + setActiveTab: (tab) => set({ activeTab: tab }), +})) diff --git a/web-client/src/types.ts b/web-client/src/types.ts new file mode 100644 index 0000000..ca5bb0b --- /dev/null +++ b/web-client/src/types.ts @@ -0,0 +1,31 @@ +import type { components } from './api' + +type S = components['schemas'] + +export type Member = S['Member'] +export type MemberSummary = S['MemberSummary'] +export type MemberCreate = S['MemberCreate'] +export type MemberPartialUpdate = S['MemberPartialUpdate'] + +export type SportEvent = S['Event'] +export type EventSummary = S['EventSummary'] +export type EventCreate = S['EventCreate'] +export type EventPartialUpdate = S['EventPartialUpdate'] + +export type Sport = S['Sport'] +export type SportCreate = S['SportCreate'] +export type SportPartialUpdate = S['SportPartialUpdate'] + +export type Team = S['Team'] +export type TeamCreate = S['TeamCreate'] +export type TeamPartialUpdate = S['TeamPartialUpdate'] + +export type Feedback = S['Feedback'] +export type FeedbackSummary = S['FeedbackSummary'] +export type FeedbackCreate = S['FeedbackCreate'] +export type FeedbackPartialUpdate = S['FeedbackPartialUpdate'] + +export type Transaction = S['Transaction'] +export type TransactionCreate = S['TransactionCreate'] +export type TransactionPartialUpdate = S['TransactionPartialUpdate'] +export type Balance = S['Balance'] From f95aacb848175fc6055ed1f3baa24135f19a6637 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 05/19] feat: restructure features into feature-sliced design with query hooks --- .../src/features/feedback/{ => api}/client.ts | 0 .../feedback/{api.ts => api/index.ts} | 6 +- .../src/features/feedback/api/queries.ts | 57 +++++++++ web-client/src/features/feedback/index.ts | 3 + .../features/feedback/pages/FeedbackPage.tsx | 7 ++ .../src/features/feedback/types/index.ts | 1 + web-client/src/features/helper/api/client.ts | 3 + web-client/src/features/helper/api/index.ts | 2 + web-client/src/features/helper/api/queries.ts | 15 +++ web-client/src/features/helper/index.ts | 3 + .../src/features/helper/pages/HelperPage.tsx | 7 ++ web-client/src/features/helper/types/index.ts | 3 + .../src/features/letters/{ => api}/client.ts | 0 .../features/letters/{api.ts => api/index.ts} | 6 +- .../src/features/letters/api/queries.ts | 23 ++++ web-client/src/features/letters/index.ts | 3 + .../features/letters/pages/LettersPage.tsx | 7 ++ .../src/features/letters/types/index.ts | 7 ++ web-client/src/features/members/api.ts | 16 --- .../src/features/members/{ => api}/client.ts | 0 web-client/src/features/members/api/index.ts | 2 + .../src/features/members/api/queries.ts | 57 +++++++++ web-client/src/features/members/index.ts | 3 + .../features/members/pages/MembersPage.tsx | 7 ++ .../src/features/members/types/index.ts | 1 + .../features/organization/{ => api}/client.ts | 0 .../organization/{api.ts => api/index.ts} | 6 +- .../src/features/organization/api/queries.ts | 114 ++++++++++++++++++ web-client/src/features/organization/index.ts | 3 + .../organization/pages/OrganizationPage.tsx | 7 ++ .../src/features/organization/types/index.ts | 1 + .../src/features/payments/{ => api}/client.ts | 0 .../payments/{api.ts => api/index.ts} | 6 +- .../src/features/payments/api/queries.ts | 74 ++++++++++++ web-client/src/features/payments/index.ts | 3 + .../features/payments/pages/PaymentsPage.tsx | 7 ++ .../src/features/payments/types/index.ts | 1 + .../src/features/sport-events/api/client.ts | 3 + .../src/features/sport-events/api/index.ts | 2 + .../src/features/sport-events/api/queries.ts | 57 +++++++++ web-client/src/features/sport-events/index.ts | 3 + .../sport-events/pages/SportEventsPage.tsx | 7 ++ .../src/features/sport-events/types/index.ts | 1 + 43 files changed, 514 insertions(+), 20 deletions(-) rename web-client/src/features/feedback/{ => api}/client.ts (100%) rename web-client/src/features/feedback/{api.ts => api/index.ts} (59%) create mode 100644 web-client/src/features/feedback/api/queries.ts create mode 100644 web-client/src/features/feedback/index.ts create mode 100644 web-client/src/features/feedback/pages/FeedbackPage.tsx create mode 100644 web-client/src/features/feedback/types/index.ts create mode 100644 web-client/src/features/helper/api/client.ts create mode 100644 web-client/src/features/helper/api/index.ts create mode 100644 web-client/src/features/helper/api/queries.ts create mode 100644 web-client/src/features/helper/index.ts create mode 100644 web-client/src/features/helper/pages/HelperPage.tsx create mode 100644 web-client/src/features/helper/types/index.ts rename web-client/src/features/letters/{ => api}/client.ts (100%) rename web-client/src/features/letters/{api.ts => api/index.ts} (59%) create mode 100644 web-client/src/features/letters/api/queries.ts create mode 100644 web-client/src/features/letters/index.ts create mode 100644 web-client/src/features/letters/pages/LettersPage.tsx create mode 100644 web-client/src/features/letters/types/index.ts delete mode 100644 web-client/src/features/members/api.ts rename web-client/src/features/members/{ => api}/client.ts (100%) create mode 100644 web-client/src/features/members/api/index.ts create mode 100644 web-client/src/features/members/api/queries.ts create mode 100644 web-client/src/features/members/index.ts create mode 100644 web-client/src/features/members/pages/MembersPage.tsx create mode 100644 web-client/src/features/members/types/index.ts rename web-client/src/features/organization/{ => api}/client.ts (100%) rename web-client/src/features/organization/{api.ts => api/index.ts} (59%) create mode 100644 web-client/src/features/organization/api/queries.ts create mode 100644 web-client/src/features/organization/index.ts create mode 100644 web-client/src/features/organization/pages/OrganizationPage.tsx create mode 100644 web-client/src/features/organization/types/index.ts rename web-client/src/features/payments/{ => api}/client.ts (100%) rename web-client/src/features/payments/{api.ts => api/index.ts} (59%) create mode 100644 web-client/src/features/payments/api/queries.ts create mode 100644 web-client/src/features/payments/index.ts create mode 100644 web-client/src/features/payments/pages/PaymentsPage.tsx create mode 100644 web-client/src/features/payments/types/index.ts create mode 100644 web-client/src/features/sport-events/api/client.ts create mode 100644 web-client/src/features/sport-events/api/index.ts create mode 100644 web-client/src/features/sport-events/api/queries.ts create mode 100644 web-client/src/features/sport-events/index.ts create mode 100644 web-client/src/features/sport-events/pages/SportEventsPage.tsx create mode 100644 web-client/src/features/sport-events/types/index.ts diff --git a/web-client/src/features/feedback/client.ts b/web-client/src/features/feedback/api/client.ts similarity index 100% rename from web-client/src/features/feedback/client.ts rename to web-client/src/features/feedback/api/client.ts diff --git a/web-client/src/features/feedback/api.ts b/web-client/src/features/feedback/api/index.ts similarity index 59% rename from web-client/src/features/feedback/api.ts rename to web-client/src/features/feedback/api/index.ts index 80b939f..54f11e9 100644 --- a/web-client/src/features/feedback/api.ts +++ b/web-client/src/features/feedback/api/index.ts @@ -1,6 +1,10 @@ -import { feedbackClient } from '@/features/feedback/client' +export * from './client' +export * from './queries' + +import { feedbackClient } from './client' export async function getFeedbackHello(): Promise { const res = await feedbackClient.get('/hello') + return res.data } diff --git a/web-client/src/features/feedback/api/queries.ts b/web-client/src/features/feedback/api/queries.ts new file mode 100644 index 0000000..4e5e67e --- /dev/null +++ b/web-client/src/features/feedback/api/queries.ts @@ -0,0 +1,57 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { feedbackClient } from './client' +import type { Feedback, FeedbackCreate, FeedbackPartialUpdate, FeedbackSummary } from '../types' + +export const feedbackKeys = { + all: ['feedback'] as const, + detail: (id: string) => ['feedback', id] as const, +} + +export function useFeedbackList() { + return useQuery({ + queryKey: feedbackKeys.all, + queryFn: () => feedbackClient.get('/').then(r => r.data), + }) +} + +export function useFeedback(id: string) { + return useQuery({ + queryKey: feedbackKeys.detail(id), + queryFn: () => feedbackClient.get(`/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateFeedback() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => feedbackClient.post('/', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: feedbackKeys.all }), + }) +} + +export function useUpdateFeedback() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => feedbackClient.patch(`/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: feedbackKeys.all }) + qc.invalidateQueries({ queryKey: feedbackKeys.detail(id) }) + }, + }) +} + +export function useDeleteFeedback() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => feedbackClient.delete(`/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: feedbackKeys.all }) + qc.removeQueries({ queryKey: feedbackKeys.detail(id) }) + }, + }) +} diff --git a/web-client/src/features/feedback/index.ts b/web-client/src/features/feedback/index.ts new file mode 100644 index 0000000..4144de1 --- /dev/null +++ b/web-client/src/features/feedback/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { FeedbackPage } from './pages/FeedbackPage' diff --git a/web-client/src/features/feedback/pages/FeedbackPage.tsx b/web-client/src/features/feedback/pages/FeedbackPage.tsx new file mode 100644 index 0000000..7de50c3 --- /dev/null +++ b/web-client/src/features/feedback/pages/FeedbackPage.tsx @@ -0,0 +1,7 @@ +export function FeedbackPage() { + return ( +
+

Feedback

+
+ ) +} diff --git a/web-client/src/features/feedback/types/index.ts b/web-client/src/features/feedback/types/index.ts new file mode 100644 index 0000000..f8224e9 --- /dev/null +++ b/web-client/src/features/feedback/types/index.ts @@ -0,0 +1 @@ +export type { Feedback, FeedbackSummary, FeedbackCreate, FeedbackPartialUpdate } from '@/types' diff --git a/web-client/src/features/helper/api/client.ts b/web-client/src/features/helper/api/client.ts new file mode 100644 index 0000000..6f141c5 --- /dev/null +++ b/web-client/src/features/helper/api/client.ts @@ -0,0 +1,3 @@ +import { createApiClient } from '@/lib/keycloak' + +export const helperClient = createApiClient('/api/v1/helper') diff --git a/web-client/src/features/helper/api/index.ts b/web-client/src/features/helper/api/index.ts new file mode 100644 index 0000000..290cc9f --- /dev/null +++ b/web-client/src/features/helper/api/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './queries' diff --git a/web-client/src/features/helper/api/queries.ts b/web-client/src/features/helper/api/queries.ts new file mode 100644 index 0000000..eb7dc13 --- /dev/null +++ b/web-client/src/features/helper/api/queries.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query' + +import { helperClient } from './client' + +export const helperKeys = { + report: (memberId: string) => ['helper', 'report', memberId] as const, +} + +export function useMemberReport(memberId: string) { + return useQuery({ + queryKey: helperKeys.report(memberId), + queryFn: () => helperClient.get(`/report/${memberId}`).then(r => r.data), + enabled: !!memberId, + }) +} diff --git a/web-client/src/features/helper/index.ts b/web-client/src/features/helper/index.ts new file mode 100644 index 0000000..a4d8d01 --- /dev/null +++ b/web-client/src/features/helper/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { HelperPage } from './pages/HelperPage' diff --git a/web-client/src/features/helper/pages/HelperPage.tsx b/web-client/src/features/helper/pages/HelperPage.tsx new file mode 100644 index 0000000..116ac0c --- /dev/null +++ b/web-client/src/features/helper/pages/HelperPage.tsx @@ -0,0 +1,7 @@ +export function HelperPage() { + return ( +
+

GenAI Helper

+
+ ) +} diff --git a/web-client/src/features/helper/types/index.ts b/web-client/src/features/helper/types/index.ts new file mode 100644 index 0000000..c17499c --- /dev/null +++ b/web-client/src/features/helper/types/index.ts @@ -0,0 +1,3 @@ +export interface HelperReport { + content: string +} diff --git a/web-client/src/features/letters/client.ts b/web-client/src/features/letters/api/client.ts similarity index 100% rename from web-client/src/features/letters/client.ts rename to web-client/src/features/letters/api/client.ts diff --git a/web-client/src/features/letters/api.ts b/web-client/src/features/letters/api/index.ts similarity index 59% rename from web-client/src/features/letters/api.ts rename to web-client/src/features/letters/api/index.ts index c6f1b69..9057350 100644 --- a/web-client/src/features/letters/api.ts +++ b/web-client/src/features/letters/api/index.ts @@ -1,6 +1,10 @@ -import { lettersClient } from '@/features/letters/client' +export * from './client' +export * from './queries' + +import { lettersClient } from './client' export async function getLettersHello(): Promise { const res = await lettersClient.get('/hello') + return res.data } diff --git a/web-client/src/features/letters/api/queries.ts b/web-client/src/features/letters/api/queries.ts new file mode 100644 index 0000000..1c68e4e --- /dev/null +++ b/web-client/src/features/letters/api/queries.ts @@ -0,0 +1,23 @@ +import { useMutation } from '@tanstack/react-query' + +import { lettersClient } from './client' +import type { GeneratePdfRequest, SendMailRequest } from '../types' + +export function useSendMail() { + return useMutation({ + mutationFn: data => + lettersClient.post('/mail', data.html, { + headers: { 'Content-Type': 'text/html' }, + }).then(() => undefined), + }) +} + +export function useGeneratePdf() { + return useMutation({ + mutationFn: data => + lettersClient.post('/pdf', data.html, { + headers: { 'Content-Type': 'text/html' }, + responseType: 'blob', + }).then(r => r.data), + }) +} diff --git a/web-client/src/features/letters/index.ts b/web-client/src/features/letters/index.ts new file mode 100644 index 0000000..e878b99 --- /dev/null +++ b/web-client/src/features/letters/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { LettersPage } from './pages/LettersPage' diff --git a/web-client/src/features/letters/pages/LettersPage.tsx b/web-client/src/features/letters/pages/LettersPage.tsx new file mode 100644 index 0000000..325fea2 --- /dev/null +++ b/web-client/src/features/letters/pages/LettersPage.tsx @@ -0,0 +1,7 @@ +export function LettersPage() { + return ( +
+

Letters

+
+ ) +} diff --git a/web-client/src/features/letters/types/index.ts b/web-client/src/features/letters/types/index.ts new file mode 100644 index 0000000..3317698 --- /dev/null +++ b/web-client/src/features/letters/types/index.ts @@ -0,0 +1,7 @@ +export interface SendMailRequest { + html: string +} + +export interface GeneratePdfRequest { + html: string +} diff --git a/web-client/src/features/members/api.ts b/web-client/src/features/members/api.ts deleted file mode 100644 index d5ef2a9..0000000 --- a/web-client/src/features/members/api.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { membersClient } from '@/features/members/client' - -export async function getMembersHello(): Promise { - const res = await membersClient.get('/hello') - return res.data -} - -export async function getMembersAdminHello(): Promise { - try { - const res = await membersClient.get('/helloAdmin') - return res.data - } - catch { - return "You are not logged into an administrator account" - } -} diff --git a/web-client/src/features/members/client.ts b/web-client/src/features/members/api/client.ts similarity index 100% rename from web-client/src/features/members/client.ts rename to web-client/src/features/members/api/client.ts diff --git a/web-client/src/features/members/api/index.ts b/web-client/src/features/members/api/index.ts new file mode 100644 index 0000000..290cc9f --- /dev/null +++ b/web-client/src/features/members/api/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './queries' diff --git a/web-client/src/features/members/api/queries.ts b/web-client/src/features/members/api/queries.ts new file mode 100644 index 0000000..f5d5e5d --- /dev/null +++ b/web-client/src/features/members/api/queries.ts @@ -0,0 +1,57 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { membersClient } from './client' +import type { Member, MemberCreate, MemberPartialUpdate, MemberSummary } from '../types' + +export const membersKeys = { + all: ['members'] as const, + detail: (id: string) => ['members', id] as const, +} + +export function useMembers() { + return useQuery({ + queryKey: membersKeys.all, + queryFn: () => membersClient.get('/').then(r => r.data), + }) +} + +export function useMember(id: string) { + return useQuery({ + queryKey: membersKeys.detail(id), + queryFn: () => membersClient.get(`/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateMember() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => membersClient.post('/', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: membersKeys.all }), + }) +} + +export function useUpdateMember() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => membersClient.patch(`/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: membersKeys.all }) + qc.invalidateQueries({ queryKey: membersKeys.detail(id) }) + }, + }) +} + +export function useDeleteMember() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => membersClient.delete(`/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: membersKeys.all }) + qc.removeQueries({ queryKey: membersKeys.detail(id) }) + }, + }) +} diff --git a/web-client/src/features/members/index.ts b/web-client/src/features/members/index.ts new file mode 100644 index 0000000..8fc39e2 --- /dev/null +++ b/web-client/src/features/members/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { MembersPage } from './pages/MembersPage' diff --git a/web-client/src/features/members/pages/MembersPage.tsx b/web-client/src/features/members/pages/MembersPage.tsx new file mode 100644 index 0000000..d867a94 --- /dev/null +++ b/web-client/src/features/members/pages/MembersPage.tsx @@ -0,0 +1,7 @@ +export function MembersPage() { + return ( +
+

Members

+
+ ) +} diff --git a/web-client/src/features/members/types/index.ts b/web-client/src/features/members/types/index.ts new file mode 100644 index 0000000..1684bbd --- /dev/null +++ b/web-client/src/features/members/types/index.ts @@ -0,0 +1 @@ +export type { Member, MemberSummary, MemberCreate, MemberPartialUpdate } from '@/types' diff --git a/web-client/src/features/organization/client.ts b/web-client/src/features/organization/api/client.ts similarity index 100% rename from web-client/src/features/organization/client.ts rename to web-client/src/features/organization/api/client.ts diff --git a/web-client/src/features/organization/api.ts b/web-client/src/features/organization/api/index.ts similarity index 59% rename from web-client/src/features/organization/api.ts rename to web-client/src/features/organization/api/index.ts index 76f6b0e..ce9415d 100644 --- a/web-client/src/features/organization/api.ts +++ b/web-client/src/features/organization/api/index.ts @@ -1,6 +1,10 @@ -import { organizationClient } from '@/features/organization/client' +export * from './client' +export * from './queries' + +import { organizationClient } from './client' export async function getOrganizationHello(): Promise { const res = await organizationClient.get('/hello') + return res.data } diff --git a/web-client/src/features/organization/api/queries.ts b/web-client/src/features/organization/api/queries.ts new file mode 100644 index 0000000..dc66dcc --- /dev/null +++ b/web-client/src/features/organization/api/queries.ts @@ -0,0 +1,114 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { organizationClient } from './client' +import type { + Sport, + SportCreate, + SportPartialUpdate, + Team, + TeamCreate, + TeamPartialUpdate, +} from '../types' + +export const organizationKeys = { + sports: ['organization', 'sports'] as const, + sport: (name: string) => ['organization', 'sports', name] as const, + teams: ['organization', 'teams'] as const, + team: (id: string) => ['organization', 'teams', id] as const, +} + +export function useSports() { + return useQuery({ + queryKey: organizationKeys.sports, + queryFn: () => organizationClient.get('/sports').then(r => r.data), + }) +} + +export function useSport(name: string) { + return useQuery({ + queryKey: organizationKeys.sport(name), + queryFn: () => organizationClient.get(`/sports/${name}`).then(r => r.data), + enabled: !!name, + }) +} + +export function useCreateSport() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => organizationClient.post('/sports', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: organizationKeys.sports }), + }) +} + +export function useUpdateSport() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ name, ...data }) => organizationClient.patch(`/sports/${name}`, data).then(r => r.data), + onSuccess: (_, { name }) => { + qc.invalidateQueries({ queryKey: organizationKeys.sports }) + qc.invalidateQueries({ queryKey: organizationKeys.sport(name) }) + }, + }) +} + +export function useDeleteSport() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: name => organizationClient.delete(`/sports/${name}`).then(() => undefined), + onSuccess: (_, name) => { + qc.invalidateQueries({ queryKey: organizationKeys.sports }) + qc.removeQueries({ queryKey: organizationKeys.sport(name) }) + }, + }) +} + +export function useTeams() { + return useQuery({ + queryKey: organizationKeys.teams, + queryFn: () => organizationClient.get('/teams').then(r => r.data), + }) +} + +export function useTeam(id: string) { + return useQuery({ + queryKey: organizationKeys.team(id), + queryFn: () => organizationClient.get(`/teams/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateTeam() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => organizationClient.post('/teams', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: organizationKeys.teams }), + }) +} + +export function useUpdateTeam() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => organizationClient.patch(`/teams/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: organizationKeys.teams }) + qc.invalidateQueries({ queryKey: organizationKeys.team(id) }) + }, + }) +} + +export function useDeleteTeam() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => organizationClient.delete(`/teams/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: organizationKeys.teams }) + qc.removeQueries({ queryKey: organizationKeys.team(id) }) + }, + }) +} diff --git a/web-client/src/features/organization/index.ts b/web-client/src/features/organization/index.ts new file mode 100644 index 0000000..e74a24f --- /dev/null +++ b/web-client/src/features/organization/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { OrganizationPage } from './pages/OrganizationPage' diff --git a/web-client/src/features/organization/pages/OrganizationPage.tsx b/web-client/src/features/organization/pages/OrganizationPage.tsx new file mode 100644 index 0000000..4dc0a38 --- /dev/null +++ b/web-client/src/features/organization/pages/OrganizationPage.tsx @@ -0,0 +1,7 @@ +export function OrganizationPage() { + return ( +
+

Organization

+
+ ) +} diff --git a/web-client/src/features/organization/types/index.ts b/web-client/src/features/organization/types/index.ts new file mode 100644 index 0000000..028d19c --- /dev/null +++ b/web-client/src/features/organization/types/index.ts @@ -0,0 +1 @@ +export type { Sport, SportCreate, SportPartialUpdate, Team, TeamCreate, TeamPartialUpdate } from '@/types' diff --git a/web-client/src/features/payments/client.ts b/web-client/src/features/payments/api/client.ts similarity index 100% rename from web-client/src/features/payments/client.ts rename to web-client/src/features/payments/api/client.ts diff --git a/web-client/src/features/payments/api.ts b/web-client/src/features/payments/api/index.ts similarity index 59% rename from web-client/src/features/payments/api.ts rename to web-client/src/features/payments/api/index.ts index bb6f870..c625700 100644 --- a/web-client/src/features/payments/api.ts +++ b/web-client/src/features/payments/api/index.ts @@ -1,6 +1,10 @@ -import { paymentsClient } from '@/features/payments/client' +export * from './client' +export * from './queries' + +import { paymentsClient } from './client' export async function getPaymentsHello(): Promise { const res = await paymentsClient.get('/hello') + return res.data } diff --git a/web-client/src/features/payments/api/queries.ts b/web-client/src/features/payments/api/queries.ts new file mode 100644 index 0000000..1a003ce --- /dev/null +++ b/web-client/src/features/payments/api/queries.ts @@ -0,0 +1,74 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { paymentsClient } from './client' +import type { Balance, Transaction, TransactionCreate, TransactionPartialUpdate } from '../types' + +export const paymentsKeys = { + balances: ['payments', 'balances'] as const, + balance: (memberId: string) => ['payments', 'balances', memberId] as const, + transactions: ['payments', 'transactions'] as const, + transaction: (id: string) => ['payments', 'transactions', id] as const, +} + +export function useBalances() { + return useQuery({ + queryKey: paymentsKeys.balances, + queryFn: () => paymentsClient.get('/balances').then(r => r.data), + }) +} + +export function useMemberBalance(memberId: string) { + return useQuery({ + queryKey: paymentsKeys.balance(memberId), + queryFn: () => paymentsClient.get(`/balances/${memberId}`).then(r => r.data), + enabled: !!memberId, + }) +} + +export function useTransactions() { + return useQuery({ + queryKey: paymentsKeys.transactions, + queryFn: () => paymentsClient.get('/transactions').then(r => r.data), + }) +} + +export function useTransaction(id: string) { + return useQuery({ + queryKey: paymentsKeys.transaction(id), + queryFn: () => paymentsClient.get(`/transactions/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateTransaction() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => paymentsClient.post('/transactions', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: paymentsKeys.transactions }), + }) +} + +export function useUpdateTransaction() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => paymentsClient.patch(`/transactions/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: paymentsKeys.transactions }) + qc.invalidateQueries({ queryKey: paymentsKeys.transaction(id) }) + }, + }) +} + +export function useDeleteTransaction() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => paymentsClient.delete(`/transactions/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: paymentsKeys.transactions }) + qc.removeQueries({ queryKey: paymentsKeys.transaction(id) }) + }, + }) +} diff --git a/web-client/src/features/payments/index.ts b/web-client/src/features/payments/index.ts new file mode 100644 index 0000000..0158ec5 --- /dev/null +++ b/web-client/src/features/payments/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { PaymentsPage } from './pages/PaymentsPage' diff --git a/web-client/src/features/payments/pages/PaymentsPage.tsx b/web-client/src/features/payments/pages/PaymentsPage.tsx new file mode 100644 index 0000000..0947c7b --- /dev/null +++ b/web-client/src/features/payments/pages/PaymentsPage.tsx @@ -0,0 +1,7 @@ +export function PaymentsPage() { + return ( +
+

Payments

+
+ ) +} diff --git a/web-client/src/features/payments/types/index.ts b/web-client/src/features/payments/types/index.ts new file mode 100644 index 0000000..6991f72 --- /dev/null +++ b/web-client/src/features/payments/types/index.ts @@ -0,0 +1 @@ +export type { Transaction, TransactionCreate, TransactionPartialUpdate, Balance } from '@/types' diff --git a/web-client/src/features/sport-events/api/client.ts b/web-client/src/features/sport-events/api/client.ts new file mode 100644 index 0000000..8706cd7 --- /dev/null +++ b/web-client/src/features/sport-events/api/client.ts @@ -0,0 +1,3 @@ +import { createApiClient } from '@/lib/keycloak' + +export const sportEventsClient = createApiClient('/api/v1/events') diff --git a/web-client/src/features/sport-events/api/index.ts b/web-client/src/features/sport-events/api/index.ts new file mode 100644 index 0000000..290cc9f --- /dev/null +++ b/web-client/src/features/sport-events/api/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './queries' diff --git a/web-client/src/features/sport-events/api/queries.ts b/web-client/src/features/sport-events/api/queries.ts new file mode 100644 index 0000000..37222bc --- /dev/null +++ b/web-client/src/features/sport-events/api/queries.ts @@ -0,0 +1,57 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +import { sportEventsClient } from './client' +import type { SportEvent, EventCreate, EventPartialUpdate, EventSummary } from '../types' + +export const sportEventsKeys = { + all: ['sport-events'] as const, + detail: (id: string) => ['sport-events', id] as const, +} + +export function useSportEvents() { + return useQuery({ + queryKey: sportEventsKeys.all, + queryFn: () => sportEventsClient.get('/').then(r => r.data), + }) +} + +export function useSportEvent(id: string) { + return useQuery({ + queryKey: sportEventsKeys.detail(id), + queryFn: () => sportEventsClient.get(`/${id}`).then(r => r.data), + enabled: !!id, + }) +} + +export function useCreateSportEvent() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: data => sportEventsClient.post('/', data).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: sportEventsKeys.all }), + }) +} + +export function useUpdateSportEvent() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: ({ id, ...data }) => sportEventsClient.patch(`/${id}`, data).then(r => r.data), + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: sportEventsKeys.all }) + qc.invalidateQueries({ queryKey: sportEventsKeys.detail(id) }) + }, + }) +} + +export function useDeleteSportEvent() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: id => sportEventsClient.delete(`/${id}`).then(() => undefined), + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: sportEventsKeys.all }) + qc.removeQueries({ queryKey: sportEventsKeys.detail(id) }) + }, + }) +} diff --git a/web-client/src/features/sport-events/index.ts b/web-client/src/features/sport-events/index.ts new file mode 100644 index 0000000..5be00ba --- /dev/null +++ b/web-client/src/features/sport-events/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './types' +export { SportEventsPage } from './pages/SportEventsPage' diff --git a/web-client/src/features/sport-events/pages/SportEventsPage.tsx b/web-client/src/features/sport-events/pages/SportEventsPage.tsx new file mode 100644 index 0000000..86bea5a --- /dev/null +++ b/web-client/src/features/sport-events/pages/SportEventsPage.tsx @@ -0,0 +1,7 @@ +export function SportEventsPage() { + return ( +
+

Events

+
+ ) +} diff --git a/web-client/src/features/sport-events/types/index.ts b/web-client/src/features/sport-events/types/index.ts new file mode 100644 index 0000000..b29331c --- /dev/null +++ b/web-client/src/features/sport-events/types/index.ts @@ -0,0 +1 @@ +export type { SportEvent, EventSummary, EventCreate, EventPartialUpdate } from '@/types' From 4f2478e257e8eb3b8a8776a0c6ba823198c8dee0 Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:56:21 +0200 Subject: [PATCH 06/19] refactor: remove old flat events api and client files --- web-client/src/features/events/api.ts | 6 ------ web-client/src/features/events/client.ts | 3 --- 2 files changed, 9 deletions(-) delete mode 100644 web-client/src/features/events/api.ts delete mode 100644 web-client/src/features/events/client.ts diff --git a/web-client/src/features/events/api.ts b/web-client/src/features/events/api.ts deleted file mode 100644 index e308447..0000000 --- a/web-client/src/features/events/api.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { eventsClient } from '@/features/events/client' - -export async function getEventsHello(): Promise { - const res = await eventsClient.get('/hello') - return res.data -} diff --git a/web-client/src/features/events/client.ts b/web-client/src/features/events/client.ts deleted file mode 100644 index 710d172..0000000 --- a/web-client/src/features/events/client.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createApiClient } from '@/lib/keycloak' - -export const eventsClient = createApiClient('/api/v1/events') From bfeb69044380cf0a824b34b9311588011552745d Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:53:33 +0200 Subject: [PATCH 07/19] feat: display hello messages from all backend services on feature pages --- web-client/src/features/feedback/api/queries.ts | 8 ++++++++ .../src/features/feedback/pages/FeedbackPage.tsx | 5 +++++ web-client/src/features/helper/api/queries.ts | 11 +++++++++++ web-client/src/features/helper/pages/HelperPage.tsx | 5 +++++ web-client/src/features/letters/api/queries.ts | 13 ++++++++++++- .../src/features/letters/pages/LettersPage.tsx | 5 +++++ web-client/src/features/members/api/queries.ts | 8 ++++++++ .../src/features/members/pages/MembersPage.tsx | 5 +++++ web-client/src/features/organization/api/queries.ts | 8 ++++++++ .../organization/pages/OrganizationPage.tsx | 5 +++++ web-client/src/features/payments/api/queries.ts | 8 ++++++++ .../src/features/payments/pages/PaymentsPage.tsx | 5 +++++ web-client/src/features/sport-events/api/queries.ts | 8 ++++++++ .../features/sport-events/pages/SportEventsPage.tsx | 5 +++++ 14 files changed, 98 insertions(+), 1 deletion(-) diff --git a/web-client/src/features/feedback/api/queries.ts b/web-client/src/features/feedback/api/queries.ts index 4e5e67e..d53af6d 100644 --- a/web-client/src/features/feedback/api/queries.ts +++ b/web-client/src/features/feedback/api/queries.ts @@ -6,6 +6,14 @@ import type { Feedback, FeedbackCreate, FeedbackPartialUpdate, FeedbackSummary } export const feedbackKeys = { all: ['feedback'] as const, detail: (id: string) => ['feedback', id] as const, + hello: ['feedback', 'hello'] as const, +} + +export function useFeedbackHello() { + return useQuery({ + queryKey: feedbackKeys.hello, + queryFn: () => feedbackClient.get('/hello').then(r => r.data), + }) } export function useFeedbackList() { diff --git a/web-client/src/features/feedback/pages/FeedbackPage.tsx b/web-client/src/features/feedback/pages/FeedbackPage.tsx index 7de50c3..9ac09d2 100644 --- a/web-client/src/features/feedback/pages/FeedbackPage.tsx +++ b/web-client/src/features/feedback/pages/FeedbackPage.tsx @@ -1,7 +1,12 @@ +import { useFeedbackHello } from '../api' + export function FeedbackPage() { + const { data: hello } = useFeedbackHello() + return (

Feedback

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/helper/api/queries.ts b/web-client/src/features/helper/api/queries.ts index eb7dc13..434334a 100644 --- a/web-client/src/features/helper/api/queries.ts +++ b/web-client/src/features/helper/api/queries.ts @@ -3,9 +3,20 @@ import { useQuery } from '@tanstack/react-query' import { helperClient } from './client' export const helperKeys = { + hello: ['helper', 'hello'] as const, report: (memberId: string) => ['helper', 'report', memberId] as const, } +export function useHelperHello() { + return useQuery({ + queryKey: helperKeys.hello, + queryFn: () => + helperClient.get('/hello').then(r => + r.data.replace(/<[^>]+>/g, '').trim() + ), + }) +} + export function useMemberReport(memberId: string) { return useQuery({ queryKey: helperKeys.report(memberId), diff --git a/web-client/src/features/helper/pages/HelperPage.tsx b/web-client/src/features/helper/pages/HelperPage.tsx index 116ac0c..3fd9cc3 100644 --- a/web-client/src/features/helper/pages/HelperPage.tsx +++ b/web-client/src/features/helper/pages/HelperPage.tsx @@ -1,7 +1,12 @@ +import { useHelperHello } from '../api' + export function HelperPage() { + const { data: hello } = useHelperHello() + return (

GenAI Helper

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/letters/api/queries.ts b/web-client/src/features/letters/api/queries.ts index 1c68e4e..6701028 100644 --- a/web-client/src/features/letters/api/queries.ts +++ b/web-client/src/features/letters/api/queries.ts @@ -1,8 +1,19 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { lettersClient } from './client' import type { GeneratePdfRequest, SendMailRequest } from '../types' +export const lettersKeys = { + hello: ['letters', 'hello'] as const, +} + +export function useLettersHello() { + return useQuery({ + queryKey: lettersKeys.hello, + queryFn: () => lettersClient.get('/hello').then(r => r.data), + }) +} + export function useSendMail() { return useMutation({ mutationFn: data => diff --git a/web-client/src/features/letters/pages/LettersPage.tsx b/web-client/src/features/letters/pages/LettersPage.tsx index 325fea2..f2fb8e3 100644 --- a/web-client/src/features/letters/pages/LettersPage.tsx +++ b/web-client/src/features/letters/pages/LettersPage.tsx @@ -1,7 +1,12 @@ +import { useLettersHello } from '../api' + export function LettersPage() { + const { data: hello } = useLettersHello() + return (

Letters

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/members/api/queries.ts b/web-client/src/features/members/api/queries.ts index f5d5e5d..7a1d1d7 100644 --- a/web-client/src/features/members/api/queries.ts +++ b/web-client/src/features/members/api/queries.ts @@ -4,10 +4,18 @@ import { membersClient } from './client' import type { Member, MemberCreate, MemberPartialUpdate, MemberSummary } from '../types' export const membersKeys = { + hello: ['members', 'hello'] as const, all: ['members'] as const, detail: (id: string) => ['members', id] as const, } +export function useMembersHello() { + return useQuery({ + queryKey: membersKeys.hello, + queryFn: () => membersClient.get('/hello').then(r => r.data), + }) +} + export function useMembers() { return useQuery({ queryKey: membersKeys.all, diff --git a/web-client/src/features/members/pages/MembersPage.tsx b/web-client/src/features/members/pages/MembersPage.tsx index d867a94..c12ffd7 100644 --- a/web-client/src/features/members/pages/MembersPage.tsx +++ b/web-client/src/features/members/pages/MembersPage.tsx @@ -1,7 +1,12 @@ +import { useMembersHello } from '../api' + export function MembersPage() { + const { data: hello } = useMembersHello() + return (

Members

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/organization/api/queries.ts b/web-client/src/features/organization/api/queries.ts index dc66dcc..cbb4378 100644 --- a/web-client/src/features/organization/api/queries.ts +++ b/web-client/src/features/organization/api/queries.ts @@ -11,12 +11,20 @@ import type { } from '../types' export const organizationKeys = { + hello: ['organization', 'hello'] as const, sports: ['organization', 'sports'] as const, sport: (name: string) => ['organization', 'sports', name] as const, teams: ['organization', 'teams'] as const, team: (id: string) => ['organization', 'teams', id] as const, } +export function useOrganizationHello() { + return useQuery({ + queryKey: organizationKeys.hello, + queryFn: () => organizationClient.get('/hello').then(r => r.data), + }) +} + export function useSports() { return useQuery({ queryKey: organizationKeys.sports, diff --git a/web-client/src/features/organization/pages/OrganizationPage.tsx b/web-client/src/features/organization/pages/OrganizationPage.tsx index 4dc0a38..0b180d8 100644 --- a/web-client/src/features/organization/pages/OrganizationPage.tsx +++ b/web-client/src/features/organization/pages/OrganizationPage.tsx @@ -1,7 +1,12 @@ +import { useOrganizationHello } from '../api' + export function OrganizationPage() { + const { data: hello } = useOrganizationHello() + return (

Organization

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/payments/api/queries.ts b/web-client/src/features/payments/api/queries.ts index 1a003ce..0ef75cf 100644 --- a/web-client/src/features/payments/api/queries.ts +++ b/web-client/src/features/payments/api/queries.ts @@ -4,12 +4,20 @@ import { paymentsClient } from './client' import type { Balance, Transaction, TransactionCreate, TransactionPartialUpdate } from '../types' export const paymentsKeys = { + hello: ['payments', 'hello'] as const, balances: ['payments', 'balances'] as const, balance: (memberId: string) => ['payments', 'balances', memberId] as const, transactions: ['payments', 'transactions'] as const, transaction: (id: string) => ['payments', 'transactions', id] as const, } +export function usePaymentsHello() { + return useQuery({ + queryKey: paymentsKeys.hello, + queryFn: () => paymentsClient.get('/hello').then(r => r.data), + }) +} + export function useBalances() { return useQuery({ queryKey: paymentsKeys.balances, diff --git a/web-client/src/features/payments/pages/PaymentsPage.tsx b/web-client/src/features/payments/pages/PaymentsPage.tsx index 0947c7b..9ca646b 100644 --- a/web-client/src/features/payments/pages/PaymentsPage.tsx +++ b/web-client/src/features/payments/pages/PaymentsPage.tsx @@ -1,7 +1,12 @@ +import { usePaymentsHello } from '../api' + export function PaymentsPage() { + const { data: hello } = usePaymentsHello() + return (

Payments

+ {hello &&

{hello}

}
) } diff --git a/web-client/src/features/sport-events/api/queries.ts b/web-client/src/features/sport-events/api/queries.ts index 37222bc..97794bc 100644 --- a/web-client/src/features/sport-events/api/queries.ts +++ b/web-client/src/features/sport-events/api/queries.ts @@ -4,10 +4,18 @@ import { sportEventsClient } from './client' import type { SportEvent, EventCreate, EventPartialUpdate, EventSummary } from '../types' export const sportEventsKeys = { + hello: ['sport-events', 'hello'] as const, all: ['sport-events'] as const, detail: (id: string) => ['sport-events', id] as const, } +export function useSportEventsHello() { + return useQuery({ + queryKey: sportEventsKeys.hello, + queryFn: () => sportEventsClient.get('/hello').then(r => r.data), + }) +} + export function useSportEvents() { return useQuery({ queryKey: sportEventsKeys.all, diff --git a/web-client/src/features/sport-events/pages/SportEventsPage.tsx b/web-client/src/features/sport-events/pages/SportEventsPage.tsx index 86bea5a..52b70cf 100644 --- a/web-client/src/features/sport-events/pages/SportEventsPage.tsx +++ b/web-client/src/features/sport-events/pages/SportEventsPage.tsx @@ -1,7 +1,12 @@ +import { useSportEventsHello } from '../api' + export function SportEventsPage() { + const { data: hello } = useSportEventsHello() + return (

Events

+ {hello &&

{hello}

}
) } From 067a2d4ab1f8d434bbf47da5cd496ccc1f24365b Mon Sep 17 00:00:00 2001 From: FadyGergesRezk <84906847+FadyGergesRezk@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:05:14 +0200 Subject: [PATCH 08/19] fix: simplify hello message retrieval by removing HTML stripping --- web-client/src/features/helper/api/queries.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web-client/src/features/helper/api/queries.ts b/web-client/src/features/helper/api/queries.ts index 434334a..68abbd3 100644 --- a/web-client/src/features/helper/api/queries.ts +++ b/web-client/src/features/helper/api/queries.ts @@ -10,10 +10,7 @@ export const helperKeys = { export function useHelperHello() { return useQuery({ queryKey: helperKeys.hello, - queryFn: () => - helperClient.get('/hello').then(r => - r.data.replace(/<[^>]+>/g, '').trim() - ), + queryFn: () => helperClient.get('/hello').then(r => r.data), }) } From 3d356466c0d1b16a59e5b869f8a6ab9a72f7c5b7 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Wed, 10 Jun 2026 22:51:29 +0200 Subject: [PATCH 09/19] fix auth requirement for cluster --- .../helm/team-devoops/files/realm-config.json | 3 +- .../team-devoops/templates/forward-auth.yaml | 58 ++++++----------- .../helm/team-devoops/templates/ingress.yaml | 62 +++++++++++++------ infra/helm/team-devoops/values.yaml | 6 +- infra/keycloak/realm-config.json | 3 +- 5 files changed, 71 insertions(+), 61 deletions(-) diff --git a/infra/helm/team-devoops/files/realm-config.json b/infra/helm/team-devoops/files/realm-config.json index f9648eb..20c5dce 100644 --- a/infra/helm/team-devoops/files/realm-config.json +++ b/infra/helm/team-devoops/files/realm-config.json @@ -70,12 +70,13 @@ "directAccessGrantsEnabled": false, "redirectUris": [ "https://team-devoops.uaenorth.cloudapp.azure.com/_oauth", - "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/oauth2/callback", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/_oauth", "http://localhost/_oauth" ], "webOrigins": [ "https://team-devoops.uaenorth.cloudapp.azure.com", "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de", "http://localhost" ] } diff --git a/infra/helm/team-devoops/templates/forward-auth.yaml b/infra/helm/team-devoops/templates/forward-auth.yaml index e8d904d..2ad3809 100644 --- a/infra/helm/team-devoops/templates/forward-auth.yaml +++ b/infra/helm/team-devoops/templates/forward-auth.yaml @@ -2,80 +2,62 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: oauth2-proxy + name: traefik-forward-auth labels: - {{- include "team-devoops.labels" (dict "name" "oauth2-proxy" "root" $) | nindent 4 }} + {{- include "team-devoops.labels" (dict "name" "traefik-forward-auth" "root" $) | nindent 4 }} spec: replicas: 1 strategy: type: RollingUpdate selector: matchLabels: - {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 6 }} + {{- include "team-devoops.selectorLabels" (dict "name" "traefik-forward-auth") | nindent 6 }} template: metadata: labels: - {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 8 }} + {{- include "team-devoops.selectorLabels" (dict "name" "traefik-forward-auth") | nindent 8 }} spec: {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} containers: - - name: oauth2-proxy - image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 + - name: traefik-forward-auth + image: thomseddon/traefik-forward-auth:2 imagePullPolicy: IfNotPresent ports: - - containerPort: 4180 + - containerPort: 4181 env: - - name: OAUTH2_PROXY_PROVIDER + - name: DEFAULT_PROVIDER value: oidc - - name: OAUTH2_PROXY_OIDC_ISSUER_URL + - name: PROVIDERS_OIDC_ISSUER_URL value: {{ .Values.forwardAuth.oidcIssuerUrl | quote }} - - name: OAUTH2_PROXY_CLIENT_ID + - name: PROVIDERS_OIDC_CLIENT_ID value: {{ .Values.forwardAuth.clientId | quote }} - - name: OAUTH2_PROXY_CLIENT_SECRET + - name: PROVIDERS_OIDC_CLIENT_SECRET value: {{ .Values.forwardAuth.clientSecret | quote }} - - name: OAUTH2_PROXY_COOKIE_SECRET + - name: SECRET value: {{ .Values.forwardAuth.cookieSecret | quote }} - - name: OAUTH2_PROXY_EMAIL_DOMAINS - value: "*" - - name: OAUTH2_PROXY_UPSTREAM - value: "static://202" - - name: OAUTH2_PROXY_HTTP_ADDRESS - value: "0.0.0.0:4180" - - name: OAUTH2_PROXY_REDIRECT_URL - value: {{ printf "https://%s/oauth2/callback" .Values.ingress.host | quote }} - - name: OAUTH2_PROXY_COOKIE_SECURE - value: "true" - - name: OAUTH2_PROXY_SKIP_PROVIDER_BUTTON - value: "true" - - name: OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL - value: "true" - - name: OAUTH2_PROXY_OIDC_EMAIL_CLAIM - value: "sub" - - name: OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST - value: "true" - - name: OAUTH2_PROXY_COOKIE_CSRF_EXPIRE - value: "5m" + - name: INSECURE_COOKIE + value: "false" resources: requests: cpu: 10m memory: 16Mi limits: cpu: 50m - memory: 32Mi + memory: 64Mi --- apiVersion: v1 kind: Service metadata: - name: oauth2-proxy + name: traefik-forward-auth labels: - {{- include "team-devoops.labels" (dict "name" "oauth2-proxy" "root" $) | nindent 4 }} + {{- include "team-devoops.labels" (dict "name" "traefik-forward-auth" "root" $) | nindent 4 }} spec: selector: - {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 4 }} + {{- include "team-devoops.selectorLabels" (dict "name" "traefik-forward-auth") | nindent 4 }} ports: - - port: 80 - targetPort: 4180 + - port: 4181 + targetPort: 4181 {{- end }} diff --git a/infra/helm/team-devoops/templates/ingress.yaml b/infra/helm/team-devoops/templates/ingress.yaml index 8e25ca1..c7e9318 100644 --- a/infra/helm/team-devoops/templates/ingress.yaml +++ b/infra/helm/team-devoops/templates/ingress.yaml @@ -2,11 +2,13 @@ {{- $host := .Values.ingress.host }} {{- $tls := .Values.ingress.tls }} {{- $fa := .Values.forwardAuth }} +{{- $fa := .Values.forwardAuth }} # --------------------------------------------------------------------------- # Stripped ingress: services whose path prefix must be removed before the # request reaches the backend (Traefik stripPrefix parity). Uses a regex # capture group so `/api/v1/members/foo` -> `/foo`. # Auth-protected when forwardAuth is enabled. +# Auth-protected when forwardAuth is enabled. # --------------------------------------------------------------------------- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -21,8 +23,10 @@ metadata: cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }} {{- end }} {{- if $fa.enabled }} - nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.{{ $.Release.Namespace }}.svc.cluster.local/oauth2/auth" - nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/oauth2/start?rd=$escaped_request_uri" + nginx.ingress.kubernetes.io/auth-url: "http://traefik-forward-auth:4181" + nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/_oauth" + nginx.ingress.kubernetes.io/auth-snippet: | + proxy_set_header X-Forwarded-Uri $request_uri; {{- end }} spec: ingressClassName: {{ .Values.ingress.className }} @@ -53,6 +57,7 @@ spec: # --------------------------------------------------------------------------- # Plain ingress: services served at their path as-is (web-client, api-docs). # Auth-protected when forwardAuth is enabled. +# Auth-protected when forwardAuth is enabled. # --------------------------------------------------------------------------- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -61,12 +66,15 @@ metadata: labels: {{- include "team-devoops.labels" (dict "name" "ingress-plain" "root" $) | nindent 4 }} annotations: + {{- if and $tls.enabled $tls.clusterIssuer }} {{- if and $tls.enabled $tls.clusterIssuer }} cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }} {{- end }} {{- if $fa.enabled }} - nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.{{ $.Release.Namespace }}.svc.cluster.local/oauth2/auth" - nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/oauth2/start?rd=$escaped_request_uri" + nginx.ingress.kubernetes.io/auth-url: "http://traefik-forward-auth:4181" + nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/_oauth" + nginx.ingress.kubernetes.io/auth-snippet: | + proxy_set_header X-Forwarded-Uri $request_uri; {{- end }} spec: ingressClassName: {{ .Values.ingress.className }} @@ -100,6 +108,35 @@ spec: # --------------------------------------------------------------------------- apiVersion: networking.k8s.io/v1 kind: Ingress +metadata: + name: team-devoops-open + labels: + {{- include "team-devoops.labels" (dict "name" "ingress-open" "root" $) | nindent 4 }} + {{- if and $tls.enabled $tls.clusterIssuer }} + annotations: + cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if $tls.enabled }} + tls: + - hosts: + - {{ $host | quote }} + {{- if $tls.secretName }} + secretName: {{ $tls.secretName }} + {{- end }} + {{- end }} + rules: + - host: {{ $host | quote }} + http: + paths: +--- +# --------------------------------------------------------------------------- +# Open ingress: Keycloak (auth provider) and the forward-auth OAuth callback +# must never be behind forward-auth to avoid redirect loops. +# --------------------------------------------------------------------------- +apiVersion: networking.k8s.io/v1 +kind: Ingress metadata: name: team-devoops-open labels: @@ -132,23 +169,12 @@ spec: number: 8080 {{- end }} {{- if $fa.enabled }} - - path: /oauth2/ + - path: /_oauth pathType: Prefix backend: service: - name: oauth2-proxy + name: traefik-forward-auth port: - number: 80 - {{- end }} - {{- range $name, $svc := .Values.services }} - {{- if $svc.open }} - - path: {{ $svc.path }} - pathType: Prefix - backend: - service: - name: {{ $name }} - port: - number: {{ $svc.port }} - {{- end }} + number: 4181 {{- end }} {{- end }} diff --git a/infra/helm/team-devoops/values.yaml b/infra/helm/team-devoops/values.yaml index 521f4d3..9db73f8 100644 --- a/infra/helm/team-devoops/values.yaml +++ b/infra/helm/team-devoops/values.yaml @@ -96,9 +96,9 @@ ingress: clusterIssuer: letsencrypt-prod # --------------------------------------------------------------------------- -# Forward-auth: deploys oauth2-proxy as an OIDC session proxy and wires all -# nginx ingresses through it via auth_request. Keycloak and /oauth2/ are -# excluded from auth to prevent redirect loops. +# Forward-auth: deploys thomseddon/traefik-forward-auth as an OIDC session +# proxy and wires all nginx ingresses through it (parity with the VM's +# traefik-forward-auth middleware). Keycloak and /_oauth are excluded. # --------------------------------------------------------------------------- forwardAuth: enabled: true diff --git a/infra/keycloak/realm-config.json b/infra/keycloak/realm-config.json index f9648eb..20c5dce 100644 --- a/infra/keycloak/realm-config.json +++ b/infra/keycloak/realm-config.json @@ -70,12 +70,13 @@ "directAccessGrantsEnabled": false, "redirectUris": [ "https://team-devoops.uaenorth.cloudapp.azure.com/_oauth", - "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/oauth2/callback", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/_oauth", "http://localhost/_oauth" ], "webOrigins": [ "https://team-devoops.uaenorth.cloudapp.azure.com", "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de", "http://localhost" ] } From 29e3af9e2cd3cc083fe36e60799f2bfb23d447c8 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Wed, 10 Jun 2026 23:22:51 +0200 Subject: [PATCH 10/19] fix cluster deploy --- infra/helm/team-devoops/templates/forward-auth.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/helm/team-devoops/templates/forward-auth.yaml b/infra/helm/team-devoops/templates/forward-auth.yaml index 2ad3809..6418644 100644 --- a/infra/helm/team-devoops/templates/forward-auth.yaml +++ b/infra/helm/team-devoops/templates/forward-auth.yaml @@ -46,7 +46,7 @@ spec: memory: 16Mi limits: cpu: 50m - memory: 64Mi + memory: 32Mi --- apiVersion: v1 kind: Service From 72269e36aafd2500d29c141b2f62a7283a0690f2 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Wed, 10 Jun 2026 23:36:58 +0200 Subject: [PATCH 11/19] fix cluster deploy again --- infra/helm/team-devoops/templates/ingress.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/infra/helm/team-devoops/templates/ingress.yaml b/infra/helm/team-devoops/templates/ingress.yaml index c7e9318..e62556f 100644 --- a/infra/helm/team-devoops/templates/ingress.yaml +++ b/infra/helm/team-devoops/templates/ingress.yaml @@ -25,8 +25,6 @@ metadata: {{- if $fa.enabled }} nginx.ingress.kubernetes.io/auth-url: "http://traefik-forward-auth:4181" nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/_oauth" - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Uri $request_uri; {{- end }} spec: ingressClassName: {{ .Values.ingress.className }} @@ -73,8 +71,6 @@ metadata: {{- if $fa.enabled }} nginx.ingress.kubernetes.io/auth-url: "http://traefik-forward-auth:4181" nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/_oauth" - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Uri $request_uri; {{- end }} spec: ingressClassName: {{ .Values.ingress.className }} From 858749cfb05bbb463e81f715d99dbb272893965b Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Thu, 11 Jun 2026 00:10:38 +0200 Subject: [PATCH 12/19] fix cluster deploy hopefully --- infra/helm/team-devoops/templates/forward-auth.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infra/helm/team-devoops/templates/forward-auth.yaml b/infra/helm/team-devoops/templates/forward-auth.yaml index 6418644..ea9a70d 100644 --- a/infra/helm/team-devoops/templates/forward-auth.yaml +++ b/infra/helm/team-devoops/templates/forward-auth.yaml @@ -40,6 +40,8 @@ spec: value: {{ .Values.forwardAuth.cookieSecret | quote }} - name: INSECURE_COOKIE value: "false" + - name: AUTH_HOST + value: {{ .Values.ingress.host | quote }} resources: requests: cpu: 10m From 071501778d21fe7782b74ba8b46c14bc135c4c47 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Thu, 11 Jun 2026 00:40:50 +0200 Subject: [PATCH 13/19] fix cluster deploy now --- .../helm/team-devoops/files/realm-config.json | 2 +- .../team-devoops/templates/forward-auth.yaml | 56 ++++++++++++------- .../helm/team-devoops/templates/ingress.yaml | 25 ++++++--- infra/helm/team-devoops/values.yaml | 6 +- infra/keycloak/realm-config.json | 2 +- 5 files changed, 58 insertions(+), 33 deletions(-) diff --git a/infra/helm/team-devoops/files/realm-config.json b/infra/helm/team-devoops/files/realm-config.json index 20c5dce..c74075a 100644 --- a/infra/helm/team-devoops/files/realm-config.json +++ b/infra/helm/team-devoops/files/realm-config.json @@ -70,7 +70,7 @@ "directAccessGrantsEnabled": false, "redirectUris": [ "https://team-devoops.uaenorth.cloudapp.azure.com/_oauth", - "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/_oauth", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/oauth2/callback", "http://localhost/_oauth" ], "webOrigins": [ diff --git a/infra/helm/team-devoops/templates/forward-auth.yaml b/infra/helm/team-devoops/templates/forward-auth.yaml index ea9a70d..f2f94e3 100644 --- a/infra/helm/team-devoops/templates/forward-auth.yaml +++ b/infra/helm/team-devoops/templates/forward-auth.yaml @@ -2,46 +2,60 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: traefik-forward-auth + name: oauth2-proxy labels: - {{- include "team-devoops.labels" (dict "name" "traefik-forward-auth" "root" $) | nindent 4 }} + {{- include "team-devoops.labels" (dict "name" "oauth2-proxy" "root" $) | nindent 4 }} spec: replicas: 1 strategy: type: RollingUpdate selector: matchLabels: - {{- include "team-devoops.selectorLabels" (dict "name" "traefik-forward-auth") | nindent 6 }} + {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 6 }} template: metadata: labels: - {{- include "team-devoops.selectorLabels" (dict "name" "traefik-forward-auth") | nindent 8 }} + {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 8 }} spec: {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} containers: - - name: traefik-forward-auth - image: thomseddon/traefik-forward-auth:2 + - name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 imagePullPolicy: IfNotPresent ports: - - containerPort: 4181 + - containerPort: 4180 env: - - name: DEFAULT_PROVIDER + - name: OAUTH2_PROXY_PROVIDER value: oidc - - name: PROVIDERS_OIDC_ISSUER_URL + - name: OAUTH2_PROXY_OIDC_ISSUER_URL value: {{ .Values.forwardAuth.oidcIssuerUrl | quote }} - - name: PROVIDERS_OIDC_CLIENT_ID + - name: OAUTH2_PROXY_CLIENT_ID value: {{ .Values.forwardAuth.clientId | quote }} - - name: PROVIDERS_OIDC_CLIENT_SECRET + - name: OAUTH2_PROXY_CLIENT_SECRET value: {{ .Values.forwardAuth.clientSecret | quote }} - - name: SECRET + - name: OAUTH2_PROXY_COOKIE_SECRET value: {{ .Values.forwardAuth.cookieSecret | quote }} - - name: INSECURE_COOKIE - value: "false" - - name: AUTH_HOST - value: {{ .Values.ingress.host | quote }} + - name: OAUTH2_PROXY_EMAIL_DOMAINS + value: "*" + - name: OAUTH2_PROXY_UPSTREAM + value: "static://202" + - name: OAUTH2_PROXY_HTTP_ADDRESS + value: "0.0.0.0:4180" + - name: OAUTH2_PROXY_REDIRECT_URL + value: {{ printf "https://%s/oauth2/callback" .Values.ingress.host | quote }} + - name: OAUTH2_PROXY_COOKIE_SECURE + value: "true" + - name: OAUTH2_PROXY_SKIP_PROVIDER_BUTTON + value: "true" + - name: OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL + value: "true" + - name: OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST + value: "true" + - name: OAUTH2_PROXY_COOKIE_CSRF_EXPIRE + value: "5m" resources: requests: cpu: 10m @@ -53,13 +67,13 @@ spec: apiVersion: v1 kind: Service metadata: - name: traefik-forward-auth + name: oauth2-proxy labels: - {{- include "team-devoops.labels" (dict "name" "traefik-forward-auth" "root" $) | nindent 4 }} + {{- include "team-devoops.labels" (dict "name" "oauth2-proxy" "root" $) | nindent 4 }} spec: selector: - {{- include "team-devoops.selectorLabels" (dict "name" "traefik-forward-auth") | nindent 4 }} + {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 4 }} ports: - - port: 4181 - targetPort: 4181 + - port: 80 + targetPort: 4180 {{- end }} diff --git a/infra/helm/team-devoops/templates/ingress.yaml b/infra/helm/team-devoops/templates/ingress.yaml index e62556f..02356d2 100644 --- a/infra/helm/team-devoops/templates/ingress.yaml +++ b/infra/helm/team-devoops/templates/ingress.yaml @@ -23,8 +23,8 @@ metadata: cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }} {{- end }} {{- if $fa.enabled }} - nginx.ingress.kubernetes.io/auth-url: "http://traefik-forward-auth:4181" - nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/_oauth" + nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.{{ $.Release.Namespace }}.svc.cluster.local/oauth2/auth" + nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/oauth2/start?rd=$escaped_request_uri" {{- end }} spec: ingressClassName: {{ .Values.ingress.className }} @@ -69,8 +69,8 @@ metadata: cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }} {{- end }} {{- if $fa.enabled }} - nginx.ingress.kubernetes.io/auth-url: "http://traefik-forward-auth:4181" - nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/_oauth" + nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.{{ $.Release.Namespace }}.svc.cluster.local/oauth2/auth" + nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/oauth2/start?rd=$escaped_request_uri" {{- end }} spec: ingressClassName: {{ .Values.ingress.className }} @@ -165,12 +165,23 @@ spec: number: 8080 {{- end }} {{- if $fa.enabled }} - - path: /_oauth + - path: /oauth2/ pathType: Prefix backend: service: - name: traefik-forward-auth + name: oauth2-proxy port: - number: 4181 + number: 80 + {{- end }} + {{- range $name, $svc := .Values.services }} + {{- if $svc.open }} + - path: {{ $svc.path }} + pathType: Prefix + backend: + service: + name: {{ $name }} + port: + number: {{ $svc.port }} + {{- end }} {{- end }} {{- end }} diff --git a/infra/helm/team-devoops/values.yaml b/infra/helm/team-devoops/values.yaml index 9db73f8..521f4d3 100644 --- a/infra/helm/team-devoops/values.yaml +++ b/infra/helm/team-devoops/values.yaml @@ -96,9 +96,9 @@ ingress: clusterIssuer: letsencrypt-prod # --------------------------------------------------------------------------- -# Forward-auth: deploys thomseddon/traefik-forward-auth as an OIDC session -# proxy and wires all nginx ingresses through it (parity with the VM's -# traefik-forward-auth middleware). Keycloak and /_oauth are excluded. +# Forward-auth: deploys oauth2-proxy as an OIDC session proxy and wires all +# nginx ingresses through it via auth_request. Keycloak and /oauth2/ are +# excluded from auth to prevent redirect loops. # --------------------------------------------------------------------------- forwardAuth: enabled: true diff --git a/infra/keycloak/realm-config.json b/infra/keycloak/realm-config.json index 20c5dce..c74075a 100644 --- a/infra/keycloak/realm-config.json +++ b/infra/keycloak/realm-config.json @@ -70,7 +70,7 @@ "directAccessGrantsEnabled": false, "redirectUris": [ "https://team-devoops.uaenorth.cloudapp.azure.com/_oauth", - "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/_oauth", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/oauth2/callback", "http://localhost/_oauth" ], "webOrigins": [ From 5a9bb8289b27a2670231e9dcb018a57f8ab64570 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Thu, 11 Jun 2026 01:02:21 +0200 Subject: [PATCH 14/19] fix cluster sub claim --- infra/helm/team-devoops/templates/forward-auth.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infra/helm/team-devoops/templates/forward-auth.yaml b/infra/helm/team-devoops/templates/forward-auth.yaml index f2f94e3..e8d904d 100644 --- a/infra/helm/team-devoops/templates/forward-auth.yaml +++ b/infra/helm/team-devoops/templates/forward-auth.yaml @@ -52,6 +52,8 @@ spec: value: "true" - name: OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL value: "true" + - name: OAUTH2_PROXY_OIDC_EMAIL_CLAIM + value: "sub" - name: OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST value: "true" - name: OAUTH2_PROXY_COOKIE_CSRF_EXPIRE From ab0da87202369a61ac0d915f8c2c62b1cbbcbaf2 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Thu, 11 Jun 2026 17:57:11 +0200 Subject: [PATCH 15/19] fix local traefik websecure labels --- infra/docker-compose.override.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infra/docker-compose.override.yml b/infra/docker-compose.override.yml index fa850f7..8339da7 100644 --- a/infra/docker-compose.override.yml +++ b/infra/docker-compose.override.yml @@ -123,6 +123,11 @@ services: - "traefik.http.services.web-client.loadbalancer.server.port=8080" keycloak: + labels: !override + - "traefik.enable=true" + - "traefik.http.routers.keycloak.entrypoints=web" + - "traefik.http.routers.keycloak.rule=PathPrefix(`/auth`)" + - "traefik.http.services.keycloak.loadbalancer.server.port=8080" environment: KC_HOSTNAME: "http://localhost:8081/auth" From ffb69e56c397efd2c997a03ccbc9874fcddf8a14 Mon Sep 17 00:00:00 2001 From: Raphael Frank <04.raphael.frank@gmail.com> Date: Thu, 11 Jun 2026 20:36:43 +0200 Subject: [PATCH 16/19] fix local authentication --- infra/docker-compose.override.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/infra/docker-compose.override.yml b/infra/docker-compose.override.yml index 8339da7..a592764 100644 --- a/infra/docker-compose.override.yml +++ b/infra/docker-compose.override.yml @@ -25,6 +25,8 @@ services: - "--providers.docker=true" - "--providers.docker.exposedByDefault=false" - "--providers.docker.network=proxy" + - "--providers.file.directory=/etc/traefik/dynamic" + - "--providers.file.watch=true" - "--entrypoints.web.address=:80" ports: !override - "80:80" @@ -36,7 +38,7 @@ services: - "traefik.http.routers.py-genai-helper.entrypoints=web" - "traefik.http.routers.py-genai-helper.rule=PathPrefix(`/api/v1/helper`)" - "traefik.http.middlewares.helper-stripprefix.stripprefix.prefixes=/api/v1/helper" - - "traefik.http.routers.py-genai-helper.middlewares=helper-stripprefix" + - "traefik.http.routers.py-genai-helper.middlewares=helper-stripprefix,forward-auth@file" - "traefik.http.services.py-genai-helper.loadbalancer.server.port=5000" organization-service: @@ -47,7 +49,7 @@ services: - "traefik.http.routers.organization-service.entrypoints=web" - "traefik.http.routers.organization-service.rule=PathPrefix(`/api/v1/organization`)" - "traefik.http.middlewares.organization-stripprefix.stripprefix.prefixes=/api/v1/organization" - - "traefik.http.routers.organization-service.middlewares=organization-stripprefix" + - "traefik.http.routers.organization-service.middlewares=organization-stripprefix,forward-auth@file" - "traefik.http.services.organization-service.loadbalancer.server.port=8080" member-service: @@ -58,7 +60,7 @@ services: - "traefik.http.routers.member-service.entrypoints=web" - "traefik.http.routers.member-service.rule=PathPrefix(`/api/v1/members`)" - "traefik.http.middlewares.member-stripprefix.stripprefix.prefixes=/api/v1/members" - - "traefik.http.routers.member-service.middlewares=member-stripprefix" + - "traefik.http.routers.member-service.middlewares=member-stripprefix,forward-auth@file" - "traefik.http.services.member-service.loadbalancer.server.port=8080" event-service: @@ -69,7 +71,7 @@ services: - "traefik.http.routers.event-service.entrypoints=web" - "traefik.http.routers.event-service.rule=PathPrefix(`/api/v1/events`)" - "traefik.http.middlewares.event-stripprefix.stripprefix.prefixes=/api/v1/events" - - "traefik.http.routers.event-service.middlewares=event-stripprefix" + - "traefik.http.routers.event-service.middlewares=event-stripprefix,forward-auth@file" - "traefik.http.services.event-service.loadbalancer.server.port=8080" feedback-service: @@ -80,7 +82,7 @@ services: - "traefik.http.routers.feedback-service.entrypoints=web" - "traefik.http.routers.feedback-service.rule=PathPrefix(`/api/v1/feedback`)" - "traefik.http.middlewares.feedback-stripprefix.stripprefix.prefixes=/api/v1/feedback" - - "traefik.http.routers.feedback-service.middlewares=feedback-stripprefix" + - "traefik.http.routers.feedback-service.middlewares=feedback-stripprefix,forward-auth@file" - "traefik.http.services.feedback-service.loadbalancer.server.port=8080" finance-service: @@ -91,7 +93,7 @@ services: - "traefik.http.routers.finance-service.entrypoints=web" - "traefik.http.routers.finance-service.rule=PathPrefix(`/api/v1/finance`)" - "traefik.http.middlewares.finance-stripprefix.stripprefix.prefixes=/api/v1/finance" - - "traefik.http.routers.finance-service.middlewares=finance-stripprefix" + - "traefik.http.routers.finance-service.middlewares=finance-stripprefix,forward-auth@file" - "traefik.http.services.finance-service.loadbalancer.server.port=8080" letter-service: @@ -102,7 +104,7 @@ services: - "traefik.http.routers.letter-service.entrypoints=web" - "traefik.http.routers.letter-service.rule=PathPrefix(`/api/v1/letters`)" - "traefik.http.middlewares.letter-stripprefix.stripprefix.prefixes=/api/v1/letters" - - "traefik.http.routers.letter-service.middlewares=letter-stripprefix" + - "traefik.http.routers.letter-service.middlewares=letter-stripprefix,forward-auth@file" - "traefik.http.services.letter-service.loadbalancer.server.port=8080" api-docs: @@ -120,6 +122,7 @@ services: - "traefik.enable=true" - "traefik.http.routers.web-client.entrypoints=web" - "traefik.http.routers.web-client.rule=PathPrefix(`/`)" + - "traefik.http.routers.web-client.middlewares=forward-auth@file" - "traefik.http.services.web-client.loadbalancer.server.port=8080" keycloak: @@ -132,6 +135,8 @@ services: KC_HOSTNAME: "http://localhost:8081/auth" traefik-forward-auth: + extra_hosts: + - "localhost:host-gateway" labels: !override - "traefik.enable=false" environment: From 0d9a4713bc81ac8fa0c64f03f87db4c3dc7513c4 Mon Sep 17 00:00:00 2001 From: f-s-h Date: Fri, 12 Jun 2026 12:50:30 +0200 Subject: [PATCH 17/19] Added ollama service --- infra/docker-compose.yml | 13 +++++++++++++ infra/ollama/entrypoint_ollama.sh | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 infra/ollama/entrypoint_ollama.sh diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 99c83db..037e043 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -365,10 +365,23 @@ services: networks: - data + ollama: + image: ollama/ollama:latest + container_name: ollama + expose: + - 11434 + volumes: + - ollama_data:/root/.ollama + - ./ollama/entrypoint_ollama.sh:/entrypoint_ollama.sh + networks: + - proxy + entrypoint: ["/usr/bin/bash", "/entrypoint_ollama.sh"] + volumes: member_db_data: keycloak_db_data: letsencrypt: + ollama_data: networks: proxy: diff --git a/infra/ollama/entrypoint_ollama.sh b/infra/ollama/entrypoint_ollama.sh new file mode 100644 index 0000000..f874b4f --- /dev/null +++ b/infra/ollama/entrypoint_ollama.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Start Ollama in the background. +/bin/ollama serve & +# Record Process ID. +pid=$! + +# Pause for Ollama to start. +sleep 5 + +echo "Retrieve model..." +ollama pull qwen3:8b +echo "Done!" + +# Wait for Ollama process to finish. +wait $pid \ No newline at end of file From 5a5edeeebfe7791f4aaf5e06f015acad9617ab74 Mon Sep 17 00:00:00 2001 From: f-s-h Date: Fri, 12 Jun 2026 15:31:35 +0200 Subject: [PATCH 18/19] Fixes for internal error --- infra/ollama/entrypoint_ollama.sh | 1 + services/py-genai-helper/Dockerfile | 2 +- services/py-genai-helper/app.py | 2 +- services/py-genai-helper/rag.py | 5 +++-- services/py-genai-helper/service.py | 11 +++++++++-- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/infra/ollama/entrypoint_ollama.sh b/infra/ollama/entrypoint_ollama.sh index f874b4f..78da65a 100644 --- a/infra/ollama/entrypoint_ollama.sh +++ b/infra/ollama/entrypoint_ollama.sh @@ -10,6 +10,7 @@ sleep 5 echo "Retrieve model..." ollama pull qwen3:8b +ollama pull nomic-embed-text echo "Done!" # Wait for Ollama process to finish. diff --git a/services/py-genai-helper/Dockerfile b/services/py-genai-helper/Dockerfile index 0c4da64..d4c045a 100644 --- a/services/py-genai-helper/Dockerfile +++ b/services/py-genai-helper/Dockerfile @@ -34,4 +34,4 @@ EXPOSE 5000 HEALTHCHECK --interval=30s --timeout=3s --start-period=15s CMD wget -qO- http://127.0.0.1:5000/health || exit 1 # Run app -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--timeout", "300", "app:app"] diff --git a/services/py-genai-helper/app.py b/services/py-genai-helper/app.py index f0fe683..cacba3f 100644 --- a/services/py-genai-helper/app.py +++ b/services/py-genai-helper/app.py @@ -29,7 +29,7 @@ def rag_response(): return {"response": response}, 200 @app.route("/rag-response-local", methods=["POST"]) -def rag_response(): +def rag_response_local(): # Get the json of the object. force=True ignores the stated MimeType data = request.get_json(force=True) or {} question = data.get("question") diff --git a/services/py-genai-helper/rag.py b/services/py-genai-helper/rag.py index 712b34f..192dc0a 100644 --- a/services/py-genai-helper/rag.py +++ b/services/py-genai-helper/rag.py @@ -28,14 +28,15 @@ def _load_pdfs(embeddings) -> FAISS | None: return FAISS.from_documents(docs, embedding=embeddings) -_local_vector_store = _load_pdfs(OllamaEmbeddings(model="nomic-embed-text")) +_local_vector_store = _load_pdfs(OllamaEmbeddings(model="nomic-embed-text", base_url="http://ollama:11434")) _remote_vector_store = _load_pdfs(OpenAIEmbeddings(model="text-embedding-3-large")) def get_rag_agent(local: bool): + global _local_vector_store, _remote_vector_store if local: vector_store = _local_vector_store - model = ChatOllama(model="llama3.2") + model = ChatOllama(model="qwen3:8b", base_url="http://ollama:11434", think=False) else: vector_store = _remote_vector_store model = "gpt-4.1-mini" diff --git a/services/py-genai-helper/service.py b/services/py-genai-helper/service.py index 410f17b..c41b9a2 100644 --- a/services/py-genai-helper/service.py +++ b/services/py-genai-helper/service.py @@ -5,11 +5,18 @@ from rag import get_rag_agent load_dotenv() -agent = create_agent("gpt-4.1-mini") +_agent = None + + +def _get_agent(): + global _agent + if _agent is None: + _agent = create_agent("gpt-4.1-mini") + return _agent def hello(): - response = agent.invoke( + response = _get_agent().invoke( { "messages": [ SystemMessage( From 1bf0972ceee4b0bbb180c51a8c606b0e0c82f91d Mon Sep 17 00:00:00 2001 From: f-s-h Date: Fri, 12 Jun 2026 15:36:24 +0200 Subject: [PATCH 19/19] Lint fixes --- services/py-genai-helper/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/py-genai-helper/app.py b/services/py-genai-helper/app.py index cacba3f..94f98e6 100644 --- a/services/py-genai-helper/app.py +++ b/services/py-genai-helper/app.py @@ -38,4 +38,4 @@ def rag_response_local(): return {"error": "Missing required field: 'question'"}, 400 response = generate_rag_response(question, local=True) - return {"response": response}, 200 \ No newline at end of file + return {"response": response}, 200