mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-13 10:49:03 +00:00
303 lines
9.2 KiB
TypeScript
303 lines
9.2 KiB
TypeScript
import type { AuthenticatedRequest } from "../../../types/index.js";
|
|||
|
|
import express from "express";
|
||
|
|
import { db } from "../db/index.js";
|
||
|
|
import { userOpenTabs } from "../db/schema.js";
|
||
|
|
import { eq, and, sql } from "drizzle-orm";
|
||
|
|
import type { Request, Response } from "express";
|
||
|
|
import { databaseLogger } from "../../utils/logger.js";
|
||
|
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||
|
|
import { sessionManager } from "../../ssh/terminal-session-manager.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 TAB_TTL_MS = 30 * 60 * 1000;
|
||
|
|
|
||
|
|
router.get("/", authenticateJWT, async (req: Request, res: Response) => {
|
||
|
|
const userId = (req as AuthenticatedRequest).userId;
|
||
|
|
try {
|
||
|
|
const cutoff = new Date(Date.now() - TAB_TTL_MS).toISOString();
|
||
|
|
const tabs = db
|
||
|
|
.select()
|
||
|
|
.from(userOpenTabs)
|
||
|
|
.where(and(eq(userOpenTabs.userId, userId), sql`${userOpenTabs.updatedAt} > ${cutoff}`))
|
||
|
|
.orderBy(userOpenTabs.tabOrder)
|
||
|
|
.all();
|
||
|
|
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 {
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
const existing = db.select().from(userOpenTabs).where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId))).all();
|
||
|
|
if (existing.length > 0) {
|
||
|
|
// Preserve existing backendSessionId when not explicitly provided
|
||
|
|
const sessionId = backendSessionId !== undefined ? backendSessionId : existing[0].backendSessionId;
|
||
|
|
db.update(userOpenTabs)
|
||
|
|
.set({ tabType, hostId: hostId ?? null, label, tabOrder, backendSessionId: sessionId ?? null, updatedAt: now })
|
||
|
|
.where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId)))
|
||
|
|
.run();
|
||
|
|
} else {
|
||
|
|
db.insert(userOpenTabs).values({ id, userId, tabType, hostId: hostId ?? null, label, tabOrder, backendSessionId: backendSessionId ?? null, updatedAt: now }).run();
|
||
|
|
}
|
||
|
|
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 {
|
||
|
|
db.delete(userOpenTabs).where(eq(userOpenTabs.userId, userId)).run();
|
||
|
|
if (tabs.length > 0) {
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
db.insert(userOpenTabs).values(
|
||
|
|
tabs.map((t) => ({
|
||
|
|
id: t.id,
|
||
|
|
userId,
|
||
|
|
tabType: t.tabType,
|
||
|
|
hostId: t.hostId ?? null,
|
||
|
|
label: t.label,
|
||
|
|
tabOrder: t.tabOrder,
|
||
|
|
backendSessionId: t.backendSessionId ?? null,
|
||
|
|
updatedAt: now,
|
||
|
|
})),
|
||
|
|
).run();
|
||
|
|
}
|
||
|
|
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<{
|
||
|
|
label: string;
|
||
|
|
tabOrder: number;
|
||
|
|
backendSessionId: string | null;
|
||
|
|
}>;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = db
|
||
|
|
.update(userOpenTabs)
|
||
|
|
.set({ ...updates, updatedAt: new Date().toISOString() })
|
||
|
|
.where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId)))
|
||
|
|
.run();
|
||
|
|
|
||
|
|
if (result.changes === 0) {
|
||
|
|
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 {
|
||
|
|
db.delete(userOpenTabs)
|
||
|
|
.where(and(eq(userOpenTabs.id, id), eq(userOpenTabs.userId, userId)))
|
||
|
|
.run();
|
||
|
|
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/active-sessions:
|
||
|
|
* get:
|
||
|
|
* summary: Get all active backend sessions for the current user
|
||
|
|
* description: Returns live terminal sessions from the session manager. Used by the Active Connections panel and tab restore logic.
|
||
|
|
* tags:
|
||
|
|
* - Open Tabs
|
||
|
|
* responses:
|
||
|
|
* 200:
|
||
|
|
* description: List of 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
|
||
|
|
*/
|
||
|
|
router.get("/active-sessions", authenticateJWT, async (req: Request, res: Response) => {
|
||
|
|
const userId = (req as AuthenticatedRequest).userId;
|
||
|
|
try {
|
||
|
|
const sessions = sessionManager.getUserSessions(userId);
|
||
|
|
return res.json(
|
||
|
|
sessions.map((s) => ({
|
||
|
|
sessionId: s.id,
|
||
|
|
hostId: s.hostId,
|
||
|
|
hostName: s.hostName,
|
||
|
|
tabInstanceId: s.attachedTabInstanceId ?? s.tabInstanceId ?? null,
|
||
|
|
isConnected: s.isConnected,
|
||
|
|
createdAt: s.createdAt,
|
||
|
|
})),
|
||
|
|
);
|
||
|
|
} 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;
|