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

14 KiB
Raw Blame History

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

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:

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):

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