db9cf7b315
Original task (#38): When admins rename a group or add/remove members, post a system message into the chat thread so other members see who changed what and when, with bilingual (AR/EN) text and real-time delivery. Changes - lib/db/src/schema/conversations.ts: added `kind` (varchar default "user") and `meta` (jsonb) columns on the `messages` table. Pushed the new columns directly via ALTER TABLE since drizzle-kit push prompted on unrelated rename ambiguities. Also healed pre-existing schema drift on `users.clock_hour12`, `conversations.avatar_url`, and `conversation_participants.is_muted/is_archived` so the API could start. - lib/api-spec/openapi.yaml: extended MessageWithSender with `kind` (enum: user | group_renamed | members_added | member_removed) and optional `meta`. Re-ran codegen. - artifacts/api-server/src/routes/conversations.ts: added insertAndEmitSystemMessage + small user-display helpers; PATCH conversation now emits a `group_renamed` system message when a name actually changes; add-participants emits `members_added` with the actor + added users; remove-participant emits `member_removed` with actor + removed user. Each system message is broadcast over Socket.IO via the existing `new_message` channel so all current members receive it immediately. - artifacts/teaboy-os/src/pages/chat.tsx: render messages with `kind != "user"` as centered, muted pill bubbles (no avatar / sender label) using a new renderSystemMessage helper that picks the language-appropriate name out of meta. Conversation list preview also uses it so the last activity reads sensibly when the most recent message is a system message. - artifacts/teaboy-os/src/locales/{en,ar}.json: added chat.system.* strings (groupRenamed, membersAdded with plural variants, memberRemoved, someone, listSeparator). Verification - Typecheck (libs + artifacts) passes. - e2e via testing skill: registered fresh users, created a group, renamed it, added a member, removed a member; all three centered system messages appeared in the thread in order with the expected copy. Notes / deviations - Used the actor's userId as senderId for system messages (kept existing NOT NULL FK) instead of introducing a nullable sender, which keeps the migration lightweight. This means system messages count toward unread for non-actor members; flagged as a follow-up. Replit-Task-Id: 6dfa2b99-fbac-4146-b59b-8c04a14c9e96
1906 lines
44 KiB
YAML
1906 lines
44 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
# Do not change the title, if the title changes, the import paths will be broken
|
|
title: Api
|
|
version: 0.1.0
|
|
description: TeaBoy OS API specification
|
|
servers:
|
|
- url: /api
|
|
description: Base API path
|
|
tags:
|
|
- name: health
|
|
description: Health operations
|
|
- name: auth
|
|
description: Authentication
|
|
- name: apps
|
|
description: App management
|
|
- name: services
|
|
description: Internal services (khidmati)
|
|
- name: conversations
|
|
description: Chat conversations
|
|
- name: messages
|
|
description: Chat messages
|
|
- name: notifications
|
|
description: Notifications
|
|
- name: users
|
|
description: User management
|
|
- name: stats
|
|
description: Dashboard stats
|
|
- name: storage
|
|
description: Object storage upload and serving endpoints
|
|
|
|
paths:
|
|
/healthz:
|
|
get:
|
|
operationId: healthCheck
|
|
tags: [health]
|
|
summary: Health check
|
|
responses:
|
|
"200":
|
|
description: Healthy
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/HealthStatus"
|
|
|
|
# Auth
|
|
/auth/register:
|
|
post:
|
|
operationId: register
|
|
tags: [auth]
|
|
summary: Register a new user
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RegisterBody"
|
|
responses:
|
|
"201":
|
|
description: Registered
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/login:
|
|
post:
|
|
operationId: login
|
|
tags: [auth]
|
|
summary: Login
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LoginBody"
|
|
responses:
|
|
"200":
|
|
description: Logged in
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"401":
|
|
description: Unauthorized
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/logout:
|
|
post:
|
|
operationId: logout
|
|
tags: [auth]
|
|
summary: Logout
|
|
responses:
|
|
"200":
|
|
description: Logged out
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
|
|
/auth/forgot-password:
|
|
post:
|
|
operationId: forgotPassword
|
|
tags: [auth]
|
|
summary: Request a password reset link
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForgotPasswordBody"
|
|
responses:
|
|
"200":
|
|
description: Reset request accepted
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForgotPasswordResponse"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/reset-password:
|
|
post:
|
|
operationId: resetPassword
|
|
tags: [auth]
|
|
summary: Set a new password using a reset token
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ResetPasswordBody"
|
|
responses:
|
|
"200":
|
|
description: Password updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
"400":
|
|
description: Invalid or expired token
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/reset-password/verify:
|
|
post:
|
|
operationId: verifyResetToken
|
|
tags: [auth]
|
|
summary: Check whether a reset token is valid
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/VerifyResetTokenBody"
|
|
responses:
|
|
"200":
|
|
description: Token check result
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/VerifyResetTokenResponse"
|
|
|
|
/auth/admin/users/{id}/issue-reset-link:
|
|
post:
|
|
operationId: adminIssueResetLink
|
|
tags: [auth]
|
|
summary: Admin generates a one-time password reset link for a user
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Reset link issued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AdminResetLinkResponse"
|
|
"403":
|
|
description: Forbidden
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
"404":
|
|
description: User not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/me:
|
|
get:
|
|
operationId: getMe
|
|
tags: [auth]
|
|
summary: Get current user
|
|
responses:
|
|
"200":
|
|
description: Current user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"401":
|
|
description: Unauthorized
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/me/language:
|
|
patch:
|
|
operationId: updateLanguage
|
|
tags: [auth]
|
|
summary: Update preferred language
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateLanguageBody"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
|
|
/auth/me/clock-style:
|
|
patch:
|
|
operationId: updateClockStyle
|
|
tags: [auth]
|
|
summary: Update preferred home-screen clock style
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateClockStyleBody"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/me/clock-hour12:
|
|
patch:
|
|
operationId: updateClockHour12
|
|
tags: [auth]
|
|
summary: Update preferred 12/24-hour clock format
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateClockHour12Body"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
# Apps
|
|
/apps:
|
|
get:
|
|
operationId: listApps
|
|
tags: [apps]
|
|
summary: List all active apps
|
|
responses:
|
|
"200":
|
|
description: Apps list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/App"
|
|
|
|
post:
|
|
operationId: createApp
|
|
tags: [apps]
|
|
summary: Create a new app (admin)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateAppBody"
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/App"
|
|
|
|
/apps/{id}:
|
|
get:
|
|
operationId: getApp
|
|
tags: [apps]
|
|
summary: Get app by ID
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: App
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/App"
|
|
|
|
patch:
|
|
operationId: updateApp
|
|
tags: [apps]
|
|
summary: Update app (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateAppBody"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/App"
|
|
|
|
delete:
|
|
operationId: deleteApp
|
|
tags: [apps]
|
|
summary: Delete app (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"204":
|
|
description: Deleted
|
|
|
|
/apps/{id}/open:
|
|
post:
|
|
operationId: logAppOpen
|
|
tags: [apps]
|
|
summary: Log an app open event for the current user
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"204":
|
|
description: Logged
|
|
"404":
|
|
description: App not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/me/app-order:
|
|
put:
|
|
operationId: updateMyAppOrder
|
|
tags: [apps]
|
|
summary: Set the current user's preferred home apps order
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateMyAppOrderBody"
|
|
responses:
|
|
"200":
|
|
description: Updated apps in new order
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/App"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
# Settings
|
|
/settings:
|
|
get:
|
|
operationId: getAppSettings
|
|
tags: [settings]
|
|
summary: Get application settings (public)
|
|
responses:
|
|
"200":
|
|
description: Current settings
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppSettings"
|
|
patch:
|
|
operationId: updateAppSettings
|
|
tags: [settings]
|
|
summary: Update application settings (admin)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateAppSettingsBody"
|
|
responses:
|
|
"200":
|
|
description: Updated settings
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppSettings"
|
|
"400":
|
|
description: Invalid input
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
# Storage
|
|
/storage/uploads/request-url:
|
|
post:
|
|
operationId: requestUploadUrl
|
|
tags: [storage]
|
|
summary: Request a presigned URL for file upload
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RequestUploadUrlBody"
|
|
responses:
|
|
"200":
|
|
description: Presigned upload URL generated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RequestUploadUrlResponse"
|
|
"400":
|
|
description: Invalid input
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
# Services
|
|
/services:
|
|
get:
|
|
operationId: listServices
|
|
tags: [services]
|
|
summary: List all services
|
|
responses:
|
|
"200":
|
|
description: Services list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Service"
|
|
|
|
post:
|
|
operationId: createService
|
|
tags: [services]
|
|
summary: Create a service (admin)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateServiceBody"
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Service"
|
|
|
|
/services/{id}:
|
|
get:
|
|
operationId: getService
|
|
tags: [services]
|
|
summary: Get service by ID
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Service
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Service"
|
|
|
|
patch:
|
|
operationId: updateService
|
|
tags: [services]
|
|
summary: Update service (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateServiceBody"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Service"
|
|
|
|
delete:
|
|
operationId: deleteService
|
|
tags: [services]
|
|
summary: Delete service (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"204":
|
|
description: Deleted
|
|
|
|
/service-categories:
|
|
get:
|
|
operationId: listServiceCategories
|
|
tags: [services]
|
|
summary: List service categories
|
|
responses:
|
|
"200":
|
|
description: Categories
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ServiceCategory"
|
|
|
|
# Conversations
|
|
/conversations:
|
|
get:
|
|
operationId: listConversations
|
|
tags: [conversations]
|
|
summary: List conversations for current user
|
|
responses:
|
|
"200":
|
|
description: Conversations
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ConversationWithDetails"
|
|
|
|
post:
|
|
operationId: createConversation
|
|
tags: [conversations]
|
|
summary: Create a conversation
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateConversationBody"
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ConversationWithDetails"
|
|
|
|
/conversations/{id}:
|
|
get:
|
|
operationId: getConversation
|
|
tags: [conversations]
|
|
summary: Get conversation by ID
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Conversation
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ConversationWithDetails"
|
|
|
|
patch:
|
|
operationId: updateConversation
|
|
tags: [conversations]
|
|
summary: Update conversation properties (admin only)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateConversationBody"
|
|
responses:
|
|
"200":
|
|
description: Updated conversation
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ConversationWithDetails"
|
|
|
|
/conversations/{id}/participants:
|
|
post:
|
|
operationId: addConversationParticipants
|
|
tags: [conversations]
|
|
summary: Add participants to a group conversation (admin only)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AddParticipantsBody"
|
|
responses:
|
|
"200":
|
|
description: Updated conversation
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ConversationWithDetails"
|
|
|
|
/conversations/{id}/participants/{userId}:
|
|
delete:
|
|
operationId: removeConversationParticipant
|
|
tags: [conversations]
|
|
summary: Remove a participant from a group conversation (admin only)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
- name: userId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Updated conversation
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ConversationWithDetails"
|
|
|
|
/conversations/{id}/state:
|
|
patch:
|
|
operationId: updateConversationState
|
|
tags: [conversations]
|
|
summary: Update per-user state (mute, archive)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateConversationStateBody"
|
|
responses:
|
|
"200":
|
|
description: Updated conversation
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ConversationWithDetails"
|
|
|
|
/conversations/{id}/leave:
|
|
post:
|
|
operationId: leaveConversation
|
|
tags: [conversations]
|
|
summary: Leave a group conversation
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Left conversation
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
|
|
/conversations/{id}/messages:
|
|
get:
|
|
operationId: listMessages
|
|
tags: [messages]
|
|
summary: List messages in a conversation
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Messages
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/MessageWithSender"
|
|
|
|
post:
|
|
operationId: sendMessage
|
|
tags: [messages]
|
|
summary: Send a message
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SendMessageBody"
|
|
responses:
|
|
"201":
|
|
description: Message sent
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/MessageWithSender"
|
|
|
|
/conversations/{id}/read:
|
|
post:
|
|
operationId: markConversationRead
|
|
tags: [messages]
|
|
summary: Mark all messages in conversation as read
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Marked as read
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
|
|
# Notifications
|
|
/notifications:
|
|
get:
|
|
operationId: listNotifications
|
|
tags: [notifications]
|
|
summary: List notifications for current user
|
|
responses:
|
|
"200":
|
|
description: Notifications
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Notification"
|
|
|
|
/notifications/{id}/read:
|
|
patch:
|
|
operationId: markNotificationRead
|
|
tags: [notifications]
|
|
summary: Mark notification as read
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Marked as read
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Notification"
|
|
|
|
/notifications/read-all:
|
|
post:
|
|
operationId: markAllNotificationsRead
|
|
tags: [notifications]
|
|
summary: Mark all notifications as read
|
|
responses:
|
|
"200":
|
|
description: Done
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
|
|
# Users (admin)
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
tags: [users]
|
|
summary: List all users (admin)
|
|
responses:
|
|
"200":
|
|
description: Users
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/UserProfile"
|
|
|
|
post:
|
|
operationId: createUser
|
|
tags: [users]
|
|
summary: Create a user (admin)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RegisterBody"
|
|
responses:
|
|
"201":
|
|
description: Created user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UserProfile"
|
|
"409":
|
|
description: Username already taken
|
|
|
|
/users/{id}:
|
|
get:
|
|
operationId: getUser
|
|
tags: [users]
|
|
summary: Get user by ID (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: User
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UserProfile"
|
|
|
|
patch:
|
|
operationId: updateUser
|
|
tags: [users]
|
|
summary: Update user (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateUserBody"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UserProfile"
|
|
|
|
delete:
|
|
operationId: deleteUser
|
|
tags: [users]
|
|
summary: Delete user (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"204":
|
|
description: Deleted
|
|
|
|
# Stats
|
|
/stats/home:
|
|
get:
|
|
operationId: getHomeStats
|
|
tags: [stats]
|
|
summary: Get home screen stats
|
|
responses:
|
|
"200":
|
|
description: Stats
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/HomeStats"
|
|
|
|
/stats/admin:
|
|
get:
|
|
operationId: getAdminStats
|
|
tags: [stats]
|
|
summary: Get admin dashboard trend stats
|
|
parameters:
|
|
- in: query
|
|
name: range
|
|
required: false
|
|
schema:
|
|
type: string
|
|
enum: ["7d", "30d", "90d", "custom"]
|
|
default: "7d"
|
|
description: Time range for trend stats. Use "custom" with from/to.
|
|
- in: query
|
|
name: from
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
description: Start date (inclusive, YYYY-MM-DD UTC) when range=custom
|
|
- in: query
|
|
name: to
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
description: End date (inclusive, YYYY-MM-DD UTC) when range=custom
|
|
responses:
|
|
"200":
|
|
description: Admin stats
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AdminStats"
|
|
"400":
|
|
description: Invalid range parameters (e.g. range=custom with missing/malformed from/to or inverted dates)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
components:
|
|
schemas:
|
|
HealthStatus:
|
|
type: object
|
|
properties:
|
|
status:
|
|
type: string
|
|
required:
|
|
- status
|
|
|
|
ErrorResponse:
|
|
type: object
|
|
properties:
|
|
error:
|
|
type: string
|
|
required:
|
|
- error
|
|
|
|
SuccessResponse:
|
|
type: object
|
|
properties:
|
|
success:
|
|
type: boolean
|
|
required:
|
|
- success
|
|
|
|
RegisterBody:
|
|
type: object
|
|
properties:
|
|
username:
|
|
type: string
|
|
email:
|
|
type: string
|
|
password:
|
|
type: string
|
|
displayNameAr:
|
|
type: ["string", "null"]
|
|
displayNameEn:
|
|
type: ["string", "null"]
|
|
preferredLanguage:
|
|
type: string
|
|
default: "ar"
|
|
required:
|
|
- username
|
|
- email
|
|
- password
|
|
|
|
LoginBody:
|
|
type: object
|
|
properties:
|
|
username:
|
|
type: string
|
|
password:
|
|
type: string
|
|
required:
|
|
- username
|
|
- password
|
|
|
|
ForgotPasswordBody:
|
|
type: object
|
|
properties:
|
|
identifier:
|
|
type: string
|
|
description: Username or email
|
|
required:
|
|
- identifier
|
|
|
|
ForgotPasswordResponse:
|
|
type: object
|
|
properties:
|
|
success:
|
|
type: boolean
|
|
required:
|
|
- success
|
|
|
|
ResetPasswordBody:
|
|
type: object
|
|
properties:
|
|
token:
|
|
type: string
|
|
newPassword:
|
|
type: string
|
|
minLength: 6
|
|
required:
|
|
- token
|
|
- newPassword
|
|
|
|
VerifyResetTokenBody:
|
|
type: object
|
|
properties:
|
|
token:
|
|
type: string
|
|
required:
|
|
- token
|
|
|
|
AdminResetLinkResponse:
|
|
type: object
|
|
properties:
|
|
resetUrl:
|
|
type: string
|
|
expiresAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- resetUrl
|
|
- expiresAt
|
|
|
|
VerifyResetTokenResponse:
|
|
type: object
|
|
properties:
|
|
valid:
|
|
type: boolean
|
|
required:
|
|
- valid
|
|
|
|
UpdateLanguageBody:
|
|
type: object
|
|
properties:
|
|
language:
|
|
type: string
|
|
required:
|
|
- language
|
|
|
|
ClockStyle:
|
|
type: string
|
|
enum: [full, digital, digital-no-seconds, analog, minimal]
|
|
description: Per-user home-screen clock style preference
|
|
|
|
UpdateClockStyleBody:
|
|
type: object
|
|
properties:
|
|
clockStyle:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/ClockStyle"
|
|
- type: "null"
|
|
description: Clock style; pass null to clear and use the default
|
|
required:
|
|
- clockStyle
|
|
|
|
UpdateClockHour12Body:
|
|
type: object
|
|
properties:
|
|
clockHour12:
|
|
type: ["boolean", "null"]
|
|
description: Whether to use 12-hour (AM/PM) clock format; null clears and uses the default (24-hour)
|
|
required:
|
|
- clockHour12
|
|
|
|
AuthUser:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
username:
|
|
type: string
|
|
email:
|
|
type: string
|
|
displayNameAr:
|
|
type: ["string", "null"]
|
|
displayNameEn:
|
|
type: ["string", "null"]
|
|
preferredLanguage:
|
|
type: string
|
|
clockStyle:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/ClockStyle"
|
|
- type: "null"
|
|
clockHour12:
|
|
type: ["boolean", "null"]
|
|
avatarUrl:
|
|
type: ["string", "null"]
|
|
isActive:
|
|
type: boolean
|
|
roles:
|
|
type: array
|
|
items:
|
|
type: string
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- id
|
|
- username
|
|
- email
|
|
- preferredLanguage
|
|
- isActive
|
|
- roles
|
|
- createdAt
|
|
|
|
UserProfile:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
username:
|
|
type: string
|
|
email:
|
|
type: string
|
|
displayNameAr:
|
|
type: ["string", "null"]
|
|
displayNameEn:
|
|
type: ["string", "null"]
|
|
preferredLanguage:
|
|
type: string
|
|
clockStyle:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/ClockStyle"
|
|
- type: "null"
|
|
clockHour12:
|
|
type: ["boolean", "null"]
|
|
avatarUrl:
|
|
type: ["string", "null"]
|
|
isActive:
|
|
type: boolean
|
|
roles:
|
|
type: array
|
|
items:
|
|
type: string
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- id
|
|
- username
|
|
- email
|
|
- preferredLanguage
|
|
- isActive
|
|
- roles
|
|
- createdAt
|
|
|
|
UpdateUserBody:
|
|
type: object
|
|
properties:
|
|
displayNameAr:
|
|
type: ["string", "null"]
|
|
displayNameEn:
|
|
type: ["string", "null"]
|
|
isActive:
|
|
type: boolean
|
|
preferredLanguage:
|
|
type: string
|
|
|
|
App:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
slug:
|
|
type: string
|
|
nameAr:
|
|
type: string
|
|
nameEn:
|
|
type: string
|
|
descriptionAr:
|
|
type: ["string", "null"]
|
|
descriptionEn:
|
|
type: ["string", "null"]
|
|
iconName:
|
|
type: string
|
|
route:
|
|
type: string
|
|
color:
|
|
type: string
|
|
isActive:
|
|
type: boolean
|
|
isSystem:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
updatedAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- id
|
|
- slug
|
|
- nameAr
|
|
- nameEn
|
|
- iconName
|
|
- route
|
|
- color
|
|
- isActive
|
|
- isSystem
|
|
- sortOrder
|
|
- createdAt
|
|
- updatedAt
|
|
|
|
CreateAppBody:
|
|
type: object
|
|
properties:
|
|
slug:
|
|
type: string
|
|
nameAr:
|
|
type: string
|
|
nameEn:
|
|
type: string
|
|
descriptionAr:
|
|
type: ["string", "null"]
|
|
descriptionEn:
|
|
type: ["string", "null"]
|
|
iconName:
|
|
type: string
|
|
route:
|
|
type: string
|
|
color:
|
|
type: string
|
|
isActive:
|
|
type: boolean
|
|
isSystem:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
required:
|
|
- slug
|
|
- nameAr
|
|
- nameEn
|
|
- iconName
|
|
- route
|
|
- color
|
|
|
|
UpdateAppBody:
|
|
type: object
|
|
properties:
|
|
nameAr:
|
|
type: string
|
|
nameEn:
|
|
type: string
|
|
descriptionAr:
|
|
type: ["string", "null"]
|
|
descriptionEn:
|
|
type: ["string", "null"]
|
|
iconName:
|
|
type: string
|
|
route:
|
|
type: string
|
|
color:
|
|
type: string
|
|
isActive:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
|
|
Service:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
categoryId:
|
|
type: ["integer", "null"]
|
|
nameAr:
|
|
type: string
|
|
nameEn:
|
|
type: string
|
|
descriptionAr:
|
|
type: ["string", "null"]
|
|
descriptionEn:
|
|
type: ["string", "null"]
|
|
imageUrl:
|
|
type: ["string", "null"]
|
|
price:
|
|
type: ["string", "null"]
|
|
isAvailable:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
updatedAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- id
|
|
- nameAr
|
|
- nameEn
|
|
- isAvailable
|
|
- sortOrder
|
|
- createdAt
|
|
- updatedAt
|
|
|
|
CreateServiceBody:
|
|
type: object
|
|
properties:
|
|
categoryId:
|
|
type: ["integer", "null"]
|
|
nameAr:
|
|
type: string
|
|
nameEn:
|
|
type: string
|
|
descriptionAr:
|
|
type: ["string", "null"]
|
|
descriptionEn:
|
|
type: ["string", "null"]
|
|
imageUrl:
|
|
type: ["string", "null"]
|
|
price:
|
|
type: ["string", "null"]
|
|
isAvailable:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
required:
|
|
- nameAr
|
|
- nameEn
|
|
|
|
UpdateServiceBody:
|
|
type: object
|
|
properties:
|
|
categoryId:
|
|
type: ["integer", "null"]
|
|
nameAr:
|
|
type: string
|
|
nameEn:
|
|
type: string
|
|
descriptionAr:
|
|
type: ["string", "null"]
|
|
descriptionEn:
|
|
type: ["string", "null"]
|
|
imageUrl:
|
|
type: ["string", "null"]
|
|
price:
|
|
type: ["string", "null"]
|
|
isAvailable:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
|
|
ServiceCategory:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
nameAr:
|
|
type: string
|
|
nameEn:
|
|
type: string
|
|
sortOrder:
|
|
type: integer
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- id
|
|
- nameAr
|
|
- nameEn
|
|
- sortOrder
|
|
- createdAt
|
|
|
|
ConversationWithDetails:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
nameAr:
|
|
type: ["string", "null"]
|
|
nameEn:
|
|
type: ["string", "null"]
|
|
avatarUrl:
|
|
type: ["string", "null"]
|
|
isGroup:
|
|
type: boolean
|
|
createdBy:
|
|
type: integer
|
|
participants:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ParticipantInfo"
|
|
lastMessage:
|
|
$ref: "#/components/schemas/MessageWithSender"
|
|
nullable: true
|
|
unreadCount:
|
|
type: integer
|
|
isMuted:
|
|
type: boolean
|
|
isArchived:
|
|
type: boolean
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
updatedAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- id
|
|
- isGroup
|
|
- createdBy
|
|
- participants
|
|
- unreadCount
|
|
- isMuted
|
|
- isArchived
|
|
- createdAt
|
|
- updatedAt
|
|
|
|
ParticipantInfo:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
username:
|
|
type: string
|
|
displayNameAr:
|
|
type: ["string", "null"]
|
|
displayNameEn:
|
|
type: ["string", "null"]
|
|
avatarUrl:
|
|
type: ["string", "null"]
|
|
isAdmin:
|
|
type: boolean
|
|
required:
|
|
- id
|
|
- username
|
|
- isAdmin
|
|
|
|
CreateConversationBody:
|
|
type: object
|
|
properties:
|
|
participantIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
nameAr:
|
|
type: ["string", "null"]
|
|
nameEn:
|
|
type: ["string", "null"]
|
|
avatarUrl:
|
|
type: ["string", "null"]
|
|
isGroup:
|
|
type: boolean
|
|
required:
|
|
- participantIds
|
|
|
|
UpdateConversationBody:
|
|
type: object
|
|
properties:
|
|
avatarUrl:
|
|
type: ["string", "null"]
|
|
nameAr:
|
|
type: ["string", "null"]
|
|
nameEn:
|
|
type: ["string", "null"]
|
|
|
|
UpdateConversationStateBody:
|
|
type: object
|
|
properties:
|
|
isMuted:
|
|
type: boolean
|
|
isArchived:
|
|
type: boolean
|
|
|
|
AddParticipantsBody:
|
|
type: object
|
|
properties:
|
|
userIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
required:
|
|
- userIds
|
|
|
|
MessageWithSender:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
conversationId:
|
|
type: integer
|
|
senderId:
|
|
type: integer
|
|
content:
|
|
type: string
|
|
kind:
|
|
type: string
|
|
enum: [user, group_renamed, members_added, member_removed]
|
|
meta:
|
|
type: object
|
|
nullable: true
|
|
additionalProperties: true
|
|
sender:
|
|
$ref: "#/components/schemas/ParticipantInfo"
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
updatedAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- id
|
|
- conversationId
|
|
- senderId
|
|
- content
|
|
- kind
|
|
- sender
|
|
- createdAt
|
|
- updatedAt
|
|
|
|
SendMessageBody:
|
|
type: object
|
|
properties:
|
|
content:
|
|
type: string
|
|
required:
|
|
- content
|
|
|
|
Notification:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
userId:
|
|
type: integer
|
|
titleAr:
|
|
type: string
|
|
titleEn:
|
|
type: string
|
|
bodyAr:
|
|
type: ["string", "null"]
|
|
bodyEn:
|
|
type: ["string", "null"]
|
|
type:
|
|
type: string
|
|
isRead:
|
|
type: boolean
|
|
relatedId:
|
|
type: ["integer", "null"]
|
|
relatedType:
|
|
type: ["string", "null"]
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
required:
|
|
- id
|
|
- userId
|
|
- titleAr
|
|
- titleEn
|
|
- type
|
|
- isRead
|
|
- createdAt
|
|
|
|
AppSettings:
|
|
type: object
|
|
required: [siteNameAr, siteNameEn, registrationOpen, footerTextAr, footerTextEn]
|
|
properties:
|
|
siteNameAr:
|
|
type: string
|
|
siteNameEn:
|
|
type: string
|
|
registrationOpen:
|
|
type: boolean
|
|
footerTextAr:
|
|
type: string
|
|
footerTextEn:
|
|
type: string
|
|
updatedAt:
|
|
type: string
|
|
format: date-time
|
|
|
|
UpdateMyAppOrderBody:
|
|
type: object
|
|
required: [order]
|
|
properties:
|
|
order:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: App IDs in the desired display order
|
|
|
|
UpdateAppSettingsBody:
|
|
type: object
|
|
properties:
|
|
siteNameAr:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
siteNameEn:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
registrationOpen:
|
|
type: boolean
|
|
footerTextAr:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 300
|
|
footerTextEn:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 300
|
|
|
|
RequestUploadUrlBody:
|
|
type: object
|
|
required: [name, size, contentType]
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
size:
|
|
type: integer
|
|
minimum: 1
|
|
contentType:
|
|
type: string
|
|
minLength: 1
|
|
|
|
RequestUploadUrlResponse:
|
|
type: object
|
|
required: [uploadURL, objectPath]
|
|
properties:
|
|
uploadURL:
|
|
type: string
|
|
format: uri
|
|
objectPath:
|
|
type: string
|
|
metadata:
|
|
$ref: "#/components/schemas/RequestUploadUrlBody"
|
|
|
|
HomeStats:
|
|
type: object
|
|
properties:
|
|
totalApps:
|
|
type: integer
|
|
totalServices:
|
|
type: integer
|
|
unreadNotifications:
|
|
type: integer
|
|
unreadMessages:
|
|
type: integer
|
|
totalUsers:
|
|
type: integer
|
|
required:
|
|
- totalApps
|
|
- totalServices
|
|
- unreadNotifications
|
|
- unreadMessages
|
|
- totalUsers
|
|
|
|
AdminStats:
|
|
type: object
|
|
properties:
|
|
range:
|
|
type: string
|
|
enum: ["7d", "30d", "90d", "custom"]
|
|
rangeDays:
|
|
type: integer
|
|
rangeFrom:
|
|
type: string
|
|
description: Start date (YYYY-MM-DD UTC) of the selected window
|
|
rangeTo:
|
|
type: string
|
|
description: End date (YYYY-MM-DD UTC) of the selected window
|
|
newUsersInRange:
|
|
type: integer
|
|
newUsersPrevRange:
|
|
type: integer
|
|
activeServices:
|
|
type: integer
|
|
inactiveServices:
|
|
type: integer
|
|
signupsByDay:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
date:
|
|
type: string
|
|
count:
|
|
type: integer
|
|
required:
|
|
- date
|
|
- count
|
|
appOpensByDay:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
date:
|
|
type: string
|
|
count:
|
|
type: integer
|
|
required:
|
|
- date
|
|
- count
|
|
appOpensInRange:
|
|
type: integer
|
|
appOpensPrevRange:
|
|
type: integer
|
|
servicesCreatedByDay:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
date:
|
|
type: string
|
|
count:
|
|
type: integer
|
|
required:
|
|
- date
|
|
- count
|
|
servicesCreatedInRange:
|
|
type: integer
|
|
topApps:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/TopAppItem"
|
|
mostActiveUsers:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/TopUserItem"
|
|
required:
|
|
- range
|
|
- rangeDays
|
|
- newUsersInRange
|
|
- newUsersPrevRange
|
|
- activeServices
|
|
- inactiveServices
|
|
- signupsByDay
|
|
- appOpensByDay
|
|
- appOpensInRange
|
|
- appOpensPrevRange
|
|
- servicesCreatedByDay
|
|
- servicesCreatedInRange
|
|
- topApps
|
|
- mostActiveUsers
|
|
|
|
TopAppItem:
|
|
type: object
|
|
properties:
|
|
appId:
|
|
type: integer
|
|
slug:
|
|
type: string
|
|
nameAr:
|
|
type: string
|
|
nameEn:
|
|
type: string
|
|
iconName:
|
|
type: string
|
|
color:
|
|
type: string
|
|
count:
|
|
type: integer
|
|
required:
|
|
- appId
|
|
- slug
|
|
- nameAr
|
|
- nameEn
|
|
- iconName
|
|
- color
|
|
- count
|
|
|
|
TopUserItem:
|
|
type: object
|
|
properties:
|
|
userId:
|
|
type: integer
|
|
username:
|
|
type: string
|
|
displayNameAr:
|
|
type: ["string", "null"]
|
|
displayNameEn:
|
|
type: ["string", "null"]
|
|
avatarUrl:
|
|
type: ["string", "null"]
|
|
count:
|
|
type: integer
|
|
required:
|
|
- userId
|
|
- username
|
|
- count
|