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/impact-preview: post: operationId: previewRolePermissionsImpact tags: [roles] summary: Preview the impact of changing a role's permission set (admin) description: | Given a candidate set of permission IDs for the role, returns the list of permissions that would be removed and, for each one, how many users would lose the permission (because they hold this role directly or via a group and have no other role granting the permission), plus the groups that currently grant this role. parameters: - name: id in: path required: true schema: type: integer requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RolePermissionsImpactBody" responses: "200": description: Impact preview for the proposed permission set content: application/json: schema: $ref: "#/components/schemas/RolePermissionsImpact" "400": description: Invalid request body content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "404": description: Role was not found. Unknown permission IDs in the request body are tolerated and simply do not appear in `removed`. 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 RolePermissionsImpactBody: type: object properties: permissionIds: type: array items: type: integer description: Candidate set of permission IDs to evaluate against the role's current assignments. required: - permissionIds RolePermissionImpactGroup: type: object properties: id: type: integer name: type: string required: - id - name RolePermissionImpactItem: type: object properties: permissionId: type: integer permissionName: type: string userCount: type: integer description: Distinct users who currently have this permission via this role and would lose it (no other role they hold grants the permission). groupCount: type: integer description: Number of groups that currently grant this role. groups: type: array items: $ref: "#/components/schemas/RolePermissionImpactGroup" description: Groups that currently grant this role and therefore propagate the removal to their members. required: - permissionId - permissionName - userCount - groupCount - groups RolePermissionsImpact: type: object properties: removed: type: array items: $ref: "#/components/schemas/RolePermissionImpactItem" description: One entry per permission that is currently assigned to the role but not in the candidate set. totalAffectedUsers: type: integer description: Distinct users that would lose at least one permission across all removals. required: - removed - totalAffectedUsers 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