f2bf04fd42
Update OpenAPI spec and generated schemas to permit relative paths for upload URLs, resolving validation errors with the local storage driver.
5136 lines
137 KiB
YAML
5136 lines
137 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: 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
|
|
- name: system
|
|
description: System metadata (version, update availability)
|
|
|
|
paths:
|
|
/healthz:
|
|
get:
|
|
operationId: healthCheck
|
|
tags: [health]
|
|
summary: Health check
|
|
responses:
|
|
"200":
|
|
description: Healthy
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/HealthStatus"
|
|
|
|
/system/version:
|
|
get:
|
|
operationId: getSystemVersion
|
|
tags: [system]
|
|
summary: Get current and latest system version
|
|
description: |
|
|
Returns the local build version (read from version.json embedded at
|
|
build time) and, if `LATEST_VERSION_URL` is configured on the
|
|
server, the latest published version fetched from that URL. Admin
|
|
only. Never performs an update — pure read.
|
|
responses:
|
|
"200":
|
|
description: System version info
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SystemVersionResponse"
|
|
|
|
# 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/notification-preferences:
|
|
patch:
|
|
operationId: updateNotificationPreferences
|
|
tags: [auth]
|
|
summary: Update notification sound, vibration & mute preferences
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateNotificationPreferencesBody"
|
|
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"
|
|
|
|
# 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"
|
|
|
|
/push/vapid-public-key:
|
|
get:
|
|
operationId: getPushVapidPublicKey
|
|
tags: [notifications]
|
|
summary: Get the server's VAPID public key for Web Push subscriptions
|
|
responses:
|
|
"200":
|
|
description: VAPID public key
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
publicKey:
|
|
type: string
|
|
required: [publicKey]
|
|
"503":
|
|
description: Web Push not configured on this server
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/push/subscribe:
|
|
post:
|
|
operationId: subscribePush
|
|
tags: [notifications]
|
|
summary: Register a Web Push subscription for the current user
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
endpoint:
|
|
type: string
|
|
keys:
|
|
type: object
|
|
properties:
|
|
p256dh:
|
|
type: string
|
|
auth:
|
|
type: string
|
|
required: [p256dh, auth]
|
|
required: [endpoint, keys]
|
|
responses:
|
|
"200":
|
|
description: Subscribed
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
delete:
|
|
operationId: deletePushSubscription
|
|
tags: [notifications]
|
|
summary: Remove a Web Push subscription by endpoint (spec-aligned alias of POST /push/unsubscribe)
|
|
parameters:
|
|
- in: query
|
|
name: endpoint
|
|
required: true
|
|
schema:
|
|
type: string
|
|
description: The push endpoint URL to unsubscribe
|
|
responses:
|
|
"200":
|
|
description: Unsubscribed
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SuccessResponse"
|
|
|
|
/push/unsubscribe:
|
|
post:
|
|
operationId: unsubscribePush
|
|
tags: [notifications]
|
|
summary: Remove a Web Push subscription for the current user
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
endpoint:
|
|
type: string
|
|
required: [endpoint]
|
|
responses:
|
|
"200":
|
|
description: Unsubscribed
|
|
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/bulk-delete:
|
|
post:
|
|
operationId: bulkDeleteServiceOrders
|
|
tags: [orders]
|
|
summary: Delete multiple service orders in one request
|
|
description: |
|
|
Deletes several orders in one round-trip. Authorization is still
|
|
enforced per id using the same rules as DELETE /orders/{id}, so the
|
|
client cannot bypass the permission check by batching. Returns a
|
|
summary of which ids were deleted and which were rejected.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BulkDeleteServiceOrdersBody"
|
|
responses:
|
|
"200":
|
|
description: Bulk delete summary
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BulkDeleteServiceOrdersResponse"
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/orders/{id}:
|
|
delete:
|
|
operationId: deleteServiceOrder
|
|
tags: [orders]
|
|
summary: Delete a service order
|
|
description: |
|
|
Permanently deletes the order. Allowed when:
|
|
- the caller is an admin (any order, any status); or
|
|
- the caller owns the order AND it is in a terminal status
|
|
(completed/cancelled); or
|
|
- the caller holds the `orders.receive` permission AND the order
|
|
is currently visible in their own incoming view — i.e. an
|
|
unclaimed pending order, or one they have themselves claimed
|
|
and is still active (received/preparing). Receivers cannot
|
|
delete another receiver's claimed order, nor terminal orders.
|
|
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}/audit.csv:
|
|
get:
|
|
operationId: exportRolePermissionAuditCsv
|
|
tags: [roles]
|
|
summary: Download permission-change audit entries for a role as CSV (admin)
|
|
description: |
|
|
Returns the same audit rows as `GET /roles/{id}/audit` but as a
|
|
downloadable CSV file with permission IDs resolved to names.
|
|
Honors the optional `actorUserId`, `from`, and `to` filters
|
|
identically. Pagination parameters are NOT accepted: the file
|
|
contains all matching rows up to a server-side cap (currently
|
|
10,000 — narrow the date range for a longer history).
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
- 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: |
|
|
UTF-8 CSV (with BOM) containing columns: timestamp,
|
|
actor_username, added_permissions, removed_permissions.
|
|
content:
|
|
text/csv:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
"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: actorUserId
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
description: |
|
|
Restrict results to entries written by a specific acting user
|
|
(matches `actor_user_id`). Combines with the other filters.
|
|
- 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`, `to`, `targetType`,
|
|
`targetId`, and `actorUserId` filters as the list endpoint, so an
|
|
export always matches whatever the admin is currently viewing.
|
|
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.
|
|
- in: query
|
|
name: actorUserId
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
description: |
|
|
Restrict the export to entries written by a specific acting user.
|
|
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"
|
|
|
|
/admin/apps/{id}/dependents/groups:
|
|
get:
|
|
operationId: getAdminAppDependentGroups
|
|
tags: [apps]
|
|
summary: List groups granting access to an app
|
|
description: |
|
|
Returns the groups behind the inline "<n> groups" badge on the
|
|
admin Apps row, paginated. Admin only.
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
- 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: Paged list of groups granting the app
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppDependentGroupsPage"
|
|
"404":
|
|
description: App not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/admin/apps/{id}/dependents/restrictions:
|
|
get:
|
|
operationId: getAdminAppDependentRestrictions
|
|
tags: [apps]
|
|
summary: List permission restrictions on an app
|
|
description: |
|
|
Returns the legacy permissions gating an app, with the role names
|
|
currently holding each permission, paginated. Admin only.
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
- 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: Paged list of restrictions on the app
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppDependentRestrictionsPage"
|
|
"404":
|
|
description: App not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/admin/apps/{id}/dependents/opens:
|
|
get:
|
|
operationId: getAdminAppDependentOpens
|
|
tags: [apps]
|
|
summary: List recent opens for an app (no time filter)
|
|
description: |
|
|
Returns every recorded open for the app newest-first, paginated.
|
|
Unlike /stats/admin/app-opens/by-app/{appId}, this endpoint does
|
|
not apply a date range so the badge total and the list match.
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
- 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: Paged list of opens
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AppDependentOpensPage"
|
|
"404":
|
|
description: App not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/admin/services/{id}/dependents/orders:
|
|
get:
|
|
operationId: getAdminServiceDependentOrders
|
|
tags: [services]
|
|
summary: List orders placed against a service
|
|
description: |
|
|
Returns the service orders behind the inline "<n> orders" badge
|
|
on the admin Services row, paginated, newest-first. Admin only.
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
- 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: Paged list of orders for the service
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ServiceDependentOrdersPage"
|
|
"404":
|
|
description: Service not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
# ----- Notes (in-app messaging) -----
|
|
/notes/my:
|
|
get:
|
|
operationId: listMyNotesAlias
|
|
tags: [notes]
|
|
summary: Alias of GET /notes — list the caller's own notes
|
|
parameters:
|
|
- in: query
|
|
name: archived
|
|
required: false
|
|
schema: { type: boolean, default: false }
|
|
responses:
|
|
"200":
|
|
description: The caller's notes
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/MyNote" }
|
|
/notes/sent:
|
|
get:
|
|
operationId: listSentNotes
|
|
tags: [notes]
|
|
summary: List notes the caller has sent
|
|
responses:
|
|
"200":
|
|
description: Sent notes with per-recipient status
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/SentNote" }
|
|
/notes/received:
|
|
get:
|
|
operationId: listReceivedNotes
|
|
tags: [notes]
|
|
summary: List notes sent to the caller
|
|
parameters:
|
|
- in: query
|
|
name: archived
|
|
required: false
|
|
schema: { type: boolean, default: false }
|
|
responses:
|
|
"200":
|
|
description: Received notes with sender + status
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/ReceivedNote" }
|
|
/notes/{id}:
|
|
get:
|
|
operationId: getNoteDetail
|
|
tags: [notes]
|
|
summary: Get a note's full thread (note + recipients + replies)
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Note thread, scoped to caller (sender, recipient, or admin)
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/NoteThread" }
|
|
"403":
|
|
description: Not a participant
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
|
"404":
|
|
description: Note not found
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
|
/notes/{id}/send:
|
|
post:
|
|
operationId: sendNote
|
|
tags: [notes]
|
|
summary: Owner sends an existing note to a list of recipients
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [recipientUserIds]
|
|
properties:
|
|
recipientUserIds:
|
|
type: array
|
|
items: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Sent (idempotent on existing recipients)
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SendNoteResult" }
|
|
/notes/{id}/read:
|
|
post:
|
|
operationId: markNoteRead
|
|
tags: [notes]
|
|
summary: Recipient marks their copy of a note read
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Marked read
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SuccessResponse" }
|
|
/notes/{id}/archive:
|
|
post:
|
|
operationId: archiveReceivedNote
|
|
tags: [notes]
|
|
summary: Recipient archives or unarchives their copy of a note
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [archived]
|
|
properties:
|
|
archived: { type: boolean }
|
|
responses:
|
|
"200":
|
|
description: Archive state updated
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SuccessResponse" }
|
|
/notes/{id}/reply:
|
|
post:
|
|
operationId: replyToNote
|
|
tags: [notes]
|
|
summary: Sender or recipient adds a reply to a note's thread
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [content]
|
|
properties:
|
|
content: { type: string }
|
|
recipientUserId:
|
|
type: integer
|
|
description: Required when the note owner replies and there is more than one recipient.
|
|
responses:
|
|
"201":
|
|
description: Reply created
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/NoteReply" }
|
|
|
|
# ----- Note folders (user-defined) -----
|
|
/note-folders:
|
|
get:
|
|
operationId: listNoteFolders
|
|
tags: [notes]
|
|
summary: List the caller's note folders with per-tab note counts
|
|
parameters:
|
|
- in: query
|
|
name: archived
|
|
required: false
|
|
description: Tab scope for noteCount (false = My Notes, true = Archive)
|
|
schema: { type: boolean, default: false }
|
|
responses:
|
|
"200":
|
|
description: Folders owned by the caller
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/NoteFolder" }
|
|
post:
|
|
operationId: createNoteFolder
|
|
tags: [notes]
|
|
summary: Create a new note folder owned by the caller
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string, minLength: 1, maxLength: 80 }
|
|
responses:
|
|
"201":
|
|
description: Folder created
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/NoteFolder" }
|
|
"400":
|
|
description: Invalid folder name (empty or too long)
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
|
"409":
|
|
description: Duplicate folder name for this user
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
|
/note-folders/{id}:
|
|
patch:
|
|
operationId: updateNoteFolder
|
|
tags: [notes]
|
|
summary: Rename a note folder
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
- in: query
|
|
name: archived
|
|
required: false
|
|
description: Tab scope for the returned noteCount (false = My Notes, true = Archive)
|
|
schema: { type: boolean, default: false }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string, minLength: 1, maxLength: 80 }
|
|
responses:
|
|
"200":
|
|
description: Folder updated
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/NoteFolder" }
|
|
"404":
|
|
description: Folder not found or not owned by caller
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
|
"409":
|
|
description: Duplicate folder name for this user
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
|
delete:
|
|
operationId: deleteNoteFolder
|
|
tags: [notes]
|
|
summary: Delete a note folder. Notes inside are preserved (folderId set to null).
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: Folder deleted
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SuccessResponse" }
|
|
|
|
/admin/users/{id}/dependents/notes:
|
|
get:
|
|
operationId: getAdminUserDependentNotes
|
|
tags: [users]
|
|
summary: List notes owned by a user
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
- 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: Paged list of notes
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UserDependentNotesPage"
|
|
"404":
|
|
description: User not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
/admin/users/{id}/dependents/orders:
|
|
get:
|
|
operationId: getAdminUserDependentOrders
|
|
tags: [users]
|
|
summary: List service orders placed by a user
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema: { type: integer }
|
|
- 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: Paged list of orders
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UserDependentOrdersPage"
|
|
"404":
|
|
description: User not found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|
|
|
components:
|
|
schemas:
|
|
HealthStatus:
|
|
type: object
|
|
properties:
|
|
status:
|
|
type: string
|
|
required:
|
|
- status
|
|
|
|
SystemVersionInfo:
|
|
type: object
|
|
properties:
|
|
version:
|
|
type: string
|
|
build:
|
|
type: string
|
|
required:
|
|
- version
|
|
- build
|
|
|
|
SystemVersionResponse:
|
|
type: object
|
|
properties:
|
|
current:
|
|
$ref: "#/components/schemas/SystemVersionInfo"
|
|
latest:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/SystemVersionInfo"
|
|
- type: "null"
|
|
status:
|
|
type: string
|
|
enum: [up-to-date, update-available, not-configured, error]
|
|
errorMessage:
|
|
type: ["string", "null"]
|
|
checkedAt:
|
|
type: ["string", "null"]
|
|
format: date-time
|
|
configured:
|
|
type: boolean
|
|
startedAt:
|
|
type: string
|
|
format: date-time
|
|
description: |
|
|
ISO-8601 timestamp captured when the API process booted.
|
|
Frozen for the lifetime of the process — used by the admin
|
|
System Updates panel as visible proof that the container
|
|
actually restarted (independent of build/version changes).
|
|
required:
|
|
- current
|
|
- latest
|
|
- status
|
|
- errorMessage
|
|
- checkedAt
|
|
- configured
|
|
- startedAt
|
|
|
|
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
|
|
|
|
NotificationSoundId:
|
|
type: string
|
|
enum: [ding, chime, bell, knock, pop, alert, beep, soft]
|
|
|
|
UpdateNotificationPreferencesBody:
|
|
type: object
|
|
properties:
|
|
notificationSoundOrder:
|
|
$ref: "#/components/schemas/NotificationSoundId"
|
|
notificationSoundMeeting:
|
|
$ref: "#/components/schemas/NotificationSoundId"
|
|
notificationSoundNote:
|
|
$ref: "#/components/schemas/NotificationSoundId"
|
|
notifyOrdersEnabled:
|
|
type: boolean
|
|
notifyMeetingsEnabled:
|
|
type: boolean
|
|
notifyNotesEnabled:
|
|
type: boolean
|
|
vibrationEnabledOrder:
|
|
type: boolean
|
|
vibrationEnabledMeeting:
|
|
type: boolean
|
|
vibrationEnabledNote:
|
|
type: boolean
|
|
notificationsMuted:
|
|
type: boolean
|
|
|
|
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
|
|
notificationSoundOrder:
|
|
$ref: "#/components/schemas/NotificationSoundId"
|
|
notificationSoundMeeting:
|
|
$ref: "#/components/schemas/NotificationSoundId"
|
|
notificationSoundNote:
|
|
$ref: "#/components/schemas/NotificationSoundId"
|
|
notifyOrdersEnabled:
|
|
type: boolean
|
|
notifyMeetingsEnabled:
|
|
type: boolean
|
|
notifyNotesEnabled:
|
|
type: boolean
|
|
vibrationEnabledOrder:
|
|
type: boolean
|
|
vibrationEnabledMeeting:
|
|
type: boolean
|
|
vibrationEnabledNote:
|
|
type: boolean
|
|
notificationsMuted:
|
|
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
|
|
- notificationSoundOrder
|
|
- notificationSoundMeeting
|
|
- notificationSoundNote
|
|
- notifyOrdersEnabled
|
|
- notifyMeetingsEnabled
|
|
- notifyNotesEnabled
|
|
- vibrationEnabledOrder
|
|
- vibrationEnabledMeeting
|
|
- vibrationEnabledNote
|
|
- notificationsMuted
|
|
- 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
|
|
directRoles:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: |
|
|
Roles assigned directly to the user (user_roles table only). The
|
|
`roles` field above is the effective set (direct + inherited via
|
|
group membership). The admin Users page uses the difference to
|
|
mark a role as "inherited from group X" and disables the toggle
|
|
so the admin doesn't try to remove a role that won't disappear.
|
|
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).
|
|
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
|
|
userCount:
|
|
type: integer
|
|
description: |
|
|
Number of users assigned this role. Populated only by the admin
|
|
list endpoint (GET /admin/roles) so the admin list can show the
|
|
same dependency counts inline as the Apps/Services/Users/Groups
|
|
panels. Omitted from create/update responses.
|
|
groupCount:
|
|
type: integer
|
|
description: |
|
|
Number of groups granting this role. Populated only by the admin
|
|
list endpoint (GET /admin/roles).
|
|
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
|
|
required:
|
|
- error
|
|
- noteCount
|
|
- orderCount
|
|
|
|
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
|
|
imageUrl:
|
|
type: ["string", "null"]
|
|
description: |
|
|
Optional uploaded image used in place of the Lucide icon on the
|
|
launcher tile. Stored as the object-storage path returned by the
|
|
upload presigner (e.g. "/objects/<id>").
|
|
route:
|
|
type: string
|
|
externalUrl:
|
|
type: ["string", "null"]
|
|
description: |
|
|
For openMode "external_tab" or "external_iframe": the URL the
|
|
launcher should open. Ignored when openMode is "internal".
|
|
openMode:
|
|
type: string
|
|
enum: [internal, external_tab, external_iframe]
|
|
description: |
|
|
How the launcher opens this app: "internal" navigates to `route`
|
|
inside the SPA; "external_tab" opens externalUrl in a new tab;
|
|
"external_iframe" navigates to /embedded/:id, which renders
|
|
externalUrl inside an iframe shell.
|
|
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
|
|
- openMode
|
|
- 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
|
|
imageUrl:
|
|
type: ["string", "null"]
|
|
route:
|
|
type: string
|
|
externalUrl:
|
|
type: ["string", "null"]
|
|
openMode:
|
|
type: string
|
|
enum: [internal, external_tab, external_iframe]
|
|
color:
|
|
type: string
|
|
isActive:
|
|
type: boolean
|
|
isSystem:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
permissionIds:
|
|
type: array
|
|
description: Optional set of permission IDs to gate the new app on. Inserted with onConflictDoNothing so duplicates are tolerated.
|
|
items:
|
|
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
|
|
imageUrl:
|
|
type: ["string", "null"]
|
|
route:
|
|
type: string
|
|
externalUrl:
|
|
type: ["string", "null"]
|
|
openMode:
|
|
type: string
|
|
enum: [internal, external_tab, external_iframe]
|
|
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
|
|
|
|
BulkDeleteServiceOrdersBody:
|
|
type: object
|
|
properties:
|
|
ids:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
minItems: 1
|
|
maxItems: 200
|
|
required:
|
|
- ids
|
|
|
|
BulkDeleteServiceOrdersResponse:
|
|
type: object
|
|
properties:
|
|
deletedIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: Ids that were successfully deleted.
|
|
failedIds:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: Ids that were skipped (not found, or caller is not authorized).
|
|
required:
|
|
- deletedIds
|
|
- failedIds
|
|
|
|
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
|
|
minLength: 1
|
|
description: >
|
|
Presigned upload destination. May be either an absolute URL
|
|
(S3 driver) or a same-origin path starting with `/` (local
|
|
driver) — the browser resolves the latter against
|
|
`window.location.origin` so uploads always target the host
|
|
serving the SPA.
|
|
objectPath:
|
|
type: string
|
|
metadata:
|
|
$ref: "#/components/schemas/RequestUploadUrlBody"
|
|
|
|
HomeStats:
|
|
type: object
|
|
properties:
|
|
totalApps:
|
|
type: integer
|
|
totalServices:
|
|
type: integer
|
|
unreadNotifications:
|
|
type: integer
|
|
totalUsers:
|
|
type: integer
|
|
required:
|
|
- totalApps
|
|
- totalServices
|
|
- unreadNotifications
|
|
- 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
|
|
|
|
# ===== Dependent-item drill-ins =====
|
|
# Each "<n> X" badge on the admin Apps / Services / Users rows is now
|
|
# clickable; the click loads one of the schemas below. They share the
|
|
# same envelope (totalCount + limit/offset/nextOffset) so the frontend
|
|
# can use a single LoadMoreSection for them all.
|
|
|
|
AppDependentGroupItem:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
descriptionAr: { type: ["string", "null"] }
|
|
descriptionEn: { type: ["string", "null"] }
|
|
required: [id, name, descriptionAr, descriptionEn]
|
|
|
|
AppDependentGroupsPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/AppDependentGroupItem"
|
|
totalCount: { type: integer }
|
|
limit: { type: integer }
|
|
offset: { type: integer }
|
|
nextOffset: { type: ["integer", "null"] }
|
|
required: [items, totalCount, limit, offset, nextOffset]
|
|
|
|
AppDependentRestrictionItem:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
descriptionAr: { type: ["string", "null"] }
|
|
descriptionEn: { type: ["string", "null"] }
|
|
roles:
|
|
type: array
|
|
description: Names of roles currently holding this permission.
|
|
items: { type: string }
|
|
required: [id, name, descriptionAr, descriptionEn, roles]
|
|
|
|
AppDependentRestrictionsPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/AppDependentRestrictionItem"
|
|
totalCount: { type: integer }
|
|
limit: { type: integer }
|
|
offset: { type: integer }
|
|
nextOffset: { type: ["integer", "null"] }
|
|
required: [items, totalCount, limit, offset, nextOffset]
|
|
|
|
AppDependentOpenItem:
|
|
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, displayNameAr, displayNameEn, avatarUrl]
|
|
|
|
AppDependentOpensPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/AppDependentOpenItem"
|
|
totalCount: { type: integer }
|
|
limit: { type: integer }
|
|
offset: { type: integer }
|
|
nextOffset: { type: ["integer", "null"] }
|
|
required: [items, totalCount, limit, offset, nextOffset]
|
|
|
|
ServiceDependentOrderItem:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
status: { type: string }
|
|
notes: { type: ["string", "null"] }
|
|
createdAt: { type: string, format: date-time }
|
|
userId: { type: integer }
|
|
username: { type: string }
|
|
displayNameAr: { type: ["string", "null"] }
|
|
displayNameEn: { type: ["string", "null"] }
|
|
required: [id, status, notes, createdAt, userId, username, displayNameAr, displayNameEn]
|
|
|
|
ServiceDependentOrdersPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ServiceDependentOrderItem"
|
|
totalCount: { type: integer }
|
|
limit: { type: integer }
|
|
offset: { type: integer }
|
|
nextOffset: { type: ["integer", "null"] }
|
|
required: [items, totalCount, limit, offset, nextOffset]
|
|
|
|
UserDependentNoteItem:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
isPinned: { type: boolean }
|
|
isArchived: { type: boolean }
|
|
updatedAt: { type: string, format: date-time }
|
|
required: [id, title, isPinned, isArchived, updatedAt]
|
|
|
|
NoteRecipientStatus:
|
|
type: string
|
|
enum: [unread, read, replied, archived]
|
|
|
|
NoteUserSummary:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
username: { type: string }
|
|
displayNameAr: { type: ["string", "null"] }
|
|
displayNameEn: { type: ["string", "null"] }
|
|
avatarUrl: { type: ["string", "null"] }
|
|
isActive: { type: boolean }
|
|
required: [id, username, displayNameAr, displayNameEn]
|
|
|
|
NoteRecipientSummary:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
noteId: { type: integer }
|
|
recipientUserId: { type: integer }
|
|
status: { $ref: "#/components/schemas/NoteRecipientStatus" }
|
|
sentAt: { type: string, format: date-time }
|
|
readAt: { type: ["string", "null"], format: date-time }
|
|
archivedAt: { type: ["string", "null"], format: date-time }
|
|
recipient:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/NoteUserSummary"
|
|
- type: "null"
|
|
required:
|
|
[id, recipientUserId, status, sentAt, readAt, archivedAt]
|
|
|
|
MyNote:
|
|
type: object
|
|
description: A note owned by the caller (My Notes / Archive view).
|
|
properties:
|
|
id: { type: integer }
|
|
userId: { type: integer }
|
|
title: { type: string }
|
|
content: { type: string }
|
|
color: { type: string }
|
|
isPinned: { type: boolean }
|
|
isArchived: { type: boolean }
|
|
folderId:
|
|
type: ["integer", "null"]
|
|
description: Owning folder id, or null for Unfiled.
|
|
createdAt: { type: string, format: date-time }
|
|
updatedAt: { type: string, format: date-time }
|
|
labelIds:
|
|
type: array
|
|
items: { type: integer }
|
|
required:
|
|
[id, userId, title, content, color, isPinned, isArchived, folderId,
|
|
createdAt, updatedAt, labelIds]
|
|
|
|
NoteFolder:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
userId: { type: integer }
|
|
name: { type: string }
|
|
createdAt: { type: string, format: date-time }
|
|
updatedAt: { type: string, format: date-time }
|
|
noteCount:
|
|
type: integer
|
|
description: Number of notes in this folder, scoped to the requested tab (active vs archived).
|
|
required: [id, userId, name, createdAt, updatedAt, noteCount]
|
|
|
|
SentNote:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
userId: { type: integer }
|
|
title: { type: string }
|
|
content: { type: string }
|
|
color: { type: string }
|
|
isPinned: { type: boolean }
|
|
isArchived: { type: boolean }
|
|
folderId:
|
|
type: ["integer", "null"]
|
|
description: Owning folder id, or null for Unfiled.
|
|
createdAt: { type: string, format: date-time }
|
|
updatedAt: { type: string, format: date-time }
|
|
recipients:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/NoteRecipientSummary" }
|
|
replyCount: { type: integer }
|
|
required:
|
|
[id, userId, title, content, color, createdAt, updatedAt, recipients, replyCount]
|
|
|
|
ReceivedNote:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
content: { type: string }
|
|
color: { type: string }
|
|
folderId:
|
|
type: ["integer", "null"]
|
|
description: Owning folder id on the recipient's side, or null for Unfiled.
|
|
createdAt: { type: string, format: date-time }
|
|
updatedAt: { type: string, format: date-time }
|
|
sender:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/NoteUserSummary"
|
|
- type: "null"
|
|
recipientRowId: { type: integer }
|
|
status: { $ref: "#/components/schemas/NoteRecipientStatus" }
|
|
sentAt: { type: string, format: date-time }
|
|
readAt: { type: ["string", "null"], format: date-time }
|
|
archivedAt: { type: ["string", "null"], format: date-time }
|
|
required:
|
|
[id, title, content, color, createdAt, updatedAt, recipientRowId, status,
|
|
sentAt, readAt, archivedAt]
|
|
|
|
NoteReply:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
noteId: { type: integer }
|
|
senderUserId: { type: integer }
|
|
recipientUserId: { type: integer }
|
|
content: { type: string }
|
|
createdAt: { type: string, format: date-time }
|
|
sender:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/NoteUserSummary"
|
|
- type: "null"
|
|
required:
|
|
[id, noteId, senderUserId, recipientUserId, content, createdAt]
|
|
|
|
NoteThread:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
content: { type: string }
|
|
color: { type: string }
|
|
createdAt: { type: ["string", "null"], format: date-time }
|
|
updatedAt: { type: ["string", "null"], format: date-time }
|
|
senderUserId: { type: integer }
|
|
sender:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/NoteUserSummary"
|
|
- type: "null"
|
|
isOwner: { type: boolean }
|
|
isAdmin: { type: boolean }
|
|
myStatus:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/NoteRecipientStatus"
|
|
- type: "null"
|
|
recipients:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/NoteRecipientSummary" }
|
|
replies:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/NoteReply" }
|
|
required:
|
|
[id, title, content, color, senderUserId, isOwner, isAdmin, myStatus,
|
|
recipients, replies]
|
|
|
|
SendNoteResult:
|
|
type: object
|
|
properties:
|
|
success: { type: boolean }
|
|
sent: { type: integer }
|
|
required: [success, sent]
|
|
|
|
UserDependentNotesPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/UserDependentNoteItem"
|
|
totalCount: { type: integer }
|
|
limit: { type: integer }
|
|
offset: { type: integer }
|
|
nextOffset: { type: ["integer", "null"] }
|
|
required: [items, totalCount, limit, offset, nextOffset]
|
|
|
|
UserDependentOrderItem:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
status: { type: string }
|
|
notes: { type: ["string", "null"] }
|
|
createdAt: { type: string, format: date-time }
|
|
serviceId: { type: integer }
|
|
serviceNameAr: { type: string }
|
|
serviceNameEn: { type: string }
|
|
required: [id, status, notes, createdAt, serviceId, serviceNameAr, serviceNameEn]
|
|
|
|
UserDependentOrdersPage:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/UserDependentOrderItem"
|
|
totalCount: { type: integer }
|
|
limit: { type: integer }
|
|
offset: { type: integer }
|
|
nextOffset: { type: ["integer", "null"] }
|
|
required: [items, totalCount, limit, offset, nextOffset]
|
|
|
|
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
|