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 requestBody: required: false content: application/json: schema: type: object properties: successorId: type: integer description: | When the leaver is the only admin, optionally name a specific remaining member to promote. Omit (or send null) to keep the automatic behavior of promoting the longest-tenured member. nullable: true 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" /stats/admin/app-opens/by-app/{appId}: get: operationId: getAdminAppOpensByApp tags: [stats] summary: Recent opens of a single app within the selected range parameters: - in: path name: appId required: true schema: type: integer - in: query name: range required: false schema: type: string enum: ["7d", "30d", "90d", "custom"] default: "7d" - in: query name: from required: false schema: type: string format: date - in: query name: to required: false schema: type: string format: date - in: query name: limit required: false schema: type: integer minimum: 1 maximum: 200 default: 100 - in: query name: offset required: false schema: type: integer minimum: 0 default: 0 responses: "200": description: Recent opens for the app content: application/json: schema: $ref: "#/components/schemas/AdminAppOpensByApp" "400": description: Invalid range parameters content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "404": description: App not found content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" /stats/admin/app-opens/by-user/{userId}: get: operationId: getAdminAppOpensByUser tags: [stats] summary: Recent app opens by a single user within the selected range parameters: - in: path name: userId required: true schema: type: integer - in: query name: range required: false schema: type: string enum: ["7d", "30d", "90d", "custom"] default: "7d" - in: query name: from required: false schema: type: string format: date - in: query name: to required: false schema: type: string format: date - in: query name: limit required: false schema: type: integer minimum: 1 maximum: 200 default: 100 - in: query name: offset required: false schema: type: integer minimum: 0 default: 0 responses: "200": description: Recent app opens by the user content: application/json: schema: $ref: "#/components/schemas/AdminAppOpensByUser" "400": description: Invalid range parameters content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "404": description: User not found 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, member_left, admin_promoted] 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 AppOpenByAppEntry: type: object properties: id: type: integer createdAt: type: string format: date-time userId: type: integer username: type: string displayNameAr: type: ["string", "null"] displayNameEn: type: ["string", "null"] avatarUrl: type: ["string", "null"] required: - id - createdAt - userId - username AdminAppOpensByApp: type: object properties: appId: type: integer slug: type: string nameAr: type: string nameEn: type: string iconName: type: string color: type: string range: type: string enum: ["7d", "30d", "90d", "custom"] rangeDays: type: integer rangeFrom: type: string rangeTo: type: string totalCount: type: integer limit: type: integer offset: type: integer nextOffset: type: ["integer", "null"] opens: type: array items: $ref: "#/components/schemas/AppOpenByAppEntry" required: - appId - slug - nameAr - nameEn - iconName - color - range - rangeDays - rangeFrom - rangeTo - totalCount - limit - offset - nextOffset - opens AppOpenByUserEntry: type: object properties: id: type: integer createdAt: type: string format: date-time appId: type: integer slug: type: string nameAr: type: string nameEn: type: string iconName: type: string color: type: string required: - id - createdAt - appId - slug - nameAr - nameEn - iconName - color AdminAppOpensByUser: type: object properties: userId: type: integer username: type: string displayNameAr: type: ["string", "null"] displayNameEn: type: ["string", "null"] avatarUrl: type: ["string", "null"] range: type: string enum: ["7d", "30d", "90d", "custom"] rangeDays: type: integer rangeFrom: type: string rangeTo: type: string totalCount: type: integer limit: type: integer offset: type: integer nextOffset: type: ["integer", "null"] opens: type: array items: $ref: "#/components/schemas/AppOpenByUserEntry" required: - userId - username - range - rangeDays - rangeFrom - rangeTo - totalCount - limit - offset - nextOffset - opens 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