Files
TX/lib/api-spec/openapi.yaml
T
riyadhafraa b5efd9eb12 Add system updates page to admin section
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
2026-05-17 14:59:36 +00:00

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