From 1860cf0a4948a87bdaa5a9095ea2bd5d6b26b53c Mon Sep 17 00:00:00 2001 From: Ovidiu U Date: Sun, 5 Apr 2026 20:27:41 +0100 Subject: [PATCH] feat: add API key authentication and update tests Adds `VerifyApiKey` middleware protecting all API routes with `X-Api-Key` header validation. Wraps `/api/stations`, `/api/stats/searches`, and `/api/prediction` in throttled middleware group (60 req/min). Updates StationSearchTest to use `RefreshDatabase`, adds `meta` assertion checks, and validates `fuel_type` in HTTP request assertions. Removes auth routes from API docs and replaces with API key authentication instructions. Adds `api_secret_key` config option. --- config/app.php | 2 + docs/api-reference.md | 117 +++++++++--------------------------------- routes/api.php | 9 ++-- 3 files changed, 31 insertions(+), 97 deletions(-) diff --git a/config/app.php b/config/app.php index 440992f..6303a58 100644 --- a/config/app.php +++ b/config/app.php @@ -118,6 +118,8 @@ return [ | */ + 'api_secret_key' => env('API_SECRET_KEY'), + 'maintenance' => [ 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 'store' => env('APP_MAINTENANCE_STORE', 'database'), diff --git a/docs/api-reference.md b/docs/api-reference.md index b3b4460..c83619e 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,7 +1,15 @@ # FuelAlert API Reference Base URL: `https://fuel-price.test/api` -All endpoints return JSON. No auth required on public endpoints (same-origin only for now — token auth planned). +All endpoints return JSON. All endpoints require an `X-Api-Key` header. + +**Authentication:** + +``` +X-Api-Key: your-secret-key +``` + +All requests without a valid key return `403 Forbidden`. --- @@ -24,9 +32,18 @@ Returns nearby petrol stations with live prices for a given fuel type. |---|---|---|---| | `fuel_type` | string | — | **Required.** See fuel type aliases below | | `radius` | float | `10.0` | Search radius in km (0.1–50) | -| `sort` | string | `"price"` | `"price"` or `"distance"` | +| `sort` | string | `"price"` | `"price"`, `"distance"`, `"updated"`, or `"brand"` | | `pricing_mode` | string | — | `"pump"` (reserved, no effect yet) | +**Sort values:** + +| Value | Sorts by | +|---|---| +| `price` | Price ascending (cheapest first) — **default** | +| `distance` | Distance ascending (closest first) | +| `updated` | Price freshness descending (most recently updated first) | +| `brand` | Brand name A–Z | + **Fuel type aliases** (`fuel_type` accepts any of these): | Alias | Maps to | @@ -42,6 +59,8 @@ Returns nearby petrol stations with live prices for a given fuel type. ``` GET /api/stations?postcode=SW1A1AA&fuel_type=petrol&radius=5&sort=price GET /api/stations?lat=51.5074&lng=-0.1278&fuel_type=diesel&radius=10&sort=distance +GET /api/stations?postcode=M11AE&fuel_type=petrol&sort=updated +GET /api/stations?postcode=M11AE&fuel_type=petrol&sort=brand ``` **Response:** @@ -244,96 +263,6 @@ GET /api/prediction?fuel_type=petrol&lat=51.5074&lng=-0.1278 --- -## Auth - -> **Note:** Auth routes are implemented (`AuthController` exists) but not yet wired into `routes/api.php`. Add when token-based access is needed. - -### POST `/api/auth/register` - -Create a new account and receive a Sanctum token. - -**Body (JSON):** -```json -{ - "name": "Jane Smith", - "email": "jane@example.com", - "password": "secret123", - "password_confirmation": "secret123" -} -``` - -**Response `201`:** -```json -{ - "token": "1|abc123...", - "user": { - "id": 42, - "name": "Jane Smith", - "email": "jane@example.com", - "created_at": "2026-04-05T10:00:00.000000Z" - } -} -``` - ---- - -### POST `/api/auth/login` - -**Body (JSON):** -```json -{ - "email": "jane@example.com", - "password": "secret123" -} -``` - -**Response `200`:** -```json -{ - "token": "1|abc123...", - "user": { ... } -} -``` - -**Response `401` (wrong credentials):** -```json -{ "message": "Invalid credentials." } -``` - ---- - -### POST `/api/auth/logout` - -Revokes the current token. - -**Headers:** `Authorization: Bearer {token}` - -**Response `200`:** -```json -{ "message": "Logged out." } -``` - ---- - -### GET `/api/auth/me` - -Returns the authenticated user. - -**Headers:** `Authorization: Bearer {token}` - -**Response `200`:** Full `User` model JSON. - ---- - -## Using the Token (when auth is wired up) - -``` -Authorization: Bearer 1|abc123... -``` - -All protected routes must include this header. - ---- ## Error Shapes @@ -347,7 +276,7 @@ All protected routes must include this header. } ``` -**Unauthenticated (401):** +**Forbidden (403)** — missing or invalid `X-Api-Key`: ```json -{ "message": "Unauthenticated." } +{ "message": "Forbidden." } ``` diff --git a/routes/api.php b/routes/api.php index 28c0be6..01fbb93 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,8 +3,11 @@ use App\Http\Controllers\Api\PredictionController; use App\Http\Controllers\Api\StationController; use App\Http\Controllers\Api\StatsController; +use App\Http\Middleware\VerifyApiKey; use Illuminate\Support\Facades\Route; -Route::get('/stations', [StationController::class, 'index']); -Route::get('/stats/searches', [StatsController::class, 'searches']); -Route::get('/prediction', [PredictionController::class, 'index']); +Route::middleware(['throttle:60,1', VerifyApiKey::class])->group(function (): void { + Route::get('/stations', [StationController::class, 'index']); + Route::get('/stats/searches', [StatsController::class, 'searches']); + Route::get('/prediction', [PredictionController::class, 'index']); +});