refactoring the api endpoints to be separate files to reduce context usage

This commit is contained in:
Cody Seibert
2025-12-14 17:53:21 -05:00
parent cdc8334d82
commit 6b30271441
121 changed files with 4281 additions and 2927 deletions

View File

@@ -0,0 +1,137 @@
/**
* Common utilities and state for terminal routes
*/
import { createLogger } from "../../lib/logger.js";
import type { Request, Response, NextFunction } from "express";
import { getTerminalService } from "../../services/terminal-service.js";
const logger = createLogger("Terminal");
// Read env variables lazily to ensure dotenv has loaded them
function getTerminalPassword(): string | undefined {
return process.env.TERMINAL_PASSWORD;
}
function getTerminalEnabledConfig(): boolean {
return process.env.TERMINAL_ENABLED !== "false"; // Enabled by default
}
// In-memory session tokens (would use Redis in production)
export const validTokens: Map<string, { createdAt: Date; expiresAt: Date }> =
new Map();
const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
/**
* Generate a secure random token
*/
export function generateToken(): string {
return `term-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 15)}${Math.random().toString(36).substr(2, 15)}`;
}
/**
* Clean up expired tokens
*/
export function cleanupExpiredTokens(): void {
const now = new Date();
validTokens.forEach((data, token) => {
if (data.expiresAt < now) {
validTokens.delete(token);
}
});
}
// Clean up expired tokens every 5 minutes
setInterval(cleanupExpiredTokens, 5 * 60 * 1000);
/**
* Validate a terminal session token
*/
export function validateTerminalToken(token: string | undefined): boolean {
if (!token) return false;
const tokenData = validTokens.get(token);
if (!tokenData) return false;
if (tokenData.expiresAt < new Date()) {
validTokens.delete(token);
return false;
}
return true;
}
/**
* Check if terminal requires password
*/
export function isTerminalPasswordRequired(): boolean {
return !!getTerminalPassword();
}
/**
* Check if terminal is enabled
*/
export function isTerminalEnabled(): boolean {
return getTerminalEnabledConfig();
}
/**
* Terminal authentication middleware
* Checks for valid session token if password is configured
*/
export function terminalAuthMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
// Check if terminal is enabled
if (!getTerminalEnabledConfig()) {
res.status(403).json({
success: false,
error: "Terminal access is disabled",
});
return;
}
// If no password configured, allow all requests
if (!getTerminalPassword()) {
next();
return;
}
// Check for session token
const token =
(req.headers["x-terminal-token"] as string) || (req.query.token as string);
if (!validateTerminalToken(token)) {
res.status(401).json({
success: false,
error: "Terminal authentication required",
passwordRequired: true,
});
return;
}
next();
}
export function getTerminalPasswordConfig(): string | undefined {
return getTerminalPassword();
}
export function getTerminalEnabledConfigValue(): boolean {
return getTerminalEnabledConfig();
}
export function getTokenExpiryMs(): number {
return TOKEN_EXPIRY_MS;
}
/**
* Get error message from error object
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : "Unknown error";
}

View File

@@ -0,0 +1,44 @@
/**
* Terminal routes with password protection
*
* Provides REST API for terminal session management and authentication.
* WebSocket connections for real-time I/O are handled separately in index.ts.
*/
import { Router } from "express";
import {
terminalAuthMiddleware,
validateTerminalToken,
isTerminalEnabled,
isTerminalPasswordRequired,
} from "./common.js";
import { createStatusHandler } from "./routes/status.js";
import { createAuthHandler } from "./routes/auth.js";
import { createLogoutHandler } from "./routes/logout.js";
import {
createSessionsListHandler,
createSessionsCreateHandler,
} from "./routes/sessions.js";
import { createSessionDeleteHandler } from "./routes/session-delete.js";
import { createSessionResizeHandler } from "./routes/session-resize.js";
// Re-export for use in main index.ts
export { validateTerminalToken, isTerminalEnabled, isTerminalPasswordRequired };
export function createTerminalRoutes(): Router {
const router = Router();
router.get("/status", createStatusHandler());
router.post("/auth", createAuthHandler());
router.post("/logout", createLogoutHandler());
// Apply terminal auth middleware to all routes below
router.use(terminalAuthMiddleware);
router.get("/sessions", createSessionsListHandler());
router.post("/sessions", createSessionsCreateHandler());
router.delete("/sessions/:id", createSessionDeleteHandler());
router.post("/sessions/:id/resize", createSessionResizeHandler());
return router;
}

View File

@@ -0,0 +1,66 @@
/**
* POST /auth endpoint - Authenticate with password to get a session token
*/
import type { Request, Response } from "express";
import {
getTerminalEnabledConfigValue,
getTerminalPasswordConfig,
generateToken,
validTokens,
getTokenExpiryMs,
getErrorMessage,
} from "../common.js";
export function createAuthHandler() {
return (req: Request, res: Response): void => {
if (!getTerminalEnabledConfigValue()) {
res.status(403).json({
success: false,
error: "Terminal access is disabled",
});
return;
}
const terminalPassword = getTerminalPasswordConfig();
// If no password required, return immediate success
if (!terminalPassword) {
res.json({
success: true,
data: {
authenticated: true,
passwordRequired: false,
},
});
return;
}
const { password } = req.body;
if (!password || password !== terminalPassword) {
res.status(401).json({
success: false,
error: "Invalid password",
});
return;
}
// Generate session token
const token = generateToken();
const now = new Date();
validTokens.set(token, {
createdAt: now,
expiresAt: new Date(now.getTime() + getTokenExpiryMs()),
});
res.json({
success: true,
data: {
authenticated: true,
token,
expiresIn: getTokenExpiryMs(),
},
});
};
}

View File

@@ -0,0 +1,20 @@
/**
* POST /logout endpoint - Invalidate a session token
*/
import type { Request, Response } from "express";
import { validTokens } from "../common.js";
export function createLogoutHandler() {
return (req: Request, res: Response): void => {
const token = (req.headers["x-terminal-token"] as string) || req.body.token;
if (token) {
validTokens.delete(token);
}
res.json({
success: true,
});
};
}

View File

@@ -0,0 +1,26 @@
/**
* DELETE /sessions/:id endpoint - Kill a terminal session
*/
import type { Request, Response } from "express";
import { getTerminalService } from "../../../services/terminal-service.js";
export function createSessionDeleteHandler() {
return (req: Request, res: Response): void => {
const terminalService = getTerminalService();
const { id } = req.params;
const killed = terminalService.killSession(id);
if (!killed) {
res.status(404).json({
success: false,
error: "Session not found",
});
return;
}
res.json({
success: true,
});
};
}

View File

@@ -0,0 +1,36 @@
/**
* POST /sessions/:id/resize endpoint - Resize a terminal session
*/
import type { Request, Response } from "express";
import { getTerminalService } from "../../../services/terminal-service.js";
export function createSessionResizeHandler() {
return (req: Request, res: Response): void => {
const terminalService = getTerminalService();
const { id } = req.params;
const { cols, rows } = req.body;
if (!cols || !rows) {
res.status(400).json({
success: false,
error: "cols and rows are required",
});
return;
}
const resized = terminalService.resize(id, cols, rows);
if (!resized) {
res.status(404).json({
success: false,
error: "Session not found",
});
return;
}
res.json({
success: true,
});
};
}

View File

@@ -0,0 +1,55 @@
/**
* GET /sessions endpoint - List all active terminal sessions
* POST /sessions endpoint - Create a new terminal session
*/
import type { Request, Response } from "express";
import { getTerminalService } from "../../../services/terminal-service.js";
import { getErrorMessage } from "../common.js";
import { createLogger } from "../../../lib/logger.js";
const logger = createLogger("Terminal");
export function createSessionsListHandler() {
return (_req: Request, res: Response): void => {
const terminalService = getTerminalService();
const sessions = terminalService.getAllSessions();
res.json({
success: true,
data: sessions,
});
};
}
export function createSessionsCreateHandler() {
return (req: Request, res: Response): void => {
try {
const terminalService = getTerminalService();
const { cwd, cols, rows, shell } = req.body;
const session = terminalService.createSession({
cwd,
cols: cols || 80,
rows: rows || 24,
shell,
});
res.json({
success: true,
data: {
id: session.id,
cwd: session.cwd,
shell: session.shell,
createdAt: session.createdAt,
},
});
} catch (error) {
logger.error("[Terminal] Error creating session:", error);
res.status(500).json({
success: false,
error: "Failed to create terminal session",
details: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,24 @@
/**
* GET /status endpoint - Get terminal status
*/
import type { Request, Response } from "express";
import { getTerminalService } from "../../../services/terminal-service.js";
import {
getTerminalEnabledConfigValue,
isTerminalPasswordRequired,
} from "../common.js";
export function createStatusHandler() {
return (_req: Request, res: Response): void => {
const terminalService = getTerminalService();
res.json({
success: true,
data: {
enabled: getTerminalEnabledConfigValue(),
passwordRequired: isTerminalPasswordRequired(),
platform: terminalService.getPlatformInfo(),
},
});
};
}