diff --git a/plugins/automations/src/backend/maintenance-routes.ts b/plugins/automations/src/backend/maintenance-routes.ts index 184e58daa..bb03c133e 100644 --- a/plugins/automations/src/backend/maintenance-routes.ts +++ b/plugins/automations/src/backend/maintenance-routes.ts @@ -8,6 +8,16 @@ export function registerMaintenanceRoutes( ctx: PluginContext, service: MaintenanceService, ) { + /** + * @openapi + * /plugin-api/automations/maintenance: + * get: + * summary: List maintenance windows on the caller's hosts + * tags: [Automations] + * responses: + * 200: { description: Maintenance state per host. } + * 401: { description: Not signed in. } + */ router.get( "/maintenance", ctx.rbac.require("view") as never, @@ -65,11 +75,57 @@ export function registerMaintenanceRoutes( res.status(500).json({ error: "Failed to update maintenance" }); } }; + /** + * @openapi + * /plugin-api/automations/maintenance/{hostId}: + * get: + * summary: Get a host's maintenance state and schedule + * tags: [Automations] + * parameters: + * - in: path + * name: hostId + * required: true + * schema: { type: integer } + * responses: + * 200: { description: The active window, if any, and the planned ones. } + * 404: { description: Not one of the caller's hosts. } + */ router.get( "/maintenance/:hostId", ctx.rbac.require("view") as never, handle(false), ); + /** + * @openapi + * /plugin-api/automations/maintenance/{hostId}: + * post: + * summary: Start, schedule, end or remove maintenance on a host + * description: While a host is in maintenance its downtime does not raise alerts. Past the estimated end plus the grace period, it can alert again. + * tags: [Automations] + * parameters: + * - in: path + * name: hostId + * required: true + * schema: { type: integer } + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [action] + * properties: + * action: { type: string, enum: [start, schedule, end, remove] } + * reason: { type: string } + * start: { type: string, format: date-time, description: When a scheduled window begins. } + * durationMinutes: { type: integer, description: Estimated length. } + * graceMinutes: { type: integer } + * recurrence: { type: string, description: Repeat a scheduled window, e.g. weekly or monthly. } + * responses: + * 200: { description: The new maintenance state. } + * 400: { description: Invalid action or plan. } + * 404: { description: Not one of the caller's hosts. } + */ router.post( "/maintenance/:hostId", ctx.rbac.require("edit") as never, diff --git a/plugins/file-manager/src/backend/index.ts b/plugins/file-manager/src/backend/index.ts index f6a7c021c..c1d5d308a 100644 --- a/plugins/file-manager/src/backend/index.ts +++ b/plugins/file-manager/src/backend/index.ts @@ -2050,6 +2050,30 @@ export async function activate(ctx: PluginContext) { closeDedicatedTransferSession, }; + /** + * @openapi + * /plugin-api/file-manager/transferMethodPreview: + * post: + * summary: Preview how a host to host transfer would run + * description: Says whether tar or per-file SFTP would be used, and why, without moving anything. + * tags: [File Manager] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [sourceSessionId, destSessionId, sourcePaths, destPath] + * properties: + * sourceSessionId: { type: string } + * destSessionId: { type: string } + * sourcePaths: { type: array, items: { type: string } } + * destPath: { type: string } + * methodPreference: { type: string, enum: [auto, tar, item_sftp] } + * responses: + * 200: { description: The chosen method and the reason. } + * 400: { description: Missing parameters. } + */ app.post("/transferMethodPreview", async (req, res) => { const { sourceSessionId, @@ -2095,6 +2119,34 @@ export async function activate(ctx: PluginContext) { } }); + /** + * @openapi + * /plugin-api/file-manager/transferToHost: + * post: + * summary: Copy or move files from one host to another + * description: Starts a background transfer between two of the caller's file manager sessions. + * tags: [File Manager] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [sourceSessionId, destSessionId, sourcePaths, destPath] + * properties: + * sourceSessionId: { type: string } + * destSessionId: { type: string } + * sourcePaths: { type: array, items: { type: string } } + * destPath: { type: string } + * methodPreference: { type: string, enum: [auto, tar, item_sftp] } + * move: { type: boolean, description: Delete the source after a successful copy. } + * parallelSegmentCount: { type: integer } + * responses: + * 200: { description: The new transfer id. } + * 400: { description: Missing parameters or a session is not connected. } + * 403: { description: A session belongs to someone else. } + * 500: { description: The transfer could not start. } + */ app.post("/transferToHost", async (req, res) => { const { sourceSessionId, @@ -2168,6 +2220,16 @@ export async function activate(ctx: PluginContext) { } }); + /** + * @openapi + * /plugin-api/file-manager/activeTransfers: + * get: + * summary: List the caller's running host to host transfers + * tags: [File Manager] + * responses: + * 200: { description: The active transfers. } + * 401: { description: Not signed in. } + */ app.get("/activeTransfers", async (req, res) => { const userId = ctx.currentActor(); if (!userId) @@ -2176,6 +2238,22 @@ export async function activate(ctx: PluginContext) { res.json({ transfers: listActiveTransfers(userId) }); }); + /** + * @openapi + * /plugin-api/file-manager/transferStatus/{transferId}: + * get: + * summary: Get a transfer's progress + * tags: [File Manager] + * parameters: + * - in: path + * name: transferId + * required: true + * schema: { type: string } + * responses: + * 200: { description: The transfer's status and progress. } + * 401: { description: Not signed in. } + * 404: { description: No such transfer for this user. } + */ app.get("/transferStatus/:transferId", async (req, res) => { const userId = ctx.currentActor(); const transferId = req.params.transferId; @@ -2187,6 +2265,21 @@ export async function activate(ctx: PluginContext) { res.json(status); }); + /** + * @openapi + * /plugin-api/file-manager/transferCancel/{transferId}: + * post: + * summary: Cancel a running transfer + * tags: [File Manager] + * parameters: + * - in: path + * name: transferId + * required: true + * schema: { type: string } + * responses: + * 200: { description: The transfer was asked to stop. } + * 401: { description: Not signed in. } + */ app.post("/transferCancel/:transferId", (req, res) => { const userId = ctx.currentActor(); const transferId = req.params.transferId; @@ -2200,6 +2293,22 @@ export async function activate(ctx: PluginContext) { res.json({ ok: true }); }); + /** + * @openapi + * /plugin-api/file-manager/transferCleanup/{transferId}: + * post: + * summary: Remove the partial files a cancelled transfer left + * tags: [File Manager] + * parameters: + * - in: path + * name: transferId + * required: true + * schema: { type: string } + * responses: + * 200: { description: What was removed. } + * 401: { description: Not signed in. } + * 404: { description: No such transfer for this user. } + */ app.post("/transferCleanup/:transferId", async (req, res) => { const userId = ctx.currentActor(); const transferId = req.params.transferId; @@ -2219,6 +2328,22 @@ export async function activate(ctx: PluginContext) { } }); + /** + * @openapi + * /plugin-api/file-manager/transferRetry/{transferId}: + * post: + * summary: Retry a failed or cancelled transfer + * tags: [File Manager] + * parameters: + * - in: path + * name: transferId + * required: true + * schema: { type: string } + * responses: + * 200: { description: The transfer started again. } + * 401: { description: Not signed in. } + * 404: { description: No such transfer for this user. } + */ app.post("/transferRetry/:transferId", (req, res) => { const userId = ctx.currentActor(); const transferId = req.params.transferId; diff --git a/plugins/file-manager/src/backend/operation-routes.ts b/plugins/file-manager/src/backend/operation-routes.ts index e87994061..19fd1fa05 100644 --- a/plugins/file-manager/src/backend/operation-routes.ts +++ b/plugins/file-manager/src/backend/operation-routes.ts @@ -57,6 +57,22 @@ export function registerFileOperationRoutes( return session; } + /** + * @openapi + * /plugin-api/file-manager/trash: + * get: + * summary: List items in the host's Termix trash + * tags: [File Manager] + * parameters: + * - in: query + * name: sessionId + * required: true + * schema: { type: string } + * responses: + * 200: { description: Trashed items, the retention in days and whether the caller may change it. } + * 400: { description: The session is not connected. } + * 403: { description: The session belongs to someone else. } + */ app.get("/trash", async (req, res) => { const session = await ownedSession(req, res); if (!session) return; @@ -73,6 +89,25 @@ export function registerFileOperationRoutes( } }); + /** + * @openapi + * /plugin-api/file-manager/trash/{id}/restore: + * post: + * summary: Restore a trashed item to where it was + * tags: [File Manager] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: string } + * - in: query + * name: sessionId + * required: true + * schema: { type: string } + * responses: + * 200: { description: The restored path. } + * 409: { description: Something already exists at the original path. } + */ app.post("/trash/:id/restore", async (req, res) => { const session = await ownedSession(req, res); if (!session) return; @@ -91,6 +126,25 @@ export function registerFileOperationRoutes( } }); + /** + * @openapi + * /plugin-api/file-manager/trash/{id}: + * delete: + * summary: Delete a trashed item for good + * tags: [File Manager] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: string } + * - in: query + * name: sessionId + * required: true + * schema: { type: string } + * responses: + * 200: { description: Deleted. } + * 500: { description: The delete failed on the host. } + */ app.delete("/trash/:id", async (req, res) => { const session = await ownedSession(req, res); if (!session) return; @@ -105,6 +159,21 @@ export function registerFileOperationRoutes( } }); + /** + * @openapi + * /plugin-api/file-manager/trash: + * delete: + * summary: Empty the host's Termix trash + * tags: [File Manager] + * parameters: + * - in: query + * name: sessionId + * required: true + * schema: { type: string } + * responses: + * 200: { description: How many items were deleted. } + * 500: { description: The delete failed on the host. } + */ app.delete("/trash", async (req, res) => { const session = await ownedSession(req, res); if (!session) return; @@ -115,6 +184,26 @@ export function registerFileOperationRoutes( } }); + /** + * @openapi + * /plugin-api/file-manager/trash-retention: + * put: + * summary: Set how long trashed items are kept (admin only) + * tags: [File Manager] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [retentionDays] + * properties: + * retentionDays: { type: integer, minimum: 1, maximum: 3650 } + * responses: + * 200: { description: The new retention. } + * 400: { description: Out of range. } + * 403: { description: Admin access required. } + */ app.put("/trash-retention", async (req, res) => { if (!(await canManageRetention())) { return res.status(403).json({ error: "Admin access required" }); diff --git a/plugins/proxmox/src/backend/routes.ts b/plugins/proxmox/src/backend/routes.ts index 23d9c256c..930e97828 100644 --- a/plugins/proxmox/src/backend/routes.ts +++ b/plugins/proxmox/src/backend/routes.ts @@ -900,6 +900,29 @@ router.get("/discover/stream", async (req, res) => { } }); +/** + * @openapi + * /plugin-api/proxmox/discover: + * post: + * summary: Discover the guests on a Proxmox node + * description: Lists the node's VMs and containers so they can be added as hosts. /discover/stream reports the same with progress. + * tags: [Proxmox] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [hostId] + * properties: + * hostId: { type: integer } + * responses: + * 200: { description: The guests found. } + * 400: { description: Missing or invalid hostId. } + * 403: { description: Access denied to the host. } + * 404: { description: Host not found. } + * 422: { description: Host is not a Proxmox node or is unreachable. } + */ router.post("/discover", async (req, res) => { const { hostId } = req.body as { hostId?: unknown }; const userId = pluginCtx().currentActor()!; diff --git a/plugins/remote-desktop/src/backend/routes.ts b/plugins/remote-desktop/src/backend/routes.ts index c52dc01f6..71d1dcd73 100644 --- a/plugins/remote-desktop/src/backend/routes.ts +++ b/plugins/remote-desktop/src/backend/routes.ts @@ -502,7 +502,7 @@ export function registerRoutes(router: Router, deps: RouteDeps): void { * name: probe * schema: * type: string - * description: "0" skips dialing guacd. + * description: Set to 0 to skip dialing guacd. * - in: query * name: hostId * schema: diff --git a/plugins/session-sharing/src/backend/room-routes.ts b/plugins/session-sharing/src/backend/room-routes.ts index 25b0ca2dd..8c078d8a1 100644 --- a/plugins/session-sharing/src/backend/room-routes.ts +++ b/plugins/session-sharing/src/backend/room-routes.ts @@ -200,6 +200,23 @@ export function registerRoomRoutes( } = actions; const requireUse = ctx.rbac.require("use") as never; + /** + * @openapi + * /plugin-api/session-sharing/meeting-host/{syncId}: + * get: + * summary: Find this server's id for a synced host + * description: A desktop presents a host by its sync id; this returns the matching host the caller may connect to. + * tags: [Collab] + * parameters: + * - in: path + * name: syncId + * required: true + * schema: { type: string } + * responses: + * 200: { description: The host id. } + * 401: { description: Not signed in. } + * 404: { description: No reachable host with that sync id. } + */ router.get( "/meeting-host/:syncId", requireUse, diff --git a/plugins/web-endpoint/src/backend/routes.ts b/plugins/web-endpoint/src/backend/routes.ts index 3bc7e77ef..8a8d574ac 100644 --- a/plugins/web-endpoint/src/backend/routes.ts +++ b/plugins/web-endpoint/src/backend/routes.ts @@ -147,6 +147,29 @@ function endpointUrl( export function createWebEndpointRoutes(ctx: PluginContext): Router { const router = express.Router(); + /** + * @openapi + * /plugin-api/web-endpoint/open: + * post: + * summary: Open a tunnel to a host's web endpoint + * description: For endpoints reached through SSH. Returns the local port the tunnel listens on. + * tags: [Web Endpoint] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [hostId, endpointId] + * properties: + * hostId: { type: integer } + * endpointId: { type: string } + * responses: + * 200: { description: The local port. } + * 400: { description: Invalid request, or the endpoint does not use a tunnel. } + * 401: { description: Not signed in. } + * 404: { description: Host or endpoint not found. } + */ router.post("/open", async (req, res) => { const userId = ctx.currentActor(); if (!userId) { @@ -200,6 +223,29 @@ export function createWebEndpointRoutes(ctx: PluginContext): Router { * capability check and audit line cover the whole decision, not just the * final "open a window" step. */ + /** + * @openapi + * /plugin-api/web-endpoint/open-window: + * post: + * summary: Open a host's web endpoint in a desktop window + * tags: [Web Endpoint] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [hostId, endpointId] + * properties: + * hostId: { type: integer } + * endpointId: { type: string } + * ignoreCert: { type: boolean, description: Accept a self-signed certificate for this window. } + * responses: + * 200: { description: The window opened. } + * 400: { description: Invalid request. } + * 401: { description: Not signed in. } + * 404: { description: Host not found or access denied. } + */ router.post("/open-window", async (req, res) => { const userId = ctx.currentActor(); if (!userId) { diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index 61bc9fc73..ea68f95db 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -572,7 +572,6 @@ router.post( * authType: * type: string * description: Any registered SSH auth type that works for an unsaved host (password, key, credential, agent, none, or one a plugin marks for Quick Connect). - * description: Authentication method * password: * type: string * description: Password (required if authType is password) @@ -1490,9 +1489,26 @@ router.get( ); /** - * Returns the minimum authentication material needed by the desktop app to - * connect to a shared host from the recipient's own network. The response is - * deliberately transient: callers must not persist it or include it in logs. + * @openapi + * /host/db/host/{id}/local-connection-auth: + * get: + * summary: Get the login a desktop needs to reach a shared host itself + * description: The minimum authentication material for connecting to a shared host from the recipient's own network. Transient; callers must not store or log it. + * tags: + * - SSH + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The connection auth. + * 400: + * description: Invalid id. + * 404: + * description: Shared host not found. */ router.get( "/db/host/:id/local-connection-auth", diff --git a/src/backend/database/routes/rbac.ts b/src/backend/database/routes/rbac.ts index 8e895f724..69b0fb9b3 100644 --- a/src/backend/database/routes/rbac.ts +++ b/src/backend/database/routes/rbac.ts @@ -671,6 +671,49 @@ router.delete( }, ); +/** + * @openapi + * /rbac/host/{id}/access/{accessId}: + * patch: + * summary: Change a host access rule + * description: Updates the permission level or the expiry of one share. Requires the right to manage sharing on the host. + * tags: + * - RBAC + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * - in: path + * name: accessId + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * permissionLevel: + * type: string + * enum: [connect, view, edit, manage] + * durationHours: + * type: number + * nullable: true + * description: Hours from now until the share expires. Null removes the expiry. + * responses: + * 200: + * description: The updated rule. + * 400: + * description: Invalid id or nothing to change. + * 403: + * description: Not allowed to manage sharing on this host. + * 404: + * description: No such rule. + */ router.patch( "/host/:id/access/:accessId", authenticateJWT,