Added liking and security measures
This commit is contained in:
@@ -2,32 +2,62 @@
|
||||
|
||||
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}`; 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 |
|
||||
| `GET /profiles/me` | yes | no body/query | `200 {id,name,username,profile_link}` for the token user |
|
||||
| `PUT /profiles/me` | yes | multipart: optional `name`, `username`, `email`, `password`, `current_password`, file `image`, `remove_image=true\|false` | `200 {id,name,username,profile_link}`; registration-style rules on changed text fields; upload replaces picture; `remove_image=true` clears it; image + remove is invalid |
|
||||
| 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}`; email/password are never exposed |
|
||||
| `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 | no body/query | `200` all posts; ownership does not limit viewing |
|
||||
| `GET /posts/me` | yes | no body/query | `200` only posts with `author_id = token.user_id`; each includes `author_username` |
|
||||
| `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/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 |
|
||||
| `POST /create_relationship?me=<id>&them=<id>` | yes | query: `me`, `them` only | `200`; `me` must equal token user ID |
|
||||
| `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.
|
||||
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` 1–100 chars; `username` 1–50, no whitespace; `email` 1–254, trimmed/lowercased, valid format, no whitespace, not disposable; `password` 1–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`, 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.
|
||||
- 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.
|
||||
@@ -48,7 +78,25 @@ curl -X PUT "http://localhost:3000/posts/edit?id=12" \
|
||||
-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.
|
||||
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):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user