Files
project-manager/README.md
T
aneurinandClaude Sonnet 5 5e3b8dbd7e Add stage 2: Vue/TypeScript PWA shell with auth-gated routing
Backend: new migration adds users.email_verified_at (null = unverified);
registration leaves it null, and the register/login/me payloads now expose
email_verified and email_verified_at.

Frontend (web/): Vite + Vue 3 + TypeScript PWA (vite-plugin-pwa). Pinia auth
store keeps the token in localStorage and validates it via GET /api/me on
load. vue-router guards redirect unauthenticated visitors to /login,
preserving the intended path; /register creates an account and signs in
immediately (with the email unverified). Placeholder home page, minimal
styling, generated icons. Dev server proxies /api to the API.

docker-compose.yml gains an optional "web" service (profile: frontend) so
`docker compose --profile frontend up -d` runs the dev server alongside the
API; `docker compose up -d` still starts the API alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 18:12:31 +01:00

229 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PHP Todo List
A small todo-list application: a REST API written in PHP (Slim 4) backed by an
SQLite file, plus a Vue 3 + TypeScript PWA frontend in [web/](web/).
## Status
| Stage | Scope | State |
|-------|-------|-------|
| 1 | Auth API — register, login, `GET /me` | ✅ done |
| 2 | Frontend shell — Vite PWA, auth-gated routing, register/login pages | ✅ done |
| 3 | Todo CRUD (API + UI) | planned |
Registration signs the user in immediately, with the account's email marked
unverified (`user.email_verified` is `false` until a future stage adds a
verification endpoint).
## Run with Docker
The only requirement is Docker with the Compose plugin.
```bash
docker compose up -d
```
This builds a PHP 8.3 + Apache image, applies migrations, and serves the API at
<http://localhost:8080> (e.g. `curl http://localhost:8080/api/health`).
- The project directory is bind-mounted into the container, so editing PHP
source takes effect without a rebuild (within ~2s, due to the opcache
revalidation interval). `vendor/` is used from the host — run `composer`
once first if it is missing (see "Run without Docker" below, or
`docker compose run --rm --entrypoint composer app install`).
- The SQLite database and the generated JWT signing key live in the `storage`
named volume, mounted at `/var/www/storage` (outside the bind-mounted source),
so they survive `docker compose restart` / `down` + `up`.
- Rebuild only after changing the `Dockerfile`: `docker compose up -d --build`.
- `docker compose down -v` removes the volume and gives you a clean database.
- Override settings via the environment or a `.env` file in this directory
(Compose substitutes `APP_DEBUG`, `JWT_SECRET`, `JWT_TTL` — see
[docker-compose.yml](docker-compose.yml)).
## Run without Docker
Requires PHP 8.1+ with the `pdo_sqlite` and `mbstring` extensions, plus
[Composer](https://getcomposer.org/). On Fedora:
```bash
sudo dnf install php-cli php-pdo php-mbstring composer
```
### Setup
```bash
composer install
cp .env.example .env # optional; sane defaults are used without it
composer migrate # creates storage/database.sqlite and its tables
```
### Running
```bash
composer serve # http://localhost:8080 (php -S localhost:8080 -t public)
```
Any web server can serve the app as long as the document root is `public/` and
unknown paths fall through to `public/index.php`.
## Frontend
The Vue/TypeScript PWA lives in [web/](web/) and talks to this API. With the API
running (`docker compose up -d`):
```bash
cd web
npm install
npm run dev # http://localhost:5173, proxies /api to localhost:8080
```
Or run it inside Compose alongside the API:
```bash
docker compose --profile frontend up -d
```
Unauthenticated visitors are redirected to `/login`; `/register` creates an
account and signs in immediately. See [web/README.md](web/README.md).
## Configuration
All settings are optional environment variables (read from `.env` or the real
environment). See [.env.example](.env.example).
| Variable | Default | Purpose |
|----------|---------|---------|
| `APP_DEBUG` | `false` | Include exception details in error responses |
| `DATABASE_PATH` | `storage/database.sqlite` | SQLite file location |
| `JWT_SECRET` | auto-generated into `storage/secret.key` | Token signing key |
| `JWT_TTL` | `86400` | Token lifetime in seconds |
## API
Base path: `/api`. All request and response bodies are JSON; send
`Content-Type: application/json`.
### `GET /api/health`
```json
{ "status": "ok" }
```
### `POST /api/auth/register`
Request:
```json
{ "email": "ada@example.com", "password": "correct horse battery staple" }
```
`201 Created`:
```json
{
"user": {
"id": 1,
"email": "ada@example.com",
"email_verified": false,
"email_verified_at": null,
"created_at": "2026-09-03T12:00:00Z"
},
"token": "<jwt>",
"expires_at": "2026-09-04T12:00:00+00:00"
}
```
New accounts are created with an unverified email (`email_verified: false`).
Errors: `422` invalid input, `409` email already registered.
Validation: `email` must be a valid address (≤ 255 chars); `password` must be
872 characters.
### `POST /api/auth/login`
Request:
```json
{ "email": "ada@example.com", "password": "correct horse battery staple" }
```
`200 OK`: same shape as register. `401` on bad credentials (the message does not
say whether it was the email or the password that was wrong).
### `GET /api/me`
Requires `Authorization: Bearer <jwt>`.
`200 OK`:
```json
{
"user": {
"id": 1,
"email": "ada@example.com",
"email_verified": false,
"email_verified_at": null,
"created_at": "2026-09-03T12:00:00Z"
}
}
```
`401` if the header is missing, malformed, or the token is invalid/expired.
### Error shape
Every error response looks like:
```json
{ "error": { "message": "The submitted data was invalid.", "details": { "email": ["Email must be a valid address."] } } }
```
`details` is present only when relevant (e.g. validation).
## Try it
```bash
BASE=http://localhost:8080
curl -s -X POST $BASE/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"password123"}'
TOKEN=$(curl -s -X POST $BASE/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"password123"}' | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
curl -s $BASE/api/me -H "Authorization: Bearer $TOKEN"
```
## Tests
```bash
composer install # installs phpunit (require-dev)
vendor/bin/phpunit
```
## Layout
```
public/index.php Front controller
src/bootstrap.php App wiring and route definitions
src/Support/Config.php Environment-driven configuration
src/Support/Database.php PDO/SQLite connection
src/Auth/JwtService.php Issue/verify JWTs
src/Auth/AuthMiddleware.php Bearer-token authentication
src/Http/JsonErrorHandler.php Uniform JSON error envelope
src/Http/Controllers/ Request handlers
src/Repository/ Database access
migrations/*.sql Schema, applied by bin/migrate.php
Dockerfile PHP 8.3 + Apache image
docker-compose.yml One-command local stack (API; web via --profile frontend)
docker/ Apache vhost + container entrypoint
web/ Vue 3 + TypeScript + Vite PWA frontend
```
## Provenance
This project was generated with [Claude Code](https://claude.com/claude-code).