docs: add design spec for auth + React frontend
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
201
docs/superpowers/specs/2026-03-28-auth-react-frontend-design.md
Normal file
201
docs/superpowers/specs/2026-03-28-auth-react-frontend-design.md
Normal 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
|
||||||
Reference in New Issue
Block a user