Files
Termix/src/backend/database/routes/open-tabs.ts
T

378 lines
10 KiB
TypeScript
Raw Normal View History

2026-05-28 22:05:25 -04:00
import type { AuthenticatedRequest } from "../../../types/index.js";
+5
2026-08-19 14:12:06 -05:00
import express, { type Request, type Response } from "express";
2026-05-28 22:05:25 -04:00
import { databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
+8
2026-07-19 12:29:52 -05:00
import { sessionManager } from "../../hosts/terminal/session-manager.js";
import {
getCurrentSettingValue,
createCurrentOpenTabRepository,
+2
2026-07-26 18:47:27 -05:00
createCurrentSessionShareRepository,
+8
2026-07-19 12:29:52 -05:00
} from "../repositories/factory.js";
2026-05-28 22:05:25 -04:00
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.
*/
2026-06-21 15:55:41 -05:00
const DEFAULT_TAB_TTL_MINUTES = 30;
function getTabTtlMs(): number {
try {
+8
2026-07-19 12:29:52 -05:00
const value = getCurrentSettingValue("terminal_session_timeout_minutes");
if (value) {
const minutes = parseInt(value, 10);
2026-06-21 15:55:41 -05:00
if (!isNaN(minutes) && minutes > 0) return minutes * 60_000;
}
} catch {
// DB not available, use default
}
return DEFAULT_TAB_TTL_MINUTES * 60_000;
}
2026-05-28 22:05:25 -04:00
+2
2026-06-16 15:59:53 -05:00
// Legacy tab types that were renamed. Normalize on read so previously saved
// tabs still restore to the correct (renamed) tab type.
const LEGACY_TAB_TYPE_MAP: Record<string, string> = {
stats: "host-metrics",
};
function normalizeTabType(tabType: string): string {
return LEGACY_TAB_TYPE_MAP[tabType] ?? tabType;
}
2026-05-28 22:05:25 -04:00
router.get("/", authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
try {
2026-06-21 15:55:41 -05:00
const cutoff = new Date(Date.now() - getTabTtlMs()).toISOString();
+8
2026-07-19 12:29:52 -05:00
const tabs = await createCurrentOpenTabRepository().listRecentForUser(
userId,
cutoff,
);
+2
2026-06-16 15:59:53 -05:00
return res.json(
tabs.map((tab) => ({ ...tab, tabType: normalizeTabType(tab.tabType) })),
);
2026-05-28 22:05:25 -04:00
} catch (e) {
2026-06-04 15:16:53 -04:00
databaseLogger.error("Failed to get open tabs", e, {
operation: "get_open_tabs",
userId,
});
2026-05-28 22:05:25 -04:00
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;
2026-06-04 15:16:53 -04:00
const { id, tabType, hostId, label, tabOrder, backendSessionId } =
req.body as {
id: string;
tabType: string;
hostId?: number | null;
label: string;
tabOrder: number;
backendSessionId?: string | null;
};
2026-05-28 22:05:25 -04:00
if (!id || !tabType || !label) {
2026-06-04 15:16:53 -04:00
return res
.status(400)
.json({ error: "id, tabType, and label are required" });
2026-05-28 22:05:25 -04:00
}
try {
+8
2026-07-19 12:29:52 -05:00
await createCurrentOpenTabRepository().upsertForUser(userId, {
id,
tabType,
hostId,
label,
tabOrder,
backendSessionId,
});
2026-05-28 22:05:25 -04:00
return res.json({ success: true });
} catch (e) {
2026-06-04 15:16:53 -04:00
databaseLogger.error("Failed to upsert open tab", e, {
operation: "upsert_open_tab",
userId,
id,
});
2026-05-28 22:05:25 -04:00
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 {
+8
2026-07-19 12:29:52 -05:00
await createCurrentOpenTabRepository().replaceForUser(userId, tabs);
2026-05-28 22:05:25 -04:00
return res.json({ success: true });
} catch (e) {
2026-06-04 15:16:53 -04:00
databaseLogger.error("Failed to sync open tabs", e, {
operation: "sync_open_tabs",
userId,
});
2026-05-28 22:05:25 -04:00
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<{
+5
2026-08-19 14:12:06 -05:00
hostId: number | null;
2026-05-28 22:05:25 -04:00
label: string;
tabOrder: number;
backendSessionId: string | null;
}>;
try {
+8
2026-07-19 12:29:52 -05:00
const updated = await createCurrentOpenTabRepository().updateForUser(
userId,
id,
updates,
);
2026-05-28 22:05:25 -04:00
+8
2026-07-19 12:29:52 -05:00
if (!updated) {
2026-05-28 22:05:25 -04:00
return res.status(404).json({ error: "Tab not found" });
}
return res.json({ success: true });
} catch (e) {
2026-06-04 15:16:53 -04:00
databaseLogger.error("Failed to update open tab", e, {
operation: "update_open_tab",
userId,
id,
});
2026-05-28 22:05:25 -04:00
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 {
+8
2026-07-19 12:29:52 -05:00
await createCurrentOpenTabRepository().deleteForUser(userId, id);
2026-05-28 22:05:25 -04:00
return res.json({ success: true });
} catch (e) {
2026-06-04 15:16:53 -04:00
databaseLogger.error("Failed to delete open tab", e, {
operation: "delete_open_tab",
userId,
id,
});
2026-05-28 22:05:25 -04:00
return res.status(500).json({ error: "Failed to delete open tab" });
}
});
/**
* @openapi
* /open-tabs/active-sessions:
* get:
* summary: Get all active backend sessions for the current user
+2
2026-07-26 18:47:27 -05:00
* description: >
* Returns live terminal sessions from the session manager, both sessions the
* caller owns and SSH sessions shared to the caller by another user (via
* an in-app session share). Used by the Active Connections panel and tab restore logic.
2026-05-28 22:05:25 -04:00
* tags:
* - Open Tabs
* responses:
* 200:
+2
2026-07-26 18:47:27 -05:00
* description: List of active sessions (own and shared-with-me).
2026-05-28 22:05:25 -04:00
* 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
+2
2026-07-26 18:47:27 -05:00
* isOwnSession:
* type: boolean
* sharedByUsername:
* type: string
* nullable: true
* permissionLevel:
* type: string
* nullable: true
* shareId:
* type: string
* nullable: true
2026-05-28 22:05:25 -04:00
*/
2026-06-04 15:16:53 -04:00
router.get(
"/active-sessions",
authenticateJWT,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
try {
+2
2026-07-26 18:47:27 -05:00
const ownSessions = sessionManager.getUserSessions(userId);
const result = ownSessions.map((s) => ({
sessionId: s.id,
hostId: s.hostId,
hostName: s.hostName,
tabInstanceId: s.attachedTabInstanceId ?? s.tabInstanceId ?? null,
isConnected: s.isConnected,
createdAt: s.createdAt,
isOwnSession: true,
sharedByUsername: null as string | null,
permissionLevel: null as string | null,
shareId: null as string | null,
}));
const sharedWithMe =
await createCurrentSessionShareRepository().findSharesTargetingUser(
userId,
);
for (const share of sharedWithMe) {
if (share.protocol !== "ssh") continue;
const sharedSession = sessionManager.getSession(share.sessionId);
if (!sharedSession || !sharedSession.isConnected) continue;
result.push({
sessionId: sharedSession.id,
hostId: sharedSession.hostId,
hostName: sharedSession.hostName,
tabInstanceId:
sharedSession.attachedTabInstanceId ??
sharedSession.tabInstanceId ??
null,
isConnected: sharedSession.isConnected,
createdAt: sharedSession.createdAt,
isOwnSession: false,
sharedByUsername: share.ownerUsername,
permissionLevel: share.permissionLevel,
shareId: share.id,
});
}
return res.json(result);
2026-06-04 15:16:53 -04:00
} 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" });
}
},
);
2026-05-28 22:05:25 -04:00
export default router;