Normalize audio waveform peaks for Flutter
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
121
docs/artist-dashboard-api.md
Normal file
121
docs/artist-dashboard-api.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Artist Dashboard API
|
||||||
|
|
||||||
|
This endpoint powers the Flutter artist profile dashboard for the currently authenticated user.
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
`GET /api/v1/users/me/dashboard`
|
||||||
|
|
||||||
|
## Headers
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <accessToken>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This endpoint is private to the authenticated user.
|
||||||
|
- It does not accept a `userId` parameter.
|
||||||
|
- It does not expose another user's private analytics.
|
||||||
|
- It keeps the existing `GET /api/v1/users/:id/profile-overview` endpoint unchanged.
|
||||||
|
- Earnings, audience non-followers, and chart data are not faked. They return safe empty/default values until dedicated analytics storage exists.
|
||||||
|
|
||||||
|
## Success Response Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"profile": {
|
||||||
|
"_id": "665f00000000000000000001",
|
||||||
|
"coverImage": "",
|
||||||
|
"avatar": "",
|
||||||
|
"name": "Artist Name",
|
||||||
|
"stageName": "Stage Name",
|
||||||
|
"bio": "Short artist bio",
|
||||||
|
"isVerified": false
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"followersCount": 120,
|
||||||
|
"followingCount": 45,
|
||||||
|
"postsCount": 12,
|
||||||
|
"collaborationsCount": 2,
|
||||||
|
"viewCount": 3000,
|
||||||
|
"playCount": 980,
|
||||||
|
"listenCount": 980,
|
||||||
|
"likesCount": 220,
|
||||||
|
"commentsCount": 40,
|
||||||
|
"savesCount": 18,
|
||||||
|
"sharesCount": 11,
|
||||||
|
"engagementRate": 7.26,
|
||||||
|
"scorePercentage": 0,
|
||||||
|
"earnings": 0
|
||||||
|
},
|
||||||
|
"audience": {
|
||||||
|
"followers": 120,
|
||||||
|
"nonFollowers": 0,
|
||||||
|
"newFollowersThisWeek": 4,
|
||||||
|
"newFollowersThisMonth": 19
|
||||||
|
},
|
||||||
|
"engagementChart": [],
|
||||||
|
"topContent": [
|
||||||
|
{
|
||||||
|
"_id": "665f00000000000000000002",
|
||||||
|
"postType": "audio",
|
||||||
|
"thumbnailUrl": "",
|
||||||
|
"displayUrl": "",
|
||||||
|
"content": "New track",
|
||||||
|
"viewCount": 1000,
|
||||||
|
"playCount": 600,
|
||||||
|
"likesCount": 90,
|
||||||
|
"commentsCount": 10,
|
||||||
|
"savesCount": 8,
|
||||||
|
"engagementScore": 113,
|
||||||
|
"createdAt": "2026-06-08T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recentActivity": [],
|
||||||
|
"meta": {
|
||||||
|
"generatedAt": "2026-06-08T00:00:00.000Z",
|
||||||
|
"chartAvailable": false,
|
||||||
|
"earningsAvailable": false,
|
||||||
|
"audienceAnalyticsAvailable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flutter Model Suggestion
|
||||||
|
|
||||||
|
- `ArtistDashboard`
|
||||||
|
- `ArtistDashboardProfile`
|
||||||
|
- `ArtistDashboardStats`
|
||||||
|
- `ArtistDashboardAudience`
|
||||||
|
- `ArtistDashboardTopContent`
|
||||||
|
- `ArtistDashboardRecentActivity`
|
||||||
|
- `ArtistDashboardMeta`
|
||||||
|
|
||||||
|
## UI Sections
|
||||||
|
|
||||||
|
1. Profile header: use `profile.coverImage`, `profile.avatar`, `profile.name`, `profile.stageName`, `profile.bio`, `profile.isVerified`.
|
||||||
|
2. Stats cards: use `stats.followersCount`, `stats.followingCount`, `stats.viewCount`, `stats.listenCount`, `stats.engagementRate`.
|
||||||
|
3. Engagement chart: render only when `meta.chartAvailable === true`; otherwise show the empty state.
|
||||||
|
4. Top content list: use `topContent`.
|
||||||
|
5. Audience summary: use `audience.followers`, `audience.nonFollowers`, `audience.newFollowersThisWeek`, `audience.newFollowersThisMonth`.
|
||||||
|
6. Recent activity: use `recentActivity`.
|
||||||
|
7. Empty states: show clear UI for no posts, no analytics, chart unavailable, and earnings unavailable.
|
||||||
|
|
||||||
|
## Empty States
|
||||||
|
|
||||||
|
- No posts yet: `stats.postsCount === 0`.
|
||||||
|
- No analytics yet: `stats.viewCount === 0 && stats.playCount === 0`.
|
||||||
|
- Chart not available: `meta.chartAvailable === false`.
|
||||||
|
- Earnings not available: `meta.earningsAvailable === false`.
|
||||||
|
|
||||||
|
## Future Analytics Upgrade
|
||||||
|
|
||||||
|
Daily chart data requires event-level or snapshot storage. Recommended future collections:
|
||||||
|
|
||||||
|
- `post_view_events`
|
||||||
|
- `post_play_events`
|
||||||
|
- `post_engagement_events`
|
||||||
|
- `daily_artist_analytics`
|
||||||
|
|
||||||
|
Audience split requires viewer identity tracking per view/play event. Earnings requires a monetization/revenue module. Until those exist, the backend intentionally returns safe defaults instead of fake values.
|
||||||
@@ -5599,6 +5599,382 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Realtime Socket.IO",
|
||||||
|
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Connection - Chat Namespace",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Connection - Notifications Namespace",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit join_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit leave_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit send_message",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit typing",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit mark_delivered",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit mark_seen",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen joined_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen left_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen new_message",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen message_delivered",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen message_seen",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen presence",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen socket_error",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen notification:new",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen notification:unread_counts",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Legacy Notification Events",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Feed",
|
"name": "Feed",
|
||||||
"item": [
|
"item": [
|
||||||
@@ -9782,6 +10158,22 @@
|
|||||||
{
|
{
|
||||||
"key": "notificationCategory",
|
"key": "notificationCategory",
|
||||||
"value": "interactions"
|
"value": "interactions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "socketBaseUrl",
|
||||||
|
"value": "http://localhost:4000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "chatSocketUrl",
|
||||||
|
"value": "{{socketBaseUrl}}/chat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "notificationsSocketUrl",
|
||||||
|
"value": "{{socketBaseUrl}}/notifications"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "socketAccessToken",
|
||||||
|
"value": "{{accessToken}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4684,6 +4684,382 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Realtime Socket.IO",
|
||||||
|
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Connection - Chat Namespace",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Connection - Notifications Namespace",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit join_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit leave_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit send_message",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit typing",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit mark_delivered",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit mark_seen",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen joined_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen left_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen new_message",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen message_delivered",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen message_seen",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen presence",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen socket_error",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen notification:new",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen notification:unread_counts",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Legacy Notification Events",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Support Tickets",
|
"name": "Support Tickets",
|
||||||
"item": [
|
"item": [
|
||||||
@@ -5003,6 +5379,382 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Realtime Socket.IO",
|
||||||
|
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Connection - Chat Namespace",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Connection - Notifications Namespace",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit join_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit leave_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit send_message",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit typing",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit mark_delivered",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit mark_seen",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen joined_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen left_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen new_message",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen message_delivered",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen message_seen",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen presence",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen socket_error",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen notification:new",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen notification:unread_counts",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Legacy Notification Events",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"variable": [
|
"variable": [
|
||||||
@@ -5576,6 +6328,22 @@
|
|||||||
{
|
{
|
||||||
"key": "notificationCategory",
|
"key": "notificationCategory",
|
||||||
"value": "interactions"
|
"value": "interactions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "socketBaseUrl",
|
||||||
|
"value": "http://localhost:4000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "chatSocketUrl",
|
||||||
|
"value": "{{socketBaseUrl}}/chat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "notificationsSocketUrl",
|
||||||
|
"value": "{{socketBaseUrl}}/notifications"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "socketAccessToken",
|
||||||
|
"value": "{{accessToken}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5025,6 +5025,382 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Realtime Socket.IO",
|
||||||
|
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Connection - Chat Namespace",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Connection - Notifications Namespace",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit join_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit leave_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit send_message",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit typing",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit mark_delivered",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emit mark_seen",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen joined_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen left_conversation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen new_message",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen message_delivered",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen message_seen",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen presence",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen socket_error",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{chatSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen notification:new",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Listen notification:unread_counts",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Legacy Notification Events",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Socket.IO",
|
||||||
|
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Auth",
|
||||||
|
"value": "auth: { token: '{{socketAccessToken}}' }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "{{notificationsSocketUrl}}",
|
||||||
|
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Feed",
|
"name": "Feed",
|
||||||
"item": [
|
"item": [
|
||||||
@@ -6998,6 +7374,22 @@
|
|||||||
{
|
{
|
||||||
"key": "notificationCategory",
|
"key": "notificationCategory",
|
||||||
"value": "interactions"
|
"value": "interactions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "socketBaseUrl",
|
||||||
|
"value": "http://localhost:4000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "chatSocketUrl",
|
||||||
|
"value": "{{socketBaseUrl}}/chat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "notificationsSocketUrl",
|
||||||
|
"value": "{{socketBaseUrl}}/notifications"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "socketAccessToken",
|
||||||
|
"value": "{{accessToken}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/modules/users/dto/artist-dashboard-response.dto.ts
Normal file
76
src/modules/users/dto/artist-dashboard-response.dto.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
export type ArtistDashboardProfile = {
|
||||||
|
_id: string;
|
||||||
|
coverImage: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
stageName: string;
|
||||||
|
bio: string;
|
||||||
|
isVerified: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArtistDashboardStats = {
|
||||||
|
followersCount: number;
|
||||||
|
followingCount: number;
|
||||||
|
postsCount: number;
|
||||||
|
collaborationsCount: number;
|
||||||
|
viewCount: number;
|
||||||
|
playCount: number;
|
||||||
|
listenCount: number;
|
||||||
|
likesCount: number;
|
||||||
|
commentsCount: number;
|
||||||
|
savesCount: number;
|
||||||
|
sharesCount: number;
|
||||||
|
engagementRate: number;
|
||||||
|
scorePercentage: number;
|
||||||
|
earnings: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArtistDashboardAudience = {
|
||||||
|
followers: number;
|
||||||
|
nonFollowers: number;
|
||||||
|
newFollowersThisWeek: number;
|
||||||
|
newFollowersThisMonth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArtistDashboardTopContentItem = {
|
||||||
|
_id: string;
|
||||||
|
postType: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
displayUrl: string;
|
||||||
|
content: string;
|
||||||
|
viewCount: number;
|
||||||
|
playCount: number;
|
||||||
|
likesCount: number;
|
||||||
|
commentsCount: number;
|
||||||
|
savesCount: number;
|
||||||
|
engagementScore: number;
|
||||||
|
createdAt: string | Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArtistDashboardRecentActivityItem = {
|
||||||
|
_id: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
previewText: string;
|
||||||
|
resourceType: string;
|
||||||
|
referenceId: string | null;
|
||||||
|
deepLink: string;
|
||||||
|
createdAt: string | Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArtistDashboardMeta = {
|
||||||
|
generatedAt: string;
|
||||||
|
chartAvailable: boolean;
|
||||||
|
earningsAvailable: boolean;
|
||||||
|
audienceAnalyticsAvailable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArtistDashboardResponse = {
|
||||||
|
profile: ArtistDashboardProfile;
|
||||||
|
stats: ArtistDashboardStats;
|
||||||
|
audience: ArtistDashboardAudience;
|
||||||
|
engagementChart: unknown[];
|
||||||
|
topContent: ArtistDashboardTopContentItem[];
|
||||||
|
recentActivity: ArtistDashboardRecentActivityItem[];
|
||||||
|
meta: ArtistDashboardMeta;
|
||||||
|
};
|
||||||
15
src/modules/users/users.controller.spec.ts
Normal file
15
src/modules/users/users.controller.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { UsersController } from './users.controller';
|
||||||
|
|
||||||
|
describe('UsersController artist dashboard', () => {
|
||||||
|
it('uses the authenticated user id for the private dashboard endpoint', async () => {
|
||||||
|
const usersService = {
|
||||||
|
getMyDashboard: jest.fn().mockResolvedValue({ profile: { _id: 'user-1' } }),
|
||||||
|
};
|
||||||
|
const controller = new UsersController(usersService as any);
|
||||||
|
|
||||||
|
const result = await controller.getMyDashboard({ sub: 'user-1' } as any);
|
||||||
|
|
||||||
|
expect(usersService.getMyDashboard).toHaveBeenCalledWith('user-1');
|
||||||
|
expect(result).toEqual({ profile: { _id: 'user-1' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -281,6 +281,13 @@ export class UsersController {
|
|||||||
return this.usersService.discoverTalents(query);
|
return this.usersService.discoverTalents(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('me/dashboard')
|
||||||
|
async getMyDashboard(@CurrentUser() user: JwtPayload) {
|
||||||
|
return this.usersService.getMyDashboard(user.sub);
|
||||||
|
}
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get(':id/profile-overview')
|
@Get(':id/profile-overview')
|
||||||
|
|||||||
185
src/modules/users/users.service.spec.ts
Normal file
185
src/modules/users/users.service.spec.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
|
const chain = <T>(value: T) => ({
|
||||||
|
sort: jest.fn().mockReturnThis(),
|
||||||
|
limit: jest.fn().mockReturnThis(),
|
||||||
|
project: jest.fn().mockReturnThis(),
|
||||||
|
toArray: jest.fn().mockResolvedValue(value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createService = (options: {
|
||||||
|
user?: Record<string, any> | null;
|
||||||
|
postStats?: Record<string, number>;
|
||||||
|
topContent?: Record<string, any>[];
|
||||||
|
collaborationsCount?: number;
|
||||||
|
newFollowersThisWeek?: number;
|
||||||
|
newFollowersThisMonth?: number;
|
||||||
|
recentActivity?: Record<string, any>[];
|
||||||
|
} = {}) => {
|
||||||
|
const userId = new Types.ObjectId().toString();
|
||||||
|
const user = options.user ?? {
|
||||||
|
_id: new Types.ObjectId(userId),
|
||||||
|
name: 'Artist',
|
||||||
|
stageName: 'Stage',
|
||||||
|
bio: 'Bio',
|
||||||
|
avatar: '',
|
||||||
|
coverImage: '',
|
||||||
|
followersCount: 10,
|
||||||
|
followingCount: 4,
|
||||||
|
isVerified: true,
|
||||||
|
isDisabled: false,
|
||||||
|
toObject() {
|
||||||
|
return {
|
||||||
|
_id: userId,
|
||||||
|
name: this.name,
|
||||||
|
stageName: this.stageName,
|
||||||
|
bio: this.bio,
|
||||||
|
avatar: this.avatar,
|
||||||
|
coverImage: this.coverImage,
|
||||||
|
followersCount: this.followersCount,
|
||||||
|
followingCount: this.followingCount,
|
||||||
|
isVerified: this.isVerified,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const postsAggregate = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
toArray: jest.fn().mockResolvedValue(options.postStats ? [options.postStats] : []),
|
||||||
|
})
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
toArray: jest.fn().mockResolvedValue(options.topContent ?? []),
|
||||||
|
});
|
||||||
|
|
||||||
|
const postsCollection = {
|
||||||
|
aggregate: postsAggregate,
|
||||||
|
countDocuments: jest.fn().mockResolvedValue(options.collaborationsCount ?? 0),
|
||||||
|
};
|
||||||
|
const followsCollection = {
|
||||||
|
countDocuments: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(options.newFollowersThisWeek ?? 0)
|
||||||
|
.mockResolvedValueOnce(options.newFollowersThisMonth ?? 0),
|
||||||
|
};
|
||||||
|
const notificationsCollection = {
|
||||||
|
find: jest.fn().mockReturnValue(chain(options.recentActivity ?? [])),
|
||||||
|
};
|
||||||
|
const connection = {
|
||||||
|
collection: jest.fn((name: string) => {
|
||||||
|
if (name === 'posts') {
|
||||||
|
return postsCollection;
|
||||||
|
}
|
||||||
|
if (name === 'follows') {
|
||||||
|
return followsCollection;
|
||||||
|
}
|
||||||
|
if (name === 'notifications') {
|
||||||
|
return notificationsCollection;
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected collection ${name}`);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const usersRepository = {
|
||||||
|
findById: jest.fn().mockResolvedValue(user),
|
||||||
|
};
|
||||||
|
const service = new UsersService(
|
||||||
|
connection as any,
|
||||||
|
usersRepository as any,
|
||||||
|
{ logSuperAdminAction: jest.fn() } as any,
|
||||||
|
{ get: jest.fn() } as any,
|
||||||
|
{ saveFile: jest.fn(), deleteFile: jest.fn() } as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { service, userId, usersRepository, postsCollection, followsCollection };
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('UsersService artist dashboard', () => {
|
||||||
|
it('returns zeros and safe empty arrays for an authenticated user with no posts', async () => {
|
||||||
|
const { service, userId } = createService();
|
||||||
|
|
||||||
|
const result = await service.getMyDashboard(userId);
|
||||||
|
|
||||||
|
expect(result.profile._id).toBe(userId);
|
||||||
|
expect(result.stats.postsCount).toBe(0);
|
||||||
|
expect(result.stats.viewCount).toBe(0);
|
||||||
|
expect(result.stats.playCount).toBe(0);
|
||||||
|
expect(result.stats.listenCount).toBe(0);
|
||||||
|
expect(result.stats.engagementRate).toBe(0);
|
||||||
|
expect(result.stats.earnings).toBe(0);
|
||||||
|
expect(result.stats.scorePercentage).toBe(0);
|
||||||
|
expect(result.audience.followers).toBe(10);
|
||||||
|
expect(result.audience.nonFollowers).toBe(0);
|
||||||
|
expect(result.engagementChart).toEqual([]);
|
||||||
|
expect(result.topContent).toEqual([]);
|
||||||
|
expect(result.meta.chartAvailable).toBe(false);
|
||||||
|
expect(result.meta.earningsAvailable).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aggregates owned posts and calculates engagement rate safely', async () => {
|
||||||
|
const postId = new Types.ObjectId();
|
||||||
|
const { service, userId } = createService({
|
||||||
|
postStats: {
|
||||||
|
postsCount: 2,
|
||||||
|
viewCount: 100,
|
||||||
|
playCount: 50,
|
||||||
|
likesCount: 12,
|
||||||
|
commentsCount: 3,
|
||||||
|
savesCount: 2,
|
||||||
|
sharesCount: 1,
|
||||||
|
},
|
||||||
|
topContent: [
|
||||||
|
{
|
||||||
|
_id: postId,
|
||||||
|
postType: 'audio',
|
||||||
|
content: 'Track',
|
||||||
|
thumbnailUrl: '',
|
||||||
|
audioUrl: '/uploads/posts/audios/audio.mp3',
|
||||||
|
viewCount: 100,
|
||||||
|
playCount: 50,
|
||||||
|
likesCount: 12,
|
||||||
|
commentsCount: 3,
|
||||||
|
savesCount: 2,
|
||||||
|
engagementScore: 18,
|
||||||
|
createdAt: new Date('2026-06-08T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
collaborationsCount: 2,
|
||||||
|
newFollowersThisWeek: 1,
|
||||||
|
newFollowersThisMonth: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.getMyDashboard(userId);
|
||||||
|
|
||||||
|
expect(result.stats.postsCount).toBe(2);
|
||||||
|
expect(result.stats.viewCount).toBe(100);
|
||||||
|
expect(result.stats.playCount).toBe(50);
|
||||||
|
expect(result.stats.listenCount).toBe(50);
|
||||||
|
expect(result.stats.likesCount).toBe(12);
|
||||||
|
expect(result.stats.commentsCount).toBe(3);
|
||||||
|
expect(result.stats.savesCount).toBe(2);
|
||||||
|
expect(result.stats.sharesCount).toBe(1);
|
||||||
|
expect(result.stats.collaborationsCount).toBe(2);
|
||||||
|
expect(result.stats.engagementRate).toBe(12);
|
||||||
|
expect(result.audience.newFollowersThisWeek).toBe(1);
|
||||||
|
expect(result.audience.newFollowersThisMonth).toBe(3);
|
||||||
|
expect(result.topContent).toHaveLength(1);
|
||||||
|
expect(result.topContent[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: postId.toString(),
|
||||||
|
postType: 'audio',
|
||||||
|
content: 'Track',
|
||||||
|
engagementScore: 18,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid current user ids', async () => {
|
||||||
|
const { service } = createService();
|
||||||
|
|
||||||
|
await expect(service.getMyDashboard('not-object-id')).rejects.toBeInstanceOf(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,12 +13,18 @@ import { ExperienceLevel } from '../../common/enums/experience-level.enum';
|
|||||||
import { MusicRole } from '../../common/enums/music-role.enum';
|
import { MusicRole } from '../../common/enums/music-role.enum';
|
||||||
import { hashValue } from '../../common/utils/hash.util';
|
import { hashValue } from '../../common/utils/hash.util';
|
||||||
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
|
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
|
||||||
|
import { resolveManagedFileUrl } from '../../common/utils/public-url.util';
|
||||||
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
|
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
|
||||||
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
|
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
|
||||||
import { AuditService } from '../audit/audit.service';
|
import { AuditService } from '../audit/audit.service';
|
||||||
import { UserRole } from '../../common/enums/user-role.enum';
|
import { UserRole } from '../../common/enums/user-role.enum';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
import { AdminDisableUserDto } from './dto/admin-disable-user.dto';
|
import { AdminDisableUserDto } from './dto/admin-disable-user.dto';
|
||||||
|
import {
|
||||||
|
ArtistDashboardRecentActivityItem,
|
||||||
|
ArtistDashboardResponse,
|
||||||
|
ArtistDashboardTopContentItem,
|
||||||
|
} from './dto/artist-dashboard-response.dto';
|
||||||
import { CreateAdminDto } from './dto/create-admin.dto';
|
import { CreateAdminDto } from './dto/create-admin.dto';
|
||||||
import { MusicSetupDto } from './dto/music-setup.dto';
|
import { MusicSetupDto } from './dto/music-setup.dto';
|
||||||
import { ProfileSetupDto } from './dto/profile-setup.dto';
|
import { ProfileSetupDto } from './dto/profile-setup.dto';
|
||||||
@@ -36,6 +42,16 @@ type UploadedImageFile = {
|
|||||||
originalname?: string;
|
originalname?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UserPostStats = {
|
||||||
|
postsCount: number;
|
||||||
|
viewCount: number;
|
||||||
|
playCount: number;
|
||||||
|
likesCount: number;
|
||||||
|
commentsCount: number;
|
||||||
|
savesCount: number;
|
||||||
|
sharesCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -790,6 +806,94 @@ export class UsersService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMyDashboard(currentUserId: string): Promise<ArtistDashboardResponse> {
|
||||||
|
if (!Types.ObjectId.isValid(currentUserId)) {
|
||||||
|
throw new BadRequestException('Invalid user id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.findByIdOrFail(currentUserId);
|
||||||
|
if (user.isDisabled) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectUserId = new Types.ObjectId(currentUserId);
|
||||||
|
const now = new Date();
|
||||||
|
const weekAgo = new Date(now);
|
||||||
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||||
|
const monthAgo = new Date(now);
|
||||||
|
monthAgo.setDate(monthAgo.getDate() - 30);
|
||||||
|
|
||||||
|
const [postStats, collaborationsCount, topContent, recentActivity, newFollowersThisWeek, newFollowersThisMonth] =
|
||||||
|
await Promise.all([
|
||||||
|
this.aggregateUserPostStats(objectUserId),
|
||||||
|
this.countUserCollaborations(objectUserId),
|
||||||
|
this.findTopDashboardContent(objectUserId, 5),
|
||||||
|
this.findArtistRecentActivity(objectUserId, 10),
|
||||||
|
this.connection.collection('follows').countDocuments({
|
||||||
|
followingId: objectUserId,
|
||||||
|
createdAt: { $gte: weekAgo },
|
||||||
|
}),
|
||||||
|
this.connection.collection('follows').countDocuments({
|
||||||
|
followingId: objectUserId,
|
||||||
|
createdAt: { $gte: monthAgo },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const followersCount = this.toSafeNumber(user.followersCount);
|
||||||
|
const followingCount = this.toSafeNumber(user.followingCount);
|
||||||
|
const totalEngagement =
|
||||||
|
postStats.likesCount + postStats.commentsCount + postStats.savesCount + postStats.sharesCount;
|
||||||
|
const totalReach = postStats.viewCount + postStats.playCount;
|
||||||
|
const engagementRate = this.roundPercent((totalEngagement / Math.max(totalReach, 1)) * 100);
|
||||||
|
const userObject = typeof user.toObject === 'function' ? (user.toObject() as Record<string, any>) : (user as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile: {
|
||||||
|
_id: currentUserId,
|
||||||
|
coverImage: userObject.coverImage ?? '',
|
||||||
|
avatar: userObject.avatar ?? '',
|
||||||
|
name: userObject.name ?? '',
|
||||||
|
stageName: userObject.stageName ?? '',
|
||||||
|
bio: userObject.bio ?? '',
|
||||||
|
isVerified: userObject.isVerified ?? false,
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
followersCount,
|
||||||
|
followingCount,
|
||||||
|
postsCount: postStats.postsCount,
|
||||||
|
collaborationsCount,
|
||||||
|
viewCount: postStats.viewCount,
|
||||||
|
playCount: postStats.playCount,
|
||||||
|
listenCount: postStats.playCount,
|
||||||
|
likesCount: postStats.likesCount,
|
||||||
|
commentsCount: postStats.commentsCount,
|
||||||
|
savesCount: postStats.savesCount,
|
||||||
|
sharesCount: postStats.sharesCount,
|
||||||
|
engagementRate,
|
||||||
|
// TODO: Replace with real artist score logic when product scoring rules exist.
|
||||||
|
scorePercentage: 0,
|
||||||
|
// TODO: Replace with real monetization calculation when earnings module exists.
|
||||||
|
earnings: 0,
|
||||||
|
},
|
||||||
|
audience: {
|
||||||
|
followers: followersCount,
|
||||||
|
// TODO: Requires viewer-level analytics/events to calculate non-followers.
|
||||||
|
nonFollowers: 0,
|
||||||
|
newFollowersThisWeek,
|
||||||
|
newFollowersThisMonth,
|
||||||
|
},
|
||||||
|
engagementChart: [],
|
||||||
|
topContent,
|
||||||
|
recentActivity,
|
||||||
|
meta: {
|
||||||
|
generatedAt: now.toISOString(),
|
||||||
|
chartAvailable: false,
|
||||||
|
earningsAvailable: false,
|
||||||
|
audienceAnalyticsAvailable: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async searchUsersForSuperAdmin(query: UserQueryDto): Promise<{
|
async searchUsersForSuperAdmin(query: UserQueryDto): Promise<{
|
||||||
items: UserDocument[];
|
items: UserDocument[];
|
||||||
page: number;
|
page: number;
|
||||||
@@ -819,6 +923,193 @@ export class UsersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async aggregateUserPostStats(authorId: Types.ObjectId): Promise<UserPostStats> {
|
||||||
|
const [row] = await this.connection
|
||||||
|
.collection('posts')
|
||||||
|
.aggregate<UserPostStats>([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
authorId,
|
||||||
|
isDeleted: { $ne: true },
|
||||||
|
isArchived: { $ne: true },
|
||||||
|
moderationStatus: { $ne: ModerationStatus.HIDDEN },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
postsCount: { $sum: 1 },
|
||||||
|
viewCount: { $sum: { $ifNull: ['$viewCount', 0] } },
|
||||||
|
playCount: { $sum: { $ifNull: ['$playCount', 0] } },
|
||||||
|
likesCount: { $sum: { $ifNull: ['$likesCount', 0] } },
|
||||||
|
commentsCount: { $sum: { $ifNull: ['$commentsCount', 0] } },
|
||||||
|
savesCount: { $sum: { $ifNull: ['$savesCount', 0] } },
|
||||||
|
sharesCount: { $sum: { $ifNull: ['$shareCount', 0] } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $project: { _id: 0 } },
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return {
|
||||||
|
postsCount: this.toSafeNumber(row?.postsCount),
|
||||||
|
viewCount: this.toSafeNumber(row?.viewCount),
|
||||||
|
playCount: this.toSafeNumber(row?.playCount),
|
||||||
|
likesCount: this.toSafeNumber(row?.likesCount),
|
||||||
|
commentsCount: this.toSafeNumber(row?.commentsCount),
|
||||||
|
savesCount: this.toSafeNumber(row?.savesCount),
|
||||||
|
sharesCount: this.toSafeNumber(row?.sharesCount),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async countUserCollaborations(userId: Types.ObjectId): Promise<number> {
|
||||||
|
return this.connection.collection('posts').countDocuments({
|
||||||
|
isDeleted: { $ne: true },
|
||||||
|
isArchived: { $ne: true },
|
||||||
|
moderationStatus: { $ne: ModerationStatus.HIDDEN },
|
||||||
|
$or: [
|
||||||
|
{ authorId: userId, 'taggedUserIds.0': { $exists: true } },
|
||||||
|
{ authorId: { $ne: userId }, taggedUserIds: userId },
|
||||||
|
{ collaboratorIds: userId },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findTopDashboardContent(
|
||||||
|
authorId: Types.ObjectId,
|
||||||
|
limit: number,
|
||||||
|
): Promise<ArtistDashboardTopContentItem[]> {
|
||||||
|
const rows = await this.connection
|
||||||
|
.collection('posts')
|
||||||
|
.aggregate<Record<string, any>>([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
authorId,
|
||||||
|
isDeleted: { $ne: true },
|
||||||
|
isArchived: { $ne: true },
|
||||||
|
moderationStatus: { $ne: ModerationStatus.HIDDEN },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addFields: {
|
||||||
|
engagementScore: {
|
||||||
|
$add: [
|
||||||
|
{ $ifNull: ['$likesCount', 0] },
|
||||||
|
{ $ifNull: ['$commentsCount', 0] },
|
||||||
|
{ $ifNull: ['$savesCount', 0] },
|
||||||
|
{ $ifNull: ['$shareCount', 0] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { engagementScore: -1, viewCount: -1, playCount: -1, createdAt: -1 } },
|
||||||
|
{ $limit: Math.max(1, limit) },
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
postType: 1,
|
||||||
|
thumbnailUrl: 1,
|
||||||
|
imageUrls: 1,
|
||||||
|
imageItems: 1,
|
||||||
|
videoUrl: 1,
|
||||||
|
audioUrl: 1,
|
||||||
|
content: 1,
|
||||||
|
viewCount: 1,
|
||||||
|
playCount: 1,
|
||||||
|
likesCount: 1,
|
||||||
|
commentsCount: 1,
|
||||||
|
savesCount: 1,
|
||||||
|
engagementScore: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return rows.map((post) => {
|
||||||
|
const thumbnailUrl = this.resolveManagedStringUrl(post.thumbnailUrl);
|
||||||
|
const fallbackDisplayUrl = this.resolveTopContentDisplayUrl(post);
|
||||||
|
return {
|
||||||
|
_id: post._id?.toString?.() ?? '',
|
||||||
|
postType: post.postType ?? '',
|
||||||
|
thumbnailUrl,
|
||||||
|
displayUrl: thumbnailUrl || fallbackDisplayUrl,
|
||||||
|
content: post.content ?? '',
|
||||||
|
viewCount: this.toSafeNumber(post.viewCount),
|
||||||
|
playCount: this.toSafeNumber(post.playCount),
|
||||||
|
likesCount: this.toSafeNumber(post.likesCount),
|
||||||
|
commentsCount: this.toSafeNumber(post.commentsCount),
|
||||||
|
savesCount: this.toSafeNumber(post.savesCount),
|
||||||
|
engagementScore: this.toSafeNumber(post.engagementScore),
|
||||||
|
createdAt: post.createdAt ?? '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findArtistRecentActivity(
|
||||||
|
userId: Types.ObjectId,
|
||||||
|
limit: number,
|
||||||
|
): Promise<ArtistDashboardRecentActivityItem[]> {
|
||||||
|
const rows = await this.connection
|
||||||
|
.collection('notifications')
|
||||||
|
.find({
|
||||||
|
$or: [{ recipientId: userId }, { actorId: userId }],
|
||||||
|
})
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(Math.max(1, limit))
|
||||||
|
.project({
|
||||||
|
type: 1,
|
||||||
|
title: 1,
|
||||||
|
previewText: 1,
|
||||||
|
resourceType: 1,
|
||||||
|
referenceId: 1,
|
||||||
|
deepLink: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return rows.map((activity) => ({
|
||||||
|
_id: activity._id?.toString?.() ?? '',
|
||||||
|
type: activity.type ?? '',
|
||||||
|
title: activity.title ?? '',
|
||||||
|
previewText: activity.previewText ?? '',
|
||||||
|
resourceType: activity.resourceType ?? '',
|
||||||
|
referenceId: activity.referenceId?.toString?.() ?? null,
|
||||||
|
deepLink: activity.deepLink ?? '',
|
||||||
|
createdAt: activity.createdAt ?? '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveTopContentDisplayUrl(post: Record<string, any>): string {
|
||||||
|
const imageItems = Array.isArray(post.imageItems) ? post.imageItems : [];
|
||||||
|
const imageUrls = Array.isArray(post.imageUrls) ? post.imageUrls : [];
|
||||||
|
const candidate =
|
||||||
|
imageItems.find((item) => typeof item?.url === 'string' && item.url)?.url ??
|
||||||
|
imageUrls.find((url) => typeof url === 'string' && url) ??
|
||||||
|
post.videoUrl ??
|
||||||
|
post.audioUrl ??
|
||||||
|
'';
|
||||||
|
|
||||||
|
return this.resolveManagedStringUrl(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveManagedStringUrl(value: unknown): string {
|
||||||
|
const resolved = resolveManagedFileUrl(typeof value === 'string' ? value : '');
|
||||||
|
return typeof resolved === 'string' ? resolved : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private toSafeNumber(value: unknown): number {
|
||||||
|
const numeric = Number(value ?? 0);
|
||||||
|
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private roundPercent(value: number): number {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(value * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
private async assertTargetIsAdmin(userId: string): Promise<UserDocument> {
|
private async assertTargetIsAdmin(userId: string): Promise<UserDocument> {
|
||||||
const user = await this.findByIdOrFail(userId);
|
const user = await this.findByIdOrFail(userId);
|
||||||
if (user.role !== UserRole.ADMIN) {
|
if (user.role !== UserRole.ADMIN) {
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم