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
2026-09-03 17:35:10 +01:00
2026-09-03 17:35:10 +01:00

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/.

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.

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).

Run without Docker

Requires PHP 8.1+ with the pdo_sqlite and mbstring extensions, plus Composer. On Fedora:

sudo dnf install php-cli php-pdo php-mbstring composer

Setup

composer install
cp .env.example .env      # optional; sane defaults are used without it
composer migrate          # creates storage/database.sqlite and its tables

Running

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/ and talks to this API. With the API running (docker compose up -d):

cd web
npm install
npm run dev        # http://localhost:5173, proxies /api to localhost:8080

Or run it inside Compose alongside the API:

docker compose --profile frontend up -d

Unauthenticated visitors are redirected to /login; /register creates an account and signs in immediately. See web/README.md.

Configuration

All settings are optional environment variables (read from .env or the real environment). See .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

{ "status": "ok" }

POST /api/auth/register

Request:

{ "email": "ada@example.com", "password": "correct horse battery staple" }

201 Created:

{
  "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:

{ "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:

{
  "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:

{ "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

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

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.

S
Description
No description provided
Readme
588 KiB
Languages
PHP 61%
Vue 21.4%
TypeScript 8.1%
CSS 7.8%
Dockerfile 1.3%
Other 0.4%