mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
357 lines
9.7 KiB
TypeScript
357 lines
9.7 KiB
TypeScript
import type { AuthenticatedRequest } from "../../../types/index.js";
|
|
import express, { type Request, type Response } from "express";
|
|
import { databaseLogger } from "../../utils/logger.js";
|
|
import { AuthManager } from "../../utils/auth-manager.js";
|
|
import { liveTerminalSessions } from "../../hosts/live-terminal-sessions.js";
|
|
import { createCurrentOpenTabRepository } from "../repositories/factory.js";
|
|
|
|
const router = express.Router();
|
|
const authManager = AuthManager.getInstance();
|
|
const authenticateJWT = authManager.createAuthMiddleware();
|
|
|
|
/**
|
|
* @openapi
|
|
* /open-tabs:
|
|
* get:
|
|
* summary: Get all open tabs for the current user
|
|
* tags:
|
|
* - Open Tabs
|
|
* responses:
|
|
* 200:
|
|
* description: List of open tabs ordered by tab_order.
|
|
*/
|
|
const DEFAULT_TAB_TTL_MINUTES = 30;
|
|
|
|
// Tabs live as long as the terminal keeps a detached session.
|
|
function getTabTtlMs(): number {
|
|
const minutes = liveTerminalSessions.idleTimeoutMinutes();
|
|
return (minutes && minutes > 0 ? minutes : DEFAULT_TAB_TTL_MINUTES) * 60_000;
|
|
}
|
|
|
|
router.get("/", authenticateJWT, async (req: Request, res: Response) => {
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
try {
|
|
const cutoff = new Date(Date.now() - getTabTtlMs()).toISOString();
|
|
const tabs = await createCurrentOpenTabRepository().listRecentForUser(
|
|
userId,
|
|
cutoff,
|
|
);
|
|
return res.json(tabs);
|
|
} catch (e) {
|
|
databaseLogger.error("Failed to get open tabs", e, {
|
|
operation: "get_open_tabs",
|
|
userId,
|
|
});
|
|
return res.status(500).json({ error: "Failed to get open tabs" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /open-tabs:
|
|
* post:
|
|
* summary: Upsert a single open tab for the current user
|
|
* tags:
|
|
* - Open Tabs
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required: [id, tabType, label, tabOrder]
|
|
* properties:
|
|
* id:
|
|
* type: string
|
|
* tabType:
|
|
* type: string
|
|
* hostId:
|
|
* type: integer
|
|
* nullable: true
|
|
* label:
|
|
* type: string
|
|
* tabOrder:
|
|
* type: integer
|
|
* backendSessionId:
|
|
* type: string
|
|
* nullable: true
|
|
* responses:
|
|
* 200:
|
|
* description: Tab upserted successfully.
|
|
*/
|
|
router.post("/", authenticateJWT, async (req: Request, res: Response) => {
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
const { id, tabType, hostId, label, tabOrder, backendSessionId } =
|
|
req.body as {
|
|
id: string;
|
|
tabType: string;
|
|
hostId?: number | null;
|
|
label: string;
|
|
tabOrder: number;
|
|
backendSessionId?: string | null;
|
|
};
|
|
|
|
if (!id || !tabType || !label) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: "id, tabType, and label are required" });
|
|
}
|
|
|
|
try {
|
|
await createCurrentOpenTabRepository().upsertForUser(userId, {
|
|
id,
|
|
tabType,
|
|
hostId,
|
|
label,
|
|
tabOrder,
|
|
backendSessionId,
|
|
});
|
|
return res.json({ success: true });
|
|
} catch (e) {
|
|
databaseLogger.error("Failed to upsert open tab", e, {
|
|
operation: "upsert_open_tab",
|
|
userId,
|
|
id,
|
|
});
|
|
return res.status(500).json({ error: "Failed to upsert open tab" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /open-tabs:
|
|
* put:
|
|
* summary: Bulk replace all open tabs for the current user
|
|
* tags:
|
|
* - Open Tabs
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* tabs:
|
|
* type: array
|
|
* responses:
|
|
* 200:
|
|
* description: Tabs updated successfully.
|
|
*/
|
|
router.put("/", authenticateJWT, async (req: Request, res: Response) => {
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
const { tabs } = req.body as {
|
|
tabs: Array<{
|
|
id: string;
|
|
tabType: string;
|
|
hostId?: number | null;
|
|
label: string;
|
|
tabOrder: number;
|
|
backendSessionId?: string | null;
|
|
}>;
|
|
};
|
|
|
|
if (!Array.isArray(tabs)) {
|
|
return res.status(400).json({ error: "tabs must be an array" });
|
|
}
|
|
|
|
try {
|
|
await createCurrentOpenTabRepository().replaceForUser(userId, tabs);
|
|
return res.json({ success: true });
|
|
} catch (e) {
|
|
databaseLogger.error("Failed to sync open tabs", e, {
|
|
operation: "sync_open_tabs",
|
|
userId,
|
|
});
|
|
return res.status(500).json({ error: "Failed to sync open tabs" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /open-tabs/{id}:
|
|
* patch:
|
|
* summary: Update a single open tab
|
|
* tags:
|
|
* - Open Tabs
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Tab updated successfully.
|
|
* 404:
|
|
* description: Tab not found.
|
|
*/
|
|
router.patch("/:id", authenticateJWT, async (req: Request, res: Response) => {
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
const id = String(req.params.id);
|
|
const updates = req.body as Partial<{
|
|
hostId: number | null;
|
|
label: string;
|
|
tabOrder: number;
|
|
backendSessionId: string | null;
|
|
}>;
|
|
|
|
try {
|
|
const updated = await createCurrentOpenTabRepository().updateForUser(
|
|
userId,
|
|
id,
|
|
updates,
|
|
);
|
|
|
|
if (!updated) {
|
|
return res.status(404).json({ error: "Tab not found" });
|
|
}
|
|
return res.json({ success: true });
|
|
} catch (e) {
|
|
databaseLogger.error("Failed to update open tab", e, {
|
|
operation: "update_open_tab",
|
|
userId,
|
|
id,
|
|
});
|
|
return res.status(500).json({ error: "Failed to update open tab" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /open-tabs/{id}:
|
|
* delete:
|
|
* summary: Delete a single open tab
|
|
* tags:
|
|
* - Open Tabs
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Tab deleted successfully.
|
|
*/
|
|
router.delete("/:id", authenticateJWT, async (req: Request, res: Response) => {
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
const id = String(req.params.id);
|
|
|
|
try {
|
|
await createCurrentOpenTabRepository().deleteForUser(userId, id);
|
|
return res.json({ success: true });
|
|
} catch (e) {
|
|
databaseLogger.error("Failed to delete open tab", e, {
|
|
operation: "delete_open_tab",
|
|
userId,
|
|
id,
|
|
});
|
|
return res.status(500).json({ error: "Failed to delete open tab" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /open-tabs/session-timeout:
|
|
* get:
|
|
* summary: Get how long a detached session is kept
|
|
* description: The terminal's idle timeout for detached sessions, which is also how long a saved open tab lives. 30 while the terminal is off.
|
|
* tags:
|
|
* - Open Tabs
|
|
* responses:
|
|
* 200:
|
|
* description: The timeout in minutes.
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* minutes:
|
|
* type: integer
|
|
*/
|
|
router.get(
|
|
"/session-timeout",
|
|
authenticateJWT,
|
|
(_req: Request, res: Response) => {
|
|
res.json({ minutes: getTabTtlMs() / 60_000 });
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /open-tabs/active-sessions:
|
|
* get:
|
|
* summary: Get all active backend sessions for the current user
|
|
* description: >
|
|
* Returns the live terminal sessions the caller owns. Sessions other
|
|
* users shared with the caller come from the session sharing plugin.
|
|
* Used by the Active Connections panel and tab restore logic.
|
|
* tags:
|
|
* - Open Tabs
|
|
* responses:
|
|
* 200:
|
|
* description: List of the caller's active sessions.
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: array
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* sessionId:
|
|
* type: string
|
|
* hostId:
|
|
* type: integer
|
|
* hostName:
|
|
* type: string
|
|
* tabInstanceId:
|
|
* type: string
|
|
* isConnected:
|
|
* type: boolean
|
|
* createdAt:
|
|
* type: number
|
|
* isOwnSession:
|
|
* type: boolean
|
|
* sharedByUsername:
|
|
* type: string
|
|
* nullable: true
|
|
* permissionLevel:
|
|
* type: string
|
|
* nullable: true
|
|
* shareId:
|
|
* type: string
|
|
* nullable: true
|
|
*/
|
|
router.get(
|
|
"/active-sessions",
|
|
authenticateJWT,
|
|
async (req: Request, res: Response) => {
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
try {
|
|
const ownSessions = liveTerminalSessions.listForUser(userId);
|
|
const result = ownSessions.map((s) => ({
|
|
sessionId: s.id,
|
|
hostId: s.hostId,
|
|
hostName: s.hostName,
|
|
tabInstanceId: s.tabInstanceId,
|
|
isConnected: s.isConnected,
|
|
createdAt: s.createdAt,
|
|
isOwnSession: true,
|
|
sharedByUsername: null as string | null,
|
|
permissionLevel: null as string | null,
|
|
shareId: null as string | null,
|
|
}));
|
|
|
|
return res.json(result);
|
|
} catch (e) {
|
|
databaseLogger.error("Failed to get active sessions", e, {
|
|
operation: "get_active_sessions",
|
|
userId,
|
|
});
|
|
return res.status(500).json({ error: "Failed to get active sessions" });
|
|
}
|
|
},
|
|
);
|
|
|
|
export default router;
|