feat: fix network stats merge and add openapi jsdocs comments

This commit is contained in:
LukeGus
2026-01-12 19:12:08 -06:00
parent 8ce4c6f364
commit 8ae8520c44
22 changed files with 4332 additions and 1068 deletions

View File

@@ -58,6 +58,31 @@ app.use(express.json({ limit: "1mb" }));
app.use(authManager.createAuthMiddleware());
/**
* @openapi
* /uptime:
* get:
* summary: Get server uptime
* description: Returns the uptime of the server in various formats.
* tags:
* - Dashboard
* responses:
* 200:
* description: Server uptime information.
* content:
* application/json:
* schema:
* type: object
* properties:
* uptimeMs:
* type: number
* uptimeSeconds:
* type: number
* formatted:
* type: string
* 500:
* description: Failed to get uptime.
*/
app.get("/uptime", async (req, res) => {
try {
const uptimeMs = Date.now() - serverStartTime;
@@ -77,6 +102,28 @@ app.get("/uptime", async (req, res) => {
}
});
/**
* @openapi
* /activity/recent:
* get:
* summary: Get recent activity
* description: Fetches the most recent activities for the authenticated user.
* tags:
* - Dashboard
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* description: The maximum number of activities to return.
* responses:
* 200:
* description: A list of recent activities.
* 401:
* description: Session expired.
* 500:
* description: Failed to get recent activity.
*/
app.get("/activity/recent", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -108,6 +155,40 @@ app.get("/activity/recent", async (req, res) => {
}
});
/**
* @openapi
* /activity/log:
* post:
* summary: Log a new activity
* description: Logs a new user activity, such as accessing a terminal or file manager. This endpoint is rate-limited.
* tags:
* - Dashboard
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* type:
* type: string
* enum: [terminal, file_manager, server_stats, tunnel, docker]
* hostId:
* type: integer
* hostName:
* type: string
* responses:
* 200:
* description: Activity logged successfully or rate-limited.
* 400:
* description: Invalid request body.
* 401:
* description: Session expired.
* 404:
* description: Host not found or access denied.
* 500:
* description: Failed to log activity.
*/
app.post("/activity/log", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -224,6 +305,22 @@ app.post("/activity/log", async (req, res) => {
}
});
/**
* @openapi
* /activity/reset:
* delete:
* summary: Reset recent activity
* description: Clears all recent activity for the authenticated user.
* tags:
* - Dashboard
* responses:
* 200:
* description: Recent activity cleared.
* 401:
* description: Session expired.
* 500:
* description: Failed to reset activity.
*/
app.delete("/activity/reset", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;

View File

@@ -206,10 +206,46 @@ app.use(bodyParser.urlencoded({ limit: "1gb", extended: true }));
app.use(bodyParser.raw({ limit: "5gb", type: "application/octet-stream" }));
app.use(cookieParser());
/**
* @openapi
* /health:
* get:
* summary: Health check
* description: Returns the health status of the server.
* tags:
* - General
* responses:
* 200:
* description: Server is healthy.
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: ok
*/
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
/**
* @openapi
* /version:
* get:
* summary: Get version information
* description: Returns the local and remote version of the application.
* tags:
* - General
* responses:
* 200:
* description: Version information.
* 404:
* description: Local version not set.
* 500:
* description: Fetch error.
*/
app.get("/version", authenticateJWT, async (req, res) => {
let localVersion = process.env.VERSION;
@@ -308,6 +344,31 @@ app.get("/version", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /releases/rss:
* get:
* summary: Get releases in RSS format
* description: Returns the latest releases from the GitHub repository in an RSS-like JSON format.
* tags:
* - General
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: The page number of the releases to fetch.
* - in: query
* name: per_page
* schema:
* type: integer
* description: The number of releases to fetch per page.
* responses:
* 200:
* description: Releases in RSS format.
* 500:
* description: Failed to generate RSS format.
*/
app.get("/releases/rss", authenticateJWT, async (req, res) => {
try {
const page = parseInt(req.query.page as string) || 1;
@@ -364,6 +425,20 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /encryption/status:
* get:
* summary: Get encryption status
* description: Returns the security status of the application.
* tags:
* - Encryption
* responses:
* 200:
* description: Security status.
* 500:
* description: Failed to get security status.
*/
app.get("/encryption/status", requireAdmin, async (req, res) => {
try {
const securityStatus = {
@@ -385,6 +460,20 @@ app.get("/encryption/status", requireAdmin, async (req, res) => {
}
});
/**
* @openapi
* /encryption/initialize:
* post:
* summary: Initialize security system
* description: Initializes the security system for the application.
* tags:
* - Encryption
* responses:
* 200:
* description: Security system initialized successfully.
* 500:
* description: Failed to initialize security system.
*/
app.post("/encryption/initialize", requireAdmin, async (req, res) => {
try {
const authManager = AuthManager.getInstance();
@@ -408,6 +497,20 @@ app.post("/encryption/initialize", requireAdmin, async (req, res) => {
}
});
/**
* @openapi
* /encryption/regenerate:
* post:
* summary: Regenerate JWT secret
* description: Regenerates the system JWT secret. This will invalidate all existing JWT tokens.
* tags:
* - Encryption
* responses:
* 200:
* description: System JWT secret regenerated.
* 500:
* description: Failed to regenerate JWT secret.
*/
app.post("/encryption/regenerate", requireAdmin, async (req, res) => {
try {
apiLogger.warn("System JWT secret regenerated via API", {
@@ -429,6 +532,20 @@ app.post("/encryption/regenerate", requireAdmin, async (req, res) => {
}
});
/**
* @openapi
* /encryption/regenerate-jwt:
* post:
* summary: Regenerate JWT secret
* description: Regenerates the JWT secret. This will invalidate all existing JWT tokens.
* tags:
* - Encryption
* responses:
* 200:
* description: New JWT secret generated.
* 500:
* description: Failed to regenerate JWT secret.
*/
app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
try {
apiLogger.warn("JWT secret regenerated via API", {
@@ -449,6 +566,33 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
}
});
/**
* @openapi
* /database/export:
* post:
* summary: Export user data
* description: Exports the user's data as a SQLite database file.
* tags:
* - Database
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* password:
* type: string
* responses:
* 200:
* description: User data exported successfully.
* 400:
* description: Password required for export.
* 401:
* description: Invalid password.
* 500:
* description: Failed to export user data.
*/
app.post("/database/export", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -899,6 +1043,36 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /database/import:
* post:
* summary: Import user data
* description: Imports user data from a SQLite database file.
* tags:
* - Database
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* file:
* type: string
* format: binary
* password:
* type: string
* responses:
* 200:
* description: Incremental import completed successfully.
* 400:
* description: No file uploaded or password required for import.
* 401:
* description: Invalid password.
* 500:
* description: Failed to import SQLite data.
*/
app.post(
"/database/import",
authenticateJWT,
@@ -1363,6 +1537,31 @@ app.post(
},
);
/**
* @openapi
* /database/export/preview:
* post:
* summary: Preview user data export
* description: Generates a preview of the user data export, including statistics about the data.
* tags:
* - Database
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* scope:
* type: string
* includeCredentials:
* type: boolean
* responses:
* 200:
* description: Export preview generated successfully.
* 500:
* description: Failed to generate export preview.
*/
app.post("/database/export/preview", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -1398,6 +1597,33 @@ app.post("/database/export/preview", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /database/restore:
* post:
* summary: Restore database from backup
* description: Restores the database from an encrypted backup file.
* tags:
* - Database
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* backupPath:
* type: string
* targetPath:
* type: string
* responses:
* 200:
* description: Database restored successfully.
* 400:
* description: Backup path is required or invalid encrypted backup file.
* 500:
* description: Database restore failed.
*/
app.post("/database/restore", requireAdmin, async (req, res) => {
try {
const { backupPath, targetPath } = req.body;
@@ -1479,6 +1705,20 @@ async function initializeSecurity() {
}
}
/**
* @openapi
* /database/migration/status:
* get:
* summary: Get database migration status
* description: Returns the status of the database migration.
* tags:
* - Database
* responses:
* 200:
* description: Migration status.
* 500:
* description: Failed to get migration status.
*/
app.get(
"/database/migration/status",
authenticateJWT,
@@ -1532,6 +1772,20 @@ app.get(
},
);
/**
* @openapi
* /database/migration/history:
* get:
* summary: Get database migration history
* description: Returns the history of database migrations.
* tags:
* - Database
* responses:
* 200:
* description: Migration history.
* 500:
* description: Failed to get migration history.
*/
app.get(
"/database/migration/history",
authenticateJWT,

View File

@@ -671,6 +671,13 @@ const migrateSchema = () => {
`);
} catch (createError) {
databaseLogger.warn("Failed to create network_topology table", {
operation: "schema_migration",
error: createError,
});
}
}
try {
sqlite.prepare("SELECT id FROM host_access LIMIT 1").get();
} catch {
try {

View File

@@ -301,6 +301,14 @@ export const networkTopology = sqliteTable("network_topology", {
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
topology: text("topology"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
export const hostAccess = sqliteTable("host_access", {
id: integer("id").primaryKey({ autoIncrement: true }),
hostId: integer("host_id")

View File

@@ -99,8 +99,20 @@ const router = express.Router();
const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
// Route: Get alerts for the authenticated user (excluding dismissed ones)
// GET /alerts
/**
* @openapi
* /alerts:
* get:
* summary: Get active alerts
* description: Fetches active alerts for the authenticated user, excluding those that have been dismissed.
* tags:
* - Alerts
* responses:
* 200:
* description: A list of active alerts.
* 500:
* description: Failed to fetch alerts.
*/
router.get("/", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -131,8 +143,33 @@ router.get("/", authenticateJWT, async (req, res) => {
}
});
// Route: Dismiss an alert for the authenticated user
// POST /alerts/dismiss
/**
* @openapi
* /alerts/dismiss:
* post:
* summary: Dismiss an alert
* description: Marks an alert as dismissed for the authenticated user.
* tags:
* - Alerts
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* alertId:
* type: string
* responses:
* 200:
* description: Alert dismissed successfully.
* 400:
* description: Alert ID is required.
* 409:
* description: Alert already dismissed.
* 500:
* description: Failed to dismiss alert.
*/
router.post("/dismiss", authenticateJWT, async (req, res) => {
try {
const { alertId } = req.body;
@@ -170,8 +207,20 @@ router.post("/dismiss", authenticateJWT, async (req, res) => {
}
});
// Route: Get dismissed alerts for a user
// GET /alerts/dismissed/:userId
/**
* @openapi
* /alerts/dismissed:
* get:
* summary: Get dismissed alerts
* description: Fetches a list of alerts that have been dismissed by the authenticated user.
* tags:
* - Alerts
* responses:
* 200:
* description: A list of dismissed alerts.
* 500:
* description: Failed to fetch dismissed alerts.
*/
router.get("/dismissed", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
@@ -194,8 +243,33 @@ router.get("/dismissed", authenticateJWT, async (req, res) => {
}
});
// Route: Undismiss an alert for the authenticated user (remove from dismissed list)
// DELETE /alerts/dismiss
/**
* @openapi
* /alerts/dismiss:
* delete:
* summary: Undismiss an alert
* description: Removes an alert from the dismissed list for the authenticated user.
* tags:
* - Alerts
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* alertId:
* type: string
* responses:
* 200:
* description: Alert undismissed successfully.
* 400:
* description: Alert ID is required.
* 404:
* description: Dismissed alert not found.
* 500:
* description: Failed to undismiss alert.
*/
router.delete("/dismiss", authenticateJWT, async (req, res) => {
try {
const { alertId } = req.body;

View File

@@ -84,8 +84,52 @@ const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
// Create a new credential
// POST /credentials
/**
* @openapi
* /credentials:
* post:
* summary: Create a new credential
* description: Creates a new SSH credential for the authenticated user.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* folder:
* type: string
* tags:
* type: array
* items:
* type: string
* authType:
* type: string
* enum: [password, key]
* username:
* type: string
* password:
* type: string
* key:
* type: string
* keyPassword:
* type: string
* keyType:
* type: string
* responses:
* 201:
* description: Credential created successfully.
* 400:
* description: Invalid request body.
* 500:
* description: Failed to create credential.
*/
router.post(
"/",
authenticateJWT,
@@ -231,8 +275,22 @@ router.post(
},
);
// Get all credentials for the authenticated user
// GET /credentials
/**
* @openapi
* /credentials:
* get:
* summary: Get all credentials
* description: Retrieves all SSH credentials for the authenticated user.
* tags:
* - Credentials
* responses:
* 200:
* description: A list of credentials.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch credentials.
*/
router.get(
"/",
authenticateJWT,
@@ -264,8 +322,22 @@ router.get(
},
);
// Get all unique credential folders for the authenticated user
// GET /credentials/folders
/**
* @openapi
* /credentials/folders:
* get:
* summary: Get credential folders
* description: Retrieves all unique credential folders for the authenticated user.
* tags:
* - Credentials
* responses:
* 200:
* description: A list of folder names.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch credential folders.
*/
router.get(
"/folders",
authenticateJWT,
@@ -302,8 +374,30 @@ router.get(
},
);
// Get a specific credential by ID (with plain text secrets)
// GET /credentials/:id
/**
* @openapi
* /credentials/{id}:
* get:
* summary: Get a specific credential
* description: Retrieves a specific credential by its ID, including secrets.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The requested credential.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to fetch credential.
*/
router.get(
"/:id",
authenticateJWT,
@@ -366,8 +460,41 @@ router.get(
},
);
// Update a credential
// PUT /credentials/:id
/**
* @openapi
* /credentials/{id}:
* put:
* summary: Update a credential
* description: Updates a specific credential by its ID.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* responses:
* 200:
* description: The updated credential.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to update credential.
*/
router.put(
"/:id",
authenticateJWT,
@@ -510,8 +637,30 @@ router.put(
},
);
// Delete a credential
// DELETE /credentials/:id
/**
* @openapi
* /credentials/{id}:
* delete:
* summary: Delete a credential
* description: Deletes a specific credential by its ID.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Credential deleted successfully.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to delete credential.
*/
router.delete(
"/:id",
authenticateJWT,
@@ -626,8 +775,35 @@ router.delete(
},
);
// Apply a credential to an SSH host (for quick application)
// POST /credentials/:id/apply-to-host/:hostId
/**
* @openapi
* /credentials/{id}/apply-to-host/{hostId}:
* post:
* summary: Apply a credential to a host
* description: Applies a credential to an SSH host for quick application.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Credential applied to host successfully.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to apply credential to host.
*/
router.post(
"/:id/apply-to-host/:hostId",
authenticateJWT,
@@ -705,8 +881,28 @@ router.post(
},
);
// Get hosts using a specific credential
// GET /credentials/:id/hosts
/**
* @openapi
* /credentials/{id}/hosts:
* get:
* summary: Get hosts using a credential
* description: Retrieves a list of hosts that are using a specific credential.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of hosts.
* 400:
* description: Invalid request.
* 500:
* description: Failed to fetch hosts using credential.
*/
router.get(
"/:id/hosts",
authenticateJWT,
@@ -800,8 +996,33 @@ function formatSSHHostOutput(
};
}
// Rename a credential folder
// PUT /credentials/folders/rename
/**
* @openapi
* /credentials/folders/rename:
* put:
* summary: Rename a credential folder
* description: Renames a credential folder for the authenticated user.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* oldName:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Folder renamed successfully.
* 400:
* description: Both oldName and newName are required.
* 500:
* description: Failed to rename folder.
*/
router.put(
"/folders/rename",
authenticateJWT,
@@ -840,8 +1061,33 @@ router.put(
},
);
// Detect SSH key type endpoint
// POST /credentials/detect-key-type
/**
* @openapi
* /credentials/detect-key-type:
* post:
* summary: Detect SSH key type
* description: Detects the type of an SSH private key.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* privateKey:
* type: string
* keyPassword:
* type: string
* responses:
* 200:
* description: Key type detection result.
* 400:
* description: Private key is required.
* 500:
* description: Failed to detect key type.
*/
router.post(
"/detect-key-type",
authenticateJWT,
@@ -874,8 +1120,31 @@ router.post(
},
);
// Detect SSH public key type endpoint
// POST /credentials/detect-public-key-type
/**
* @openapi
* /credentials/detect-public-key-type:
* post:
* summary: Detect SSH public key type
* description: Detects the type of an SSH public key.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* publicKey:
* type: string
* responses:
* 200:
* description: Key type detection result.
* 400:
* description: Public key is required.
* 500:
* description: Failed to detect public key type.
*/
router.post(
"/detect-public-key-type",
authenticateJWT,
@@ -909,8 +1178,35 @@ router.post(
},
);
// Validate SSH key pair endpoint
// POST /credentials/validate-key-pair
/**
* @openapi
* /credentials/validate-key-pair:
* post:
* summary: Validate SSH key pair
* description: Validates if a given SSH private key and public key match.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* privateKey:
* type: string
* publicKey:
* type: string
* keyPassword:
* type: string
* responses:
* 200:
* description: Key pair validation result.
* 400:
* description: Private key and public key are required.
* 500:
* description: Failed to validate key pair.
*/
router.post(
"/validate-key-pair",
authenticateJWT,
@@ -953,8 +1249,32 @@ router.post(
},
);
// Generate new SSH key pair endpoint
// POST /credentials/generate-key-pair
/**
* @openapi
* /credentials/generate-key-pair:
* post:
* summary: Generate new SSH key pair
* description: Generates a new SSH key pair.
* tags:
* - Credentials
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* keyType:
* type: string
* keySize:
* type: integer
* passphrase:
* type: string
* responses:
* 200:
* description: The new key pair.
* 500:
* description: Failed to generate SSH key pair.
*/
router.post(
"/generate-key-pair",
authenticateJWT,
@@ -996,8 +1316,33 @@ router.post(
},
);
// Generate public key from private key endpoint
// POST /credentials/generate-public-key
/**
* @openapi
* /credentials/generate-public-key:
* post:
* summary: Generate public key from private key
* description: Generates a public key from a given private key.
* tags:
* - Credentials
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* privateKey:
* type: string
* keyPassword:
* type: string
* responses:
* 200:
* description: The generated public key.
* 400:
* description: Private key is required.
* 500:
* description: Failed to generate public key.
*/
router.post(
"/generate-public-key",
authenticateJWT,
@@ -1283,7 +1628,7 @@ async function deploySSHKeyToHost(
.replace(/'/g, "'\\''");
conn.exec(
`printf '%s\\n' '${escapedKey} ${credData.name}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
`printf '%s\n' '${escapedKey} ${credData.name}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
(err, stream) => {
if (err) {
clearTimeout(addTimeout);
@@ -1502,8 +1847,41 @@ async function deploySSHKeyToHost(
});
}
// Deploy SSH Key to Host endpoint
// POST /credentials/:id/deploy-to-host
/**
* @openapi
* /credentials/{id}/deploy-to-host:
* post:
* summary: Deploy SSH key to a host
* description: Deploys an SSH public key to a target host's authorized_keys file.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* targetHostId:
* type: integer
* responses:
* 200:
* description: SSH key deployed successfully.
* 400:
* description: Credential ID and target host ID are required.
* 401:
* description: Authentication required.
* 404:
* description: Credential or target host not found.
* 500:
* description: Failed to deploy SSH key.
*/
router.post(
"/:id/deploy-to-host",
authenticateJWT,

View File

@@ -9,6 +9,22 @@ const router = express.Router();
const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
/**
* @openapi
* /network-topology:
* get:
* summary: Get network topology
* description: Retrieves the network topology for the authenticated user.
* tags:
* - Network Topology
* responses:
* 200:
* description: The network topology.
* 401:
* description: User not authenticated.
* 500:
* description: Failed to fetch network topology.
*/
router.get(
"/",
authenticateJWT,
@@ -24,7 +40,7 @@ router.get(
.select()
.from(networkTopology)
.where(eq(networkTopology.userId, userId));
if (result.length > 0) {
const topologyStr = result[0].topology;
const topology = topologyStr ? JSON.parse(topologyStr) : null;
@@ -34,11 +50,43 @@ router.get(
}
} catch (error) {
console.error("Error fetching network topology:", error);
return res.status(500).json({ error: "Failed to fetch network topology", details: (error as Error).message });
return res
.status(500)
.json({
error: "Failed to fetch network topology",
details: (error as Error).message,
});
}
},
);
/**
* @openapi
* /network-topology:
* post:
* summary: Save network topology
* description: Saves the network topology for the authenticated user.
* tags:
* - Network Topology
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* topology:
* type: object
* responses:
* 200:
* description: Network topology saved successfully.
* 400:
* description: Topology data is required.
* 401:
* description: User not authenticated.
* 500:
* description: Failed to save network topology.
*/
router.post(
"/",
authenticateJWT,
@@ -55,15 +103,16 @@ router.post(
}
const db = getDb();
// Ensure topology is a string
const topologyStr = typeof topology === 'string' ? topology : JSON.stringify(topology);
const topologyStr =
typeof topology === "string" ? topology : JSON.stringify(topology);
const existing = await db
.select()
.from(networkTopology)
.where(eq(networkTopology.userId, userId));
if (existing.length > 0) {
// Update existing record
await db
@@ -76,11 +125,16 @@ router.post(
.insert(networkTopology)
.values({ userId, topology: topologyStr });
}
return res.json({ success: true });
} catch (error) {
console.error("Error saving network topology:", error);
return res.status(500).json({ error: "Failed to save network topology", details: (error as Error).message });
return res
.status(500)
.json({
error: "Failed to save network topology",
details: (error as Error).message,
});
}
},
);

View File

@@ -27,8 +27,51 @@ function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
//Share a host with a user or role
//POST /rbac/host/:id/share
/**
* @openapi
* /rbac/host/{id}/share:
* post:
* summary: Share a host
* description: Shares a host with a user or a role.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* targetType:
* type: string
* enum: [user, role]
* targetUserId:
* type: string
* targetRoleId:
* type: integer
* durationHours:
* type: number
* permissionLevel:
* type: string
* enum: [view]
* responses:
* 200:
* description: Host shared successfully.
* 400:
* description: Invalid request body.
* 403:
* description: Not host owner.
* 404:
* description: Target user or role not found.
* 500:
* description: Failed to share host.
*/
router.post(
"/host/:id/share",
authenticateJWT,
@@ -227,8 +270,35 @@ router.post(
},
);
// Revoke host access
// DELETE /rbac/host/:id/access/:accessId
/**
* @openapi
* /rbac/host/{id}/access/{accessId}:
* delete:
* summary: Revoke host access
* description: Revokes a user's or role's access to a host.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* - in: path
* name: accessId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Access revoked successfully.
* 400:
* description: Invalid ID.
* 403:
* description: Not host owner.
* 500:
* description: Failed to revoke access.
*/
router.delete(
"/host/:id/access/:accessId",
authenticateJWT,
@@ -267,8 +337,30 @@ router.delete(
},
);
// Get host access list
// GET /rbac/host/:id/access
/**
* @openapi
* /rbac/host/{id}/access:
* get:
* summary: Get host access list
* description: Retrieves the list of users and roles that have access to a host.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The access list for the host.
* 400:
* description: Invalid host ID.
* 403:
* description: Not host owner.
* 500:
* description: Failed to get access list.
*/
router.get(
"/host/:id/access",
authenticateJWT,
@@ -338,8 +430,20 @@ router.get(
},
);
// Get user's shared hosts (hosts shared WITH this user)
// GET /rbac/shared-hosts
/**
* @openapi
* /rbac/shared-hosts:
* get:
* summary: Get shared hosts
* description: Retrieves the list of hosts that have been shared with the authenticated user.
* tags:
* - RBAC
* responses:
* 200:
* description: A list of shared hosts.
* 500:
* description: Failed to get shared hosts.
*/
router.get(
"/shared-hosts",
authenticateJWT,
@@ -385,8 +489,20 @@ router.get(
},
);
// Get all roles
// GET /rbac/roles
/**
* @openapi
* /rbac/roles:
* get:
* summary: Get all roles
* description: Retrieves a list of all roles.
* tags:
* - RBAC
* responses:
* 200:
* description: A list of roles.
* 500:
* description: Failed to get roles.
*/
router.get(
"/roles",
authenticateJWT,
@@ -413,8 +529,20 @@ router.get(
},
);
// Get all roles
// GET /rbac/roles
/**
* @openapi
* /rbac/roles:
* get:
* summary: Get all roles
* description: Retrieves a list of all roles.
* tags:
* - RBAC
* responses:
* 200:
* description: A list of roles.
* 500:
* description: Failed to get roles.
*/
router.get(
"/roles",
authenticateJWT,
@@ -443,8 +571,37 @@ router.get(
},
);
// Create new role
// POST /rbac/roles
/**
* @openapi
* /rbac/roles:
* post:
* summary: Create a new role
* description: Creates a new role.
* tags:
* - RBAC
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* displayName:
* type: string
* description:
* type: string
* responses:
* 201:
* description: Role created successfully.
* 400:
* description: Invalid request body.
* 409:
* description: A role with this name already exists.
* 500:
* description: Failed to create role.
*/
router.post(
"/roles",
authenticateJWT,
@@ -503,8 +660,41 @@ router.post(
},
);
// Update role
// PUT /rbac/roles/:id
/**
* @openapi
* /rbac/roles/{id}:
* put:
* summary: Update a role
* description: Updates a role by its ID.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* displayName:
* type: string
* description:
* type: string
* responses:
* 200:
* description: Role updated successfully.
* 400:
* description: Invalid request body or role ID.
* 404:
* description: Role not found.
* 500:
* description: Failed to update role.
*/
router.put(
"/roles/:id",
authenticateJWT,
@@ -570,8 +760,32 @@ router.put(
},
);
// Delete role
// DELETE /rbac/roles/:id
/**
* @openapi
* /rbac/roles/{id}:
* delete:
* summary: Delete a role
* description: Deletes a role by its ID.
* tags:
* - RBAC
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Role deleted successfully.
* 400:
* description: Invalid role ID.
* 403:
* description: Cannot delete system roles.
* 404:
* description: Role not found.
* 500:
* description: Failed to delete role.
*/
router.delete(
"/roles/:id",
authenticateJWT,
@@ -634,8 +848,43 @@ router.delete(
},
);
// Assign role to user
// POST /rbac/users/:userId/roles
/**
* @openapi
* /rbac/users/{userId}/roles:
* post:
* summary: Assign a role to a user
* description: Assigns a role to a user.
* tags:
* - RBAC
* parameters:
* - in: path
* name: userId
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* roleId:
* type: integer
* responses:
* 200:
* description: Role assigned successfully.
* 400:
* description: Role ID is required.
* 403:
* description: System roles cannot be manually assigned.
* 404:
* description: User or role not found.
* 409:
* description: Role already assigned.
* 500:
* description: Failed to assign role.
*/
router.post(
"/users/:userId/roles",
authenticateJWT,
@@ -746,8 +995,37 @@ router.post(
},
);
// Remove role from user
// DELETE /rbac/users/:userId/roles/:roleId
/**
* @openapi
* /rbac/users/{userId}/roles/{roleId}:
* delete:
* summary: Remove a role from a user
* description: Removes a role from a user.
* tags:
* - RBAC
* parameters:
* - in: path
* name: userId
* required: true
* schema:
* type: string
* - in: path
* name: roleId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Role removed successfully.
* 400:
* description: Invalid role ID.
* 403:
* description: System roles cannot be removed.
* 404:
* description: Role not found.
* 500:
* description: Failed to remove role.
*/
router.delete(
"/users/:userId/roles/:roleId",
authenticateJWT,
@@ -805,8 +1083,28 @@ router.delete(
},
);
// Get user's roles
// GET /rbac/users/:userId/roles
/**
* @openapi
* /rbac/users/{userId}/roles:
* get:
* summary: Get user's roles
* description: Retrieves a list of roles for a specific user.
* tags:
* - RBAC
* parameters:
* - in: path
* name: userId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: A list of roles.
* 403:
* description: Access denied.
* 500:
* description: Failed to get user roles.
*/
router.get(
"/users/:userId/roles",
authenticateJWT,

View File

@@ -17,8 +17,22 @@ const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
// Get all snippet folders
// GET /snippets/folders
/**
* @openapi
* /snippets/folders:
* get:
* summary: Get all snippet folders
* description: Retrieves all snippet folders for the authenticated user.
* tags:
* - Snippets
* responses:
* 200:
* description: A list of snippet folders.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch snippet folders.
*/
router.get(
"/folders",
authenticateJWT,
@@ -46,8 +60,37 @@ router.get(
},
);
// Create a new snippet folder
// POST /snippets/folders
/**
* @openapi
* /snippets/folders:
* post:
* summary: Create a new snippet folder
* description: Creates a new snippet folder for the authenticated user.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* color:
* type: string
* icon:
* type: string
* responses:
* 201:
* description: Snippet folder created successfully.
* 400:
* description: Folder name is required.
* 409:
* description: Folder with this name already exists.
* 500:
* description: Failed to create snippet folder.
*/
router.post(
"/folders",
authenticateJWT,
@@ -110,8 +153,41 @@ router.post(
},
);
// Update snippet folder metadata (color, icon)
// PUT /snippets/folders/:name/metadata
/**
* @openapi
* /snippets/folders/{name}/metadata:
* put:
* summary: Update snippet folder metadata
* description: Updates the metadata (color, icon) of a snippet folder.
* tags:
* - Snippets
* parameters:
* - in: path
* name: name
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* color:
* type: string
* icon:
* type: string
* responses:
* 200:
* description: Snippet folder metadata updated successfully.
* 400:
* description: Invalid request.
* 404:
* description: Folder not found.
* 500:
* description: Failed to update snippet folder metadata.
*/
router.put(
"/folders/:name/metadata",
authenticateJWT,
@@ -194,8 +270,37 @@ router.put(
},
);
// Rename snippet folder
// PUT /snippets/folders/rename
/**
* @openapi
* /snippets/folders/rename:
* put:
* summary: Rename a snippet folder
* description: Renames a snippet folder for the authenticated user.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* oldName:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Folder renamed successfully.
* 400:
* description: Invalid request.
* 404:
* description: Folder not found.
* 409:
* description: Folder with new name already exists.
* 500:
* description: Failed to rename snippet folder.
*/
router.put(
"/folders/rename",
authenticateJWT,
@@ -282,8 +387,28 @@ router.put(
},
);
// Delete snippet folder
// DELETE /snippets/folders/:name
/**
* @openapi
* /snippets/folders/{name}:
* delete:
* summary: Delete a snippet folder
* description: Deletes a snippet folder and moves its snippets to the root.
* tags:
* - Snippets
* parameters:
* - in: path
* name: name
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Snippet folder deleted successfully.
* 400:
* description: Invalid request.
* 500:
* description: Failed to delete snippet folder.
*/
router.delete(
"/folders/:name",
authenticateJWT,
@@ -338,8 +463,40 @@ router.delete(
},
);
// Reorder snippets (bulk update)
// PUT /snippets/reorder
/**
* @openapi
* /snippets/reorder:
* put:
* summary: Reorder snippets
* description: Bulk updates the order and folder of snippets.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* snippets:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* order:
* type: integer
* folder:
* type: string
* responses:
* 200:
* description: Snippets reordered successfully.
* 400:
* description: Invalid request.
* 500:
* description: Failed to reorder snippets.
*/
router.put(
"/reorder",
authenticateJWT,
@@ -405,8 +562,35 @@ router.put(
},
);
// Execute a snippet on a host
// POST /snippets/execute
/**
* @openapi
* /snippets/execute:
* post:
* summary: Execute a snippet on a host
* description: Executes a snippet on a specified host.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* snippetId:
* type: integer
* hostId:
* type: integer
* responses:
* 200:
* description: Snippet executed successfully.
* 400:
* description: Snippet ID and Host ID are required.
* 404:
* description: Snippet or host not found.
* 500:
* description: Failed to execute snippet.
*/
router.post(
"/execute",
authenticateJWT,
@@ -662,8 +846,22 @@ router.post(
},
);
// Get all snippets for the authenticated user
// GET /snippets
/**
* @openapi
* /snippets:
* get:
* summary: Get all snippets
* description: Retrieves all snippets for the authenticated user.
* tags:
* - Snippets
* responses:
* 200:
* description: A list of snippets.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch snippets.
*/
router.get(
"/",
authenticateJWT,
@@ -696,8 +894,30 @@ router.get(
},
);
// Get a specific snippet by ID
// GET /snippets/:id
/**
* @openapi
* /snippets/{id}:
* get:
* summary: Get a specific snippet
* description: Retrieves a specific snippet by its ID.
* tags:
* - Snippets
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The requested snippet.
* 400:
* description: Invalid request parameters.
* 404:
* description: Snippet not found.
* 500:
* description: Failed to fetch snippet.
*/
router.get(
"/:id",
authenticateJWT,
@@ -735,8 +955,39 @@ router.get(
},
);
// Create a new snippet
// POST /snippets
/**
* @openapi
* /snippets:
* post:
* summary: Create a new snippet
* description: Creates a new snippet for the authenticated user.
* tags:
* - Snippets
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* content:
* type: string
* description:
* type: string
* folder:
* type: string
* order:
* type: integer
* responses:
* 201:
* description: Snippet created successfully.
* 400:
* description: Name and content are required.
* 500:
* description: Failed to create snippet.
*/
router.post(
"/",
authenticateJWT,
@@ -806,8 +1057,47 @@ router.post(
},
);
// Update a snippet
// PUT /snippets/:id
/**
* @openapi
* /snippets/{id}:
* put:
* summary: Update a snippet
* description: Updates a specific snippet by its ID.
* tags:
* - Snippets
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* content:
* type: string
* description:
* type: string
* folder:
* type: string
* order:
* type: integer
* responses:
* 200:
* description: The updated snippet.
* 400:
* description: Invalid request.
* 404:
* description: Snippet not found.
* 500:
* description: Failed to update snippet.
*/
router.put(
"/:id",
authenticateJWT,
@@ -883,8 +1173,30 @@ router.put(
},
);
// Delete a snippet
// DELETE /snippets/:id
/**
* @openapi
* /snippets/{id}:
* delete:
* summary: Delete a snippet
* description: Deletes a specific snippet by its ID.
* tags:
* - Snippets
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Snippet deleted successfully.
* 400:
* description: Invalid request.
* 404:
* description: Snippet not found.
* 500:
* description: Failed to delete snippet.
*/
router.delete(
"/:id",
authenticateJWT,

View File

@@ -53,6 +53,22 @@ const permissionManager = PermissionManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
/**
* @openapi
* /ssh/db/host/internal:
* get:
* summary: Get internal SSH host data
* description: Returns internal SSH host data for autostart tunnels. Requires internal auth token.
* tags:
* - SSH
* responses:
* 200:
* description: A list of autostart hosts.
* 403:
* description: Forbidden.
* 500:
* description: Failed to fetch autostart SSH data.
*/
router.get("/db/host/internal", async (req: Request, res: Response) => {
try {
const internalToken = req.headers["x-internal-auth-token"];
@@ -135,6 +151,22 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
}
});
/**
* @openapi
* /ssh/db/host/internal/all:
* get:
* summary: Get all internal SSH host data
* description: Returns all internal SSH host data. Requires internal auth token.
* tags:
* - SSH
* responses:
* 200:
* description: A list of all hosts.
* 401:
* description: Invalid or missing internal authentication token.
* 500:
* description: Failed to fetch all hosts.
*/
router.get("/db/host/internal/all", async (req: Request, res: Response) => {
try {
const internalToken = req.headers["x-internal-auth-token"];
@@ -194,8 +226,22 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => {
}
});
// Route: Create SSH data (requires JWT)
// POST /ssh/host
/**
* @openapi
* /ssh/db/host:
* post:
* summary: Create SSH host
* description: Creates a new SSH host configuration.
* tags:
* - SSH
* responses:
* 200:
* description: Host created successfully.
* 400:
* description: Invalid SSH data.
* 500:
* description: Failed to save SSH data.
*/
router.post(
"/db/host",
authenticateJWT,
@@ -438,8 +484,32 @@ router.post(
},
);
// Route: Update SSH data (requires JWT)
// PUT /ssh/host/:id
/**
* @openapi
* /ssh/db/host/{id}:
* put:
* summary: Update SSH host
* description: Updates an existing SSH host configuration.
* tags:
* - SSH
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Host updated successfully.
* 400:
* description: Invalid SSH data.
* 403:
* description: Access denied.
* 404:
* description: Host not found.
* 500:
* description: Failed to update SSH data.
*/
router.put(
"/db/host/:id",
authenticateJWT,
@@ -785,8 +855,22 @@ router.put(
},
);
// Route: Get SSH data for the authenticated user (requires JWT)
// GET /ssh/host
/**
* @openapi
* /ssh/db/host:
* get:
* summary: Get all SSH hosts
* description: Retrieves all SSH hosts for the authenticated user.
* tags:
* - SSH
* responses:
* 200:
* description: A list of SSH hosts.
* 400:
* description: Invalid userId.
* 500:
* description: Failed to fetch SSH data.
*/
router.get(
"/db/host",
authenticateJWT,
@@ -966,8 +1050,30 @@ router.get(
},
);
// Route: Get SSH host by ID (requires JWT)
// GET /ssh/host/:id
/**
* @openapi
* /ssh/db/host/{id}:
* get:
* summary: Get SSH host by ID
* description: Retrieves a specific SSH host by its ID.
* tags:
* - SSH
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The requested SSH host.
* 400:
* description: Invalid userId or hostId.
* 404:
* description: SSH host not found.
* 500:
* description: Failed to fetch SSH host.
*/
router.get(
"/db/host/:id",
authenticateJWT,
@@ -1041,8 +1147,30 @@ router.get(
},
);
// Route: Export SSH host with decrypted credentials (requires data access)
// GET /ssh/db/host/:id/export
/**
* @openapi
* /ssh/db/host/{id}/export:
* get:
* summary: Export SSH host
* description: Exports a specific SSH host with decrypted credentials.
* tags:
* - SSH
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The exported SSH host.
* 400:
* description: Invalid userId or hostId.
* 404:
* description: SSH host not found.
* 500:
* description: Failed to export SSH host.
*/
router.get(
"/db/host/:id/export",
authenticateJWT,
@@ -1121,8 +1249,30 @@ router.get(
},
);
// Route: Delete SSH host by id (requires JWT)
// DELETE /ssh/host/:id
/**
* @openapi
* /ssh/db/host/{id}:
* delete:
* summary: Delete SSH host
* description: Deletes an SSH host by its ID.
* tags:
* - SSH
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: SSH host deleted successfully.
* 400:
* description: Invalid userId or id.
* 404:
* description: SSH host not found.
* 500:
* description: Failed to delete SSH host.
*/
router.delete(
"/db/host/:id",
authenticateJWT,
@@ -1237,8 +1387,28 @@ router.delete(
},
);
// Route: Get recent files (requires JWT)
// GET /ssh/file_manager/recent
/**
* @openapi
* /ssh/file_manager/recent:
* get:
* summary: Get recent files
* description: Retrieves a list of recent files for a specific host.
* tags:
* - SSH
* parameters:
* - in: query
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of recent files.
* 400:
* description: Invalid userId or hostId.
* 500:
* description: Failed to fetch recent files.
*/
router.get(
"/file_manager/recent",
authenticateJWT,
@@ -1279,8 +1449,35 @@ router.get(
},
);
// Route: Add recent file (requires JWT)
// POST /ssh/file_manager/recent
/**
* @openapi
* /ssh/file_manager/recent:
* post:
* summary: Add recent file
* description: Adds a file to the list of recent files for a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* name:
* type: string
* responses:
* 200:
* description: Recent file added.
* 400:
* description: Invalid data.
* 500:
* description: Failed to add recent file.
*/
router.post(
"/file_manager/recent",
authenticateJWT,
@@ -1328,8 +1525,33 @@ router.post(
},
);
// Route: Remove recent file (requires JWT)
// DELETE /ssh/file_manager/recent
/**
* @openapi
* /ssh/file_manager/recent:
* delete:
* summary: Remove recent file
* description: Removes a file from the list of recent files for a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* responses:
* 200:
* description: Recent file removed.
* 400:
* description: Invalid data.
* 500:
* description: Failed to remove recent file.
*/
router.delete(
"/file_manager/recent",
authenticateJWT,
@@ -1361,8 +1583,28 @@ router.delete(
},
);
// Route: Get pinned files (requires JWT)
// GET /ssh/file_manager/pinned
/**
* @openapi
* /ssh/file_manager/pinned:
* get:
* summary: Get pinned files
* description: Retrieves a list of pinned files for a specific host.
* tags:
* - SSH
* parameters:
* - in: query
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of pinned files.
* 400:
* description: Invalid userId or hostId.
* 500:
* description: Failed to fetch pinned files.
*/
router.get(
"/file_manager/pinned",
authenticateJWT,
@@ -1402,8 +1644,37 @@ router.get(
},
);
// Route: Add pinned file (requires JWT)
// POST /ssh/file_manager/pinned
/**
* @openapi
* /ssh/file_manager/pinned:
* post:
* summary: Add pinned file
* description: Adds a file to the list of pinned files for a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* name:
* type: string
* responses:
* 200:
* description: File pinned.
* 400:
* description: Invalid data.
* 409:
* description: File already pinned.
* 500:
* description: Failed to pin file.
*/
router.post(
"/file_manager/pinned",
authenticateJWT,
@@ -1448,8 +1719,33 @@ router.post(
},
);
// Route: Remove pinned file (requires JWT)
// DELETE /ssh/file_manager/pinned
/**
* @openapi
* /ssh/file_manager/pinned:
* delete:
* summary: Remove pinned file
* description: Removes a file from the list of pinned files for a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* responses:
* 200:
* description: Pinned file removed.
* 400:
* description: Invalid data.
* 500:
* description: Failed to remove pinned file.
*/
router.delete(
"/file_manager/pinned",
authenticateJWT,
@@ -1481,8 +1777,28 @@ router.delete(
},
);
// Route: Get shortcuts (requires JWT)
// GET /ssh/file_manager/shortcuts
/**
* @openapi
* /ssh/file_manager/shortcuts:
* get:
* summary: Get shortcuts
* description: Retrieves a list of shortcuts for a specific host.
* tags:
* - SSH
* parameters:
* - in: query
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of shortcuts.
* 400:
* description: Invalid userId or hostId.
* 500:
* description: Failed to fetch shortcuts.
*/
router.get(
"/file_manager/shortcuts",
authenticateJWT,
@@ -1522,8 +1838,37 @@ router.get(
},
);
// Route: Add shortcut (requires JWT)
// POST /ssh/file_manager/shortcuts
/**
* @openapi
* /ssh/file_manager/shortcuts:
* post:
* summary: Add shortcut
* description: Adds a shortcut for a specific host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* name:
* type: string
* responses:
* 200:
* description: Shortcut added.
* 400:
* description: Invalid data.
* 409:
* description: Shortcut already exists.
* 500:
* description: Failed to add shortcut.
*/
router.post(
"/file_manager/shortcuts",
authenticateJWT,
@@ -1568,8 +1913,33 @@ router.post(
},
);
// Route: Remove shortcut (requires JWT)
// DELETE /ssh/file_manager/shortcuts
/**
* @openapi
* /ssh/file_manager/shortcuts:
* delete:
* summary: Remove shortcut
* description: Removes a shortcut for a specific host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* path:
* type: string
* responses:
* 200:
* description: Shortcut removed.
* 400:
* description: Invalid data.
* 500:
* description: Failed to remove shortcut.
*/
router.delete(
"/file_manager/shortcuts",
authenticateJWT,
@@ -1601,8 +1971,28 @@ router.delete(
},
);
// Route: Get command history for a host
// GET /ssh/command-history/:hostId
/**
* @openapi
* /ssh/command-history/{hostId}:
* get:
* summary: Get command history
* description: Retrieves the command history for a specific host.
* tags:
* - SSH
* parameters:
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of commands.
* 400:
* description: Invalid userId or hostId.
* 500:
* description: Failed to fetch command history.
*/
router.get(
"/command-history/:hostId",
authenticateJWT,
@@ -1647,8 +2037,33 @@ router.get(
},
);
// Route: Delete command from history
// DELETE /ssh/command-history
/**
* @openapi
* /ssh/command-history:
* delete:
* summary: Delete command from history
* description: Deletes a specific command from the history of a host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* command:
* type: string
* responses:
* 200:
* description: Command deleted from history.
* 400:
* description: Invalid data.
* 500:
* description: Failed to delete command.
*/
router.delete(
"/command-history",
authenticateJWT,
@@ -1700,9 +2115,8 @@ async function resolveHostCredentials(
if (requestingUserId && requestingUserId !== ownerId) {
try {
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
host.id as number,
@@ -1790,8 +2204,33 @@ async function resolveHostCredentials(
}
}
// Route: Rename folder (requires JWT)
// PUT /ssh/db/folders/rename
/**
* @openapi
* /ssh/folders/rename:
* put:
* summary: Rename folder
* description: Renames a folder for SSH hosts and credentials.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* oldName:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Folder renamed successfully.
* 400:
* description: Old name and new name are required.
* 500:
* description: Failed to rename folder.
*/
router.put(
"/folders/rename",
authenticateJWT,
@@ -1865,8 +2304,22 @@ router.put(
},
);
// Route: Get all folders with metadata (requires JWT)
// GET /ssh/db/folders
/**
* @openapi
* /ssh/folders:
* get:
* summary: Get all folders
* description: Retrieves all folders for the authenticated user.
* tags:
* - SSH
* responses:
* 200:
* description: A list of folders.
* 400:
* description: Invalid user ID.
* 500:
* description: Failed to fetch folders.
*/
router.get("/folders", authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
@@ -1890,8 +2343,35 @@ router.get("/folders", authenticateJWT, async (req: Request, res: Response) => {
}
});
// Route: Update folder metadata (requires JWT)
// PUT /ssh/db/folders/metadata
/**
* @openapi
* /ssh/folders/metadata:
* put:
* summary: Update folder metadata
* description: Updates the metadata (color, icon) of a folder.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* color:
* type: string
* icon:
* type: string
* responses:
* 200:
* description: Folder metadata updated successfully.
* 400:
* description: Folder name is required.
* 500:
* description: Failed to update folder metadata.
*/
router.put(
"/folders/metadata",
authenticateJWT,
@@ -1944,8 +2424,28 @@ router.put(
},
);
// Route: Delete all hosts in folder (requires JWT)
// DELETE /ssh/db/folders/:name/hosts
/**
* @openapi
* /ssh/folders/{name}/hosts:
* delete:
* summary: Delete all hosts in folder
* description: Deletes all SSH hosts within a specific folder.
* tags:
* - SSH
* parameters:
* - in: path
* name: name
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Hosts deleted successfully.
* 400:
* description: Invalid folder name.
* 500:
* description: Failed to delete hosts in folder.
*/
router.delete(
"/folders/:name/hosts",
authenticateJWT,
@@ -2063,8 +2563,31 @@ router.delete(
},
);
// Route: Bulk import SSH hosts (requires JWT)
// POST /ssh/bulk-import
/**
* @openapi
* /ssh/bulk-import:
* post:
* summary: Bulk import SSH hosts
* description: Bulk imports multiple SSH hosts.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hosts:
* type: array
* items:
* type: object
* responses:
* 200:
* description: Import completed.
* 400:
* description: Invalid request body.
*/
router.post(
"/bulk-import",
authenticateJWT,
@@ -2221,8 +2744,33 @@ router.post(
},
);
// Route: Enable autostart for SSH configuration (requires JWT)
// POST /ssh/autostart/enable
/**
* @openapi
* /ssh/autostart/enable:
* post:
* summary: Enable autostart for SSH configuration
* description: Enables autostart for a specific SSH configuration.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sshConfigId:
* type: number
* responses:
* 200:
* description: AutoStart enabled successfully.
* 400:
* description: Valid sshConfigId is required.
* 404:
* description: SSH configuration not found.
* 500:
* description: Internal server error.
*/
router.post(
"/autostart/enable",
authenticateJWT,
@@ -2375,8 +2923,31 @@ router.post(
},
);
// Route: Disable autostart for SSH configuration (requires JWT)
// DELETE /ssh/autostart/disable
/**
* @openapi
* /ssh/autostart/disable:
* delete:
* summary: Disable autostart for SSH configuration
* description: Disables autostart for a specific SSH configuration.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sshConfigId:
* type: number
* responses:
* 200:
* description: AutoStart disabled successfully.
* 400:
* description: Valid sshConfigId is required.
* 500:
* description: Internal server error.
*/
router.delete(
"/autostart/disable",
authenticateJWT,
@@ -2421,8 +2992,20 @@ router.delete(
},
);
// Route: Get autostart status for user's SSH configurations (requires JWT)
// GET /ssh/autostart/status
/**
* @openapi
* /ssh/autostart/status:
* get:
* summary: Get autostart status
* description: Retrieves the autostart status for the user's SSH configurations.
* tags:
* - SSH
* responses:
* 200:
* description: A list of autostart configurations.
* 500:
* description: Internal server error.
*/
router.get(
"/autostart/status",
authenticateJWT,

View File

@@ -17,8 +17,33 @@ const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();
// Save command to history
// POST /terminal/command_history
/**
* @openapi
* /terminal/command_history:
* post:
* summary: Save command to history
* description: Saves a command to the command history for a specific host.
* tags:
* - Terminal
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* command:
* type: string
* responses:
* 201:
* description: Command saved successfully.
* 400:
* description: Missing required parameters.
* 500:
* description: Failed to save command.
*/
router.post(
"/command_history",
authenticateJWT,
@@ -59,8 +84,28 @@ router.post(
},
);
// Get command history for a specific host
// GET /terminal/command_history/:hostId
/**
* @openapi
* /terminal/command_history/{hostId}:
* get:
* summary: Get command history
* description: Retrieves the command history for a specific host.
* tags:
* - Terminal
* parameters:
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: A list of commands.
* 400:
* description: Invalid request parameters.
* 500:
* description: Failed to fetch history.
*/
router.get(
"/command_history/:hostId",
authenticateJWT,
@@ -107,8 +152,33 @@ router.get(
},
);
// Delete a specific command from history
// POST /terminal/command_history/delete
/**
* @openapi
* /terminal/command_history/delete:
* post:
* summary: Delete a specific command from history
* description: Deletes a specific command from the history of a host.
* tags:
* - Terminal
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* command:
* type: string
* responses:
* 200:
* description: Command deleted successfully.
* 400:
* description: Missing required parameters.
* 500:
* description: Failed to delete command.
*/
router.post(
"/command_history/delete",
authenticateJWT,
@@ -150,8 +220,28 @@ router.post(
},
);
// Clear command history for a specific host (optional feature)
// DELETE /terminal/command_history/:hostId
/**
* @openapi
* /terminal/command_history/{hostId}:
* delete:
* summary: Clear command history
* description: Clears the entire command history for a specific host.
* tags:
* - Terminal
* parameters:
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Command history cleared successfully.
* 400:
* description: Invalid request.
* 500:
* description: Failed to clear history.
*/
router.delete(
"/command_history/:hostId",
authenticateJWT,

File diff suppressed because it is too large Load Diff

View File

@@ -365,7 +365,34 @@ app.use(express.urlencoded({ limit: "100mb", extended: true }));
const authManager = AuthManager.getInstance();
app.use(authManager.createAuthMiddleware());
// POST /docker/ssh/connect - Establish SSH session
/**
* @openapi
* /docker/ssh/connect:
* post:
* summary: Establish SSH session for Docker
* description: Establishes an SSH session to a host for Docker operations.
* tags:
* - Docker
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* responses:
* 200:
* description: SSH connection established.
* 400:
* description: Missing sessionId or hostId.
* 401:
* description: Authentication required.
* 403:
* description: Docker is not enabled for this host.
* 404:
* description: Host not found.
* 500:
* description: SSH connection failed.
*/
app.post("/docker/ssh/connect", async (req, res) => {
const {
sessionId,
@@ -929,7 +956,29 @@ app.post("/docker/ssh/connect", async (req, res) => {
}
});
// POST /docker/ssh/disconnect - Close SSH session
/**
* @openapi
* /docker/ssh/disconnect:
* post:
* summary: Disconnect SSH session
* description: Closes an active SSH session for Docker operations.
* tags:
* - Docker
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* responses:
* 200:
* description: SSH session disconnected.
* 400:
* description: Session ID is required.
*/
app.post("/docker/ssh/disconnect", async (req, res) => {
const { sessionId } = req.body;
@@ -942,7 +991,35 @@ app.post("/docker/ssh/disconnect", async (req, res) => {
res.json({ success: true, message: "SSH session disconnected" });
});
// POST /docker/ssh/connect-totp - Verify TOTP and complete connection
/**
* @openapi
* /docker/ssh/connect-totp:
* post:
* summary: Verify TOTP and complete connection
* description: Verifies the TOTP code and completes the SSH connection.
* tags:
* - Docker
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* totpCode:
* type: string
* responses:
* 200:
* description: TOTP verified, SSH connection established.
* 400:
* description: Session ID and TOTP code required.
* 401:
* description: Invalid TOTP code.
* 404:
* description: TOTP session expired.
*/
app.post("/docker/ssh/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body;
const userId = (req as any).userId;
@@ -1105,7 +1182,29 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
session.finish(responses);
});
// POST /docker/ssh/keepalive - Keep session alive
/**
* @openapi
* /docker/ssh/keepalive:
* post:
* summary: Keep SSH session alive
* description: Keeps an active SSH session alive.
* tags:
* - Docker
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* responses:
* 200:
* description: Session keepalive successful.
* 400:
* description: Session ID is required or session not found.
*/
app.post("/docker/ssh/keepalive", async (req, res) => {
const { sessionId } = req.body;
@@ -1133,7 +1232,26 @@ app.post("/docker/ssh/keepalive", async (req, res) => {
});
});
// GET /docker/ssh/status - Check session status
/**
* @openapi
* /docker/ssh/status:
* get:
* summary: Check SSH session status
* description: Checks the status of an active SSH session.
* tags:
* - Docker
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Session status.
* 400:
* description: Session ID is required.
*/
app.get("/docker/ssh/status", async (req, res) => {
const sessionId = req.query.sessionId as string;
@@ -1146,7 +1264,28 @@ app.get("/docker/ssh/status", async (req, res) => {
res.json({ success: true, connected: isConnected });
});
// GET /docker/validate/:sessionId - Validate Docker availability
/**
* @openapi
* /docker/validate/{sessionId}:
* get:
* summary: Validate Docker availability
* description: Validates if Docker is available on the host.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Docker availability status.
* 400:
* description: SSH session not found or not connected.
* 500:
* description: Validation failed.
*/
app.get("/docker/validate/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const userId = (req as any).userId;
@@ -1236,7 +1375,32 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
}
});
// GET /docker/containers/:sessionId - List all containers
/**
* @openapi
* /docker/containers/{sessionId}:
* get:
* summary: List all containers
* description: Lists all Docker containers on the host.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: all
* schema:
* type: boolean
* responses:
* 200:
* description: A list of containers.
* 400:
* description: SSH session not found or not connected.
* 500:
* description: Failed to list containers.
*/
app.get("/docker/containers/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const all = req.query.all !== "false";
@@ -1297,7 +1461,35 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
}
});
// GET /docker/containers/:sessionId/:containerId - Get container details
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}:
* get:
* summary: Get container details
* description: Retrieves detailed information about a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container details.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to get container details.
*/
app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
@@ -1356,7 +1548,35 @@ app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
}
});
// POST /docker/containers/:sessionId/:containerId/start - Start container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/start:
* post:
* summary: Start container
* description: Starts a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container started successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to start container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/start",
async (req, res) => {
@@ -1414,7 +1634,35 @@ app.post(
},
);
// POST /docker/containers/:sessionId/:containerId/stop - Stop container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/stop:
* post:
* summary: Stop container
* description: Stops a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container stopped successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to stop container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/stop",
async (req, res) => {
@@ -1472,7 +1720,35 @@ app.post(
},
);
// POST /docker/containers/:sessionId/:containerId/restart - Restart container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/restart:
* post:
* summary: Restart container
* description: Restarts a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container restarted successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to restart container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/restart",
async (req, res) => {
@@ -1530,7 +1806,35 @@ app.post(
},
);
// POST /docker/containers/:sessionId/:containerId/pause - Pause container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/pause:
* post:
* summary: Pause container
* description: Pauses a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container paused successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to pause container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/pause",
async (req, res) => {
@@ -1588,7 +1892,35 @@ app.post(
},
);
// POST /docker/containers/:sessionId/:containerId/unpause - Unpause container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/unpause:
* post:
* summary: Unpause container
* description: Unpauses a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container unpaused successfully.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to unpause container.
*/
app.post(
"/docker/containers/:sessionId/:containerId/unpause",
async (req, res) => {
@@ -1646,7 +1978,39 @@ app.post(
},
);
// DELETE /docker/containers/:sessionId/:containerId/remove - Remove container
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/remove:
* delete:
* summary: Remove container
* description: Removes a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* - in: query
* name: force
* schema:
* type: boolean
* responses:
* 200:
* description: Container removed successfully.
* 400:
* description: SSH session not found or not connected, or cannot remove a running container.
* 404:
* description: Container not found.
* 500:
* description: Failed to remove container.
*/
app.delete(
"/docker/containers/:sessionId/:containerId/remove",
async (req, res) => {
@@ -1718,7 +2082,51 @@ app.delete(
},
);
// GET /docker/containers/:sessionId/:containerId/logs - Get container logs
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/logs:
* get:
* summary: Get container logs
* description: Retrieves logs for a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* - in: query
* name: tail
* schema:
* type: integer
* - in: query
* name: timestamps
* schema:
* type: boolean
* - in: query
* name: since
* schema:
* type: string
* - in: query
* name: until
* schema:
* type: string
* responses:
* 200:
* description: Container logs.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to get container logs.
*/
app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
const { sessionId, containerId } = req.params;
const tail = req.query.tail ? parseInt(req.query.tail as string) : 100;
@@ -1795,7 +2203,35 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
}
});
// GET /docker/containers/:sessionId/:containerId/stats - Get container stats
/**
* @openapi
* /docker/containers/{sessionId}/{containerId}/stats:
* get:
* summary: Get container stats
* description: Retrieves stats for a specific container.
* tags:
* - Docker
* parameters:
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* - in: path
* name: containerId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Container stats.
* 400:
* description: SSH session not found or not connected.
* 404:
* description: Container not found.
* 500:
* description: Failed to get container stats.
*/
app.get(
"/docker/containers/:sessionId/:containerId/stats",
async (req, res) => {

View File

@@ -416,6 +416,24 @@ function detectBinary(buffer: Buffer): boolean {
return nullBytes / sampleSize > 0.01;
}
/**
* @openapi
* /ssh/file_manager/ssh/connect:
* post:
* summary: Connect to SSH for file manager
* description: Establishes an SSH connection for file manager operations.
* tags:
* - File Manager
* responses:
* 200:
* description: SSH connection established.
* 400:
* description: Missing SSH connection parameters.
* 401:
* description: Authentication required.
* 500:
* description: SSH connection failed.
*/
app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
const {
sessionId,
@@ -986,6 +1004,26 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
}
});
/**
* @openapi
* /ssh/file_manager/ssh/connect-totp:
* post:
* summary: Verify TOTP and complete connection
* description: Verifies the TOTP code and completes the SSH connection for file manager.
* tags:
* - File Manager
* responses:
* 200:
* description: TOTP verified, SSH connection established.
* 400:
* description: Session ID and TOTP code required.
* 401:
* description: Invalid TOTP code or authentication required.
* 404:
* description: TOTP session expired.
* 408:
* description: TOTP session timeout.
*/
app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body;
@@ -1149,18 +1187,71 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
session.finish(responses);
});
/**
* @openapi
* /ssh/file_manager/ssh/disconnect:
* post:
* summary: Disconnect from SSH
* description: Closes an active SSH connection for file manager.
* tags:
* - File Manager
* responses:
* 200:
* description: SSH connection disconnected.
*/
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
const { sessionId } = req.body;
cleanupSession(sessionId);
res.json({ status: "success", message: "SSH connection disconnected" });
});
/**
* @openapi
* /ssh/file_manager/ssh/status:
* get:
* summary: Get SSH connection status
* description: Checks the status of an SSH connection for file manager.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: SSH connection status.
*/
app.get("/ssh/file_manager/ssh/status", (req, res) => {
const sessionId = req.query.sessionId as string;
const isConnected = !!sshSessions[sessionId]?.isConnected;
res.json({ status: "success", connected: isConnected });
});
/**
* @openapi
* /ssh/file_manager/ssh/keepalive:
* post:
* summary: Keep SSH session alive
* description: Keeps an active SSH session for file manager alive.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* responses:
* 200:
* description: Session keepalive successful.
* 400:
* description: Session ID is required or session not found.
*/
app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
const { sessionId } = req.body;
@@ -1188,6 +1279,33 @@ app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/listFiles:
* get:
* summary: List files in a directory
* description: Lists the files and directories in a given path on the remote host.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: A list of files and directories.
* 400:
* description: Session ID is required or SSH connection not established.
* 500:
* description: Failed to list files.
*/
app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
@@ -1387,6 +1505,33 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
trySFTP();
});
/**
* @openapi
* /ssh/file_manager/ssh/identifySymlink:
* get:
* summary: Identify symbolic link
* description: Identifies the target of a symbolic link.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Symbolic link information.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to identify symbolic link.
*/
app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
@@ -1454,6 +1599,35 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/readFile:
* get:
* summary: Read a file
* description: Reads the content of a file from the remote host.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The content of the file.
* 400:
* description: Missing required parameters or file too large.
* 404:
* description: File not found.
* 500:
* description: Failed to read file.
*/
app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
@@ -1592,6 +1766,35 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
);
});
/**
* @openapi
* /ssh/file_manager/ssh/writeFile:
* post:
* summary: Write to a file
* description: Writes content to a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* content:
* type: string
* responses:
* 200:
* description: File written successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to write file.
*/
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
const { sessionId, path: filePath, content } = req.body;
const sshConn = sshSessions[sessionId];
@@ -1737,10 +1940,15 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
if (err) {
fileLogger.error("Fallback write command failed:", err);
if (!res.headersSent) {
return res.status(500).json({
error: `Write failed: ${err.message}`,
toast: { type: "error", message: `Write failed: ${err.message}` },
});
return res
.status(500)
.json({
error: `Write failed: ${err.message}`,
toast: {
type: "error",
message: `Write failed: ${err.message}`,
},
});
}
return;
}
@@ -1803,6 +2011,37 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
trySFTP();
});
/**
* @openapi
* /ssh/file_manager/ssh/uploadFile:
* post:
* summary: Upload a file
* description: Uploads a file to the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* content:
* type: string
* fileName:
* type: string
* responses:
* 200:
* description: File uploaded successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to upload file.
*/
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
const { sessionId, path: filePath, content, fileName } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2103,6 +2342,37 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
trySFTP();
});
/**
* @openapi
* /ssh/file_manager/ssh/createFile:
* post:
* summary: Create a file
* description: Creates an empty file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* fileName:
* type: string
* responses:
* 200:
* description: File created successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to create file.
*/
app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
const { sessionId, path: filePath, fileName } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2204,6 +2474,37 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/createFolder:
* post:
* summary: Create a folder
* description: Creates a new folder on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* folderName:
* type: string
* responses:
* 200:
* description: Folder created successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to create folder.
*/
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
const { sessionId, path: folderPath, folderName } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2305,6 +2606,37 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/deleteItem:
* delete:
* summary: Delete a file or directory
* description: Deletes a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* isDirectory:
* type: boolean
* responses:
* 200:
* description: Item deleted successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to delete item.
*/
app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
const { sessionId, path: itemPath, isDirectory } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2407,6 +2739,37 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/renameItem:
* put:
* summary: Rename a file or directory
* description: Renames a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* oldPath:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Item renamed successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to rename item.
*/
app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
const { sessionId, oldPath, newName } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2515,6 +2878,39 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/moveItem:
* put:
* summary: Move a file or directory
* description: Moves a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* oldPath:
* type: string
* newPath:
* type: string
* responses:
* 200:
* description: Item moved successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 408:
* description: Move operation timed out.
* 500:
* description: Failed to move item.
*/
app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
const { sessionId, oldPath, newPath } = req.body;
const sshConn = sshSessions[sessionId];
@@ -2640,6 +3036,37 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/downloadFile:
* post:
* summary: Download a file
* description: Downloads a file from the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* hostId:
* type: integer
* userId:
* type: string
* responses:
* 200:
* description: The file content.
* 400:
* description: Missing required parameters or file too large.
* 500:
* description: Failed to download file.
*/
app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
const { sessionId, path: filePath, hostId, userId } = req.body;
@@ -2741,6 +3168,39 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/copyItem:
* post:
* summary: Copy a file or directory
* description: Copies a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* sourcePath:
* type: string
* targetDir:
* type: string
* hostId:
* type: integer
* userId:
* type: string
* responses:
* 200:
* description: Item copied successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to copy item.
*/
app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
const { sessionId, sourcePath, targetDir, hostId, userId } = req.body;
@@ -2904,6 +3364,33 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/executeFile:
* post:
* summary: Execute a file
* description: Executes a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* filePath:
* type: string
* responses:
* 200:
* description: File execution result.
* 400:
* description: Missing required parameters or SSH connection not available.
* 500:
* description: Failed to execute file.
*/
app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
const { sessionId, filePath } = req.body;
const sshConn = sshSessions[sessionId];
@@ -3002,6 +3489,37 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
});
});
/**
* @openapi
* /ssh/file_manager/ssh/changePermissions:
* post:
* summary: Change file permissions
* description: Changes the permissions of a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* permissions:
* type: string
* responses:
* 200:
* description: Permissions changed successfully.
* 400:
* description: Missing required parameters or SSH connection not available.
* 408:
* description: Permission change timed out.
* 500:
* description: Failed to change permissions.
*/
app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => {
const { sessionId, path, permissions } = req.body;
const sshConn = sshSessions[sessionId];
@@ -3319,8 +3837,39 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => {
});
});
// Route: Compress files/folders (requires JWT)
// POST /ssh/file_manager/ssh/compressFiles
/**
* @openapi
* /ssh/file_manager/ssh/compressFiles:
* post:
* summary: Compress files
* description: Compresses files and/or directories on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* paths:
* type: array
* items:
* type: string
* archiveName:
* type: string
* format:
* type: string
* responses:
* 200:
* description: Files compressed successfully.
* 400:
* description: Missing required parameters or unsupported compression format.
* 500:
* description: Failed to compress files.
*/
app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => {
const { sessionId, paths, archiveName, format } = req.body;

View File

@@ -1803,6 +1803,10 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
} catch (e) {
statsLogger.debug("Failed to collect ports metrics", {
operation: "ports_metrics_failed",
error: e instanceof Error ? e.message : String(e),
});
}
let firewall: {
type: "iptables" | "nftables" | "none";
status: "active" | "inactive" | "unknown";
@@ -1920,6 +1924,20 @@ function tcpPing(
});
}
/**
* @openapi
* /status:
* get:
* summary: Get all host statuses
* description: Retrieves the status of all hosts for the authenticated user.
* tags:
* - Server Stats
* responses:
* 200:
* description: A map of host IDs to their status entries.
* 401:
* description: Session expired - please log in again.
*/
app.get("/status", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
@@ -1942,6 +1960,28 @@ app.get("/status", async (req, res) => {
res.json(result);
});
/**
* @openapi
* /status/{id}:
* get:
* summary: Get host status by ID
* description: Retrieves the status of a specific host by its ID.
* tags:
* - Server Stats
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Host status entry.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Status not available.
*/
app.get("/status/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
@@ -1966,6 +2006,20 @@ app.get("/status/:id", validateHostId, async (req, res) => {
res.json(statusEntry);
});
/**
* @openapi
* /clear-connections:
* post:
* summary: Clear all SSH connections
* description: Clears all SSH connections from the connection pool.
* tags:
* - Server Stats
* responses:
* 200:
* description: All SSH connections cleared.
* 401:
* description: Session expired - please log in again.
*/
app.post("/clear-connections", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
@@ -1980,6 +2034,20 @@ app.post("/clear-connections", async (req, res) => {
res.json({ message: "All SSH connections cleared" });
});
/**
* @openapi
* /refresh:
* post:
* summary: Refresh polling
* description: Clears all SSH connections and refreshes host polling.
* tags:
* - Server Stats
* responses:
* 200:
* description: Polling refreshed.
* 401:
* description: Session expired - please log in again.
*/
app.post("/refresh", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
@@ -1996,6 +2064,35 @@ app.post("/refresh", async (req, res) => {
res.json({ message: "Polling refreshed" });
});
/**
* @openapi
* /host-updated:
* post:
* summary: Start polling for updated host
* description: Starts polling for a specific host after it has been updated.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* responses:
* 200:
* description: Host polling started.
* 400:
* description: Invalid hostId.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Host not found.
* 500:
* description: Failed to start polling.
*/
app.post("/host-updated", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const { hostId } = req.body;
@@ -2031,6 +2128,33 @@ app.post("/host-updated", async (req, res) => {
}
});
/**
* @openapi
* /host-deleted:
* post:
* summary: Stop polling for deleted host
* description: Stops polling for a specific host after it has been deleted.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* responses:
* 200:
* description: Host polling stopped.
* 400:
* description: Invalid hostId.
* 401:
* description: Session expired - please log in again.
* 500:
* description: Failed to stop polling.
*/
app.post("/host-deleted", async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const { hostId } = req.body;
@@ -2059,6 +2183,28 @@ app.post("/host-deleted", async (req, res) => {
}
});
/**
* @openapi
* /metrics/{id}:
* get:
* summary: Get host metrics
* description: Retrieves current metrics for a specific host including CPU, memory, disk, network, processes, and system information.
* tags:
* - Server Stats
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Host metrics data.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Metrics not available.
*/
app.get("/metrics/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
@@ -2096,6 +2242,30 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
});
});
/**
* @openapi
* /metrics/start/{id}:
* post:
* summary: Start metrics collection
* description: Establishes an SSH connection and starts collecting metrics for a specific host.
* tags:
* - Server Stats
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Metrics collection started successfully, or TOTP required.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Host not found.
* 500:
* description: Failed to start metrics collection.
*/
app.post("/metrics/start/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
@@ -2275,6 +2445,37 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
}
});
/**
* @openapi
* /metrics/stop/{id}:
* post:
* summary: Stop metrics collection
* description: Stops metrics collection for a specific host and cleans up the SSH session.
* tags:
* - Server Stats
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* viewerSessionId:
* type: string
* responses:
* 200:
* description: Metrics collection stopped successfully.
* 401:
* description: Session expired - please log in again.
* 500:
* description: Failed to stop metrics collection.
*/
app.post("/metrics/stop/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
@@ -2317,6 +2518,37 @@ app.post("/metrics/stop/:id", validateHostId, async (req, res) => {
}
});
/**
* @openapi
* /metrics/connect-totp:
* post:
* summary: Complete TOTP verification for metrics
* description: Verifies the TOTP code and completes the metrics SSH connection.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* totpCode:
* type: string
* responses:
* 200:
* description: TOTP verified, metrics connection established.
* 400:
* description: Missing sessionId or totpCode.
* 401:
* description: Session expired or invalid TOTP code.
* 404:
* description: TOTP session not found or expired.
* 500:
* description: Failed to verify TOTP.
*/
app.post("/metrics/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body;
const userId = (req as AuthenticatedRequest).userId;
@@ -2452,6 +2684,35 @@ app.post("/metrics/connect-totp", async (req, res) => {
}
});
/**
* @openapi
* /metrics/heartbeat:
* post:
* summary: Update viewer heartbeat
* description: Updates the heartbeat timestamp for a metrics viewer session to keep it alive.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* viewerSessionId:
* type: string
* responses:
* 200:
* description: Heartbeat updated successfully.
* 400:
* description: Invalid viewerSessionId.
* 401:
* description: Session expired - please log in again.
* 404:
* description: Viewer session not found.
* 500:
* description: Failed to update heartbeat.
*/
app.post("/metrics/heartbeat", async (req, res) => {
const { viewerSessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
@@ -2484,6 +2745,33 @@ app.post("/metrics/heartbeat", async (req, res) => {
}
});
/**
* @openapi
* /metrics/register-viewer:
* post:
* summary: Register metrics viewer
* description: Registers a new viewer session for a host to track who is viewing metrics.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* responses:
* 200:
* description: Viewer registered successfully.
* 400:
* description: Invalid hostId.
* 401:
* description: Session expired - please log in again.
* 500:
* description: Failed to register viewer.
*/
app.post("/metrics/register-viewer", async (req, res) => {
const { hostId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
@@ -2514,6 +2802,35 @@ app.post("/metrics/register-viewer", async (req, res) => {
}
});
/**
* @openapi
* /metrics/unregister-viewer:
* post:
* summary: Unregister metrics viewer
* description: Unregisters a viewer session when they stop viewing metrics for a host.
* tags:
* - Server Stats
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostId:
* type: integer
* viewerSessionId:
* type: string
* responses:
* 200:
* description: Viewer unregistered successfully.
* 400:
* description: Invalid hostId or viewerSessionId.
* 401:
* description: Session expired - please log in again.
* 500:
* description: Failed to unregister viewer.
*/
app.post("/metrics/unregister-viewer", async (req, res) => {
const { hostId, viewerSessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;

View File

@@ -1450,10 +1450,42 @@ async function killRemoteTunnelByMarker(
}
}
/**
* @openapi
* /ssh/tunnel/status:
* get:
* summary: Get all tunnel statuses
* description: Retrieves the status of all SSH tunnels.
* tags:
* - SSH Tunnels
* responses:
* 200:
* description: A list of all tunnel statuses.
*/
app.get("/ssh/tunnel/status", (req, res) => {
res.json(getAllTunnelStatus());
});
/**
* @openapi
* /ssh/tunnel/status/{tunnelName}:
* get:
* summary: Get tunnel status by name
* description: Retrieves the status of a specific SSH tunnel by its name.
* tags:
* - SSH Tunnels
* parameters:
* - in: path
* name: tunnelName
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Tunnel status.
* 404:
* description: Tunnel not found.
*/
app.get("/ssh/tunnel/status/:tunnelName", (req, res) => {
const { tunnelName } = req.params;
const status = connectionStatus.get(tunnelName);
@@ -1465,6 +1497,39 @@ app.get("/ssh/tunnel/status/:tunnelName", (req, res) => {
res.json({ name: tunnelName, status });
});
/**
* @openapi
* /ssh/tunnel/connect:
* post:
* summary: Connect SSH tunnel
* description: Establishes an SSH tunnel connection with the specified configuration.
* tags:
* - SSH Tunnels
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* sourceHostId:
* type: integer
* tunnelIndex:
* type: integer
* responses:
* 200:
* description: Connection request received.
* 400:
* description: Invalid tunnel configuration.
* 401:
* description: Authentication required.
* 403:
* description: Access denied to this host.
* 500:
* description: Failed to connect tunnel.
*/
app.post(
"/ssh/tunnel/connect",
authenticateJWT,
@@ -1619,6 +1684,35 @@ app.post(
},
);
/**
* @openapi
* /ssh/tunnel/disconnect:
* post:
* summary: Disconnect SSH tunnel
* description: Disconnects an active SSH tunnel.
* tags:
* - SSH Tunnels
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* tunnelName:
* type: string
* responses:
* 200:
* description: Disconnect request received.
* 400:
* description: Tunnel name required.
* 401:
* description: Authentication required.
* 403:
* description: Access denied.
* 500:
* description: Failed to disconnect tunnel.
*/
app.post(
"/ssh/tunnel/disconnect",
authenticateJWT,
@@ -1683,6 +1777,35 @@ app.post(
},
);
/**
* @openapi
* /ssh/tunnel/cancel:
* post:
* summary: Cancel tunnel retry
* description: Cancels the retry mechanism for a failed SSH tunnel connection.
* tags:
* - SSH Tunnels
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* tunnelName:
* type: string
* responses:
* 200:
* description: Cancel request received.
* 400:
* description: Tunnel name required.
* 401:
* description: Authentication required.
* 403:
* description: Access denied.
* 500:
* description: Failed to cancel tunnel retry.
*/
app.post(
"/ssh/tunnel/cancel",
authenticateJWT,

View File

@@ -194,7 +194,7 @@ class UserDataImport {
continue;
}
const newHostData = {
const newHostData: any = {
...host,
userId: targetUserId,
updatedAt: new Date().toISOString(),
@@ -204,7 +204,7 @@ class UserDataImport {
newHostData.createdAt = new Date().toISOString();
}
let processedHostData = newHostData;
let processedHostData: any = newHostData;
if (options.userDataKey) {
processedHostData = DataCrypto.encryptRecord(
"ssh_data",
@@ -275,7 +275,7 @@ class UserDataImport {
continue;
}
const newCredentialData = {
const newCredentialData: any = {
...credential,
userId: targetUserId,
updatedAt: new Date().toISOString(),
@@ -287,7 +287,7 @@ class UserDataImport {
newCredentialData.createdAt = new Date().toISOString();
}
let processedCredentialData = newCredentialData;
let processedCredentialData: any = newCredentialData;
if (options.userDataKey) {
processedCredentialData = DataCrypto.encryptRecord(
"ssh_credentials",

View File

@@ -1740,6 +1740,7 @@
"state": "State",
"process": "Process",
"noData": "No listening ports data"
},
"firewall": {
"title": "Firewall",
"active": "Active",

View File

@@ -7,7 +7,8 @@ export type WidgetType =
| "processes"
| "system"
| "login_stats"
| "ports";
| "ports"
| "firewall";
export interface ListeningPort {
protocol: "tcp" | "udp";
@@ -21,7 +22,7 @@ export interface ListeningPort {
export interface PortsMetrics {
source: "ss" | "netstat" | "none";
ports: ListeningPort[];
| "firewall";
}
export interface FirewallRule {
chain: string;

View File

@@ -1,4 +1,4 @@
import { HostManager } from "@/ui/desktop/apps/host-manager/HostManager";
import { HostManager } from "@/ui/desktop/apps/host-manager/hosts/HostManager";
import React from "react";
const HostManagerApp: React.FC = () => {

View File

@@ -270,7 +270,8 @@ export function ServerStats({
case "ports":
return (
<PortsWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "firewall":
return (
<FirewallWidget metrics={metrics} metricsHistory={metricsHistory} />

View File

@@ -361,6 +361,7 @@ export function AppView({
rightSidebarOpen={rightSidebarOpen}
rightSidebarWidth={rightSidebarWidth}
isStandalone={true}
/>
) : t.type === "tunnel" ? (
<TunnelManager
hostConfig={t.hostConfig}