158 lines
3.9 KiB
Markdown
158 lines
3.9 KiB
Markdown
# PHP Todo List
|
||||
|
|
|
|||
|
|
A small todo-list application: a REST API written in PHP (Slim 4) backed by an
|
|||
|
|
SQLite file, plus a single-page frontend (added in a later stage).
|
|||
|
|
|
|||
|
|
## Status
|
|||
|
|
|
|||
|
|
| Stage | Scope | State |
|
|||
|
|
|-------|-------|-------|
|
|||
|
|
| 1 | Auth API — register, login, `GET /me` | ✅ done |
|
|||
|
|
| 2 | Todo CRUD API | planned |
|
|||
|
|
| 3 | Single-page frontend | planned |
|
|||
|
|
|
|||
|
|
## Requirements
|
|||
|
|
|
|||
|
|
- PHP 8.1+ with the `pdo_sqlite` and `mbstring` extensions
|
|||
|
|
- [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`.
|
|||
|
|
|
|||
|
|
## 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", "created_at": "2026-09-03T12:00:00Z" },
|
|||
|
|
"token": "<jwt>",
|
|||
|
|
"expires_at": "2026-09-04T12:00:00+00:00"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Errors: `422` invalid input, `409` email already registered.
|
|||
|
|
|
|||
|
|
Validation: `email` must be a valid address (≤ 255 chars); `password` must be
|
|||
|
|
8–72 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", "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
|
|||
|
|
```
|