init prds
This commit is contained in:
+611
@@ -0,0 +1,611 @@
|
||||
# PRD: API Service
|
||||
|
||||
**Module:** `api/` | **Namespace:** `ajet.chat.api.*`
|
||||
**Status:** v1 | **Last updated:** 2026-02-17
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The API service is the stateless REST backend — the only service (besides Auth GW) with direct PostgreSQL access. It handles all data reads/writes and publishes events to NATS after mutations. All other services interact with data through the API.
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
Auth Gateway → API Service → PostgreSQL
|
||||
→ NATS (publish events)
|
||||
→ MinIO (file storage)
|
||||
```
|
||||
|
||||
- **No direct client access** — all requests arrive via Auth GW with validated session context
|
||||
- **Request headers from Auth GW:**
|
||||
- `X-User-Id: <uuid>` — authenticated user
|
||||
- `X-User-Role: <owner|admin|member>` — role in the community context
|
||||
- `X-Community-Id: <uuid>` — current community (for community-scoped endpoints)
|
||||
- `X-Trace-Id: <uuid>` — request tracing
|
||||
|
||||
## 3. Database Schema
|
||||
|
||||
### 3.1 Migrations (Migratus, sequential)
|
||||
|
||||
```
|
||||
001-create-users.up.sql
|
||||
002-create-oauth-accounts.up.sql
|
||||
003-create-communities.up.sql
|
||||
004-create-community-members.up.sql
|
||||
005-create-channel-categories.up.sql
|
||||
006-create-channels.up.sql
|
||||
007-create-channel-members.up.sql
|
||||
008-create-messages.up.sql
|
||||
009-create-attachments.up.sql
|
||||
010-create-reactions.up.sql
|
||||
011-create-mentions.up.sql
|
||||
012-create-notifications.up.sql
|
||||
013-create-sessions.up.sql
|
||||
014-create-api-users.up.sql
|
||||
015-create-api-tokens.up.sql
|
||||
016-create-webhooks.up.sql
|
||||
017-create-invites.up.sql
|
||||
018-add-search-indexes.up.sql
|
||||
```
|
||||
|
||||
### 3.2 Tables
|
||||
|
||||
```sql
|
||||
-- Core entities
|
||||
users (id uuid PK, username text UNIQUE, display_name text, email text, avatar_url text, status_text text, created_at timestamptz)
|
||||
oauth_accounts (id uuid PK, user_id uuid FK→users, provider text, provider_user_id text, created_at timestamptz, UNIQUE(provider, provider_user_id))
|
||||
communities (id uuid PK, name text, slug text UNIQUE, created_at timestamptz)
|
||||
community_members (community_id uuid FK, user_id uuid FK, role text CHECK(owner/admin/member), nickname text, avatar_url text, joined_at timestamptz, PK(community_id, user_id))
|
||||
channel_categories (id uuid PK, community_id uuid FK, name text, position int)
|
||||
channels (id uuid PK, community_id uuid FK NULL, category_id uuid FK NULL, name text, type text CHECK(text/dm/group_dm), visibility text CHECK(public/private), topic text, created_at timestamptz)
|
||||
channel_members (channel_id uuid FK, user_id uuid FK, joined_at timestamptz, last_read_message_id uuid, PK(channel_id, user_id))
|
||||
messages (id uuid PK, channel_id uuid FK, user_id uuid FK, parent_id uuid FK→messages NULL, body_md text, created_at timestamptz, edited_at timestamptz NULL)
|
||||
attachments (id uuid PK, message_id uuid FK, filename text, content_type text, size_bytes bigint, storage_key text)
|
||||
reactions (message_id uuid FK, user_id uuid FK, emoji text, created_at timestamptz, PK(message_id, user_id, emoji))
|
||||
mentions (id uuid PK, message_id uuid FK, target_type text CHECK(user/channel/here), target_id uuid NULL)
|
||||
notifications (id uuid PK, user_id uuid FK, type text CHECK(mention/dm/thread_reply/invite/system), source_id uuid, read boolean DEFAULT false, created_at timestamptz)
|
||||
webhooks (id uuid PK, community_id uuid FK, channel_id uuid FK, name text, avatar_url text, token_hash text, created_by uuid FK, created_at timestamptz)
|
||||
invites (id uuid PK, community_id uuid FK, created_by uuid FK, code text UNIQUE, max_uses int NULL, uses int DEFAULT 0, expires_at timestamptz NULL, created_at timestamptz)
|
||||
|
||||
-- Indexes
|
||||
idx_messages_channel_created ON messages(channel_id, created_at)
|
||||
idx_messages_parent ON messages(parent_id) WHERE parent_id IS NOT NULL
|
||||
idx_messages_search ON messages USING GIN(to_tsvector('english', body_md))
|
||||
idx_notifications_user_unread ON notifications(user_id, created_at) WHERE read = false
|
||||
idx_channel_members_user ON channel_members(user_id)
|
||||
idx_community_members_user ON community_members(user_id)
|
||||
```
|
||||
|
||||
## 4. API Endpoints
|
||||
|
||||
### 4.1 Communities
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| POST | `/api/communities` | Create community (first-user bootstrap or new) | User |
|
||||
| GET | `/api/communities` | List user's communities | User |
|
||||
| GET | `/api/communities/:id` | Get community details | Member |
|
||||
| PUT | `/api/communities/:id` | Update community name/slug | Owner |
|
||||
| DELETE | `/api/communities/:id` | Delete community | Owner |
|
||||
|
||||
**POST /api/communities**
|
||||
```
|
||||
Request: {"name": "My Team", "slug": "my-team"}
|
||||
Response: {"id": "uuid", "name": "My Team", "slug": "my-team", "created_at": "..."}
|
||||
Side effects:
|
||||
- Creates community
|
||||
- Adds requesting user as owner
|
||||
- Creates #general channel (text, public)
|
||||
- Publishes :community/created to chat.events.{id}
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| COM-T1 | Create community | POST creates community, user is owner, #general exists |
|
||||
| COM-T2 | Slug uniqueness | POST with existing slug returns 409 |
|
||||
| COM-T3 | Slug format validation | Slug with spaces/uppercase returns 422 |
|
||||
| COM-T4 | List communities | GET returns only communities user is a member of |
|
||||
| COM-T5 | Get community (member) | GET returns community details |
|
||||
| COM-T6 | Get community (non-member) | GET returns 403 |
|
||||
| COM-T7 | Update community (owner) | PUT updates name/slug |
|
||||
| COM-T8 | Update community (admin) | PUT returns 403 (owner-only) |
|
||||
| COM-T9 | Delete community (owner) | DELETE removes community and all associated data |
|
||||
| COM-T10 | Delete community (non-owner) | DELETE returns 403 |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Channels
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| GET | `/api/communities/:cid/channels` | List channels in community | Member |
|
||||
| POST | `/api/communities/:cid/channels` | Create channel | Admin+ |
|
||||
| GET | `/api/channels/:id` | Get channel details | Channel member (or community member if public) |
|
||||
| PUT | `/api/channels/:id` | Update channel | Admin+ |
|
||||
| DELETE | `/api/channels/:id` | Delete channel | Admin+ |
|
||||
| POST | `/api/channels/:id/join` | Join channel (public only) | Member |
|
||||
| POST | `/api/channels/:id/leave` | Leave channel | Member |
|
||||
| GET | `/api/channels/:id/members` | List channel members | Channel member |
|
||||
|
||||
**POST /api/communities/:cid/channels**
|
||||
```
|
||||
Request: {"name": "backend", "type": "text", "visibility": "public", "category_id": "uuid or null"}
|
||||
Response: {"id": "uuid", "name": "backend", ...}
|
||||
Side effects:
|
||||
- Creates channel
|
||||
- Adds creator as channel member
|
||||
- Publishes :channel/created to chat.events.{community-id}
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| CH-T1 | Create public channel | POST creates channel, creator is member, event published |
|
||||
| CH-T2 | Create private channel | POST with visibility=private works |
|
||||
| CH-T3 | Create channel (member role) | POST returns 403 for non-admin |
|
||||
| CH-T4 | Duplicate channel name | POST with existing name in same community returns 409 |
|
||||
| CH-T5 | List channels | GET returns public channels + private channels user is in |
|
||||
| CH-T6 | Join public channel | POST /join adds user to channel, publishes :member/joined |
|
||||
| CH-T7 | Join private channel | POST /join returns 403 (must be invited) |
|
||||
| CH-T8 | Leave channel | POST /leave removes membership, publishes :member/left |
|
||||
| CH-T9 | Leave last channel | Leaving #general is allowed |
|
||||
| CH-T10 | Delete channel | DELETE removes channel, all messages, publishes :channel/deleted |
|
||||
| CH-T11 | Update channel topic | PUT with new topic, publishes :channel/updated |
|
||||
| CH-T12 | Assign category | PUT with category_id moves channel to category |
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Channel Categories
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| GET | `/api/communities/:cid/categories` | List categories (ordered) | Member |
|
||||
| POST | `/api/communities/:cid/categories` | Create category | Admin+ |
|
||||
| PUT | `/api/categories/:id` | Update category (name, position) | Admin+ |
|
||||
| DELETE | `/api/categories/:id` | Delete category (channels become uncategorized) | Admin+ |
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| CAT-T1 | Create category | POST creates with position, returns category |
|
||||
| CAT-T2 | Reorder categories | PUT with new position reorders correctly |
|
||||
| CAT-T3 | Delete category | Channels in deleted category become uncategorized (category_id = null) |
|
||||
| CAT-T4 | List categories with channels | GET returns categories with nested channel list |
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Messages
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| GET | `/api/channels/:id/messages` | List messages (paginated) | Channel member |
|
||||
| POST | `/api/channels/:id/messages` | Send message | Channel member |
|
||||
| GET | `/api/messages/:id` | Get single message | Channel member |
|
||||
| PUT | `/api/messages/:id` | Edit message (1-hour window) | Author |
|
||||
| DELETE | `/api/messages/:id` | Delete message | Author or Admin+ |
|
||||
| GET | `/api/messages/:id/thread` | Get thread replies | Channel member |
|
||||
|
||||
**Pagination:** Cursor-based using message UUID.
|
||||
```
|
||||
GET /api/channels/:id/messages?before=<uuid>&limit=50
|
||||
GET /api/channels/:id/messages?after=<uuid>&limit=50
|
||||
Default limit: 50, max: 100
|
||||
```
|
||||
|
||||
**POST /api/channels/:id/messages**
|
||||
```
|
||||
Request: {"body_md": "hello @<user:uuid> check #<channel:uuid>", "parent_id": null}
|
||||
Response: {"id": "uuid", "channel_id": "...", "user_id": "...", "body_md": "...", "created_at": "..."}
|
||||
Side effects:
|
||||
- Creates message row
|
||||
- Parses mentions → creates mention rows
|
||||
- Creates notification rows for mentioned users
|
||||
- Publishes :message/created to appropriate NATS subject
|
||||
- If thread reply: notifies thread participants
|
||||
```
|
||||
|
||||
**PUT /api/messages/:id (edit)**
|
||||
```
|
||||
Request: {"body_md": "updated text"}
|
||||
Response: {"id": "uuid", ..., "edited_at": "..."}
|
||||
Validation:
|
||||
- User must be the author
|
||||
- created_at must be within 1 hour of now
|
||||
- Re-parses mentions, updates mention rows
|
||||
- Publishes :message/edited
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| MSG-T1 | Send message | POST creates message, returns with ID and timestamps |
|
||||
| MSG-T2 | Send message to channel user is not in | POST returns 403 |
|
||||
| MSG-T3 | Send empty message | POST with empty body returns 422 |
|
||||
| MSG-T4 | Send message with mentions | Mention rows created, notifications created for mentioned users |
|
||||
| MSG-T5 | Send message with @here | Notifications created for all online channel members |
|
||||
| MSG-T6 | Send thread reply | POST with parent_id creates threaded message, notifies thread participants |
|
||||
| MSG-T7 | Thread does not nest | POST with parent_id pointing to another reply is rejected (or parent_id resolves to root) |
|
||||
| MSG-T8 | Get messages (default pagination) | GET returns last 50 messages, newest last |
|
||||
| MSG-T9 | Get messages (before cursor) | GET with `?before=uuid` returns older messages |
|
||||
| MSG-T10 | Get messages (after cursor) | GET with `?after=uuid` returns newer messages |
|
||||
| MSG-T11 | Edit message within window | PUT succeeds within 1 hour, sets edited_at |
|
||||
| MSG-T12 | Edit message after window | PUT returns 403 after 1 hour |
|
||||
| MSG-T13 | Edit message (non-author) | PUT returns 403 |
|
||||
| MSG-T14 | Edit updates mentions | Editing to add @user creates new mention + notification |
|
||||
| MSG-T15 | Delete own message | DELETE removes message, publishes :message/deleted |
|
||||
| MSG-T16 | Delete other's message (admin) | DELETE succeeds for admin/owner |
|
||||
| MSG-T17 | Delete other's message (member) | DELETE returns 403 |
|
||||
| MSG-T18 | Get thread | GET /thread returns all replies to a message, ordered by created_at |
|
||||
| MSG-T19 | NATS event published | After send, :message/created event appears on correct subject |
|
||||
| MSG-T20 | DM message event | Message in DM channel publishes to `chat.dm.{channel-id}` |
|
||||
| MSG-T21 | Community message event | Message in community channel publishes to `chat.events.{community-id}` |
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Reactions
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| PUT | `/api/messages/:id/reactions/:emoji` | Add reaction (idempotent) | Channel member |
|
||||
| DELETE | `/api/messages/:id/reactions/:emoji` | Remove reaction | Reactor |
|
||||
| GET | `/api/messages/:id/reactions` | List reactions on message | Channel member |
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| RXN-T1 | Add reaction | PUT creates reaction, publishes :reaction/added |
|
||||
| RXN-T2 | Add duplicate reaction | PUT is idempotent, no error |
|
||||
| RXN-T3 | Remove reaction | DELETE removes, publishes :reaction/removed |
|
||||
| RXN-T4 | Remove non-existent reaction | DELETE returns 404 |
|
||||
| RXN-T5 | List reactions | GET returns reactions grouped by emoji with user lists |
|
||||
| RXN-T6 | Multiple users same emoji | Both reactions stored, count shows 2 |
|
||||
|
||||
---
|
||||
|
||||
### 4.6 DMs (Global)
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| GET | `/api/dms` | List user's DM channels | User |
|
||||
| POST | `/api/dms` | Create or get existing DM | User |
|
||||
| POST | `/api/dms/group` | Create group DM | User |
|
||||
|
||||
**POST /api/dms**
|
||||
```
|
||||
Request: {"user_id": "uuid"}
|
||||
Response: {"id": "channel-uuid", "type": "dm", "members": [...]}
|
||||
Behavior: Returns existing DM if one exists between the two users, otherwise creates new.
|
||||
```
|
||||
|
||||
**POST /api/dms/group**
|
||||
```
|
||||
Request: {"user_ids": ["uuid1", "uuid2", ...]}
|
||||
Response: {"id": "channel-uuid", "type": "group_dm", "members": [...]}
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| DM-T1 | Create DM | POST creates DM channel with both users as members |
|
||||
| DM-T2 | Get existing DM | POST for existing pair returns same channel |
|
||||
| DM-T3 | Create group DM | POST /group creates group_dm with all specified users |
|
||||
| DM-T4 | List DMs | GET returns all DM/group_dm channels for user, ordered by last message |
|
||||
| DM-T5 | DM with self | POST with own user_id returns 422 |
|
||||
| DM-T6 | Group DM minimum | POST /group with < 2 other users returns 422 |
|
||||
|
||||
---
|
||||
|
||||
### 4.7 Users & Profiles
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| GET | `/api/me` | Get current user | User |
|
||||
| PUT | `/api/me` | Update profile (display_name, status_text) | User |
|
||||
| GET | `/api/users/:id` | Get user profile | User |
|
||||
| GET | `/api/communities/:cid/members` | List community members | Member |
|
||||
| PUT | `/api/communities/:cid/members/:uid` | Update member (nickname, role) | Admin+ (role: Owner only) |
|
||||
| DELETE | `/api/communities/:cid/members/:uid` | Remove/kick member | Admin+ |
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| USR-T1 | Get current user | GET /me returns authenticated user's profile |
|
||||
| USR-T2 | Update display name | PUT /me with display_name updates it |
|
||||
| USR-T3 | Update status text | PUT /me with status_text updates it |
|
||||
| USR-T4 | Get user profile | GET /users/:id returns public profile |
|
||||
| USR-T5 | List community members | GET returns all members with roles and nicknames |
|
||||
| USR-T6 | Set community nickname | PUT /members/:uid with nickname (admin) |
|
||||
| USR-T7 | Change member role (owner) | Owner can promote member to admin |
|
||||
| USR-T8 | Change member role (admin) | Admin cannot change roles (403) |
|
||||
| USR-T9 | Kick member (admin) | DELETE removes member, publishes :member/kicked |
|
||||
| USR-T10 | Kick owner | Cannot kick owner (403) |
|
||||
|
||||
---
|
||||
|
||||
### 4.8 Notifications
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| GET | `/api/notifications` | List notifications (paginated) | User |
|
||||
| POST | `/api/notifications/read` | Mark notifications as read | User |
|
||||
| GET | `/api/notifications/unread-count` | Get unread count | User |
|
||||
|
||||
**Pagination:** `?after=<uuid>&limit=50&unread=true`
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| NTF-T1 | Get notifications | GET returns notifications newest-first |
|
||||
| NTF-T2 | Filter unread | GET with ?unread=true returns only unread |
|
||||
| NTF-T3 | Mark read | POST with notification IDs marks them read |
|
||||
| NTF-T4 | Unread count | GET /unread-count returns integer count |
|
||||
| NTF-T5 | Notification on mention | @mention in message creates notification |
|
||||
| NTF-T6 | Notification on DM | New DM message creates notification |
|
||||
| NTF-T7 | Notification on thread reply | Reply to your thread root creates notification |
|
||||
|
||||
---
|
||||
|
||||
### 4.9 Presence & Typing
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| POST | `/api/heartbeat` | Report online status | User |
|
||||
| GET | `/api/communities/:cid/presence` | Get online members | Member |
|
||||
|
||||
**POST /api/heartbeat**
|
||||
```
|
||||
Response: 200 OK (immediate)
|
||||
Side effects:
|
||||
- Async: update last_seen_at in DB
|
||||
- Async: publish :presence/online to chat.presence.{community-id} for each community
|
||||
```
|
||||
|
||||
**Presence logic:**
|
||||
- Online: last heartbeat < 2 minutes ago
|
||||
- Offline: last heartbeat >= 2 minutes ago
|
||||
- Clients send heartbeat every 60 seconds
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| PRS-T1 | Heartbeat returns 200 immediately | POST /heartbeat responds fast (< 50ms) |
|
||||
| PRS-T2 | Heartbeat updates last_seen | DB shows updated last_seen_at after heartbeat |
|
||||
| PRS-T3 | Online presence | User with recent heartbeat appears in presence list |
|
||||
| PRS-T4 | Offline after timeout | User without heartbeat for 2+ minutes not in online list |
|
||||
| PRS-T5 | Presence event published | Heartbeat publishes to NATS presence subject |
|
||||
|
||||
---
|
||||
|
||||
### 4.10 Search
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| GET | `/api/search` | Global search | User |
|
||||
|
||||
**Parameters:**
|
||||
```
|
||||
?q=search+term — required, search query
|
||||
&type=messages|channels|users — optional filter (default: all)
|
||||
&community_id=uuid — optional, scope to community
|
||||
&channel_id=uuid — optional, scope to channel
|
||||
&from=uuid — optional, filter by author
|
||||
&after=datetime — optional, date range start
|
||||
&before=datetime — optional, date range end
|
||||
&cursor=uuid&limit=20 — pagination
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Messages: PostgreSQL `tsvector` full-text search, only in channels user has access to
|
||||
- Channels: name substring match within user's communities
|
||||
- Users: username/display_name substring match
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| SRC-T1 | Search messages by keyword | Returns matching messages user can access |
|
||||
| SRC-T2 | Search respects access | Messages in channels user isn't in are excluded |
|
||||
| SRC-T3 | Search in specific channel | `channel_id` filter narrows results |
|
||||
| SRC-T4 | Search by author | `from` filter returns only that user's messages |
|
||||
| SRC-T5 | Search channels | `type=channels` returns matching channel names |
|
||||
| SRC-T6 | Search users | `type=users` returns matching usernames/display names |
|
||||
| SRC-T7 | Empty query | Returns 422 |
|
||||
| SRC-T8 | Pagination | Cursor-based pagination returns next page |
|
||||
| SRC-T9 | Date range filter | `after`/`before` narrows message results |
|
||||
|
||||
---
|
||||
|
||||
### 4.11 Invites
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| POST | `/api/communities/:cid/invites` | Create invite link | Admin+ |
|
||||
| GET | `/api/communities/:cid/invites` | List active invites | Admin+ |
|
||||
| DELETE | `/api/invites/:id` | Revoke invite | Admin+ |
|
||||
| POST | `/api/invites/:code/accept` | Accept invite (join community) | User |
|
||||
| POST | `/api/communities/:cid/invites/direct` | Direct invite (by user ID) | Admin+ |
|
||||
|
||||
**POST /api/communities/:cid/invites**
|
||||
```
|
||||
Request: {"max_uses": 10, "expires_in_hours": 48} — both optional
|
||||
Response: {"id": "uuid", "code": "abc123", "url": "https://chat.example.com/invite/abc123", ...}
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| INV-T1 | Create invite link | POST creates invite with unique code |
|
||||
| INV-T2 | Accept invite | POST /accept adds user to community as member |
|
||||
| INV-T3 | Accept expired invite | POST /accept returns 410 (gone) |
|
||||
| INV-T4 | Accept max-uses exhausted | POST /accept returns 410 after max uses reached |
|
||||
| INV-T5 | Accept invite (already member) | POST /accept returns 200 (idempotent) |
|
||||
| INV-T6 | List invites | GET returns active (non-expired, non-exhausted) invites |
|
||||
| INV-T7 | Revoke invite | DELETE invalidates the invite |
|
||||
| INV-T8 | Direct invite | POST /direct creates invite + notification for target user |
|
||||
| INV-T9 | Create invite (member role) | POST returns 403 |
|
||||
|
||||
---
|
||||
|
||||
### 4.12 Webhooks (Incoming)
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| POST | `/api/communities/:cid/webhooks` | Create webhook | Admin+ |
|
||||
| GET | `/api/communities/:cid/webhooks` | List webhooks | Admin+ |
|
||||
| DELETE | `/api/webhooks/:id` | Delete webhook | Admin+ |
|
||||
| POST | `/api/webhooks/:id/incoming` | Post via webhook | Webhook token |
|
||||
|
||||
**POST /api/webhooks/:id/incoming**
|
||||
```
|
||||
Headers: Authorization: Bearer <webhook-token>
|
||||
Request: {"content": "Build #42 passed!", "username": "CI Bot", "avatar_url": "..."}
|
||||
Response: 204 No Content
|
||||
Side effects: Creates message in webhook's channel, publishes event
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| WH-T1 | Create webhook | POST creates webhook with token, returns token once |
|
||||
| WH-T2 | Post via webhook | POST /incoming creates message in channel |
|
||||
| WH-T3 | Post with invalid token | Returns 401 |
|
||||
| WH-T4 | Post with custom username | Message shows webhook's custom username |
|
||||
| WH-T5 | Delete webhook | DELETE removes webhook, token becomes invalid |
|
||||
| WH-T6 | List webhooks | GET returns webhooks (without tokens) |
|
||||
|
||||
---
|
||||
|
||||
### 4.13 Slash Commands
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| POST | `/api/commands` | Execute slash command | User |
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{"command": "/kick @<user:uuid>", "channel_id": "uuid", "community_id": "uuid"}
|
||||
```
|
||||
|
||||
**Commands v1:**
|
||||
|
||||
| Command | Args | Permission | Description |
|
||||
|---------|------|------------|-------------|
|
||||
| `/help` | `[command]` | All | Show help or specific command help |
|
||||
| `/topic` | `<text>` | Admin+ | Set channel topic |
|
||||
| `/nick` | `<nickname>` | All | Set community nickname |
|
||||
| `/invite` | `[max_uses] [expires_hours]` | Admin+ | Generate invite link |
|
||||
| `/kick` | `@<user>` | Admin+ | Kick user from community |
|
||||
| `/ban` | `@<user>` | Admin+ | Ban user from community |
|
||||
| `/mute` | `@<user> <duration>` | Admin+ | Mute user for duration |
|
||||
| `/token create` | `<name> [scopes]` | Owner | Create API token |
|
||||
| `/token revoke` | `<name>` | Owner | Revoke API token |
|
||||
| `/token list` | | Owner | List API tokens |
|
||||
| `/webhook create` | `<name> [channel]` | Admin+ | Create incoming webhook |
|
||||
| `/webhook delete` | `<name>` | Admin+ | Delete webhook |
|
||||
| `/webhook list` | | Admin+ | List webhooks |
|
||||
| `/status` | `<text>` | All | Set status text |
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| CMD-T1 | /help returns command list | All commands listed with descriptions |
|
||||
| CMD-T2 | /topic sets channel topic | Channel topic updated, event published |
|
||||
| CMD-T3 | /topic (non-admin) | Returns 403 |
|
||||
| CMD-T4 | /nick sets nickname | Community member nickname updated |
|
||||
| CMD-T5 | /kick removes user | User removed from community, event published |
|
||||
| CMD-T6 | /kick (non-admin) | Returns 403 |
|
||||
| CMD-T7 | /ban bans user | User banned (cannot rejoin) |
|
||||
| CMD-T8 | /mute mutes user | User cannot send messages for duration |
|
||||
| CMD-T9 | /token create | Creates API token, returns token string once |
|
||||
| CMD-T10 | /webhook create | Creates webhook, returns webhook URL + token |
|
||||
| CMD-T11 | Unknown command | Returns error message suggesting /help |
|
||||
| CMD-T12 | /status sets status text | User's status_text updated |
|
||||
|
||||
---
|
||||
|
||||
### 4.14 File Upload
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| POST | `/api/channels/:id/upload` | Upload image | Channel member |
|
||||
|
||||
**Request:** Multipart form data with image file.
|
||||
**Constraints:** Images only (JPEG, PNG, GIF, WebP). Max size: 10MB.
|
||||
|
||||
```
|
||||
Response: {"id": "uuid", "filename": "screenshot.png", "content_type": "image/png", "size_bytes": 12345, "url": "/files/uuid/screenshot.png"}
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
1. Validate file type and size
|
||||
2. Upload to MinIO with storage key `attachments/{uuid}/{filename}`
|
||||
3. Return attachment metadata (to be referenced in message)
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| UPL-T1 | Upload JPEG | File stored in MinIO, attachment record created |
|
||||
| UPL-T2 | Upload PNG | Same as above |
|
||||
| UPL-T3 | Upload non-image | Returns 422 (only images allowed) |
|
||||
| UPL-T4 | Upload too large | Returns 413 (> 10MB) |
|
||||
| UPL-T5 | Upload to channel user isn't in | Returns 403 |
|
||||
|
||||
---
|
||||
|
||||
### 4.15 Read Tracking
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| POST | `/api/channels/:id/read` | Mark channel as read | Channel member |
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{"last_read_message_id": "uuid"}
|
||||
```
|
||||
|
||||
Updates `channel_members.last_read_message_id`. Used by SMs to calculate unread counts.
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Description |
|
||||
|----|------|-------------|
|
||||
| RD-T1 | Mark read | POST updates last_read_message_id |
|
||||
| RD-T2 | Unread count derivation | Messages after last_read_message_id are "unread" |
|
||||
| RD-T3 | Mark read (non-member) | Returns 403 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Cross-Cutting Concerns
|
||||
|
||||
### 5.1 Error Response Format
|
||||
```json
|
||||
{"error": {"code": "NOT_FOUND", "message": "Channel not found", "details": {}}}
|
||||
```
|
||||
|
||||
### 5.2 Rate Limiting
|
||||
- Applied at Auth GW level, not in API service itself
|
||||
- API trusts that Auth GW has already rate-limited
|
||||
|
||||
### 5.3 NATS Publishing
|
||||
- Every mutation endpoint publishes an event to the appropriate NATS subject
|
||||
- Events are fire-and-forget (API does not wait for subscriber acknowledgment)
|
||||
- Event publishing failure should be logged but not fail the HTTP response
|
||||
|
||||
### 5.4 Audit Trail (P2)
|
||||
- Log all admin actions (kick, ban, role change, channel delete) with actor + target + timestamp
|
||||
- Queryable by Owner via future admin API
|
||||
Reference in New Issue
Block a user