docs: add design spec for auth + React frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere
2026-03-28 13:02:09 +01:00
parent 4c5e82cb9d
commit e376a97dad

View File

@@ -0,0 +1,201 @@
# 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:
```yaml
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`)
```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:
```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