185 lines
5.1 KiB
Markdown
185 lines
5.1 KiB
Markdown
# API Reference
|
|||
|
|
|
||
|
|
Base URL (local dev): `http://localhost:8080`
|
||
|
|
|
||
|
|
All request/response bodies are JSON. Authenticated endpoints rely on the
|
||
|
|
`session_id` cookie set by `/login` or the Google OAuth callback - include
|
||
|
|
it automatically by using a cookie-aware HTTP client (browsers do this
|
||
|
|
natively; with `curl`, use `-c cookies.txt -b cookies.txt`).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `GET /health`
|
||
|
|
|
||
|
|
Liveness check. No authentication, no rate limiting beyond the global
|
||
|
|
limit.
|
||
|
|
|
||
|
|
**Response `200`**
|
||
|
|
```json
|
||
|
|
{ "status": "ok" }
|
||
|
|
```
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl http://localhost:8080/health
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `POST /register`
|
||
|
|
|
||
|
|
Creates a new password-based account.
|
||
|
|
|
||
|
|
Rate limited to **5 requests/minute per IP** (shared with `/login`).
|
||
|
|
|
||
|
|
**Request body**
|
||
|
|
```json
|
||
|
|
{ "email": "hamid@example.com", "password": "secret123" }
|
||
|
|
```
|
||
|
|
- `email` - required, must be unique across all accounts.
|
||
|
|
- `password` - required, minimum 8 characters.
|
||
|
|
|
||
|
|
**Response `201`**
|
||
|
|
```json
|
||
|
|
{ "id": 1, "email": "hamid@example.com" }
|
||
|
|
```
|
||
|
|
|
||
|
|
**Errors**
|
||
|
|
| Status | Body | Cause |
|
||
|
|
|---|---|---|
|
||
|
|
| 400 | `{"error":"invalid request body"}` | Malformed JSON |
|
||
|
|
| 400 | `{"error":"email and password are required"}` | Missing field |
|
||
|
|
| 400 | `{"error":"password must be at least 8 characters"}` | Password too short |
|
||
|
|
| 409 | `{"error":"email already registered"}` | Email already taken |
|
||
|
|
| 429 | (rate limit response) | Too many requests from this IP |
|
||
|
|
| 500 | `{"error":"internal error"}` | Unexpected server/database failure |
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -X POST http://localhost:8080/register \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"email":"hamid@example.com","password":"secret123"}'
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `POST /login`
|
||
|
|
|
||
|
|
Authenticates with email + password and starts a server-side session.
|
||
|
|
|
||
|
|
Rate limited to **5 requests/minute per IP** (shared with `/register`).
|
||
|
|
|
||
|
|
**Request body**
|
||
|
|
```json
|
||
|
|
{ "email": "hamid@example.com", "password": "secret123" }
|
||
|
|
```
|
||
|
|
|
||
|
|
**Response `200`**
|
||
|
|
```json
|
||
|
|
{ "id": 1, "email": "hamid@example.com" }
|
||
|
|
```
|
||
|
|
Also sets a `session_id` cookie (`HttpOnly`, `SameSite=Lax`, `Secure` in
|
||
|
|
production).
|
||
|
|
|
||
|
|
**Errors**
|
||
|
|
| Status | Body | Cause |
|
||
|
|
|---|---|---|
|
||
|
|
| 400 | `{"error":"invalid request body"}` | Malformed JSON |
|
||
|
|
| 401 | `{"error":"invalid email or password"}` | No such email, OR wrong password (identical message for both, deliberately - see Security notes in the README) |
|
||
|
|
| 429 | (rate limit response) | Too many requests from this IP |
|
||
|
|
| 500 | `{"error":"internal error"}` | Unexpected server/database failure |
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -c cookies.txt -X POST http://localhost:8080/login \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"email":"hamid@example.com","password":"secret123"}'
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `POST /logout`
|
||
|
|
|
||
|
|
Destroys the current session (deletes it from Redis, expires the cookie).
|
||
|
|
Not rate-limited beyond the global limit - deliberately excluded from the
|
||
|
|
strict `/login`/`/register` limit so a legitimate user can always log out.
|
||
|
|
|
||
|
|
**Response `200`**
|
||
|
|
```json
|
||
|
|
{ "message": "logged out" }
|
||
|
|
```
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -b cookies.txt -c cookies.txt -X POST http://localhost:8080/logout
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `GET /me`
|
||
|
|
|
||
|
|
**Requires authentication** (a valid `session_id` cookie from a prior
|
||
|
|
login). Returns the currently logged-in user.
|
||
|
|
|
||
|
|
**Response `200`**
|
||
|
|
```json
|
||
|
|
{ "id": 1, "email": "hamid@example.com" }
|
||
|
|
```
|
||
|
|
|
||
|
|
**Errors**
|
||
|
|
| Status | Body | Cause |
|
||
|
|
|---|---|---|
|
||
|
|
| 401 | `{"error":"unauthorized"}` | No session, expired session, or the session's user no longer exists |
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -b cookies.txt http://localhost:8080/me
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `GET /auth/google/login`
|
||
|
|
|
||
|
|
Redirects the browser to Google's OAuth2 consent screen. **Must be opened
|
||
|
|
in an actual browser** - this endpoint returns an HTTP redirect, and the
|
||
|
|
subsequent Google login page cannot be driven via `curl`.
|
||
|
|
|
||
|
|
**Response**: `307 Temporary Redirect` to `accounts.google.com`.
|
||
|
|
|
||
|
|
```
|
||
|
|
open http://localhost:8080/auth/google/login
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `GET /auth/google/callback`
|
||
|
|
|
||
|
|
Google redirects here automatically after the user approves access. Not
|
||
|
|
meant to be called directly - `state` and `code` query parameters are
|
||
|
|
supplied by Google.
|
||
|
|
|
||
|
|
On success: creates a new user (or links Google to an existing
|
||
|
|
email-matched account), starts a session exactly like `/login` does, and
|
||
|
|
returns:
|
||
|
|
|
||
|
|
**Response `200`**
|
||
|
|
```json
|
||
|
|
{ "id": 2, "email": "hamid@gmail.com" }
|
||
|
|
```
|
||
|
|
|
||
|
|
**Errors**
|
||
|
|
| Status | Body | Cause |
|
||
|
|
|---|---|---|
|
||
|
|
| 400 | `{"error":"invalid oauth state"}` | Missing/mismatched CSRF state - usually means the flow wasn't started via `/auth/google/login`, or the session expired mid-flow |
|
||
|
|
| 400 | `{"error":"missing code"}` | Google didn't include an authorization code |
|
||
|
|
| 500 | `{"error":"internal error"}` | Token exchange, Google API call, or database failure |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## General notes
|
||
|
|
|
||
|
|
- **CORS**: browser-based requests from an origin not listed in
|
||
|
|
`ALLOWED_ORIGINS` will be blocked by the browser itself, before this
|
||
|
|
API's own logic ever runs. Non-browser clients (curl, mobile apps,
|
||
|
|
server-to-server) are unaffected by CORS entirely.
|
||
|
|
- **Rate limiting**: exceeding a limit returns HTTP `429 Too Many
|
||
|
|
Requests`. The global limit (100/min/IP) applies to every route; the
|
||
|
|
strict limit (5/min/IP) applies only to `/register` and `/login`, and
|
||
|
|
stacks with the global limit.
|
||
|
|
- All error responses share the same shape: `{"error": "<message>"}`.
|