Files
ltbxd-actorle/docs/superpowers/specs/2026-03-28-auth-react-frontend-design.md
thibaud-leclere e376a97dad docs: add design spec for auth + React frontend
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 13:02:09 +01:00

7.4 KiB

Auth + React Frontend — Design Spec

Context

Actorle is a Symfony 8.0 word game (Wordle-like for actors) running on FrankenPHP with PostgreSQL. The app currently has no authentication and uses Asset Mapper with Stimulus for a minimal frontend. This spec covers adding user authentication and migrating to a React-based interactive frontend via SymfonyUX.

Approach

Hybrid Twig + React (SymfonyUX React option 2):

  • Symfony stays in control of routing, sessions, and page rendering via Twig
  • Interactive parts (the game grid) are React components mounted in Twig via {{ react_component() }}
  • Auth pages (login, register) remain pure Twig — no benefit from React here

1. Frontend Migration: Asset Mapper to Vite

Remove Asset Mapper

  • Remove symfony/asset-mapper from composer
  • Delete importmap.php
  • Remove asset_mapper.yaml config

Install Vite

  • Install pentatrion/vite-bundle (Symfony Vite integration)
  • Create vite.config.js at project root with @vitejs/plugin-react
  • Update base.html.twig to use Vite's {{ vite_entry_link_tags('app') }} and {{ vite_entry_script_tags('app') }} instead of {{ importmap() }}

Install React

  • Install symfony/ux-react
  • npm dependencies: react, react-dom, @vitejs/plugin-react
  • The UX React bundle auto-registers a Stimulus controller that mounts React components

Assets Structure

assets/
├── app.js                  (entry point: imports styles + Stimulus bootstrap)
├── bootstrap.js            (Stimulus app initialization)
├── styles/
│   └── app.css
├── controllers/            (Stimulus controllers)
│   └── csrf_protection_controller.js
└── react/
    └── controllers/        (React components mountable via {{ react_component() }})
        ├── GameGrid.jsx
        ├── GameRow.jsx
        ├── LetterInput.jsx
        └── ActorPopover.jsx

2. Docker Setup

New service: docker/node/Dockerfile

  • Base image: node:22-alpine
  • Working directory: /app
  • Runs npm install and npx vite dev server on port 5173

docker-compose.override.yaml (dev)

Add node service:

node:
  build:
    context: .
    dockerfile: docker/node/Dockerfile
  volumes:
    - .:/app
    - node_modules:/app/node_modules
  ports:
    - "5173:5173"

Production build

  • The docker/node/Dockerfile has a build stage that runs npm run build
  • The docker/app/Dockerfile prod stage copies built assets from the node build stage via COPY --from=
  • No Node.js runtime in production — only the compiled static assets

3. Authentication

User Entity

  • Fields: id (int, auto), email (string, unique), password (hashed), roles (json)
  • Implements UserInterface and PasswordAuthenticatedUserInterface
  • Doctrine repository: UserRepository
  • Database migration for the user table

Security Configuration (security.yaml)

security:
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                default_target_path: /
            logout:
                path: app_logout

    access_control:
        - { path: ^/login, roles: PUBLIC_ACCESS }
        - { path: ^/register, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: ROLE_USER }

Controllers & Routes

SecurityController:

  • GET /login — renders login form (templates/security/login.html.twig)
  • POST /login — handled by Symfony's form_login authenticator
  • GET /logout — handled by Symfony's logout handler

RegistrationController:

  • GET /register — renders registration form (templates/security/register.html.twig)
  • POST /register — validates form, hashes password, persists User, redirects to /login

Templates

  • templates/security/login.html.twig — email + password fields, CSRF token, error display, link to register
  • templates/security/register.html.twig — email + password + confirm password fields, CSRF token, validation errors, link to login
  • Both extend base.html.twig
  • Pure Twig, no React

4. Game Grid React Components

Data Flow

  1. HomepageController::index() prepares grid data (actors, letters, target) as it does today
  2. Data is passed as JSON props to the React component in Twig:
    {{ react_component('GameGrid', { grid: gridData, targetActorId: targetId }) }}
    
  3. React hydrates client-side and manages all interactivity

Components

GameGrid.jsx — Root component

  • Receives grid (array of rows) and targetActorId as props
  • Renders a list of GameRow components
  • Manages global game state (which letters have been guessed)

GameRow.jsx — One row = one actor

  • Receives row data (actor name, highlighted letter index)
  • Renders a sequence of LetterInput components
  • Contains a button that triggers ActorPopover

LetterInput.jsx — Single character input

  • <input maxLength={1}> styled to look like a game tile
  • On keyup: if a character was typed, move focus to the next LetterInput
  • On Backspace: clear and move focus to previous input
  • Highlighted letter has distinct styling (red background, as current CSS)

ActorPopover.jsx — Info popover

  • Uses @floating-ui/react for positioning
  • Triggered by a button click on the row
  • Displays actor name, movie info, or hints
  • Dismissible by clicking outside or pressing Escape

Popover Library

  • @floating-ui/react — modern, lightweight, React-native successor to Popper.js

5. What Does NOT Change

  • HomepageController — same game logic, adapted only to pass data as props to React instead of Twig variables (listed in Modified Files below)
  • Entities: Actor, Movie, MovieRole — untouched
  • PostgreSQL database and existing migrations — untouched (new migration only adds user table)
  • FrankenPHP server — untouched
  • Makefile — extended with node/npm targets, existing targets unchanged

6. File Changes Summary

New Files

  • docker/node/Dockerfile
  • vite.config.js
  • package.json (replaces importmap-based setup)
  • src/Entity/User.php
  • src/Repository/UserRepository.php
  • src/Controller/SecurityController.php
  • src/Controller/RegistrationController.php
  • src/Form/RegistrationType.php
  • templates/security/login.html.twig
  • templates/security/register.html.twig
  • assets/react/controllers/GameGrid.jsx
  • assets/react/controllers/GameRow.jsx
  • assets/react/controllers/LetterInput.jsx
  • assets/react/controllers/ActorPopover.jsx
  • Migration file for user table

Modified Files

  • composer.json — remove asset-mapper, add vite-bundle + ux-react
  • config/packages/security.yaml — full auth config
  • templates/base.html.twig — Vite tags instead of importmap
  • templates/homepage/index.html.twig — replace HTML grid with {{ react_component() }}
  • docker-compose.override.yaml — add node service
  • docker-compose.yaml — add node build step for prod
  • docker/app/Dockerfile — prod stage copies built JS assets
  • assets/app.js — entry point adjustments for Vite
  • src/Controller/HomepageController.php — pass grid data as JSON props to React component
  • Makefile — add npm/node targets