diff --git a/README.md b/README.md index 2c5d57a..f59cd8e 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,32 @@ -# Dashback API +# dashback -Backend API for the Dashback frontend. Default base URL: +Backend API for Dash — Google SSO auth, SQLite allowlist, and session cookies. -``` -http://localhost:4208/api +## Setup + +```bash +cp .env.example .env +npm install ``` -## CORS & cookies +Configure `.env` (see `.env.example`). Required for auth: `GOOGLE_CLIENT_ID`, `JWT_SECRET`, and `CORS_ORIGINS`. -All browser requests that need auth must send cookies: +## Run -```js -fetch('http://localhost:4208/api/auth/me', { - credentials: 'include', -}); +```bash +npm start ``` -Sessions use an HTTP-only `token` cookie (`Secure`, `SameSite=Strict`, 24h). The browser must talk to the API over **HTTPS** for login to work — `Secure` cookies are not sent on plain HTTP. +Default port: `4208`. -## Auth flow +## API -1. Call `GET /auth/me` to get `googleClientId` (included in 200 and 401 responses). -2. Sign in with Google on the frontend (Google Identity Services / One Tap). -3. POST the Google ID token to the API. -4. The API sets the session cookie. -5. Call protected routes with `credentials: 'include'`. The cookie is refreshed automatically when it has less than 2 hours left. +See [docs/api](docs/api/README.md) for endpoints, auth flow, and frontend integration. -### `POST /auth/login` +## Users -Public. No cookie required. +Manage the email allowlist: -**Body (JSON)** - -```json -{ "idToken": "" } +```bash +npm run manage-users ``` - -**200** - -```json -{ - "message": "Login successful", - "user": { "id": 1, "email": "you@example.com", "picture": "https://lh3.googleusercontent.com/..." } -} -``` - -**Errors** - -| Status | Body | -|--------|------| -| 400 | `{ "error": "idToken is required" }` | -| 401 | `{ "error": "Invalid Google token" }` or `{ "error": "Invalid Google account" }` | -| 403 | `{ "error": "Email is not authorized" }` | - -### `POST /auth/logout` - -Public. Clears the session cookie. - -**200** - -```json -{ "message": "Logged out successfully" } -``` - -### `GET /auth/me` - -Protected. Requires a valid session cookie. Every response includes `googleClientId`. - -**200** - -```json -{ - "user": { "id": 1, "email": "you@example.com", "picture": "https://lh3.googleusercontent.com/..." }, - "googleClientId": "your-client-id.apps.googleusercontent.com" -} -``` - -**Errors** - -| Status | Body | -|--------|------| -| 401 | `{ "error": "Unauthorized", "googleClientId": "..." }` or `{ "error": "Invalid token", "googleClientId": "..." }` | -| 403 | `{ "error": "User is no longer authorized", "googleClientId": "..." }` | - -## Frontend checklist - -- [ ] `credentials: 'include'` on every API call -- [ ] HTTPS in dev (or proxy) so `Secure` cookies work diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 0000000..07ede59 --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,41 @@ +# Dash API + +``` +http://localhost:4208/api +``` + +## CORS & cookies + +All browser requests that need auth must send cookies: + +```js +fetch('http://localhost:4208/api/auth/me', { + credentials: 'include', +}); +``` + +Sessions use an HTTP-only `token` cookie (`Secure`, `SameSite=Strict`, 24h). The browser must talk to the API over **HTTPS** for login to work — `Secure` cookies are not sent on plain HTTP. + +## Auth flow + +1. Call [`GET /auth/me`](routes/auth/me.md) to get `googleClientId` (included in 200 and 401 responses). +2. Sign in with Google on the frontend (Google Identity Services / One Tap). +3. POST the Google ID token to [`POST /auth/login`](routes/auth/login.md). +4. The API sets the session cookie. +5. Call protected routes with `credentials: 'include'`. The cookie is refreshed automatically when it has less than 2 hours left. + +## Routes + +Mirrors [`src/api/routes/`](../../src/api/routes/). + +| File | Endpoint | +|------|----------| +| [`routes/auth/login.md`](routes/auth/login.md) | `POST /auth/login` | +| [`routes/auth/logout.md`](routes/auth/logout.md) | `POST /auth/logout` | +| [`routes/auth/me.md`](routes/auth/me.md) | `GET /auth/me` | +| [`routes/health.md`](routes/health.md) | `GET /health` | + +## Frontend checklist + +- [ ] `credentials: 'include'` on every API call +- [ ] HTTPS in dev (or proxy) so `Secure` cookies work diff --git a/docs/api/routes/auth/login.md b/docs/api/routes/auth/login.md new file mode 100644 index 0000000..4bff7e9 --- /dev/null +++ b/docs/api/routes/auth/login.md @@ -0,0 +1,26 @@ +# `POST /auth/login` + +Public. No cookie required. + +**Body (JSON)** + +```json +{ "idToken": "" } +``` + +**200** + +```json +{ + "message": "Login successful", + "user": { "id": 1, "email": "you@example.com", "picture": "https://lh3.googleusercontent.com/..." } +} +``` + +**Errors** + +| Status | Body | +|--------|------| +| 400 | `{ "error": "idToken is required" }` or `{ "error": "GOOGLE_CLIENT_ID is not configured" }` | +| 401 | `{ "error": "Invalid Google token" }` or `{ "error": "Invalid Google account" }` | +| 403 | `{ "error": "Email is not authorized" }` | diff --git a/docs/api/routes/auth/logout.md b/docs/api/routes/auth/logout.md new file mode 100644 index 0000000..5ad2558 --- /dev/null +++ b/docs/api/routes/auth/logout.md @@ -0,0 +1,9 @@ +# `POST /auth/logout` + +Public. Clears the session cookie. + +**200** + +```json +{ "message": "Logged out successfully" } +``` diff --git a/docs/api/routes/auth/me.md b/docs/api/routes/auth/me.md new file mode 100644 index 0000000..3174e9a --- /dev/null +++ b/docs/api/routes/auth/me.md @@ -0,0 +1,19 @@ +# `GET /auth/me` + +Protected. Requires a valid session cookie. Every response includes `googleClientId`. + +**200** + +```json +{ + "user": { "id": 1, "email": "you@example.com", "picture": "https://lh3.googleusercontent.com/..." }, + "googleClientId": "your-client-id.apps.googleusercontent.com" +} +``` + +**Errors** + +| Status | Body | +|--------|------| +| 401 | `{ "error": "Unauthorized", "googleClientId": "..." }` or `{ "error": "Invalid token", "googleClientId": "..." }` | +| 403 | `{ "error": "User is no longer authorized", "googleClientId": "..." }` | diff --git a/docs/api/routes/health.md b/docs/api/routes/health.md new file mode 100644 index 0000000..35b9155 --- /dev/null +++ b/docs/api/routes/health.md @@ -0,0 +1,15 @@ +# `GET /health` + +Public. No cookie required. + +**200** + +```json +{ "ok": true } +``` + +**Errors** + +| Status | Body | +|--------|------| +| 503 | `{ "ok": false, "error": "..." }` | diff --git a/src/api/routes/auth/meJson.js b/src/api/auth/meJson.js similarity index 75% rename from src/api/routes/auth/meJson.js rename to src/api/auth/meJson.js index e7ad19c..9d80109 100644 --- a/src/api/routes/auth/meJson.js +++ b/src/api/auth/meJson.js @@ -1,4 +1,4 @@ -import env from '../../../config/env.js'; +import env from '../../config/env.js'; export function meJson(res, status, body) { return res.status(status).json({ ...body, googleClientId: env.googleClientId }); diff --git a/src/api/middleware/auth.js b/src/api/middleware/auth.js index ce225ca..51e7914 100644 --- a/src/api/middleware/auth.js +++ b/src/api/middleware/auth.js @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken'; import env from '../../config/env.js'; import * as db from '../../db/googleAuth.js'; -import { meJson } from '../routes/auth/meJson.js'; +import { meJson } from '../auth/meJson.js'; const userCache = new Map(); const USER_CACHE_TTL = 10 * 60 * 1000; diff --git a/src/api/routes/auth/me.js b/src/api/routes/auth/me.js index d670811..bbc121e 100644 --- a/src/api/routes/auth/me.js +++ b/src/api/routes/auth/me.js @@ -1,4 +1,4 @@ -import { meJson } from './meJson.js'; +import { meJson } from '../../auth/meJson.js'; /** * Registers the current-user route (requires prior auth middleware populating `req.user`).