b507b33cad
Mirrors the existing role-permissions impact preview UX for app
permissions. Admins now see how many currently-visible users would lose
access before they add a permission requirement to an app, plus the
groups (via group_apps) that offset the loss because their members keep
access regardless.
Backend
- New endpoint POST /api/apps/:id/permissions/impact-preview in
artifacts/api-server/src/routes/apps.ts. Implements the same OR
semantics as getVisibleAppsForUser: a user "sees" an app if they hold
ANY required permission (direct or via a group role) OR they belong
to a group granted the app via group_apps. Admins are excluded from
counts since they always see every app. Short-circuits with
noChange:true when the candidate set equals the current set.
- OpenAPI schema (lib/api-spec/openapi.yaml): adds the path,
AppPermissionsImpactBody, AppPermissionImpactGroup,
AppPermissionsImpact. Regenerated lib/api-client-react bindings.
Frontend
- AppPermissionsEditor (artifacts/tx-os/src/pages/admin.tsx): debounced
(350ms) cancel-safe preview when a pending permission is selected,
warning banner with affected/visible counts and offsetting groups,
and a confirmation dialog when affectedUserCount > 0. Add button is
disabled while the preview is loading or errored to keep the warning
trustworthy.
- i18n keys added to en.json and ar.json under
admin.appPermissions.{impactTitle, impactLoading, impactError,
impactNone, impactSummary, impactViaGroups, confirmTitle, confirmBody,
confirmAction}.
Tests
- artifacts/api-server/tests/app-permissions-impact.test.mjs: 7 tests
covering noChange short-circuit, unrestricted-app tightening,
candidate that keeps an existing permission, group_apps offset,
unknown app (404), invalid payload (400), and admin-only enforcement.
All 18 app-permissions tests pass.
- E2E flow verified via runTest: admin login → /admin → Apps → edit
app → select permission → preview banner appears → Add → confirm
dialog → cancel without writing.
Out-of-scope (filed as follow-ups #228 and #229): listing the specific
affected user IDs in the preview, and warning when REMOVING a
permission broadens access.
No deviations from the task spec.
Replit-Task-Id: 8b2ff9ea-f95e-4bdb-b268-95b5d17154ca
4299 lines
108 KiB
YAML
4299 lines
108 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
# Do not change the title, if the title changes, the import paths will be broken
|
|
title: Api
|
|
version: 0.1.0
|
|
description: Tx OS API specification
|
|
servers:
|
|
- url: /api
|
|
description: Base API path
|
|
tags:
|
|
- name: health
|
|
description: Health operations
|
|
- name: auth
|
|
description: Authentication
|
|
- name: apps
|
|
description: App management
|
|
- name: services
|
|
description: Internal services (khidmati)
|
|
- name: orders
|
|
description: Service orders workflow
|
|
- name: conversations
|
|
description: Chat conversations
|
|
- name: messages
|
|
description: Chat messages
|
|
- name: notifications
|
|
description: Notifications
|
|
- name: users
|
|
description: User management
|
|
- name: roles
|
|
description: Role assignment
|
|
- name: stats
|
|
description: Dashboard stats
|
|
- name: storage
|
|
description: Object storage upload and serving endpoints
|
|
- name: audit
|
|
description: Admin audit log
|
|
|
|
paths:
|
|
/healthz:
|
|
get:
|
|
operationId: healthCheck
|
|
tags: [health]
|
|
summary: Health check
|
|
responses:
|
|
"200":
|
|
description: Healthy
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/HealthStatus"
|
|
|
|
# Auth
|
|
/auth/register:
|
|
post:
|
|
operationId: register
|
|
tags: [auth]
|
|
summary: Register a new user
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RegisterBody"
|
|
responses:
|
|
"201":
|
|
description: Registered
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/login:
|
|
post:
|
|
operationId: login
|
|
tags: [auth]
|
|
summary: Login
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LoginBody"
|
|
responses:
|
|
"200":
|
|
description: Logged in
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"401":
|
|
description: Unauthorized
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/logout:
|
|
post:
|
|
operationId: logout
|
|
tags: [auth]
|
|
summary: Logout
|
|
responses:
|
|
"200":
|
|
description: Logged out
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
|
|
/auth/forgot-password:
|
|
post:
|
|
operationId: forgotPassword
|
|
tags: [auth]
|
|
summary: Request a password reset link
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForgotPasswordBody"
|
|
responses:
|
|
"200":
|
|
description: Reset request accepted
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForgotPasswordResponse"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/reset-password:
|
|
post:
|
|
operationId: resetPassword
|
|
tags: [auth]
|
|
summary: Set a new password using a reset token
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ResetPasswordBody"
|
|
responses:
|
|
"200":
|
|
description: Password updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
"400":
|
|
description: Invalid or expired token
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/reset-password/verify:
|
|
post:
|
|
operationId: verifyResetToken
|
|
tags: [auth]
|
|
summary: Check whether a reset token is valid
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/VerifyResetTokenBody"
|
|
responses:
|
|
"200":
|
|
description: Token check result
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/VerifyResetTokenResponse"
|
|
|
|
/auth/admin/users/{id}/issue-reset-link:
|
|
post:
|
|
operationId: adminIssueResetLink
|
|
tags: [auth]
|
|
summary: Admin generates a one-time password reset link for a user
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Reset link issued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AdminResetLinkResponse"
|
|
"403":
|
|
description: Forbidden
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
"404":
|
|
description: User not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/me:
|
|
get:
|
|
operationId: getMe
|
|
tags: [auth]
|
|
summary: Get current user
|
|
responses:
|
|
"200":
|
|
description: Current user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"401":
|
|
description: Unauthorized
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/me/language:
|
|
patch:
|
|
operationId: updateLanguage
|
|
tags: [auth]
|
|
summary: Update preferred language
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateLanguageBody"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
|
|
/auth/me/clock-style:
|
|
patch:
|
|
operationId: updateClockStyle
|
|
tags: [auth]
|
|
summary: Update preferred home-screen clock style
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateClockStyleBody"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/auth/me/clock-hour12:
|
|
patch:
|
|
operationId: updateClockHour12
|
|
tags: [auth]
|
|
summary: Update preferred 12/24-hour clock format
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateClockHour12Body"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthUser"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
# Apps
|
|
/apps:
|
|
get:
|
|
operationId: listApps
|
|
tags: [apps]
|
|
summary: List all active apps
|
|
responses:
|
|
"200":
|
|
description: Apps list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/App"
|
|
|
|
post:
|
|
operationId: createApp
|
|
tags: [apps]
|
|
summary: Create a new app (admin)
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateAppBody"
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/App"
|
|
|
|
/apps/{id}:
|
|
get:
|
|
operationId: getApp
|
|
tags: [apps]
|
|
summary: Get app by ID
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: App
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/App"
|
|
|
|
patch:
|
|
operationId: updateApp
|
|
tags: [apps]
|
|
summary: Update app (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateAppBody"
|
|
responses:
|
|
"200":
|
|
description: Updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/App"
|
|
|
|
delete:
|
|
operationId: deleteApp
|
|
tags: [apps]
|
|
summary: Delete app (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
- name: force
|
|
in: query
|
|
required: false
|
|
description: Force deletion of an app that has dependent records (records an audit log entry).
|
|
schema:
|
|
type: boolean
|
|
default: false
|
|
responses:
|
|
"204":
|
|
description: Deleted
|
|
"409":
|
|
description: App has dependent records; pass force=true to confirm deletion.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppDeletionConflict"
|
|
|
|
/apps/{id}/permissions:
|
|
get:
|
|
operationId: listAppPermissions
|
|
tags: [apps]
|
|
summary: List the permissions that gate this app (admin)
|
|
description: |
|
|
Returns the permissions currently required for users to see this app
|
|
in their launcher. An app with no rows here is unrestricted (visible
|
|
to anyone). When more than one permission is configured, holding any
|
|
one of them is sufficient.
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: Permissions gating the app
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Permission"
|
|
"404":
|
|
description: App not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
post:
|
|
operationId: addAppPermission
|
|
tags: [apps]
|
|
summary: Add a required permission to this app (admin)
|
|
description: |
|
|
Adds a row to `app_permissions` for the (app_id, permission_id) pair.
|
|
Uses `ON CONFLICT DO NOTHING` against the composite primary key so
|
|
re-adding an existing pair is a no-op.
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AddAppPermissionBody"
|
|
responses:
|
|
"201":
|
|
description: Permission requirement added (or already present)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppPermissionLink"
|
|
"404":
|
|
description: App or permission not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/apps/{id}/permissions/impact-preview:
|
|
post:
|
|
operationId: previewAppPermissionsImpact
|
|
tags: [apps]
|
|
summary: Preview the impact of changing the permissions required for an app (admin)
|
|
description: |
|
|
Given a candidate set of permission IDs that would gate this app,
|
|
returns how many users would lose visibility of the app (excluding
|
|
admins, who always see every app), the count of users who currently
|
|
see the app, and the groups that grant the app via `group_apps`. A
|
|
member of any returned group always sees the app regardless of
|
|
permission changes, so listing them lets the admin see which
|
|
populations are protected from the loss.
|
|
|
|
Mirrors `POST /roles/{id}/permissions/impact-preview` so the admin
|
|
UI can show the same kind of warning before committing the change.
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppPermissionsImpactBody"
|
|
responses:
|
|
"200":
|
|
description: Impact preview for the proposed permission set
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppPermissionsImpact"
|
|
"400":
|
|
description: Invalid request body
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
"404":
|
|
description: App not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/apps/{id}/permissions/{permissionId}:
|
|
delete:
|
|
operationId: removeAppPermission
|
|
tags: [apps]
|
|
summary: Remove a required permission from this app (admin)
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
- name: permissionId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"204":
|
|
description: Removed (or no row existed for the pair)
|
|
"404":
|
|
description: App not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/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}/audit:
|
|
get:
|
|
operationId: getRolePermissionAudit
|
|
tags: [roles]
|
|
summary: List recent permission-change audit entries for a role (admin)
|
|
description: |
|
|
Returns permission-change audit records for the given role, newest
|
|
first. Supports offset-based pagination via `limit`/`offset` and
|
|
optional filters by acting user (`actorUserId`) and a date range
|
|
(`from`/`to`, inclusive UTC days). The response includes the total
|
|
matching count and the next offset to load (or `null` when the end
|
|
has been reached). Each entry captures the actor (if known), the
|
|
previous permission IDs, the new permission IDs, and the timestamp.
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
- in: query
|
|
name: limit
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 200
|
|
default: 10
|
|
- in: query
|
|
name: offset
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
default: 0
|
|
- in: query
|
|
name: actorUserId
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
description: |
|
|
Restrict results to entries authored by the given user. Use
|
|
`0` or omit to return entries from any actor.
|
|
- 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: A page of permission-change audit entries for the role
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RolePermissionAuditList"
|
|
"400":
|
|
description: Invalid filter parameters
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
"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"
|
|
|
|
/users/{id}/audit:
|
|
get:
|
|
operationId: getUserPermissionAudit
|
|
tags: [users]
|
|
summary: List recent permission-change audit entries for a user (admin)
|
|
description: |
|
|
Returns permission-change audit records for the given user, newest
|
|
first. Mirrors the role audit endpoint shape and supports the same
|
|
`limit`/`offset`/`actorUserId`/`from`/`to` filters. Each entry
|
|
captures the actor (if known), the previous and new id sets, and
|
|
the change kind (`user.roles` or `user.groups`).
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
- in: query
|
|
name: limit
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 200
|
|
default: 10
|
|
- in: query
|
|
name: offset
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
default: 0
|
|
- in: query
|
|
name: actorUserId
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
- in: query
|
|
name: from
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
- in: query
|
|
name: to
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
responses:
|
|
"200":
|
|
description: A page of permission-change audit entries for the user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PermissionAuditList"
|
|
"400":
|
|
description: Invalid filter parameters
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
"404":
|
|
description: User not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/groups/{id}/audit:
|
|
get:
|
|
operationId: getGroupPermissionAudit
|
|
tags: [users]
|
|
summary: List recent permission-change audit entries for a group (admin)
|
|
description: |
|
|
Returns permission-change audit records for the given group, newest
|
|
first. Captures changes to the group's user, role and app sets via
|
|
the discriminator field `changeKind`.
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
- in: query
|
|
name: limit
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 200
|
|
default: 10
|
|
- in: query
|
|
name: offset
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
default: 0
|
|
- in: query
|
|
name: actorUserId
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
- in: query
|
|
name: from
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
- in: query
|
|
name: to
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
responses:
|
|
"200":
|
|
description: A page of permission-change audit entries for the group
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PermissionAuditList"
|
|
"400":
|
|
description: Invalid filter parameters
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
"404":
|
|
description: Group not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/apps/{id}/audit:
|
|
get:
|
|
operationId: getAppPermissionAudit
|
|
tags: [apps]
|
|
summary: List recent permission-change audit entries for an app (admin)
|
|
description: |
|
|
Returns permission-change audit records for the given app, newest
|
|
first. Captures changes to the set of permissions required to open
|
|
the app (`changeKind: app.permissions`).
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
- in: query
|
|
name: limit
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 200
|
|
default: 10
|
|
- in: query
|
|
name: offset
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
default: 0
|
|
- in: query
|
|
name: actorUserId
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
- in: query
|
|
name: from
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
- in: query
|
|
name: to
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
responses:
|
|
"200":
|
|
description: A page of permission-change audit entries for the app
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PermissionAuditList"
|
|
"400":
|
|
description: Invalid filter parameters
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
"404":
|
|
description: App not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
# 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: targetType
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: |
|
|
Restrict results to entries whose `target_type` exactly matches
|
|
(e.g. "group", "app", "role", "user", "service", "settings").
|
|
Combines with `targetId` and the other filters.
|
|
- in: query
|
|
name: targetId
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
description: |
|
|
Restrict results to entries whose `target_id` exactly matches.
|
|
Typically used together with `targetType` to focus on the history
|
|
of a single entity.
|
|
- 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).
|
|
- in: query
|
|
name: targetType
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: |
|
|
Restrict the export to entries whose `target_type` exactly matches.
|
|
Combines with `targetId` and the other filters.
|
|
- in: query
|
|
name: targetId
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
description: |
|
|
Restrict the export to entries whose `target_id` exactly matches.
|
|
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
|
|
|
|
AddAppPermissionBody:
|
|
type: object
|
|
properties:
|
|
permissionId:
|
|
type: integer
|
|
description: ID of the permission to require for this app.
|
|
required:
|
|
- permissionId
|
|
|
|
AppPermissionLink:
|
|
type: object
|
|
properties:
|
|
appId:
|
|
type: integer
|
|
permissionId:
|
|
type: integer
|
|
required:
|
|
- appId
|
|
- permissionId
|
|
|
|
AppPermissionsImpactBody:
|
|
type: object
|
|
properties:
|
|
permissionIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: Candidate set of permission IDs that would gate this app. Pass an empty array to model "remove every requirement" (i.e. make the app unrestricted).
|
|
required:
|
|
- permissionIds
|
|
|
|
AppPermissionImpactGroup:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
name:
|
|
type: string
|
|
required:
|
|
- id
|
|
- name
|
|
|
|
AppPermissionsImpact:
|
|
type: object
|
|
properties:
|
|
affectedUserCount:
|
|
type: integer
|
|
description: Distinct non-admin users who currently see the app and would lose visibility under the candidate permission set. Already accounts for users who keep access via `group_apps` group-grants.
|
|
currentlyVisibleUserCount:
|
|
type: integer
|
|
description: Distinct non-admin users who currently see the app under the existing permission set. Useful for showing the warning as a fraction (e.g. "12 of 80 will lose access").
|
|
groups:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/AppPermissionImpactGroup"
|
|
description: Groups that currently grant this app via `group_apps`. Members of any of these groups keep access regardless of permission changes.
|
|
noChange:
|
|
type: boolean
|
|
description: True when the candidate set is identical to the current permission set so no change would actually be saved.
|
|
required:
|
|
- affectedUserCount
|
|
- currentlyVisibleUserCount
|
|
- groups
|
|
- noChange
|
|
|
|
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
|
|
|
|
RolePermissionAuditList:
|
|
type: object
|
|
properties:
|
|
entries:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/RolePermissionAuditEntry"
|
|
totalCount:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
offset:
|
|
type: integer
|
|
nextOffset:
|
|
type: ["integer", "null"]
|
|
required:
|
|
- entries
|
|
- totalCount
|
|
- limit
|
|
- offset
|
|
- nextOffset
|
|
|
|
RolePermissionAuditEntry:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
roleId:
|
|
type: integer
|
|
previousPermissionIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
newPermissionIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
addedPermissionIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
removedPermissionIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
actor:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/AuditLogActor"
|
|
- type: "null"
|
|
required:
|
|
- id
|
|
- roleId
|
|
- previousPermissionIds
|
|
- newPermissionIds
|
|
- addedPermissionIds
|
|
- removedPermissionIds
|
|
- createdAt
|
|
- actor
|
|
|
|
PermissionAuditList:
|
|
type: object
|
|
properties:
|
|
entries:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/PermissionAuditEntry"
|
|
totalCount:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
offset:
|
|
type: integer
|
|
nextOffset:
|
|
type: ["integer", "null"]
|
|
required:
|
|
- entries
|
|
- totalCount
|
|
- limit
|
|
- offset
|
|
- nextOffset
|
|
|
|
PermissionAuditEntry:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
targetKind:
|
|
type: string
|
|
enum: [user, group, app]
|
|
targetId:
|
|
type: integer
|
|
changeKind:
|
|
type: string
|
|
enum:
|
|
- user.roles
|
|
- user.groups
|
|
- group.users
|
|
- group.roles
|
|
- group.apps
|
|
- app.permissions
|
|
previousIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
newIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
addedIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
removedIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
actor:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/AuditLogActor"
|
|
- type: "null"
|
|
required:
|
|
- id
|
|
- targetKind
|
|
- targetId
|
|
- changeKind
|
|
- previousIds
|
|
- newIds
|
|
- addedIds
|
|
- removedIds
|
|
- createdAt
|
|
- actor
|
|
|
|
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
|