add todo api
CI / Test + coverage (push) Failing after 22s

This commit is contained in:
2026-09-19 07:46:05 -09:00
parent 6ff03a45f7
commit b1783e3f85
22 changed files with 567 additions and 80 deletions
+28 -1
View File
@@ -49,6 +49,11 @@ docker run --rm -p 3000:3000 tbd
| `GET` | `/auth/callback` | OAuth redirect target: completes the login, sets the session cookie, redirects to `/` |
| `POST` | `/auth/logout` | Ends the session |
| `GET` | `/auth/me` | The logged-in user, `{:login "alice" :id 7 :admin? true}` (401 if not logged in) |
| `GET` | `/api/todos` | **Login required.** Your todos: `{:todos [{:id 1 :owner-id 7 :title "buy milk" :done false :created-at 1700000000000 :updated-at 1700000000000}]}` |
| `POST` | `/api/todos` | **Login required.** Body `{"title": "buy milk"}` creates a todo and returns it with `201` (400 if the title is blank or over 200 characters) |
| `GET` | `/api/todos/:id` | **Login required.** One todo (404 if it doesn't exist or isn't yours) |
| `PATCH` | `/api/todos/:id` | **Login required.** Body with `title` and/or `done` (`{"done": true}`) updates the todo and returns it (400 for an empty or invalid change, 404 as above) |
| `DELETE` | `/api/todos/:id` | **Login required.** Deletes the todo: `{:deleted true}` (404 as above) |
| `GET` | `/nrepl` | **Admin only.** Whether the nREPL debug server is on: `{:enabled false}`, or `{:enabled true :bind "127.0.0.1" :port 7888}` |
| `POST` | `/nrepl` | **Admin only.** Body `{"enabled": true}` (JSON or EDN) starts the nREPL debug server, `false` stops it; returns `{:enabled true :bind "127.0.0.1" :port 7888}` |
@@ -57,6 +62,24 @@ curl localhost:3000/api/ping # pong
curl 'localhost:3000/api/add?x=2&y=3' # {:result 5}
```
## Todos
Each logged-in user has their own todo list, keyed by their Gitea user id. Every query is
filtered by that id, so users can't see or change each other's todos; someone else's todo answers
`404`, exactly like a missing one. Without a login the todo routes answer `401`.
Todos are stored in a local [SQLite](https://www.sqlite.org/) file, `data/todo.db` by default
(git-ignored), which is created and migrated on the first todo request. Set `TODO_DB_PATH` to put it
elsewhere. In Docker, mount a volume at that path (e.g. `docker run -v tbd-data:/app/data ...`), or the
todos are lost with the container.
```bash
# after logging in in a browser, copy the `tbd-session` cookie value:
curl -XPOST localhost:3000/api/todos -H 'Cookie: tbd-session=...' \
-H 'Content-Type: application/json' -d '{"title": "buy milk"}'
curl localhost:3000/api/todos -H 'Cookie: tbd-session=...'
```
## Authentication and the admin API
Login is OAuth2 (authorization-code flow) against a [Gitea](https://about.gitea.com/) instance.
@@ -75,6 +98,7 @@ login, **403** for a logged-in non-admin. With no allowlist, nobody is an admin.
| `BASE_URL` | Public URL of this app; an `https://` one also marks the session cookie `Secure` |
| `ADMIN_USERS` | Comma-separated Gitea usernames (case-insensitive) allowed to use the admin API |
| `SESSION_SECRET` | Signs and encrypts session cookies. Unset = a random key per start, so logins don't survive a restart |
| `TODO_DB_PATH` | SQLite file for the todo group (default `data/todo.db`) |
| `NREPL_PORT`, `NREPL_BIND` | Where the debug nREPL listens (default `7888` on `127.0.0.1`) |
Sessions live in an encrypted, signed, `HttpOnly`, `SameSite=Lax` cookie and expire after 8 hours
@@ -156,6 +180,7 @@ src/tbd/
<group>/core.clj Business logic for the group, with no HTTP concerns
auth/ Gitea OAuth (core), session + admin middleware (middleware), /auth routes
nrepl/ On-demand nREPL server (core) and the admin-only `/nrepl` routes
todo/ Per-user todos: pure rules (core), SQLite storage (db), `/api/todos` routes
ui/ ClojureScript re-frame app (shadow-cljs project); ui/src/tbd/ui/{db,events,subs,views,core}.cljs
.env-template Settings the server reads from the environment; copy to .env (git-ignored)
resources/public/ index.html (the shell served at /) and js/ (compiled UI output, git-ignored)
@@ -166,4 +191,6 @@ script/ Bash entry points (server, ui-build, test, coverage, repl,
To add a new group, create `tbd.<group>.routes` with a `routes` fn that returns a reitit route
vector, then add it to the router in `src/tbd/core.clj`. To make a route admin-only, put
`:admin? true` in its route data.
`:admin? true` in its route data (admin allowlist), or `:login? true` (any logged-in user; the user is
passed to the handler as `:user`). A group with storage keeps it in its own `tbd.<group>.db`, apart
from its business logic in `core.clj`.