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" # 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 description (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" "404": description: Not found 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" # 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: 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" 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 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: descriptionAr: type: ["string", "null"] descriptionEn: type: ["string", "null"] RoleDeletionConflict: type: object properties: error: type: string userCount: type: integer groupCount: type: integer required: - error - userCount - groupCount 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 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 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