14 KiB
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 (1–100, 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 (1–100, 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
Postnode 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
privateboolean (defaultfalse). Set it viaPUT /profiles/mewithprivate=true|false. - Public accounts: all authenticated users can see their posts on
GET /postsandGET /posts/:id, and their full profile viaGET /profiles/:id. - Private accounts: only the account owner and their
FRIENDS_WITHconnections in the graph database can see their posts. Non-friends get the post filtered out of the feed and a404onGET /posts/:id. - Private accounts:
GET /profiles/:idreturns the full profile to the owner and friends only; non-friends get a limited{id,name,username}response (theprivateflag andprofile_linkare hidden). GET /posts/mealways shows your own posts, private or not.- Friendships live in the graph database (Neo4j) only. A request flow creates a
REQUESTEDrelationship; accepting turns it into bidirectionalFRIENDS_WITH.
Input rules
- Any undocumented body or query field returns
400; do not sendauthor_idto create posts. - Registration:
name1–100 chars;username1–50, no whitespace;email1–254, trimmed/lowercased, valid format, no whitespace, not disposable;password1–128 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, andremove_image, plus optional file fieldimage; at least one profile field or picture change is required; changingpasswordrequires 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, orid) and never accept a body. - Account deletion accepts only
password(whitespace preserved, not trimmed); failures use401 Invalid password. - Login accepts only
emailandpassword; 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/sessionslists your active sessions with acurrentboolean marker.DELETE /auth/sessionslogs out all other devices (keeps the current one);400if the current session cannot be identified.DELETE /auth/sessions/:idlogs out a specific device by session ID.POST /auth/logoutlogs 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