Files
filing-cabinet-api/API.md
T
2026-07-31 15:11:57 +02:00

112 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API contract (agent reference)
Base URL: `http://localhost:${PORT}`. Use `Authorization: Bearer <token>` or HttpOnly `fc_session_token` cookie on every endpoint except register/login. JSON uses `Content-Type: application/json`; upload routes use `multipart/form-data`.
| Method + path | Auth | Input | Result / policy |
| --------------------------------------------- | ----------: | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /auth/register` | no | JSON: `name`, `username`, `email`, `password` | `201 {id}`; `409` if username or email is already in use; strict fields; see password policy below |
| `POST /auth/login` | no | JSON: `email`, `password` | `200 {id,token,expires_at}`; sets `fc_session_token` HttpOnly cookie; `401` is generic |
| `GET /auth/me` | yes | no body/query | `200 {id,name,username,email}` session user profile |
| `GET /auth/verify` | yes | no body/query | `200 {id,name,username,email}` session verification endpoint |
| `POST /auth/logout` | yes | no body/query | `200 {message}`; deletes the current session and clears the cookie |
| `GET /profiles/me` | yes | no body/query | `200 {id,name,username,profile_link,private}` for the token user |
| `PUT /profiles/me` | yes | multipart: optional `name`, `username`, `email`, `password`, `current_password`, `private`, file `image`, `remove_image=true\|false` | `200 {id,name,username,profile_link,private}`; registration-style rules on changed text fields; upload replaces picture; `remove_image=true` clears it; image + remove is invalid |
| `DELETE /profiles/me` | yes | JSON: `password` | `200 {message}`; removes posts/images, profile picture, SQL row, graph node; clears session cookie; `401` if password is wrong |
| `GET /profiles/:id` | yes | no body/query | `200 {id,name,username,profile_link,private}`; email/password are never exposed. Private accounts return only `{id,name,username}` to non-friends |
| `GET /profiles/image/:filename` | yes | no body/query | Authenticated profile picture download |
| `GET /posts` | yes | query: optional `limit` (1100, default 20), `offset` (default 0) | `200` visible posts (see privacy rules below), newest first; ownership does not limit viewing |
| `GET /posts/me` | yes | query: optional `limit` (1100, default 20), `offset` (default 0) | `200` only posts with `author_id = token.user_id`, newest first; each includes `author_username` |
| `GET /posts/:id` | yes | no body/query | `200` single post with `{...post, comments: []}`; `404` if not found or not visible |
| `GET /posts/image/:filename` | yes | no body/query | Authenticated image download |
| `POST /posts/like?id=<id>` | yes | query: `id` only | `200 {message}`; idempotent; `404` if post not found or not visible; `400` if invalid id |
| `DELETE /posts/like?id=<id>` | yes | query: `id` only | `200 {message}`; idempotent; `404` if post not found or not visible |
| `POST /posts/create` | yes | multipart fields: `title`, `text`; optional file `image` | `201` post; `author_id` always comes from token |
| `PUT /posts/edit?id=<id>` | yes + owner | multipart: optional `title`, `text`, `image`, `remove_image=true\|false` | `200` updated post; upload replaces image; `remove_image=true` clears it; image + remove is invalid |
| `DELETE /posts/delete?id=<id>` | yes + owner | query: `id` only | `200`; deletes only matching `id AND author_id` and removes stored image |
| `GET /friends` | yes | no body/query | `200` friend list: `{id,name,username,profile_link,private}` |
| `GET /friends/requests` | yes | no body/query | `200` pending incoming friend requests (people who requested you) |
| `POST /friends/request?to=<id>` | yes | query: `to` only | `200 {message}`; `409` if already friends or a request exists; `404` if user not found |
| `POST /friends/accept?from=<id>` | yes | query: `from` only | `200 {message}`; creates bidirectional `FRIENDS_WITH`; `404` if no pending request from that user |
| `POST /friends/decline?from=<id>` | yes | query: `from` only | `200 {message}`; removes the request; `404` if no pending request from that user |
| `POST /friends/cancel?to=<id>` | yes | query: `to` only | `200 {message}`; cancels an outgoing request; `404` if none exists |
| `DELETE /friends/remove?id=<id>` | yes | query: `id` only | `200 {message}`; removes the friendship; `404` if not a friend |
| `GET /auth/sessions` | yes | no body/query | `200` session list (id, device_name, created_at, last_used_at, current) |
| `DELETE /auth/sessions` | yes | no body/query | `200` deletes every session except the current one |
| `DELETE /auth/sessions/:id` | yes | no body/query | `200` deletes a specific session; `404` if not found or not yours |
Post objects include `author_username` and `created_at` (the database creation timestamp), joined from `people.username`; the join is left-sided so an orphaned post is not silently omitted. `GET /posts/:id` additionally includes a `comments` field (currently an empty array, reserved for future use). Every post response also includes `like_count` (number of `LIKES` relationships in the graph database) and `liked_by_me` (whether the token user liked it). Likes are idempotent: liking an already-liked post or unliking a non-liked post still returns `200`.
## Likes
- Likes live in the graph database (Neo4j) as `(:Person)-[:LIKES]->(:Post)` relationships.
- Only posts the requester can see (active + not private-inaccessible) can be liked; others return `404`, so liking does not leak the existence of hidden posts.
- Creating a post also creates its `Post` node in the graph; soft-deleting a post removes the node and its likes; deleting an account removes all of the user's posts and likes from the graph.
- Old posts created before likes existed have no graph node yet; the first like creates one automatically (`MERGE`).
## Privacy and friendship
- Every profile has a `private` boolean (default `false`). Set it via `PUT /profiles/me` with `private=true|false`.
- Public accounts: all authenticated users can see their posts on `GET /posts` and `GET /posts/:id`, and their full profile via `GET /profiles/:id`.
- Private accounts: only the account owner and their `FRIENDS_WITH` connections in the graph database can see their posts. Non-friends get the post filtered out of the feed and a `404` on `GET /posts/:id`.
- Private accounts: `GET /profiles/:id` returns the full profile to the owner and friends only; non-friends get a limited `{id,name,username}` response (the `private` flag and `profile_link` are hidden).
- `GET /posts/me` always shows your own posts, private or not.
- Friendships live in the graph database (Neo4j) only. A request flow creates a `REQUESTED` relationship; accepting turns it into bidirectional `FRIENDS_WITH`.
## Input rules
- Any undocumented body or query field returns `400`; do not send `author_id` to create posts.
- Registration: `name` 1100 chars; `username` 150, no whitespace; `email` 1254, trimmed/lowercased, valid format, no whitespace, not disposable; `password` 1128 and at least 12 chars with lower/upper/number/special, no username/email-local-part, common password, triple repeat, or obvious sequence.
- Profile update accepts only `name`, `username`, `email`, `password`, `current_password`, `private`, and `remove_image`, plus optional file field `image`; at least one profile field or picture change is required; changing `password` requires the current password and re-applies the registration password rules against the resulting username/email.
- Friend routes accept only their documented query parameter (`to`, `from`, or `id`) and never accept a body.
- Account deletion accepts only `password` (whitespace preserved, not trimmed); failures use `401 Invalid password`.
- Login accepts only `email` and `password`; email is trimmed/lowercased; password is not trimmed or otherwise transformed (ordinary whitespace is significant).
- Passwords, hashes, salts, JWTs, and database details must never be logged or exposed.
## Uploads
```sh
curl -X POST http://localhost:3000/posts/create \
-H "Authorization: Bearer $TOKEN" \
-F title='Hello' -F text='Body' -F image=@photo.png
```
To edit text/title, replace the image, or remove it:
```sh
curl -X PUT "http://localhost:3000/posts/edit?id=12" \
-H "Authorization: Bearer $TOKEN" \
-F text='Updated body' -F remove_image=true
```
Omit `image` and `remove_image` to keep the current image. Stored image URLs require the same Bearer token. Uploaded files are validated both by declared `Content-Type` and by their file signature (magic bytes): a file whose bytes do not match the declared type is rejected with `400` and not stored.
Image downloads obey the same visibility rules as the content they belong to: an image of a private/inactive post or a private profile returns `404` to non-friends (even if the URL is known or leaked).
## Rate limiting
All endpoints are rate-limited per IP: a strict limit (20 per 15 minutes) applies to `POST /auth/login`, `POST /auth/register`, and `DELETE /profiles/me`; all other endpoints share a general limit (300 per 15 minutes). Exceeding a limit returns `429`.
## Session management
Login automatically creates a session (tracked by `user-agent`). Each session gets its own JWT containing the session `id` (`jti`) and a `token_version` (`tvr`).
- **Password change** invalidates all sessions — every device must log in again.
- **`GET /auth/sessions`** lists your active sessions with a `current` boolean marker.
- **`DELETE /auth/sessions`** logs out all other devices (keeps the current one); `400` if the current session cannot be identified.
- **`DELETE /auth/sessions/:id`** logs out a specific device by session ID.
- **`POST /auth/logout`** logs out the current device (deletes its session row and clears the cookie).
The `current` field in the session list tells you which session is making the request.
Profile picture (same file rules as post images):
```sh
curl -X PUT http://localhost:3000/profiles/me \
-H "Authorization: Bearer $TOKEN" \
-F image=@avatar.png
curl -X PUT http://localhost:3000/profiles/me \
-H "Authorization: Bearer $TOKEN" \
-F remove_image=true
```