Files
TX/lib/api-spec/openapi.yaml
T
riyadhafraa 59c7338d63 Task #98: Warn admins before renaming a role used in code references
- Added GET /api/roles/:id/usage endpoint returning {userCount, groupCount},
  guarded by requireAdmin and validating the role id.
- Updated PATCH /api/roles/:id audit metadata to record renamed=true,
  renamedFrom, and renamedTo when the role name changes (in addition to
  existing name fields), satisfying the traceability requirement.
- Added RoleUsage schema + /roles/{id}/usage operation to openapi.yaml,
  regenerated lib/api-zod via orval. Codegen script in lib/api-spec was
  updated intentionally to preserve `export * from './manual'` in
  api-zod's index, which orval was overwriting.
- RolesPanel edit dialog (admin.tsx) now tracks the original role name
  on open and shows an amber warning banner with from/to text plus a
  usage line (users + groups counts) whenever the trimmed name input
  differs from the original. Warning is suppressed for system or
  protected roles since their name input is disabled.
- Added en/ar locale strings: renameWarningTitle, renameWarningBody,
  renameUsage.
- Reverted accidental opengraph.jpg change picked up earlier in the
  session — it is unrelated to this task.

Verification: e2e test created a non-system role, assigned the admin
user to it, opened the edit dialog, observed the warning + usage line,
saved the rename, and confirmed both the role name persisted and the
audit log captured renamed/renamedFrom/renamedTo.

Note: The pnpm `test` workflow shows pre-existing failures in
executive-meetings.ts that are unrelated to this task (verified via
git stash before changes).

Replit-Task-Id: c1ee048d-50c6-4439-a430-ee0b7ec09959
2026-04-29 10:57:44 +00:00

3534 lines
86 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: Tx 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: orders
description: Service orders workflow
- name: conversations
description: Chat conversations
- name: messages
description: Chat messages
- name: notifications
description: Notifications
- name: users
description: User management
- name: roles
description: Role assignment
- name: stats
description: Dashboard stats
- name: storage
description: Object storage upload and serving endpoints
- name: audit
description: Admin audit log
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
- name: force
in: query
required: false
description: Force deletion of an app that has dependent records (records an audit log entry).
schema:
type: boolean
default: false
responses:
"204":
description: Deleted
"409":
description: App has dependent records; pass force=true to confirm deletion.
content:
application/json:
schema:
$ref: "#/components/schemas/AppDeletionConflict"
/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
- name: force
in: query
required: false
description: Force deletion of a service that has existing orders (records an audit log entry).
schema:
type: boolean
default: false
responses:
"204":
description: Deleted
"409":
description: Service has dependent records; pass force=true to confirm deletion.
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceDeletionConflict"
/orders:
post:
operationId: createServiceOrder
tags: [orders]
summary: Place a service order
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateServiceOrderBody"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceOrder"
"400":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/orders/my:
get:
operationId: listMyServiceOrders
tags: [orders]
summary: List orders placed by the current user
responses:
"200":
description: My orders
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ServiceOrder"
/orders/incoming:
get:
operationId: listIncomingServiceOrders
tags: [orders]
summary: List active orders for receivers (requires orders.receive permission). Returns pending+unassigned orders plus orders assigned to me that are not completed/cancelled.
responses:
"200":
description: Incoming orders
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ServiceOrder"
"403":
description: Forbidden
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/orders/{id}/status:
patch:
operationId: updateServiceOrderStatus
tags: [orders]
summary: Update order status. Preparing/completed restricted to assigned receiver or admin; cancelled allowed for owner (while pending|received) or admin (any time); pending/received are restore-from-cancelled transitions allowed for owner or admin.
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateServiceOrderStatusBody"
responses:
"200":
description: Updated order
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceOrder"
"400":
description: Invalid transition or validation error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"403":
description: Forbidden — caller is not the assignee, owner, or admin per the transition rules
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Order not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/orders/{id}/confirm-receipt:
patch:
operationId: confirmServiceOrderReceipt
tags: [orders]
summary: Receiver atomically claims a pending order (sets status=received, assigned_to=current user). Requires orders.receive permission. Returns 409 already_claimed if the order is no longer pending+unassigned.
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Order received
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceOrder"
"409":
description: Invalid transition
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/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)
parameters:
- in: query
name: q
required: false
schema:
type: string
description: Free-text search across username, email, and display names
- in: query
name: groupId
required: false
schema:
type: integer
description: Filter to users that belong to the given group
- in: query
name: status
required: false
schema:
type: string
enum: ["active", "disabled"]
description: Filter by active/disabled state
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/CreateUserBody"
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
- name: force
in: query
required: false
description: Force deletion of a user that owns dependent records (records an audit log entry).
schema:
type: boolean
default: false
responses:
"204":
description: Deleted
"409":
description: User has dependent records; pass force=true to confirm deletion.
content:
application/json:
schema:
$ref: "#/components/schemas/UserDeletionConflict"
/orders/{id}:
delete:
operationId: deleteServiceOrder
tags: [orders]
summary: Delete a service order
description: |
Permanently deletes the order. Allowed when the caller owns the order
AND it is in a terminal status (completed/cancelled), OR the caller is
an admin (any status).
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"204":
description: Deleted
"403":
description: Forbidden
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Order not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/users/{id}/roles:
post:
operationId: addUserRole
tags: [roles]
summary: Idempotently add a role to a user (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AddUserRoleBody"
responses:
"200":
description: Role added (or already present)
content:
application/json:
schema:
$ref: "#/components/schemas/UserProfile"
"400":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: User or role not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/users/{id}/roles/{roleName}:
delete:
operationId: removeUserRole
tags: [roles]
summary: Idempotently remove a role from a user (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: roleName
in: path
required: true
schema:
type: string
responses:
"200":
description: Role removed (or already absent)
content:
application/json:
schema:
$ref: "#/components/schemas/UserProfile"
"404":
description: User not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# Permissions (admin)
/permissions:
get:
operationId: listPermissions
tags: [roles]
summary: List all available permissions (admin)
responses:
"200":
description: Permissions
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Permission"
"403":
description: Forbidden
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# Roles (admin)
/roles:
get:
operationId: listRoles
tags: [users]
summary: List roles (admin)
responses:
"200":
description: Roles
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Role"
post:
operationId: createRole
tags: [users]
summary: Create role (admin)
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateRoleBody"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Role"
"400":
description: Invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: Name already taken
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/roles/{id}:
patch:
operationId: updateRole
tags: [users]
summary: Update role name and descriptions (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateRoleBody"
responses:
"200":
description: Updated
content:
application/json:
schema:
$ref: "#/components/schemas/Role"
"400":
description: Invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: Name already taken
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
delete:
operationId: deleteRole
tags: [users]
summary: Delete role (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"204":
description: Deleted
"400":
description: Cannot delete a system role
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: Role is in use
content:
application/json:
schema:
$ref: "#/components/schemas/RoleDeletionConflict"
/roles/{id}/usage:
get:
operationId: getRoleUsage
tags: [roles]
summary: Count users and groups currently assigned to a role (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Counts of users and groups using the role
content:
application/json:
schema:
$ref: "#/components/schemas/RoleUsage"
"404":
description: Role not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/roles/{id}/permissions:
get:
operationId: getRolePermissions
tags: [roles]
summary: List permissions assigned to a role (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Permissions assigned to the role
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Permission"
"404":
description: Role not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
put:
operationId: replaceRolePermissions
tags: [roles]
summary: Atomically replace the permissions assigned to a role (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ReplaceRolePermissionsBody"
responses:
"200":
description: Updated permissions list for the role
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Permission"
"400":
description: Cannot modify a system role's permissions, or invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Role or one of the permissions was not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# Groups (admin)
/groups:
get:
operationId: listGroups
tags: [users]
summary: List groups (admin)
parameters:
- in: query
name: q
required: false
schema:
type: string
responses:
"200":
description: Groups
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Group"
post:
operationId: createGroup
tags: [users]
summary: Create group (admin)
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateGroupBody"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Group"
/groups/{id}:
get:
operationId: getGroup
tags: [users]
summary: Get group with members and apps (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Group detail
content:
application/json:
schema:
$ref: "#/components/schemas/GroupDetail"
"404":
description: Not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
patch:
operationId: updateGroup
tags: [users]
summary: Update group (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateGroupBody"
responses:
"200":
description: Updated
content:
application/json:
schema:
$ref: "#/components/schemas/GroupDetail"
delete:
operationId: deleteGroup
tags: [users]
summary: Delete group (admin)
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: force
in: query
required: false
description: Force deletion of a non-empty group (records an audit log entry).
schema:
type: boolean
default: false
responses:
"204":
description: Deleted
"400":
description: Cannot delete a system group
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: Group is not empty; pass force=true to confirm deletion.
content:
application/json:
schema:
$ref: "#/components/schemas/GroupDeletionConflict"
# 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"
/admin/audit-logs:
get:
operationId: listAuditLogs
tags: [audit]
summary: List audit log entries (admin)
description: |
Returns recent entries from the audit log, newest first. Admin only.
Filter by action (exact match) and/or a date range (inclusive).
parameters:
- in: query
name: action
required: false
schema:
type: string
description: Exact action name to filter by (e.g. "group.force_delete").
- in: query
name: forcedOnly
required: false
schema:
type: boolean
default: false
description: |
When true, restrict results to forced deletions only — i.e. rows
with action `group.force_delete`, `user.force_delete`,
`app.force_delete`, `service.force_delete`, or any
`group.delete`/`user.delete`/`app.delete` row whose metadata
contains `force: true`. Overrides the `action` filter when set.
- in: query
name: from
required: false
schema:
type: string
format: date
description: Start date (inclusive, YYYY-MM-DD UTC).
- in: query
name: to
required: false
schema:
type: string
format: date
description: End date (inclusive, YYYY-MM-DD UTC).
- in: query
name: limit
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 50
- in: query
name: offset
required: false
schema:
type: integer
minimum: 0
default: 0
responses:
"200":
description: Page of audit log entries
content:
application/json:
schema:
$ref: "#/components/schemas/AuditLogList"
"400":
description: Invalid filter parameters
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/admin/audit-logs/export:
get:
operationId: exportAuditLogsCsv
tags: [audit]
summary: Export filtered audit log as CSV (admin)
description: |
Streams the filtered audit log as a CSV download.
Honors the same `action`, `forcedOnly`, `from`, and `to` filters as the
list endpoint.
Columns: action, actor_username, target_type, target_id, created_at, metadata.
Capped at 50,000 rows per export to bound response size.
parameters:
- in: query
name: action
required: false
schema:
type: string
description: Exact action name to filter by (e.g. "group.force_delete").
- in: query
name: forcedOnly
required: false
schema:
type: boolean
default: false
description: |
When true, restrict the export to forced deletions only.
See the list endpoint for the exact set of matching rows.
Overrides the `action` filter when set.
- in: query
name: from
required: false
schema:
type: string
format: date
description: Start date (inclusive, YYYY-MM-DD UTC).
- in: query
name: to
required: false
schema:
type: string
format: date
description: End date (inclusive, YYYY-MM-DD UTC).
responses:
"200":
description: CSV file download
content:
text/csv:
schema:
type: string
format: binary
"400":
description: Invalid filter parameters
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
CreateUserBody:
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"
groupIds:
type: array
description: Initial group memberships in addition to the auto-assigned Everyone group.
items:
type: integer
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
groups:
type: array
items:
$ref: "#/components/schemas/GroupSummary"
createdAt:
type: string
format: date-time
required:
- id
- username
- email
- preferredLanguage
- isActive
- roles
- groups
- 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
groups:
type: array
items:
$ref: "#/components/schemas/GroupSummary"
createdAt:
type: string
format: date-time
noteCount:
type: integer
description: |
Dependent record counts. Populated only by the admin list endpoint
(GET /users) so the delete dialog can warn before the first click.
Omitted from single-user responses (GET /users/:id, PATCH, etc).
orderCount:
type: integer
description: |
Number of service orders owned by this user. Populated only by the
admin list endpoint (GET /users).
conversationCount:
type: integer
description: |
Number of conversations created by this user. Populated only by the
admin list endpoint (GET /users).
messageCount:
type: integer
description: |
Number of messages authored by this user. Populated only by the
admin list endpoint (GET /users).
required:
- id
- username
- email
- preferredLanguage
- isActive
- roles
- groups
- createdAt
UpdateUserBody:
type: object
properties:
displayNameAr:
type: ["string", "null"]
displayNameEn:
type: ["string", "null"]
isActive:
type: boolean
preferredLanguage:
type: string
groupIds:
type: array
items:
type: integer
description: If provided, replaces the user's group memberships
AddUserRoleBody:
type: object
properties:
roleName:
type: string
minLength: 1
required:
- roleName
Role:
type: object
properties:
id:
type: integer
name:
type: string
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
isSystem:
type: boolean
createdAt:
type: string
format: date-time
required:
- id
- name
- isSystem
- createdAt
CreateRoleBody:
type: object
properties:
name:
type: string
minLength: 1
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
required:
- name
UpdateRoleBody:
type: object
properties:
name:
type: string
minLength: 1
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
RoleUsage:
type: object
properties:
userCount:
type: integer
groupCount:
type: integer
required:
- userCount
- groupCount
RoleDeletionConflict:
type: object
properties:
error:
type: string
userCount:
type: integer
groupCount:
type: integer
required:
- error
- userCount
- groupCount
Permission:
type: object
properties:
id:
type: integer
name:
type: string
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
required:
- id
- name
ReplaceRolePermissionsBody:
type: object
properties:
permissionIds:
type: array
items:
type: integer
description: Full set of permission IDs the role should grant. Existing assignments not present here are removed.
required:
- permissionIds
GroupSummary:
type: object
properties:
id:
type: integer
name:
type: string
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
required:
- id
- name
Group:
type: object
properties:
id:
type: integer
name:
type: string
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
isSystem:
type: boolean
memberCount:
type: integer
appCount:
type: integer
roleCount:
type: integer
createdAt:
type: string
format: date-time
required:
- id
- name
- isSystem
- memberCount
- appCount
- roleCount
- createdAt
GroupDeletionConflict:
type: object
properties:
error:
type: string
memberCount:
type: integer
appCount:
type: integer
roleCount:
type: integer
required:
- error
- memberCount
- appCount
- roleCount
UserDeletionConflict:
type: object
properties:
error:
type: string
noteCount:
type: integer
orderCount:
type: integer
conversationCount:
type: integer
messageCount:
type: integer
required:
- error
- noteCount
- orderCount
- conversationCount
- messageCount
AppDeletionConflict:
type: object
properties:
error:
type: string
groupCount:
type: integer
restrictionCount:
type: integer
openCount:
type: integer
required:
- error
- groupCount
- restrictionCount
- openCount
ServiceDeletionConflict:
type: object
properties:
error:
type: string
orderCount:
type: integer
required:
- error
- orderCount
GroupDetail:
type: object
properties:
id:
type: integer
name:
type: string
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
isSystem:
type: boolean
appIds:
type: array
items:
type: integer
roleIds:
type: array
items:
type: integer
userIds:
type: array
items:
type: integer
createdAt:
type: string
format: date-time
required:
- id
- name
- isSystem
- appIds
- roleIds
- userIds
- createdAt
CreateGroupBody:
type: object
properties:
name:
type: string
minLength: 1
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
appIds:
type: array
items:
type: integer
roleIds:
type: array
items:
type: integer
userIds:
type: array
items:
type: integer
required:
- name
UpdateGroupBody:
type: object
properties:
name:
type: string
minLength: 1
descriptionAr:
type: ["string", "null"]
descriptionEn:
type: ["string", "null"]
appIds:
type: array
items:
type: integer
roleIds:
type: array
items:
type: integer
userIds:
type: array
items:
type: integer
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
groupCount:
type: integer
description: |
Number of groups granting access to this app. Populated only by the
admin list endpoint (GET /admin/apps) so the delete dialog can warn
before the first click. Omitted from non-admin and single-app
responses.
restrictionCount:
type: integer
description: |
Number of legacy permission restrictions for this app. Populated
only by the admin list endpoint (GET /admin/apps).
openCount:
type: integer
description: |
Number of recorded app-open events for this app. Populated only by
the admin list endpoint (GET /admin/apps).
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
orderCount:
type: integer
description: |
Number of service_orders rows referencing this service. Populated
only by the list endpoint (GET /services) so the admin delete
dialog can warn before the first click. Omitted from single-service
responses.
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
ServiceOrderUser:
type: object
properties:
id:
type: integer
username:
type: string
displayNameAr:
type: ["string", "null"]
displayNameEn:
type: ["string", "null"]
avatarUrl:
type: ["string", "null"]
required:
- id
- username
ServiceOrderService:
type: object
properties:
id:
type: integer
nameAr:
type: string
nameEn:
type: string
imageUrl:
type: ["string", "null"]
required:
- id
- nameAr
- nameEn
ServiceOrderStatus:
type: string
enum: [pending, received, preparing, completed, cancelled]
ServiceOrder:
type: object
properties:
id:
type: integer
serviceId:
type: integer
userId:
type: integer
assignedTo:
type: ["integer", "null"]
notes:
type: ["string", "null"]
status:
$ref: "#/components/schemas/ServiceOrderStatus"
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
service:
$ref: "#/components/schemas/ServiceOrderService"
requester:
oneOf:
- $ref: "#/components/schemas/ServiceOrderUser"
- type: "null"
assignee:
oneOf:
- $ref: "#/components/schemas/ServiceOrderUser"
- type: "null"
required:
- id
- serviceId
- userId
- status
- createdAt
- updatedAt
- service
CreateServiceOrderBody:
type: object
properties:
serviceId:
type: integer
notes:
type: ["string", "null"]
maxLength: 500
required:
- serviceId
UpdateServiceOrderStatusBody:
type: object
properties:
status:
type: string
enum: [pending, received, preparing, completed, cancelled]
required:
- status
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
AuditLogActor:
type: object
properties:
id:
type: integer
username:
type: string
displayNameAr:
type: ["string", "null"]
displayNameEn:
type: ["string", "null"]
avatarUrl:
type: ["string", "null"]
required:
- id
- username
AuditLogEntry:
type: object
properties:
id:
type: integer
action:
type: string
targetType:
type: string
targetId:
type: ["integer", "null"]
metadata:
type: ["object", "null"]
additionalProperties: true
createdAt:
type: string
format: date-time
actor:
oneOf:
- $ref: "#/components/schemas/AuditLogActor"
- type: "null"
required:
- id
- action
- targetType
- targetId
- metadata
- createdAt
- actor
AuditLogList:
type: object
properties:
entries:
type: array
items:
$ref: "#/components/schemas/AuditLogEntry"
totalCount:
type: integer
limit:
type: integer
offset:
type: integer
nextOffset:
type: ["integer", "null"]
actions:
type: array
items:
type: string
description: All distinct action names present in the audit log (for building filter UI).
required:
- entries
- totalCount
- limit
- offset
- nextOffset
- actions