Add stage 1: authentication REST API

Slim 4 + SQLite todo-list API providing email/password registration,
login, and an authenticated GET /me endpoint. Stateless HS256 JWTs,
bcrypt password hashing, uniform JSON error envelope, and a SQL
migration runner. Includes PHPUnit feature tests and stage-1 docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 17:35:10 +01:00
co-authored by Claude Sonnet 5
commit 7faef6fbff
23 changed files with 4054 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# Stage 1 — Authentication API
Status: **done** and verified end-to-end (PHPUnit feature tests + a live `curl`
run against the built-in server).
## What's there
A Slim 4 REST API on SQLite with JWT bearer authentication.
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/health` | liveness check |
| POST | `/api/auth/register` | create account, returns user + token |
| POST | `/api/auth/login` | exchange email/password for a token |
| GET | `/api/me` | current user (requires `Authorization: Bearer <jwt>`) |
- **Passwords** are hashed with `password_hash()` (bcrypt). Validation is
email-format plus an 872 character password. Login returns a single generic
"Invalid email or password" message so it does not leak which emails exist.
- **Emails** are stored lower-cased in a `UNIQUE COLLATE NOCASE` column;
duplicate registration returns `409`.
- **Errors** always come back as
`{ "error": { "message": ..., "details"?: ... } }` via
[../src/Http/JsonErrorHandler.php](../src/Http/JsonErrorHandler.php).
- **Tokens** are stateless HS256 JWTs. The signing secret comes from
`JWT_SECRET`, or is auto-generated into `storage/secret.key` on first run.
- **Migrations** are plain SQL files in [../migrations/](../migrations/), applied
idempotently by [../bin/migrate.php](../bin/migrate.php) and tracked in a
`schema_migrations` table.
## Dependency note
`firebase/php-jwt` is pinned to `^7.0` — Composer blocks `6.10``6.11` for a
published security advisory (`PKSA-y2cr-5h3j-g3ys`).
## Key files
- [../src/bootstrap.php](../src/bootstrap.php) — app wiring + routes
- [../src/Http/Controllers/AuthController.php](../src/Http/Controllers/AuthController.php) — register / login / me
- [../src/Auth/JwtService.php](../src/Auth/JwtService.php), [../src/Auth/AuthMiddleware.php](../src/Auth/AuthMiddleware.php)
- [../src/Repository/UserRepository.php](../src/Repository/UserRepository.php)
- [../src/Support/Config.php](../src/Support/Config.php), [../src/Support/Database.php](../src/Support/Database.php)
- [../tests/AuthTest.php](../tests/AuthTest.php) — 6 passing feature tests
## Run it
```bash
composer install && composer migrate && composer serve # http://localhost:8080
```
## Local toolchain
This machine has no `php`/`composer` binary. Tooling was run through the official
Composer container image, which bundles PHP + Composer + `pdo_sqlite` +
`mbstring`:
```bash
podman run --rm -v "$PWD":/app:Z -w /app docker.io/library/composer:2 install
podman run --rm -v "$PWD":/app:Z -w /app docker.io/library/composer:2 vendor/bin/phpunit
```