From fb47d87d03c62d9712e8d5b9a6456b704aa2811d Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 08:48:32 +0200 Subject: [PATCH 01/77] cleanup --- backend/src/Controller/Admin/DashboardController.php | 1 - backend/src/Controller/GameController.php | 1 - 2 files changed, 2 deletions(-) diff --git a/backend/src/Controller/Admin/DashboardController.php b/backend/src/Controller/Admin/DashboardController.php index 8ec35ea..6bbcdb9 100644 --- a/backend/src/Controller/Admin/DashboardController.php +++ b/backend/src/Controller/Admin/DashboardController.php @@ -2,7 +2,6 @@ namespace App\Controller\Admin; -use App\Entity\User; use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard; use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard; use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem; diff --git a/backend/src/Controller/GameController.php b/backend/src/Controller/GameController.php index 3c3f473..9aff371 100644 --- a/backend/src/Controller/GameController.php +++ b/backend/src/Controller/GameController.php @@ -10,7 +10,6 @@ use App\Game\Command\RemoveGameCommand; use App\Game\Game; use App\Game\Games; -use App\Game\Statistic; use App\Game\Statistics; use Faker\Factory as Faker; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; From 3d5193002d42549d4bfc4e2437b7d2254380cc7e Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 08:48:41 +0200 Subject: [PATCH 02/77] prepare redesign of login --- FEATURES.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/FEATURES.md b/FEATURES.md index ca4bc96..94675dd 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -57,3 +57,31 @@ - ❌ Provision Grafana dashboard, e.g. via ConfigMap - ❌ Deploy DB via stateful set - ❌ Include Alpine image into Docker multistage build for production + +# Unify login + +- current state:: + - legacy app: + - uses form_login (see security.yaml) + - a "non-admin" user logs in via route app.login (see: backend/src/Controller/LoginController.php + - admin panel: + - uses form_login (see security.yaml) + - a "admin" user logs uses the same login controller to access the admin panel + - the admin panel is secured via firewall - the user must have ROLE_ADMIN + - modern app: + - uses json_login (see security.yaml) + - provides a login form via React frontend + +- vision: + - legacy app: + - keep form_login as it is + - when regular user logs in redirect to the GameController::index() (as it is currently) + - when admin logs in redirect to the DashboardController::index() if possible (if it is too hard to implement - don't implement the redirect) + - keep the admin route for explicit admin login + - modern app: + - get inspiration from the split-fairly app + - this is a sibling project to this project + - it uses explicit vite build docker compose service + - it provides a highly customized SpaController + - uses form_login now + - check if we can reuse the existing TWIG form from the legacy app \ No newline at end of file From 87a7961eddf5f3096715374299f74b66f9fbef76 Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 09:23:45 +0200 Subject: [PATCH 03/77] Unify login: session-based SPA with form authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement unified login process across legacy and modern apps using session-based form authentication (form_login only). React app now only available when authenticated. Backend Changes: - Create AuthenticationSuccessHandler for role-based post-login redirects * Regular users → /game/index * Admin users → /admin dashboard - Remove json_login entirely (API login no longer supported) - Create SpaController to serve React app for authenticated users - Add optional /admin/login convenience redirect - Enhance login.html.twig with professional Bootstrap styling - Remove API login/logout endpoints (API/LoginController.php, API/LogoutController.php) - Keep /api/me for session verification (checkRememberMeAsync) Frontend Changes: - Remove LoginView.tsx (login now handled by server form) - Simplify Router to authenticated-only routes - Update auth API: remove loginAsync, simplify logoutAsync - Simplify UserContext (remove login/pending logic) - Frontend now assumes session-based authentication Docker Build: - Add Node 20 build stage to compile React with Vite - Copy built frontend assets (dist/) to prod image public/dist - Single production image ships both backend + frontend Configuration: - Add SPA_BACKEND_API_URL and SPA_OTEL_COLLECTOR_ADDRESS env variables - Clarify access control rules in security.yaml - Ensure route priority: admin > login > game > spa catch-all Architecture: - Unauthenticated users see TWIG login form at /login - After login, redirected to /game or /admin based on role - Authenticated users access React SPA via SpaController - Session persisted via form_login (shared session store via Redis) - Logout clears session and redirects to login form This implements a session-based SPA pattern inspired by split-fairly, where the frontend is only served when authenticated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/.env | 5 +- backend/config/packages/security.yaml | 11 +- backend/config/routes.yaml | 5 +- backend/config/services.yaml | 7 + .../src/Controller/API/LoginController.php | 30 --- .../src/Controller/API/LogoutController.php | 28 --- backend/src/Controller/API/MeController.php | 6 +- .../AdminLoginRedirectController.php | 23 ++ backend/src/Controller/SpaController.php | 30 +++ .../Security/AuthenticationSuccessHandler.php | 28 +++ backend/templates/login/login.html.twig | 153 +++++++------- backend/templates/spa/index.html.twig | 15 ++ build/php/Dockerfile | 18 ++ frontend/index.html | 4 +- frontend/package-lock.json | 200 ++++++++++-------- frontend/src/App.tsx | 11 +- frontend/src/Router.tsx | 41 ++-- frontend/src/features/auth/LoginView.tsx | 161 -------------- frontend/src/features/auth/UserContext.tsx | 10 - .../src/features/auth/UserContextProvider.tsx | 31 +-- frontend/src/features/auth/api.ts | 46 +--- frontend/src/features/auth/index.ts | 2 +- 22 files changed, 363 insertions(+), 502 deletions(-) delete mode 100644 backend/src/Controller/API/LoginController.php delete mode 100644 backend/src/Controller/API/LogoutController.php create mode 100644 backend/src/Controller/AdminLoginRedirectController.php create mode 100644 backend/src/Controller/SpaController.php create mode 100644 backend/src/Security/AuthenticationSuccessHandler.php create mode 100644 backend/templates/spa/index.html.twig delete mode 100644 frontend/src/features/auth/LoginView.tsx diff --git a/backend/.env b/backend/.env index f325b44..f0715ba 100644 --- a/backend/.env +++ b/backend/.env @@ -57,4 +57,7 @@ LOCK_DSN=flock ###< symfony/lock ### ADMIN_EMAIL='admin@example.com' -ADMIN_PASSWORD='secret' \ No newline at end of file +ADMIN_PASSWORD='secret' +# SPA Frontend Configuration +SPA_BACKEND_API_URL="http://localhost:8080/api" +SPA_OTEL_COLLECTOR_ADDRESS="http://localhost:4318" diff --git a/backend/config/packages/security.yaml b/backend/config/packages/security.yaml index e4bf3ce..27cc6d6 100644 --- a/backend/config/packages/security.yaml +++ b/backend/config/packages/security.yaml @@ -16,15 +16,11 @@ security: main: lazy: true provider: app_user_provider - json_login: - login_path: api.login - check_path: api.login - username_path: email - password_path: password form_login: login_path: app.login check_path: app.login enable_csrf: true + success_handler: App\Security\AuthenticationSuccessHandler remember_me: secret: '%kernel.secret%' lifetime: 604800 # 1 week in seconds @@ -46,10 +42,11 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } - - { path: ^/login, roles: PUBLIC_ACCESS } + # Game routes require authenticated user - { path: ^/game, roles: ROLE_USER } + # /api/me is needed by SPA to verify session on load (checkRememberMeAsync) - { path: ^/api/me, roles: PUBLIC_ACCESS } + # Future API routes (if needed) would require authentication - { path: ^/api, roles: ROLE_USER } when@test: diff --git a/backend/config/routes.yaml b/backend/config/routes.yaml index fee3e05..e2c18ff 100644 --- a/backend/config/routes.yaml +++ b/backend/config/routes.yaml @@ -4,9 +4,12 @@ controllers: namespace: App\Controller type: attribute +# For unauthenticated users, redirect to login +# For authenticated users, SpaController will handle all remaining routes root_shortcut: path: / controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController defaults: route: 'app.index_games' - ignoreAttributes: true + ignoreAttributes: false + priority: -10 diff --git a/backend/config/services.yaml b/backend/config/services.yaml index dad4740..41c72bf 100644 --- a/backend/config/services.yaml +++ b/backend/config/services.yaml @@ -8,6 +8,8 @@ parameters: app.telemetry: '%env(APP_TELEMETRY)%' admin.email: '%env(ADMIN_EMAIL)%' admin.password: '%env(ADMIN_PASSWORD)%' + spa.backend_api_url: '%env(string:SPA_BACKEND_API_URL)%' + spa.otel_collector_address: '%env(string:SPA_OTEL_COLLECTOR_ADDRESS)%' services: # default configuration for services in *this* file @@ -50,3 +52,8 @@ services: - '%kernel.project_dir%' tags: - { name: data_collector, template: 'profiler/architecture.html.twig', id: 'app.architecture_collector' } + + App\Controller\SpaController: + arguments: + $backendApiUrl: '%spa.backend_api_url%' + $otelCollectorAddress: '%spa.otel_collector_address%' diff --git a/backend/src/Controller/API/LoginController.php b/backend/src/Controller/API/LoginController.php deleted file mode 100644 index 3b435d8..0000000 --- a/backend/src/Controller/API/LoginController.php +++ /dev/null @@ -1,30 +0,0 @@ -json( - ['message' => 'missing credentials'], - Response::HTTP_UNAUTHORIZED - ); - } - - return $this->json([ - 'user' => $user->getUserIdentifier(), - // TODO: list roles, permissions, created tokens etc. - ]); - } -} diff --git a/backend/src/Controller/API/LogoutController.php b/backend/src/Controller/API/LogoutController.php deleted file mode 100644 index d41b749..0000000 --- a/backend/src/Controller/API/LogoutController.php +++ /dev/null @@ -1,28 +0,0 @@ -invalidate(); - $tokenStorage->setToken(null); - - $response = new Response(content: null, status: Response::HTTP_NO_CONTENT); - - $response->headers->clearCookie('PHPSESSID', '/', null, false, false, 'lax'); - $response->headers->clearCookie('REMEMBERME', '/', null, false, false, 'lax'); - - return $response; - } -} diff --git a/backend/src/Controller/API/MeController.php b/backend/src/Controller/API/MeController.php index 1785683..ad91c6c 100644 --- a/backend/src/Controller/API/MeController.php +++ b/backend/src/Controller/API/MeController.php @@ -7,6 +7,11 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +/** + * API endpoint to check current user session. + * Used by frontend's checkRememberMeAsync() during app initialization. + * Required for session verification when user returns with remember-me cookie. + */ #[Route('/api', name: 'api.')] class MeController extends AbstractController { @@ -23,7 +28,6 @@ public function me(): JsonResponse return $this->json([ 'user' => $user->getUserIdentifier(), - // TODO: list roles, permissions, created tokens etc. ]); } } diff --git a/backend/src/Controller/AdminLoginRedirectController.php b/backend/src/Controller/AdminLoginRedirectController.php new file mode 100644 index 0000000..e8f88c6 --- /dev/null +++ b/backend/src/Controller/AdminLoginRedirectController.php @@ -0,0 +1,23 @@ +redirectToRoute('app.login', [ + '_username' => 'admin@example.com', + ], status: RedirectResponse::HTTP_FOUND); + } +} diff --git a/backend/src/Controller/SpaController.php b/backend/src/Controller/SpaController.php new file mode 100644 index 0000000..9304acf --- /dev/null +++ b/backend/src/Controller/SpaController.php @@ -0,0 +1,30 @@ + '^(?!api|admin|login|logout|game|_).+'], methods: ['GET'])] + public function index(): Response + { + return $this->render('spa/index.html.twig', [ + 'backend_api_url' => $this->backendApiUrl, + 'otel_collector_address' => $this->otelCollectorAddress, + ]); + } +} + diff --git a/backend/src/Security/AuthenticationSuccessHandler.php b/backend/src/Security/AuthenticationSuccessHandler.php new file mode 100644 index 0000000..59c9d77 --- /dev/null +++ b/backend/src/Security/AuthenticationSuccessHandler.php @@ -0,0 +1,28 @@ +getUser(); + + if ($user && in_array('ROLE_ADMIN', $user->getRoles(), strict: true)) { + return new RedirectResponse($this->router->generate('admin')); + } + + return new RedirectResponse($this->router->generate('app.index_games')); + } +} diff --git a/backend/templates/login/login.html.twig b/backend/templates/login/login.html.twig index b79e8f5..e5bde17 100644 --- a/backend/templates/login/login.html.twig +++ b/backend/templates/login/login.html.twig @@ -1,54 +1,98 @@ {% extends 'base.html.twig' %} -{% block title %}Log in!{% endblock %} +{% block title %}Sign In{% endblock %} {% block body %} - -
+ +
-
-
-
- {% if error %} -
{{ error.messageKey|trans(error.messageData, 'security') }}
- {% endif %} +
+
+ +
+

Welcome Back

+

Sign in to your account

+
- {% if app.user %} -
- You are logged in as {{ app.user.userIdentifier }}, Logout -
- {% endif %} + + {% if error %} + + {% endif %} -

Sign in

+ + {% if app.user %} +
+ Already signed in: You are logged in as {{ app.user.userIdentifier }} +
Logout here if you want to sign in with a different account. +
+ {% endif %} -
- - + + + +
+ + + We'll never share your email.
-
- - + + +
+ +
- {# - Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. - See https://symfony.com/doc/current/security/remember_me.html - #} -
- - + +
+ +
+ -
- -
+ + + +
+ + Need help? Contact your administrator. + +
@@ -56,47 +100,4 @@
- - {% endblock %} diff --git a/backend/templates/spa/index.html.twig b/backend/templates/spa/index.html.twig new file mode 100644 index 0000000..a6ea0bd --- /dev/null +++ b/backend/templates/spa/index.html.twig @@ -0,0 +1,15 @@ + + + + + + + React + K8s demo app + + + + +
+ + + diff --git a/build/php/Dockerfile b/build/php/Dockerfile index 6a041f5..79c0504 100644 --- a/build/php/Dockerfile +++ b/build/php/Dockerfile @@ -14,6 +14,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ COPY build/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini +# ----------------------------------- +# Frontend-Stage (Node + Vite build) +# ----------------------------------- +FROM node:20-alpine AS frontend + +WORKDIR /var/www/project/frontend + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ . + +# Build React app with Vite +RUN npm run build + # ----------------------------------- # Development-Stage (with Composer + XDebug) # ----------------------------------- @@ -76,6 +91,9 @@ COPY backend/src ./src COPY backend/symfony.lock . COPY backend/templates ./templates +# Copy built React frontend from frontend stage +COPY --from=frontend /var/www/project/frontend/dist ./public/dist + #RUN mkdir -p var/cache var/log && chmod -R 777 var # The following might not be required once we have the shared cache - check it! #RUN mkdir var && chmod -R 777 var diff --git a/frontend/index.html b/frontend/index.html index ae8e3fa..51a0b63 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,8 +5,8 @@ React + K8s demo app - - + +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 421663a..c6906eb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3336,17 +3336,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", - "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/type-utils": "8.58.0", - "@typescript-eslint/utils": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3359,23 +3359,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.58.0", + "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", - "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" }, "engines": { @@ -3391,14 +3391,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", - "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.0", - "@typescript-eslint/types": "^8.58.0", + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" }, "engines": { @@ -3413,14 +3413,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", - "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3431,9 +3431,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", - "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", "dev": true, "license": "MIT", "engines": { @@ -3448,15 +3448,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", - "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3473,9 +3473,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", - "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", "dev": true, "license": "MIT", "engines": { @@ -3487,16 +3487,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", - "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.58.0", - "@typescript-eslint/tsconfig-utils": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3515,16 +3515,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", - "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0" + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3539,13 +3539,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", - "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4211,9 +4211,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001786", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", - "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", "dev": true, "funding": [ { @@ -5018,9 +5018,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.332", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.332.tgz", - "integrity": "sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==", + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", "dev": true, "license": "ISC" }, @@ -5054,9 +5054,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -7574,9 +7574,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "dev": true, "funding": [ { @@ -7723,9 +7723,9 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", "peer": true, "engines": { @@ -7733,22 +7733,22 @@ } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.5" } }, "node_modules/react-is": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", "license": "MIT" }, "node_modules/react-refresh": { @@ -8171,14 +8171,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -8579,14 +8579,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -8817,16 +8817,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", - "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz", + "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.0", - "@typescript-eslint/parser": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/utils": "8.58.0" + "@typescript-eslint/eslint-plugin": "8.58.1", + "@typescript-eslint/parser": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9434,6 +9434,24 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 07f2390..dfd1dad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,7 @@ import { MyErrorBoundary, CustomThemeProvider, Loading } from "./components"; import Router from "./Router"; import { Notifier, useRememberMe } from "./features"; import { UserContextProvider } from "./features/auth"; +import { BrowserRouter } from "react-router-dom"; export default function App() { const { pending, me } = useRememberMe(); @@ -11,11 +12,13 @@ export default function App() { {pending ? ( - + ) : ( - - - + + + + + )} diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 7c645ea..9454da2 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -1,36 +1,27 @@ -import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { Routes, Route, Navigate } from "react-router-dom"; import { NavBar, NotFound } from "./components"; import { styled } from "@mui/material"; -import { Games, UserContext, LoginView } from "./features"; +import { Games, UserContext } from "./features"; import { useContext } from "react"; const Offset = styled("div")(({ theme }) => theme.mixins.toolbar); export default function Router() { - const { user, loginAsync, pending, logout } = useContext(UserContext); + const { user, logout } = useContext(UserContext); + + if (!user) { + return null; + } return ( - - {!user ? ( - - } - /> - } /> - - ) : ( - <> - - - - } /> - } /> - } /> - } /> - - - )} - + <> + + + + } /> + } /> + } /> + + ); } diff --git a/frontend/src/features/auth/LoginView.tsx b/frontend/src/features/auth/LoginView.tsx deleted file mode 100644 index 8686410..0000000 --- a/frontend/src/features/auth/LoginView.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useState } from "react"; -import { - TextField, - Button, - Box, - styled, - Divider, - Checkbox, - FormControlLabel, -} from "@mui/material"; -import MuiCard from "@mui/material/Card"; -import Stack from "@mui/material/Stack"; -import { useNavigate, useLocation } from "react-router-dom"; -import { Google } from "../../assets/icons/Google"; -import { Facebook } from "../../assets/icons/Facebook"; -import Flower from "../../assets/icons/Flower"; - -const Card = styled(MuiCard)(({ theme }) => ({ - display: "flex", - flexDirection: "column", - alignSelf: "center", - width: "100%", - padding: theme.spacing(4), - gap: theme.spacing(2), - margin: "auto", - [theme.breakpoints.up("sm")]: { - maxWidth: "340px", - }, - boxShadow: - "hsla(220, 30%, 5%, 0.05) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.05) 0px 15px 35px -5px", - ...theme.applyStyles("dark", { - boxShadow: - "hsla(220, 30%, 5%, 0.5) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.08) 0px 15px 35px -5px", - }), -})); - -const LoginViewContainer = styled(Stack)(({ theme }) => ({ - height: "100dvh", - width: "100vw", // Volle Breite - overflow: "hidden", // Kein Scrollen - position: "relative", - padding: theme.spacing(2), - boxSizing: "border-box", - alignItems: "center", // Horizontal zentriert - justifyContent: "center", // Vertikal zentriert - [theme.breakpoints.up("sm")]: { - padding: theme.spacing(4), - }, - "&::before": { - content: '""', - display: "block", - position: "absolute", - zIndex: -1, - inset: 0, - backgroundImage: - "radial-gradient(ellipse at 50% 50%, hsl(210, 100%, 97%), hsl(0, 0%, 100%))", - backgroundRepeat: "no-repeat", - ...theme.applyStyles("dark", { - backgroundImage: - "radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))", - }), - }, -})); - -type Props = { - loginAsync: ( - email: string, - password: string, - rememberMe: boolean, - ) => Promise; - pending: boolean; -}; - -export default function LoginView({ loginAsync, pending }: Props) { - const [email, setEmail] = useState(undefined); - const [password, setPassword] = useState(undefined); - const [rememberMe, setRememberMe] = useState(true); - const navigate = useNavigate(); - const location = useLocation(); - const from = (location.state as { from?: Location })?.from?.pathname || "/"; - - const toggleRememberMe = () => setRememberMe(!rememberMe); - - const doLogin = () => { - loginAsync(email!, password!, rememberMe).then(() => - navigate(from, { replace: true }), - ); - }; - - const canSubmit = - (email as unknown as boolean) && (password as unknown as boolean); - - const submit = (event: React.FormEvent) => { - event.preventDefault(); - doLogin(); - }; - - return ( - - - - - - - setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> - - } - label="Remember me" - /> - - - or - - - - - - - ); -} diff --git a/frontend/src/features/auth/UserContext.tsx b/frontend/src/features/auth/UserContext.tsx index 8e089ba..31bf32a 100644 --- a/frontend/src/features/auth/UserContext.tsx +++ b/frontend/src/features/auth/UserContext.tsx @@ -6,22 +6,12 @@ export interface User { type UserContextType = { user?: User; - loginAsync: ( - email: string, - password: string, - rememberMe: boolean, - ) => Promise; logout: () => void; - pending: boolean; }; const INITIAL_STATE: UserContextType = { user: undefined, - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - loginAsync: (_email: string, _password: string, _rememberMe: boolean) => - Promise.resolve(), logout: () => {}, - pending: false, }; export const UserContext = createContext(INITIAL_STATE); diff --git a/frontend/src/features/auth/UserContextProvider.tsx b/frontend/src/features/auth/UserContextProvider.tsx index 7a147ba..7d023f5 100644 --- a/frontend/src/features/auth/UserContextProvider.tsx +++ b/frontend/src/features/auth/UserContextProvider.tsx @@ -1,7 +1,7 @@ -import React, { useContext, useState } from "react"; +import React from "react"; import { User, UserContext } from "./UserContext"; import { NotifierContext } from "../notifier/NotifierContext"; -import { loginAsync, logoutAsync } from "./api"; +import { logoutAsync } from "./api"; type Props = { children: React.ReactNode; @@ -9,41 +9,22 @@ type Props = { }; export default function UserContextProvider({ children, me }: Props) { - const [user, setUser] = useState(me); - const [pending, setPending] = useState(false); - const { show } = useContext(NotifierContext); + const [user] = React.useState(me); + const { show } = React.useContext(NotifierContext); - const login = async ( - email: string, - password: string, - rememberMe: boolean, - ) => { - setPending(true); + const logout = () => { try { - const user = await loginAsync(email, password, rememberMe); - setUser(user); + logoutAsync(); } catch (ex: unknown) { const error = ex as Error; show(error.message); - } finally { - setPending(false); } }; - const logout = () => { - setPending(true); - logoutAsync() - .then(() => setUser(undefined)) - .catch((ex: Error) => show(ex.message)) - .finally(() => setPending(false)); - }; - return ( diff --git a/frontend/src/features/auth/api.ts b/frontend/src/features/auth/api.ts index e604517..9644cc0 100644 --- a/frontend/src/features/auth/api.ts +++ b/frontend/src/features/auth/api.ts @@ -5,45 +5,13 @@ interface ErrorResponseType { error: string; } -export async function loginAsync( - email: string, - password: string, - rememberMe: boolean, -): Promise { - const url = `${BACKEND_API_URL}/login`; - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - email: email, - password: password, - _remember_me: rememberMe, - }), - }); - - const obj = await response.json(); - - if (!response.ok) { - const errorObj = obj as ErrorResponseType; - throw Error(errorObj.error); - } - - return obj; -} - -export async function logoutAsync(): Promise { - const url = `${BACKEND_API_URL}/logout`; - const response = await fetch(url, { - method: "POST", - credentials: "include", - }); - - if (!response.ok) { - throw Error(`Logout failed with ${response.status}!`); - } +/** + * Logout by redirecting to server logout endpoint. + * Server clears session and redirects to login form. + * Non-async but kept in auth API module for consistency. + */ +export function logoutAsync(): void { + window.location.href = "/logout"; } export async function checkRememberMeAsync(): Promise { diff --git a/frontend/src/features/auth/index.ts b/frontend/src/features/auth/index.ts index 477eb64..ebdbc31 100644 --- a/frontend/src/features/auth/index.ts +++ b/frontend/src/features/auth/index.ts @@ -1,4 +1,4 @@ export { UserContext, type User } from "./UserContext"; export { default as UserContextProvider } from "./UserContextProvider"; export { default as useRememberMe } from "./useRememberMe"; -export { default as LoginView } from "./LoginView"; + From b5d295032b227ec1b0ea1b0a01a3f8a5ad5003f9 Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 09:24:54 +0200 Subject: [PATCH 04/77] cleanup --- backend/src/Controller/AdminLoginRedirectController.php | 2 +- backend/src/Controller/SpaController.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/Controller/AdminLoginRedirectController.php b/backend/src/Controller/AdminLoginRedirectController.php index e8f88c6..4d28c9d 100644 --- a/backend/src/Controller/AdminLoginRedirectController.php +++ b/backend/src/Controller/AdminLoginRedirectController.php @@ -11,7 +11,7 @@ class AdminLoginRedirectController extends AbstractController /** * Convenience route for admin login. * Redirects to the shared login form with admin email prefilled (via query parameter). - * The admin can optionally prefill the email field by visiting /admin/login?email=admin@example.com + * The admin can optionally prefill the email field by visiting /admin/login?email=admin@example.com. */ #[Route('/admin/login', name: 'admin.login')] public function redirectToAdminLogin(): RedirectResponse diff --git a/backend/src/Controller/SpaController.php b/backend/src/Controller/SpaController.php index 9304acf..a71a3c2 100644 --- a/backend/src/Controller/SpaController.php +++ b/backend/src/Controller/SpaController.php @@ -27,4 +27,3 @@ public function index(): Response ]); } } - From 84bfb5e59606479b1b843131652977818a6b4186 Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 09:25:44 +0200 Subject: [PATCH 05/77] update NPM package lock --- frontend/package-lock.json | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c6906eb..a9263b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9434,24 +9434,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 2413e98f1c7fa643b5d3250bcdabbf91c0e3b682 Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 09:26:30 +0200 Subject: [PATCH 06/77] use npm ci instead --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index f50fdc3..94ecfee 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -162,7 +162,7 @@ services: - OTEL_COLLECTOR_ADDRESS=http://localhost:4318 networks: - fullstack-symfony-react - command: ["sh", "-c", "npm install && node inject.env.cjs --indexPath=./index.html && npm run dev"] + command: ["sh", "-c", "npm ci && node inject.env.cjs --indexPath=./index.html && npm run dev"] # message queue rabbitmq: From 3926b3869c07d667d63953682ad5ebda12cb48de Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 09:30:32 +0200 Subject: [PATCH 07/77] Fix: Remove unsupported priority key from routes.yaml The YAML route configuration does not support 'priority' key. Attribute-based routes (defined in controllers) automatically have precedence over YAML routes, so the priority is implicit. This fixes the routing error that prevented the app from loading. Application now correctly: - Serves login page at /login to unauthenticated users - Redirects / to /game/index (caught by firewall first if not authenticated) - Serves React app via SpaController when authenticated Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/config/routes.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/config/routes.yaml b/backend/config/routes.yaml index e2c18ff..60cdc89 100644 --- a/backend/config/routes.yaml +++ b/backend/config/routes.yaml @@ -4,12 +4,10 @@ controllers: namespace: App\Controller type: attribute -# For unauthenticated users, redirect to login -# For authenticated users, SpaController will handle all remaining routes +# Root redirect to default route (for unauthenticated users) +# Attribute-based routes (controllers) have priority and will be checked first root_shortcut: path: / controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController defaults: route: 'app.index_games' - ignoreAttributes: false - priority: -10 From 9415850c83be34089fff52dc4dbc44552e3e27ac Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 10:10:31 +0200 Subject: [PATCH 08/77] Fix: Protect SpaController with firewall and handle unauthenticated React access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend changes: - Added #[IsGranted('ROLE_USER')] to SpaController::index() - Firewall now intercepts unauthenticated requests to SPA routes - Unauthenticated users are redirected to /login (HTTP 302) Frontend changes: - Updated App.tsx to create AppContent component - Added useEffect that detects when me is undefined (not authenticated) - Redirects to /login when user is not authenticated - Provides defense-in-depth protection in case firewall is bypassed - Uses useNavigate hook for client-side redirect Testing verified: - Unauthenticated requests to /dashboard → redirected to /login ✓ - Unauthenticated requests to /login → accessible (HTTP 200) ✓ - Frontend build includes updated authentication handling ✓ This ensures the React SPA is only served to authenticated users, maintaining the session-based authentication design. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/src/Controller/SpaController.php | 4 +++ frontend/src/App.tsx | 43 ++++++++++++++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/backend/src/Controller/SpaController.php b/backend/src/Controller/SpaController.php index a71a3c2..a2a654d 100644 --- a/backend/src/Controller/SpaController.php +++ b/backend/src/Controller/SpaController.php @@ -5,6 +5,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; class SpaController extends AbstractController { @@ -17,7 +18,10 @@ public function __construct( /** * Serve the React SPA application. * This route is a catch-all that handles all frontend routes not matched by other controllers. + * Protected by firewall: only authenticated users (ROLE_USER) can access. + * Unauthenticated users are redirected to /login by the firewall. */ + #[IsGranted('ROLE_USER')] #[Route('/{reactRouting}', name: 'spa', requirements: ['reactRouting' => '^(?!api|admin|login|logout|game|_).+'], methods: ['GET'])] public function index(): Response { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dfd1dad..dea62e3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,23 +3,44 @@ import { MyErrorBoundary, CustomThemeProvider, Loading } from "./components"; import Router from "./Router"; import { Notifier, useRememberMe } from "./features"; import { UserContextProvider } from "./features/auth"; -import { BrowserRouter } from "react-router-dom"; +import { BrowserRouter, useNavigate } from "react-router-dom"; +import { useEffect } from "react"; -export default function App() { +function AppContent() { const { pending, me } = useRememberMe(); + const navigate = useNavigate(); + + // Redirect to login if user is not authenticated (fallback defense-in-depth) + useEffect(() => { + if (!pending && !me) { + navigate("/login", { replace: true }); + } + }, [pending, me, navigate]); + + if (pending) { + return ; + } + + if (!me) { + // This should rarely happen due to the effect above, but guard against it + return ; + } + + return ( + + + + ); +} + +export default function App() { return ( - {pending ? ( - - ) : ( - - - - - - )} + + + From e2690778cab31ea6e64878eb3fbe34330ee2762d Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 10:19:52 +0200 Subject: [PATCH 09/77] Fix: Redirect to backend login URL in development environment Issue: When not authenticated on port 8090 (Vite dev frontend), the app would show 'Redirecting to login...' but nothing would happen. Root Cause: React Router's useNavigate('/login') creates a relative URL redirect to http://localhost:8090/login (current domain), but the login form is only served by the backend at http://localhost:8080/login. Solution: - Use window.location.href instead of React Router navigation - Extract backend base URL from BACKEND_API_URL config - Calculate full redirect URL: http://localhost:8080/login - Works in both development (port 8090) and production (port 8080) Changes: - Removed useNavigate hook (not needed for cross-domain redirects) - Added BACKEND_API_URL import - Changed to window.location.href for full page reload and redirect - Simplified dependency array Testing: - Frontend build verified - Redirect URL calculation verified (outputs correct login URL) - Vite dev server responding with new build This fixes the development workflow where users on port 8090 are now properly redirected to the backend login form on port 8080. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/index.html | 4 ++++ frontend/inject.env.cjs | 2 ++ frontend/src/App.tsx | 12 +++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 51a0b63..164842b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,4 +1,8 @@ + diff --git a/frontend/inject.env.cjs b/frontend/inject.env.cjs index bc70b32..b9f6286 100644 --- a/frontend/inject.env.cjs +++ b/frontend/inject.env.cjs @@ -17,6 +17,8 @@ const { values } = parseArgs({ const { indexPath } = values; +// TODO: Check if this is obsolete due to the React app being served via backend? +// TODO: Also check if this might be necessary for development though! fs.readFile(indexPath, "utf8", (err, data) => { if (err) { return console.error("Error reading index.html:", err); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dea62e3..0bc890e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,19 +3,21 @@ import { MyErrorBoundary, CustomThemeProvider, Loading } from "./components"; import Router from "./Router"; import { Notifier, useRememberMe } from "./features"; import { UserContextProvider } from "./features/auth"; -import { BrowserRouter, useNavigate } from "react-router-dom"; +import { BrowserRouter } from "react-router-dom"; import { useEffect } from "react"; +import { BACKEND_API_URL } from "./config/env"; function AppContent() { const { pending, me } = useRememberMe(); - const navigate = useNavigate(); - // Redirect to login if user is not authenticated (fallback defense-in-depth) + // Redirect to backend login if user is not authenticated useEffect(() => { if (!pending && !me) { - navigate("/login", { replace: true }); + // Extract backend base URL from API URL (remove /api suffix) + const backendBaseUrl = BACKEND_API_URL.replace(/\/api$/, ""); + window.location.href = `${backendBaseUrl}/login`; } - }, [pending, me, navigate]); + }, [pending, me]); if (pending) { return ; From 51eec55dd890ec22a6a532ddbaeaba7be22f01db Mon Sep 17 00:00:00 2001 From: Martin Komischke Date: Thu, 9 Apr 2026 10:26:08 +0200 Subject: [PATCH 10/77] Implement post-login redirect to original destination (Symfony _target_path) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses Symfony's official target path mechanism for post-login redirects: Backend changes: - Added target_path_parameter: _target_path to form_login in security.yaml - Added default_target_path fallback to /game/index - Updated AuthenticationSuccessHandler to check for target path first - Uses TargetPathTrait::getTargetPath() to retrieve session value - Falls back to role-based redirects if no target path Frontend changes: - Updated App.tsx to pass _target_path parameter when redirecting to login - Encodes current URL as the target path - Login flow: 8090 → /login?_target_path=http://localhost:8090 → 8090 Template changes: - Added _target_path hidden field to login form - Field is only rendered if _target_path query parameter is present - Value is passed along with form submission Flow: 1. User on http://localhost:8090 (React app) is not authenticated 2. App redirects to: /login?_target_path=http%3A%2F%2Flocalhost%3A8090%2F 3. Form stores _target_path as hidden field 4. User logs in 5. Symfony firewall reads _target_path from request 6. AuthenticationSuccessHandler retrieves it from session 7. User is redirected back to original URL (http://localhost:8090) Fallback behavior: - If no _target_path is provided (legacy login page access) - Admin users → /admin - Regular users → /game/index This implements the official Symfony pattern for post-login redirects, ensuring users are taken back to where they came from. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/config/packages/security.yaml | 5 +++++ .../src/Security/AuthenticationSuccessHandler.php | 12 +++++++++++- backend/templates/login/login.html.twig | 5 +++++ frontend/src/App.tsx | 8 +++++++- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/backend/config/packages/security.yaml b/backend/config/packages/security.yaml index 27cc6d6..16a1676 100644 --- a/backend/config/packages/security.yaml +++ b/backend/config/packages/security.yaml @@ -21,6 +21,11 @@ security: check_path: app.login enable_csrf: true success_handler: App\Security\AuthenticationSuccessHandler + # Symfony's built-in target path parameter for post-login redirects + # When login form includes _target_path, user is redirected there after successful auth + target_path_parameter: _target_path + # Default fallback if no _target_path is provided + default_target_path: /game/index remember_me: secret: '%kernel.secret%' lifetime: 604800 # 1 week in seconds diff --git a/backend/src/Security/AuthenticationSuccessHandler.php b/backend/src/Security/AuthenticationSuccessHandler.php index 59c9d77..e5fac95 100644 --- a/backend/src/Security/AuthenticationSuccessHandler.php +++ b/backend/src/Security/AuthenticationSuccessHandler.php @@ -8,17 +8,27 @@ use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Util\TargetPathTrait; readonly class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface { + use TargetPathTrait; + public function __construct(private RouterInterface $router) { } public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response { - $user = $token->getUser(); + $targetPath = $this->getTargetPath($request->getSession(), 'main'); + // If a target path was stored in session (via _target_path parameter), use it + if ($targetPath) { + return new RedirectResponse($targetPath); + } + + // Otherwise use role-based defaults + $user = $token->getUser(); if ($user && in_array('ROLE_ADMIN', $user->getRoles(), strict: true)) { return new RedirectResponse($this->router->generate('admin')); } diff --git a/backend/templates/login/login.html.twig b/backend/templates/login/login.html.twig index e5bde17..451d773 100644 --- a/backend/templates/login/login.html.twig +++ b/backend/templates/login/login.html.twig @@ -82,6 +82,11 @@ + + {% if app.request.query.get('_target_path') %} + + {% endif %} +