b5efd9eb12
Add a new API endpoint and UI section for checking system updates, including internationalized locale strings and necessary dependency updates. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 1e6de894-7c78-4557-b01a-a2c1c01f4155 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true
5111 lines
136 KiB
YAML
5111 lines
136 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
|
|
required:
|
|
- current
|
|
- latest
|
|
- status
|
|
- errorMessage
|
|
- checkedAt
|
|
- configured
|
|
|
|
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
|
|
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
|
|
format: uri
|
|
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
|