feat: enhance server stats widgets and fix TypeScript/ESLint errors #394
@@ -31,8 +31,13 @@ import {
|
||||
dismissedAlerts,
|
||||
sshCredentialUsage,
|
||||
settings,
|
||||
snippets,
|
||||
} from "./db/schema.js";
|
||||
import type {
|
||||
CacheEntry,
|
||||
GitHubRelease,
|
||||
GitHubAPIResponse,
|
||||
AuthenticatedRequest,
|
||||
} from "../../types/index.js";
|
||||
import { getDb } from "./db/index.js";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
@@ -107,17 +112,11 @@ const upload = multer({
|
||||
},
|
||||
});
|
||||
|
||||
interface CacheEntry {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
class GitHubCache {
|
||||
private cache: Map<string, CacheEntry> = new Map();
|
||||
private readonly CACHE_DURATION = 30 * 60 * 1000;
|
||||
|
||||
set(key: string, data: any): void {
|
||||
set<T>(key: string, data: T): void {
|
||||
const now = Date.now();
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
@@ -126,7 +125,7 @@ class GitHubCache {
|
||||
});
|
||||
}
|
||||
|
||||
get(key: string): any | null {
|
||||
get<T>(key: string): T | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) {
|
||||
return null;
|
||||
@@ -137,7 +136,7 @@ class GitHubCache {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
return entry.data as T;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,34 +146,16 @@ const GITHUB_API_BASE = "https://api.github.com";
|
||||
const REPO_OWNER = "LukeGus";
|
||||
const REPO_NAME = "Termix";
|
||||
|
||||
interface GitHubRelease {
|
||||
id: number;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string;
|
||||
published_at: string;
|
||||
html_url: string;
|
||||
assets: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
download_count: number;
|
||||
browser_download_url: string;
|
||||
}>;
|
||||
prerelease: boolean;
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
async function fetchGitHubAPI(
|
||||
async function fetchGitHubAPI<T>(
|
||||
endpoint: string,
|
||||
cacheKey: string,
|
||||
): Promise<any> {
|
||||
const cachedData = githubCache.get(cacheKey);
|
||||
if (cachedData) {
|
||||
): Promise<GitHubAPIResponse<T>> {
|
||||
const cachedEntry = githubCache.get<CacheEntry<T>>(cacheKey);
|
||||
if (cachedEntry) {
|
||||
return {
|
||||
data: cachedData,
|
||||
data: cachedEntry.data,
|
||||
cached: true,
|
||||
cache_age: Date.now() - cachedData.timestamp,
|
||||
cache_age: Date.now() - cachedEntry.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,8 +174,13 @@ async function fetchGitHubAPI(
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
githubCache.set(cacheKey, data);
|
||||
const data = (await response.json()) as T;
|
||||
const cacheData: CacheEntry<T> = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + 30 * 60 * 1000,
|
||||
};
|
||||
githubCache.set(cacheKey, cacheData);
|
||||
|
||||
return {
|
||||
data: data,
|
||||
@@ -274,7 +260,7 @@ app.get("/version", authenticateJWT, async (req, res) => {
|
||||
|
||||
try {
|
||||
const cacheKey = "latest_release";
|
||||
const releaseData = await fetchGitHubAPI(
|
||||
const releaseData = await fetchGitHubAPI<GitHubRelease>(
|
||||
`/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
|
||||
cacheKey,
|
||||
);
|
||||
@@ -325,12 +311,12 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
|
||||
);
|
||||
const cacheKey = `releases_rss_${page}_${per_page}`;
|
||||
|
||||
const releasesData = await fetchGitHubAPI(
|
||||
const releasesData = await fetchGitHubAPI<GitHubRelease[]>(
|
||||
`/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`,
|
||||
cacheKey,
|
||||
);
|
||||
|
||||
const rssItems = releasesData.data.map((release: GitHubRelease) => ({
|
||||
const rssItems = releasesData.data.map((release) => ({
|
||||
id: release.id,
|
||||
title: release.name || release.tag_name,
|
||||
description: release.body,
|
||||
@@ -459,7 +445,7 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
|
||||
|
||||
app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password) {
|
||||
@@ -913,7 +899,7 @@ app.post(
|
||||
return res.status(400).json({ error: "No file uploaded" });
|
||||
}
|
||||
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password) {
|
||||
@@ -1321,7 +1307,7 @@ app.post(
|
||||
|
||||
apiLogger.error("SQLite import failed", error, {
|
||||
operation: "sqlite_import_api_failed",
|
||||
userId: (req as any).userId,
|
||||
userId: (req as AuthenticatedRequest).userId,
|
||||
});
|
||||
res.status(500).json({
|
||||
error: "Failed to import SQLite data",
|
||||
@@ -1333,7 +1319,7 @@ app.post(
|
||||
|
||||
app.post("/database/export/preview", authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { scope = "user_data", includeCredentials = true } = req.body;
|
||||
|
||||
const exportData = await UserDataExport.exportUserData(userId, {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type {
|
||||
AuthenticatedRequest,
|
||||
CacheEntry,
|
||||
TermixAlert,
|
||||
} from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import { dismissedAlerts } from "../db/schema.js";
|
||||
@@ -6,17 +11,11 @@ import fetch from "node-fetch";
|
||||
import { authLogger } from "../../utils/logger.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
|
||||
interface CacheEntry {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
class AlertCache {
|
||||
private cache: Map<string, CacheEntry> = new Map();
|
||||
private readonly CACHE_DURATION = 5 * 60 * 1000;
|
||||
|
||||
set(key: string, data: any): void {
|
||||
set<T>(key: string, data: T): void {
|
||||
const now = Date.now();
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
@@ -25,7 +24,7 @@ class AlertCache {
|
||||
});
|
||||
}
|
||||
|
||||
get(key: string): any | null {
|
||||
get<T>(key: string): T | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) {
|
||||
return null;
|
||||
@@ -36,7 +35,7 @@ class AlertCache {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
return entry.data as T;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,20 +46,9 @@ const REPO_OWNER = "LukeGus";
|
||||
const REPO_NAME = "Termix-Docs";
|
||||
const ALERTS_FILE = "main/termix-alerts.json";
|
||||
|
||||
interface TermixAlert {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
expiresAt: string;
|
||||
priority?: "low" | "medium" | "high" | "critical";
|
||||
type?: "info" | "warning" | "error" | "success";
|
||||
actionUrl?: string;
|
||||
actionText?: string;
|
||||
}
|
||||
|
||||
async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
||||
const cacheKey = "termix_alerts";
|
||||
const cachedData = alertCache.get(cacheKey);
|
||||
const cachedData = alertCache.get<TermixAlert[]>(cacheKey);
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
@@ -115,7 +103,7 @@ const authenticateJWT = authManager.createAuthMiddleware();
|
||||
// GET /alerts
|
||||
router.get("/", authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
const allAlerts = await fetchAlertsFromGitHub();
|
||||
|
||||
@@ -148,7 +136,7 @@ router.get("/", authenticateJWT, async (req, res) => {
|
||||
router.post("/dismiss", authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const { alertId } = req.body;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!alertId) {
|
||||
authLogger.warn("Missing alertId in dismiss request", { userId });
|
||||
@@ -186,7 +174,7 @@ router.post("/dismiss", authenticateJWT, async (req, res) => {
|
||||
// GET /alerts/dismissed/:userId
|
||||
router.get("/dismissed", authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
const dismissedAlertRecords = await db
|
||||
.select({
|
||||
@@ -211,7 +199,7 @@ router.get("/dismissed", authenticateJWT, async (req, res) => {
|
||||
router.delete("/dismiss", authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const { alertId } = req.body;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!alertId) {
|
||||
return res.status(400).json({ error: "Alert ID is required" });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js";
|
||||
@@ -27,7 +28,11 @@ function generateSSHKeyPair(
|
||||
} {
|
||||
try {
|
||||
let ssh2Type = keyType;
|
||||
const options: any = {};
|
||||
const options: {
|
||||
bits?: number;
|
||||
passphrase?: string;
|
||||
cipher?: string;
|
||||
} = {};
|
||||
|
||||
if (keyType === "ssh-rsa") {
|
||||
ssh2Type = "rsa";
|
||||
@@ -44,6 +49,7 @@ function generateSSHKeyPair(
|
||||
options.cipher = "aes128-cbc";
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options);
|
||||
|
||||
return {
|
||||
@@ -62,7 +68,7 @@ function generateSSHKeyPair(
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isNonEmptyString(val: any): val is string {
|
||||
function isNonEmptyString(val: unknown): val is string {
|
||||
return typeof val === "string" && val.trim().length > 0;
|
||||
}
|
||||
|
||||
@@ -77,7 +83,7 @@ router.post(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
@@ -224,7 +230,7 @@ router.get(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!isNonEmptyString(userId)) {
|
||||
authLogger.warn("Invalid userId for credential fetch");
|
||||
@@ -257,7 +263,7 @@ router.get(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!isNonEmptyString(userId)) {
|
||||
authLogger.warn("Invalid userId for credential folder fetch");
|
||||
@@ -295,7 +301,7 @@ router.get(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id } = req.params;
|
||||
|
||||
if (!isNonEmptyString(userId) || !id) {
|
||||
@@ -326,19 +332,19 @@ router.get(
|
||||
const output = formatCredentialOutput(credential);
|
||||
|
||||
if (credential.password) {
|
||||
(output as any).password = credential.password;
|
||||
output.password = credential.password;
|
||||
}
|
||||
if (credential.key) {
|
||||
(output as any).key = credential.key;
|
||||
output.key = credential.key;
|
||||
}
|
||||
if (credential.private_key) {
|
||||
(output as any).privateKey = credential.private_key;
|
||||
output.privateKey = credential.private_key;
|
||||
}
|
||||
if (credential.public_key) {
|
||||
(output as any).publicKey = credential.public_key;
|
||||
output.publicKey = credential.public_key;
|
||||
}
|
||||
if (credential.key_password) {
|
||||
(output as any).keyPassword = credential.key_password;
|
||||
output.keyPassword = credential.key_password;
|
||||
}
|
||||
|
||||
res.json(output);
|
||||
@@ -359,7 +365,7 @@ router.put(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
@@ -383,7 +389,7 @@ router.put(
|
||||
return res.status(404).json({ error: "Credential not found" });
|
||||
}
|
||||
|
||||
const updateFields: any = {};
|
||||
const updateFields: Record<string, string | null | undefined> = {};
|
||||
|
||||
if (updateData.name !== undefined)
|
||||
updateFields.name = updateData.name.trim();
|
||||
@@ -495,7 +501,7 @@ router.delete(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id } = req.params;
|
||||
|
||||
if (!isNonEmptyString(userId) || !id) {
|
||||
@@ -594,7 +600,7 @@ router.post(
|
||||
"/:id/apply-to-host/:hostId",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id: credentialId, hostId } = req.params;
|
||||
|
||||
if (!isNonEmptyString(userId) || !credentialId || !hostId) {
|
||||
@@ -627,8 +633,8 @@ router.post(
|
||||
.update(sshData)
|
||||
.set({
|
||||
credentialId: parseInt(credentialId),
|
||||
username: credential.username,
|
||||
authType: credential.auth_type || credential.authType,
|
||||
username: credential.username as string,
|
||||
authType: (credential.auth_type || credential.authType) as string,
|
||||
password: null,
|
||||
key: null,
|
||||
key_password: null,
|
||||
@@ -673,7 +679,7 @@ router.get(
|
||||
"/:id/hosts",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id: credentialId } = req.params;
|
||||
|
||||
if (!isNonEmptyString(userId) || !credentialId) {
|
||||
@@ -705,7 +711,9 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
function formatCredentialOutput(credential: any): any {
|
||||
function formatCredentialOutput(
|
||||
credential: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
id: credential.id,
|
||||
name: credential.name,
|
||||
@@ -729,7 +737,9 @@ function formatCredentialOutput(credential: any): any {
|
||||
};
|
||||
}
|
||||
|
||||
function formatSSHHostOutput(host: any): any {
|
||||
function formatSSHHostOutput(
|
||||
host: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
id: host.id,
|
||||
userId: host.userId,
|
||||
@@ -749,7 +759,7 @@ function formatSSHHostOutput(host: any): any {
|
||||
enableTerminal: !!host.enableTerminal,
|
||||
enableTunnel: !!host.enableTunnel,
|
||||
tunnelConnections: host.tunnelConnections
|
||||
? JSON.parse(host.tunnelConnections)
|
||||
? JSON.parse(host.tunnelConnections as string)
|
||||
: [],
|
||||
enableFileManager: !!host.enableFileManager,
|
||||
defaultPath: host.defaultPath,
|
||||
@@ -764,7 +774,7 @@ router.put(
|
||||
"/folders/rename",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { oldName, newName } = req.body;
|
||||
|
||||
if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) {
|
||||
@@ -1117,10 +1127,10 @@ router.post(
|
||||
);
|
||||
|
||||
async function deploySSHKeyToHost(
|
||||
hostConfig: any,
|
||||
hostConfig: Record<string, unknown>,
|
||||
publicKey: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_credentialData: any,
|
||||
_credentialData: Record<string, unknown>,
|
||||
): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const conn = new Client();
|
||||
@@ -1364,7 +1374,7 @@ async function deploySSHKeyToHost(
|
||||
});
|
||||
|
||||
try {
|
||||
const connectionConfig: any = {
|
||||
const connectionConfig: Record<string, unknown> = {
|
||||
host: hostConfig.ip,
|
||||
port: hostConfig.port || 22,
|
||||
username: hostConfig.username,
|
||||
@@ -1411,14 +1421,15 @@ async function deploySSHKeyToHost(
|
||||
connectionConfig.password = hostConfig.password;
|
||||
} else if (hostConfig.authType === "key" && hostConfig.privateKey) {
|
||||
try {
|
||||
const privateKey = hostConfig.privateKey as string;
|
||||
if (
|
||||
!hostConfig.privateKey.includes("-----BEGIN") ||
|
||||
!hostConfig.privateKey.includes("-----END")
|
||||
!privateKey.includes("-----BEGIN") ||
|
||||
!privateKey.includes("-----END")
|
||||
) {
|
||||
throw new Error("Invalid private key format");
|
||||
}
|
||||
|
||||
const cleanKey = hostConfig.privateKey
|
||||
const cleanKey = privateKey
|
||||
.trim()
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n");
|
||||
@@ -1473,7 +1484,7 @@ router.post(
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
@@ -1540,7 +1551,7 @@ router.post(
|
||||
};
|
||||
|
||||
if (hostData.authType === "credential" && hostData.credentialId) {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
if (!userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -1554,7 +1565,7 @@ router.post(
|
||||
db
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(eq(sshCredentials.id, hostData.credentialId))
|
||||
.where(eq(sshCredentials.id, hostData.credentialId as number))
|
||||
.limit(1),
|
||||
"ssh_credentials",
|
||||
userId,
|
||||
@@ -1589,7 +1600,7 @@ router.post(
|
||||
|
||||
const deployResult = await deploySSHKeyToHost(
|
||||
hostConfig,
|
||||
credData.publicKey,
|
||||
credData.publicKey as string,
|
||||
credData,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import { snippets } from "../db/schema.js";
|
||||
@@ -8,7 +9,7 @@ import { AuthManager } from "../../utils/auth-manager.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isNonEmptyString(val: any): val is string {
|
||||
function isNonEmptyString(val: unknown): val is string {
|
||||
return typeof val === "string" && val.trim().length > 0;
|
||||
}
|
||||
|
||||
@@ -23,7 +24,7 @@ router.get(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!isNonEmptyString(userId)) {
|
||||
authLogger.warn("Invalid userId for snippets fetch");
|
||||
@@ -52,12 +53,15 @@ router.get(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id } = req.params;
|
||||
const snippetId = parseInt(id, 10);
|
||||
|
||||
if (!isNonEmptyString(userId) || isNaN(snippetId)) {
|
||||
authLogger.warn("Invalid request for snippet fetch: invalid ID", { userId, id });
|
||||
authLogger.warn("Invalid request for snippet fetch: invalid ID", {
|
||||
userId,
|
||||
id,
|
||||
});
|
||||
return res.status(400).json({ error: "Invalid request parameters" });
|
||||
}
|
||||
|
||||
@@ -88,7 +92,7 @@ router.post(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { name, content, description } = req.body;
|
||||
|
||||
if (
|
||||
@@ -139,7 +143,7 @@ router.put(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
@@ -158,7 +162,12 @@ router.put(
|
||||
return res.status(404).json({ error: "Snippet not found" });
|
||||
}
|
||||
|
||||
const updateFields: any = {
|
||||
const updateFields: Partial<{
|
||||
updatedAt: ReturnType<typeof sql.raw>;
|
||||
name: string;
|
||||
content: string;
|
||||
description: string | null;
|
||||
}> = {
|
||||
updatedAt: sql`CURRENT_TIMESTAMP`,
|
||||
};
|
||||
|
||||
@@ -206,7 +215,7 @@ router.delete(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { id } = req.params;
|
||||
|
||||
if (!isNonEmptyString(userId) || !id) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import { db } from "../db/index.js";
|
||||
import {
|
||||
@@ -22,11 +23,11 @@ const router = express.Router();
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
function isNonEmptyString(value: any): value is string {
|
||||
function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function isValidPort(port: any): port is number {
|
||||
function isValidPort(port: unknown): port is number {
|
||||
return typeof port === "number" && port > 0 && port <= 65535;
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
|
||||
: [];
|
||||
|
||||
const hasAutoStartTunnels = tunnelConnections.some(
|
||||
(tunnel: any) => tunnel.autoStart,
|
||||
(tunnel: Record<string, unknown>) => tunnel.autoStart,
|
||||
);
|
||||
|
||||
if (!hasAutoStartTunnels) {
|
||||
@@ -99,7 +100,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
|
||||
credentialId: host.credentialId,
|
||||
enableTunnel: true,
|
||||
tunnelConnections: tunnelConnections.filter(
|
||||
(tunnel: any) => tunnel.autoStart,
|
||||
(tunnel: Record<string, unknown>) => tunnel.autoStart,
|
||||
),
|
||||
pin: !!host.pin,
|
||||
enableTerminal: !!host.enableTerminal,
|
||||
@@ -183,8 +184,8 @@ router.post(
|
||||
requireDataAccess,
|
||||
upload.single("key"),
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
let hostData: any;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
let hostData: Record<string, unknown>;
|
||||
|
||||
if (req.headers["content-type"]?.includes("multipart/form-data")) {
|
||||
if (req.body.data) {
|
||||
@@ -251,7 +252,7 @@ router.post(
|
||||
}
|
||||
|
||||
const effectiveAuthType = authType || authMethod;
|
||||
const sshDataObj: any = {
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
userId: userId,
|
||||
name,
|
||||
folder: folder || null,
|
||||
@@ -321,11 +322,11 @@ router.post(
|
||||
enableTerminal: !!createdHost.enableTerminal,
|
||||
enableTunnel: !!createdHost.enableTunnel,
|
||||
tunnelConnections: createdHost.tunnelConnections
|
||||
? JSON.parse(createdHost.tunnelConnections)
|
||||
? JSON.parse(createdHost.tunnelConnections as string)
|
||||
: [],
|
||||
enableFileManager: !!createdHost.enableFileManager,
|
||||
statsConfig: createdHost.statsConfig
|
||||
? JSON.parse(createdHost.statsConfig)
|
||||
? JSON.parse(createdHost.statsConfig as string)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
@@ -336,7 +337,7 @@ router.post(
|
||||
{
|
||||
operation: "host_create_success",
|
||||
userId,
|
||||
hostId: createdHost.id,
|
||||
hostId: createdHost.id as number,
|
||||
name,
|
||||
ip,
|
||||
port,
|
||||
@@ -367,8 +368,8 @@ router.put(
|
||||
upload.single("key"),
|
||||
async (req: Request, res: Response) => {
|
||||
const hostId = req.params.id;
|
||||
const userId = (req as any).userId;
|
||||
let hostData: any;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
let hostData: Record<string, unknown>;
|
||||
|
||||
if (req.headers["content-type"]?.includes("multipart/form-data")) {
|
||||
if (req.body.data) {
|
||||
@@ -439,7 +440,7 @@ router.put(
|
||||
}
|
||||
|
||||
const effectiveAuthType = authType || authMethod;
|
||||
const sshDataObj: any = {
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
name,
|
||||
folder,
|
||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||
@@ -526,11 +527,11 @@ router.put(
|
||||
enableTerminal: !!updatedHost.enableTerminal,
|
||||
enableTunnel: !!updatedHost.enableTunnel,
|
||||
tunnelConnections: updatedHost.tunnelConnections
|
||||
? JSON.parse(updatedHost.tunnelConnections)
|
||||
? JSON.parse(updatedHost.tunnelConnections as string)
|
||||
: [],
|
||||
enableFileManager: !!updatedHost.enableFileManager,
|
||||
statsConfig: updatedHost.statsConfig
|
||||
? JSON.parse(updatedHost.statsConfig)
|
||||
? JSON.parse(updatedHost.statsConfig as string)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
@@ -568,7 +569,7 @@ router.put(
|
||||
// Route: Get SSH data for the authenticated user (requires JWT)
|
||||
// GET /ssh/host
|
||||
router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
if (!isNonEmptyString(userId)) {
|
||||
sshLogger.warn("Invalid userId for SSH data fetch", {
|
||||
operation: "host_fetch",
|
||||
@@ -584,7 +585,7 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
|
||||
);
|
||||
|
||||
const result = await Promise.all(
|
||||
data.map(async (row: any) => {
|
||||
data.map(async (row: Record<string, unknown>) => {
|
||||
const baseHost = {
|
||||
...row,
|
||||
tags:
|
||||
@@ -597,11 +598,11 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
|
||||
enableTerminal: !!row.enableTerminal,
|
||||
enableTunnel: !!row.enableTunnel,
|
||||
tunnelConnections: row.tunnelConnections
|
||||
? JSON.parse(row.tunnelConnections)
|
||||
? JSON.parse(row.tunnelConnections as string)
|
||||
: [],
|
||||
enableFileManager: !!row.enableFileManager,
|
||||
statsConfig: row.statsConfig
|
||||
? JSON.parse(row.statsConfig)
|
||||
? JSON.parse(row.statsConfig as string)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
@@ -626,7 +627,7 @@ router.get(
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const hostId = req.params.id;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId) {
|
||||
sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", {
|
||||
@@ -692,7 +693,7 @@ router.get(
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const hostId = req.params.id;
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId) {
|
||||
return res.status(400).json({ error: "Invalid userId or hostId" });
|
||||
@@ -739,7 +740,7 @@ router.get(
|
||||
enableFileManager: !!resolvedHost.enableFileManager,
|
||||
defaultPath: resolvedHost.defaultPath,
|
||||
tunnelConnections: resolvedHost.tunnelConnections
|
||||
? JSON.parse(resolvedHost.tunnelConnections)
|
||||
? JSON.parse(resolvedHost.tunnelConnections as string)
|
||||
: [],
|
||||
};
|
||||
|
||||
@@ -767,7 +768,7 @@ router.delete(
|
||||
"/db/host/:id",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const hostId = req.params.id;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId) {
|
||||
@@ -866,7 +867,7 @@ router.get(
|
||||
"/file_manager/recent",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const hostId = req.query.hostId
|
||||
? parseInt(req.query.hostId as string)
|
||||
: null;
|
||||
@@ -908,7 +909,7 @@ router.post(
|
||||
"/file_manager/recent",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { hostId, path, name } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
@@ -957,7 +958,7 @@ router.delete(
|
||||
"/file_manager/recent",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { hostId, path } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
@@ -990,7 +991,7 @@ router.get(
|
||||
"/file_manager/pinned",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const hostId = req.query.hostId
|
||||
? parseInt(req.query.hostId as string)
|
||||
: null;
|
||||
@@ -1031,7 +1032,7 @@ router.post(
|
||||
"/file_manager/pinned",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { hostId, path, name } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
@@ -1077,7 +1078,7 @@ router.delete(
|
||||
"/file_manager/pinned",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { hostId, path } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
@@ -1110,7 +1111,7 @@ router.get(
|
||||
"/file_manager/shortcuts",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const hostId = req.query.hostId
|
||||
? parseInt(req.query.hostId as string)
|
||||
: null;
|
||||
@@ -1151,7 +1152,7 @@ router.post(
|
||||
"/file_manager/shortcuts",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { hostId, path, name } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
@@ -1197,7 +1198,7 @@ router.delete(
|
||||
"/file_manager/shortcuts",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { hostId, path } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
@@ -1224,21 +1225,26 @@ router.delete(
|
||||
},
|
||||
);
|
||||
|
||||
async function resolveHostCredentials(host: any): Promise<any> {
|
||||
async function resolveHostCredentials(
|
||||
host: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
if (host.credentialId && host.userId) {
|
||||
const credentialId = host.credentialId as number;
|
||||
const userId = host.userId as string;
|
||||
|
||||
const credentials = await SimpleDBOps.select(
|
||||
db
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(sshCredentials.id, host.credentialId),
|
||||
eq(sshCredentials.userId, host.userId),
|
||||
eq(sshCredentials.id, credentialId),
|
||||
eq(sshCredentials.userId, userId),
|
||||
),
|
||||
),
|
||||
"ssh_credentials",
|
||||
host.userId,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (credentials.length > 0) {
|
||||
@@ -1277,7 +1283,7 @@ router.put(
|
||||
"/folders/rename",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { oldName, newName } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !oldName || !newName) {
|
||||
@@ -1342,7 +1348,7 @@ router.post(
|
||||
"/bulk-import",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { hosts } = req.body;
|
||||
|
||||
if (!Array.isArray(hosts) || hosts.length === 0) {
|
||||
@@ -1414,7 +1420,7 @@ router.post(
|
||||
continue;
|
||||
}
|
||||
|
||||
const sshDataObj: any = {
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
userId: userId,
|
||||
name: hostData.name || `${hostData.username}@${hostData.ip}`,
|
||||
folder: hostData.folder || "Default",
|
||||
@@ -1472,7 +1478,7 @@ router.post(
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { sshConfigId } = req.body;
|
||||
|
||||
if (!sshConfigId || typeof sshConfigId !== "number") {
|
||||
@@ -1536,7 +1542,7 @@ router.post(
|
||||
const tunnelConnections = JSON.parse(config.tunnelConnections);
|
||||
|
||||
const resolvedConnections = await Promise.all(
|
||||
tunnelConnections.map(async (tunnel: any) => {
|
||||
tunnelConnections.map(async (tunnel: Record<string, unknown>) => {
|
||||
if (
|
||||
tunnel.autoStart &&
|
||||
tunnel.endpointHost &&
|
||||
@@ -1625,7 +1631,7 @@ router.delete(
|
||||
"/autostart/disable",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { sshConfigId } = req.body;
|
||||
|
||||
if (!sshConfigId || typeof sshConfigId !== "number") {
|
||||
@@ -1671,7 +1677,7 @@ router.get(
|
||||
"/autostart/status",
|
||||
authenticateJWT,
|
||||
async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
try {
|
||||
const autostartConfigs = await db
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||
import express from "express";
|
||||
import crypto from "crypto";
|
||||
import { db } from "../db/index.js";
|
||||
@@ -27,7 +28,7 @@ async function verifyOIDCToken(
|
||||
idToken: string,
|
||||
issuerUrl: string,
|
||||
clientId: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
const normalizedIssuerUrl = issuerUrl.endsWith("/")
|
||||
? issuerUrl.slice(0, -1)
|
||||
: issuerUrl;
|
||||
@@ -48,22 +49,25 @@ async function verifyOIDCToken(
|
||||
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
|
||||
const discoveryResponse = await fetch(discoveryUrl);
|
||||
if (discoveryResponse.ok) {
|
||||
const discovery = (await discoveryResponse.json()) as any;
|
||||
const discovery = (await discoveryResponse.json()) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
if (discovery.jwks_uri) {
|
||||
jwksUrls.unshift(discovery.jwks_uri);
|
||||
jwksUrls.unshift(discovery.jwks_uri as string);
|
||||
}
|
||||
}
|
||||
} catch (discoveryError) {
|
||||
authLogger.error(`OIDC discovery failed: ${discoveryError}`);
|
||||
}
|
||||
|
||||
let jwks: any = null;
|
||||
let jwks: Record<string, unknown> | null = null;
|
||||
|
||||
for (const url of jwksUrls) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const jwksData = (await response.json()) as any;
|
||||
const jwksData = (await response.json()) as Record<string, unknown>;
|
||||
if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
|
||||
jwks = jwksData;
|
||||
break;
|
||||
@@ -95,10 +99,12 @@ async function verifyOIDCToken(
|
||||
);
|
||||
const keyId = header.kid;
|
||||
|
||||
const publicKey = jwks.keys.find((key: any) => key.kid === keyId);
|
||||
const publicKey = jwks.keys.find(
|
||||
(key: Record<string, unknown>) => key.kid === keyId,
|
||||
);
|
||||
if (!publicKey) {
|
||||
throw new Error(
|
||||
`No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: any) => k.kid).join(", ")}`,
|
||||
`No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: Record<string, unknown>) => k.kid).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,7 +121,7 @@ async function verifyOIDCToken(
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isNonEmptyString(val: any): val is string {
|
||||
function isNonEmptyString(val: unknown): val is string {
|
||||
return typeof val === "string" && val.trim().length > 0;
|
||||
}
|
||||
|
||||
@@ -129,7 +135,7 @@ router.post("/create", async (req, res) => {
|
||||
const row = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
|
||||
.get();
|
||||
if (row && (row as any).value !== "true") {
|
||||
if (row && (row as Record<string, unknown>).value !== "true") {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Registration is currently disabled" });
|
||||
@@ -174,7 +180,7 @@ router.post("/create", async (req, res) => {
|
||||
const countResult = db.$client
|
||||
.prepare("SELECT COUNT(*) as count FROM users")
|
||||
.get();
|
||||
isFirstUser = ((countResult as any)?.count || 0) === 0;
|
||||
isFirstUser = ((countResult as { count?: number })?.count || 0) === 0;
|
||||
|
||||
const saltRounds = parseInt(process.env.SALT || "10", 10);
|
||||
const password_hash = await bcrypt.hash(password, saltRounds);
|
||||
@@ -238,7 +244,7 @@ router.post("/create", async (req, res) => {
|
||||
// Route: Create OIDC provider configuration (admin only)
|
||||
// POST /users/oidc-config
|
||||
router.post("/oidc-config", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0 || !user[0].is_admin) {
|
||||
@@ -378,7 +384,7 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
|
||||
// Route: Disable OIDC configuration (admin only)
|
||||
// DELETE /users/oidc-config
|
||||
router.delete("/oidc-config", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0 || !user[0].is_admin) {
|
||||
@@ -408,7 +414,7 @@ router.get("/oidc-config", async (req, res) => {
|
||||
return res.json(null);
|
||||
}
|
||||
|
||||
let config = JSON.parse((row as any).value);
|
||||
let config = JSON.parse((row as Record<string, unknown>).value as string);
|
||||
|
||||
if (config.client_secret) {
|
||||
if (config.client_secret.startsWith("encrypted:")) {
|
||||
@@ -485,7 +491,7 @@ router.get("/oidc/authorize", async (req, res) => {
|
||||
return res.status(404).json({ error: "OIDC not configured" });
|
||||
}
|
||||
|
||||
const config = JSON.parse((row as any).value);
|
||||
const config = JSON.parse((row as Record<string, unknown>).value as string);
|
||||
const state = nanoid();
|
||||
const nonce = nanoid();
|
||||
|
||||
@@ -540,7 +546,8 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
.status(400)
|
||||
.json({ error: "Invalid state parameter - redirect URI not found" });
|
||||
}
|
||||
const redirectUri = (storedRedirectRow as any).value;
|
||||
const redirectUri = (storedRedirectRow as Record<string, unknown>)
|
||||
.value as string;
|
||||
|
||||
try {
|
||||
const storedNonce = db.$client
|
||||
@@ -564,7 +571,9 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
return res.status(500).json({ error: "OIDC not configured" });
|
||||
}
|
||||
|
||||
const config = JSON.parse((configRow as any).value);
|
||||
const config = JSON.parse(
|
||||
(configRow as Record<string, unknown>).value as string,
|
||||
);
|
||||
|
||||
const tokenResponse = await fetch(config.token_url, {
|
||||
method: "POST",
|
||||
@@ -590,9 +599,9 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
.json({ error: "Failed to exchange authorization code" });
|
||||
}
|
||||
|
||||
const tokenData = (await tokenResponse.json()) as any;
|
||||
const tokenData = (await tokenResponse.json()) as Record<string, unknown>;
|
||||
|
||||
let userInfo: any = null;
|
||||
let userInfo: Record<string, unknown> = null;
|
||||
const userInfoUrls: string[] = [];
|
||||
|
||||
const normalizedIssuerUrl = config.issuer_url.endsWith("/")
|
||||
@@ -604,9 +613,12 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
|
||||
const discoveryResponse = await fetch(discoveryUrl);
|
||||
if (discoveryResponse.ok) {
|
||||
const discovery = (await discoveryResponse.json()) as any;
|
||||
const discovery = (await discoveryResponse.json()) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
if (discovery.userinfo_endpoint) {
|
||||
userInfoUrls.push(discovery.userinfo_endpoint);
|
||||
userInfoUrls.push(discovery.userinfo_endpoint as string);
|
||||
}
|
||||
}
|
||||
} catch (discoveryError) {
|
||||
@@ -631,14 +643,14 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
if (tokenData.id_token) {
|
||||
try {
|
||||
userInfo = await verifyOIDCToken(
|
||||
tokenData.id_token,
|
||||
tokenData.id_token as string,
|
||||
config.issuer_url,
|
||||
config.client_id,
|
||||
);
|
||||
} catch {
|
||||
// Fallback to manual decoding
|
||||
try {
|
||||
const parts = tokenData.id_token.split(".");
|
||||
const parts = (tokenData.id_token as string).split(".");
|
||||
if (parts.length === 3) {
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(parts[1], "base64").toString(),
|
||||
@@ -661,7 +673,10 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
});
|
||||
|
||||
if (userInfoResponse.ok) {
|
||||
userInfo = await userInfoResponse.json();
|
||||
userInfo = (await userInfoResponse.json()) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
break;
|
||||
} else {
|
||||
authLogger.error(
|
||||
@@ -684,7 +699,10 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
return res.status(400).json({ error: "Failed to get user information" });
|
||||
}
|
||||
|
||||
const getNestedValue = (obj: any, path: string): any => {
|
||||
const getNestedValue = (
|
||||
obj: Record<string, unknown>,
|
||||
path: string,
|
||||
): any => {
|
||||
if (!path || !obj) return null;
|
||||
return path.split(".").reduce((current, key) => current?.[key], obj);
|
||||
};
|
||||
@@ -725,7 +743,7 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
const countResult = db.$client
|
||||
.prepare("SELECT COUNT(*) as count FROM users")
|
||||
.get();
|
||||
isFirstUser = ((countResult as any)?.count || 0) === 0;
|
||||
isFirstUser = ((countResult as { count?: number })?.count || 0) === 0;
|
||||
|
||||
const id = nanoid();
|
||||
await db.insert(users).values({
|
||||
@@ -787,7 +805,10 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
expiresIn: "50d",
|
||||
});
|
||||
|
||||
let frontendUrl = redirectUri.replace("/users/oidc/callback", "");
|
||||
let frontendUrl = (redirectUri as string).replace(
|
||||
"/users/oidc/callback",
|
||||
"",
|
||||
);
|
||||
|
||||
if (frontendUrl.includes("localhost")) {
|
||||
frontendUrl = "http://localhost:5173";
|
||||
@@ -806,7 +827,10 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
} catch (err) {
|
||||
authLogger.error("OIDC callback failed", err);
|
||||
|
||||
let frontendUrl = redirectUri.replace("/users/oidc/callback", "");
|
||||
let frontendUrl = (redirectUri as string).replace(
|
||||
"/users/oidc/callback",
|
||||
"",
|
||||
);
|
||||
|
||||
if (frontendUrl.includes("localhost")) {
|
||||
frontendUrl = "http://localhost:5173";
|
||||
@@ -931,7 +955,7 @@ router.post("/login", async (req, res) => {
|
||||
dataUnlocked: true,
|
||||
});
|
||||
|
||||
const response: any = {
|
||||
const response: Record<string, unknown> = {
|
||||
success: true,
|
||||
is_admin: !!userRecord.is_admin,
|
||||
username: userRecord.username,
|
||||
@@ -962,7 +986,7 @@ router.post("/login", async (req, res) => {
|
||||
// POST /users/logout
|
||||
router.post("/logout", async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (userId) {
|
||||
authManager.logoutUser(userId);
|
||||
@@ -984,7 +1008,7 @@ router.post("/logout", async (req, res) => {
|
||||
// Route: Get current user's info using JWT
|
||||
// GET /users/me
|
||||
router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
if (!isNonEmptyString(userId)) {
|
||||
authLogger.warn("Invalid userId in JWT for /users/me");
|
||||
return res.status(401).json({ error: "Invalid userId" });
|
||||
@@ -1019,7 +1043,7 @@ router.get("/setup-required", async (req, res) => {
|
||||
const countResult = db.$client
|
||||
.prepare("SELECT COUNT(*) as count FROM users")
|
||||
.get();
|
||||
const count = (countResult as any)?.count || 0;
|
||||
const count = (countResult as { count?: number })?.count || 0;
|
||||
|
||||
res.json({
|
||||
setup_required: count === 0,
|
||||
@@ -1033,7 +1057,7 @@ router.get("/setup-required", async (req, res) => {
|
||||
// Route: Count users (admin only - for dashboard statistics)
|
||||
// GET /users/count
|
||||
router.get("/count", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user[0] || !user[0].is_admin) {
|
||||
@@ -1043,7 +1067,7 @@ router.get("/count", authenticateJWT, async (req, res) => {
|
||||
const countResult = db.$client
|
||||
.prepare("SELECT COUNT(*) as count FROM users")
|
||||
.get();
|
||||
const count = (countResult as any)?.count || 0;
|
||||
const count = (countResult as { count?: number })?.count || 0;
|
||||
res.json({ count });
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to count users", err);
|
||||
@@ -1070,7 +1094,9 @@ router.get("/registration-allowed", async (req, res) => {
|
||||
const row = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
|
||||
.get();
|
||||
res.json({ allowed: row ? (row as any).value === "true" : true });
|
||||
res.json({
|
||||
allowed: row ? (row as Record<string, unknown>).value === "true" : true,
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to get registration allowed", err);
|
||||
res.status(500).json({ error: "Failed to get registration allowed" });
|
||||
@@ -1080,7 +1106,7 @@ router.get("/registration-allowed", async (req, res) => {
|
||||
// Route: Set registration allowed status (admin only)
|
||||
// PATCH /users/registration-allowed
|
||||
router.patch("/registration-allowed", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0 || !user[0].is_admin) {
|
||||
@@ -1107,7 +1133,9 @@ router.get("/password-login-allowed", async (req, res) => {
|
||||
const row = db.$client
|
||||
.prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
|
||||
.get();
|
||||
res.json({ allowed: row ? (row as { value: string }).value === "true" : true });
|
||||
res.json({
|
||||
allowed: row ? (row as { value: string }).value === "true" : true,
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to get password login allowed", err);
|
||||
res.status(500).json({ error: "Failed to get password login allowed" });
|
||||
@@ -1117,7 +1145,7 @@ router.get("/password-login-allowed", async (req, res) => {
|
||||
// Route: Set password login allowed status (admin only)
|
||||
// PATCH /users/password-login-allowed
|
||||
router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0 || !user[0].is_admin) {
|
||||
@@ -1128,7 +1156,9 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
|
||||
return res.status(400).json({ error: "Invalid value for allowed" });
|
||||
}
|
||||
db.$client
|
||||
.prepare("UPDATE settings SET value = ? WHERE key = 'allow_password_login'")
|
||||
.prepare(
|
||||
"UPDATE settings SET value = ? WHERE key = 'allow_password_login'",
|
||||
)
|
||||
.run(allowed ? "true" : "false");
|
||||
res.json({ allowed });
|
||||
} catch (err) {
|
||||
@@ -1140,7 +1170,7 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
|
||||
// Route: Delete user account
|
||||
// DELETE /users/delete-account
|
||||
router.delete("/delete-account", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!isNonEmptyString(password)) {
|
||||
@@ -1176,7 +1206,7 @@ router.delete("/delete-account", authenticateJWT, async (req, res) => {
|
||||
const adminCount = db.$client
|
||||
.prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1")
|
||||
.get();
|
||||
if ((adminCount as any)?.count <= 1) {
|
||||
if (((adminCount as { count?: number })?.count || 0) <= 1) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Cannot delete the last admin user" });
|
||||
@@ -1266,7 +1296,9 @@ router.post("/verify-reset-code", async (req, res) => {
|
||||
.json({ error: "No reset code found for this user" });
|
||||
}
|
||||
|
||||
const resetData = JSON.parse((resetDataRow as any).value);
|
||||
const resetData = JSON.parse(
|
||||
(resetDataRow as Record<string, unknown>).value as string,
|
||||
);
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(resetData.expiresAt);
|
||||
|
||||
@@ -1324,7 +1356,9 @@ router.post("/complete-reset", async (req, res) => {
|
||||
return res.status(400).json({ error: "No temporary token found" });
|
||||
}
|
||||
|
||||
const tempTokenData = JSON.parse((tempTokenRow as any).value);
|
||||
const tempTokenData = JSON.parse(
|
||||
(tempTokenRow as Record<string, unknown>).value as string,
|
||||
);
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(tempTokenData.expiresAt);
|
||||
|
||||
@@ -1412,7 +1446,7 @@ router.post("/complete-reset", async (req, res) => {
|
||||
// Route: List all users (admin only)
|
||||
// GET /users/list
|
||||
router.get("/list", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
try {
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user || user.length === 0 || !user[0].is_admin) {
|
||||
@@ -1438,7 +1472,7 @@ router.get("/list", authenticateJWT, async (req, res) => {
|
||||
// Route: Make user admin (admin only)
|
||||
// POST /users/make-admin
|
||||
router.post("/make-admin", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { username } = req.body;
|
||||
|
||||
if (!isNonEmptyString(username)) {
|
||||
@@ -1481,7 +1515,7 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
|
||||
// Route: Remove admin status (admin only)
|
||||
// POST /users/remove-admin
|
||||
router.post("/remove-admin", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { username } = req.body;
|
||||
|
||||
if (!isNonEmptyString(username)) {
|
||||
@@ -1638,7 +1672,7 @@ router.post("/totp/verify-login", async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const response: any = {
|
||||
const response: Record<string, unknown> = {
|
||||
success: true,
|
||||
is_admin: !!userRecord.is_admin,
|
||||
username: userRecord.username,
|
||||
@@ -1668,7 +1702,7 @@ router.post("/totp/verify-login", async (req, res) => {
|
||||
// Route: Setup TOTP
|
||||
// POST /users/totp/setup
|
||||
router.post("/totp/setup", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
try {
|
||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
||||
@@ -1707,7 +1741,7 @@ router.post("/totp/setup", authenticateJWT, async (req, res) => {
|
||||
// Route: Enable TOTP
|
||||
// POST /users/totp/enable
|
||||
router.post("/totp/enable", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { totp_code } = req.body;
|
||||
|
||||
if (!totp_code) {
|
||||
@@ -1766,7 +1800,7 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => {
|
||||
// Route: Disable TOTP
|
||||
// POST /users/totp/disable
|
||||
router.post("/totp/disable", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { password, totp_code } = req.body;
|
||||
|
||||
if (!password && !totp_code) {
|
||||
@@ -1824,7 +1858,7 @@ router.post("/totp/disable", authenticateJWT, async (req, res) => {
|
||||
// Route: Generate new backup codes
|
||||
// POST /users/totp/backup-codes
|
||||
router.post("/totp/backup-codes", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { password, totp_code } = req.body;
|
||||
|
||||
if (!password && !totp_code) {
|
||||
@@ -1882,7 +1916,7 @@ router.post("/totp/backup-codes", authenticateJWT, async (req, res) => {
|
||||
// Route: Delete user (admin only)
|
||||
// DELETE /users/delete-user
|
||||
router.delete("/delete-user", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { username } = req.body;
|
||||
|
||||
if (!isNonEmptyString(username)) {
|
||||
@@ -1911,7 +1945,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
|
||||
const adminCount = db.$client
|
||||
.prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1")
|
||||
.get();
|
||||
if ((adminCount as any)?.count <= 1) {
|
||||
if (((adminCount as { count?: number })?.count || 0) <= 1) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Cannot delete the last admin user" });
|
||||
@@ -1968,7 +2002,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
|
||||
// Route: User data unlock - used when session expires
|
||||
// POST /users/unlock-data
|
||||
router.post("/unlock-data", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password) {
|
||||
@@ -2001,7 +2035,7 @@ router.post("/unlock-data", authenticateJWT, async (req, res) => {
|
||||
// Route: Check user data unlock status
|
||||
// GET /users/data-status
|
||||
router.get("/data-status", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
try {
|
||||
const isUnlocked = authManager.isUserUnlocked(userId);
|
||||
@@ -2023,7 +2057,7 @@ router.get("/data-status", authenticateJWT, async (req, res) => {
|
||||
// Route: Change user password (re-encrypt data keys)
|
||||
// POST /users/change-password
|
||||
router.post("/change-password", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { eq, and } from "drizzle-orm";
|
||||
import { statsLogger } from "../utils/logger.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import type { AuthenticatedRequest } from "../../types/index.js";
|
||||
|
||||
interface PooledConnection {
|
||||
client: Client;
|
||||
@@ -237,7 +238,7 @@ class RequestQueue {
|
||||
}
|
||||
|
||||
interface CachedMetrics {
|
||||
data: any;
|
||||
data: unknown;
|
||||
timestamp: number;
|
||||
hostId: number;
|
||||
}
|
||||
@@ -246,7 +247,7 @@ class MetricsCache {
|
||||
private cache = new Map<number, CachedMetrics>();
|
||||
private ttl = 30000;
|
||||
|
||||
get(hostId: number): any | null {
|
||||
get(hostId: number): unknown | null {
|
||||
const cached = this.cache.get(hostId);
|
||||
if (cached && Date.now() - cached.timestamp < this.ttl) {
|
||||
return cached.data;
|
||||
@@ -254,7 +255,7 @@ class MetricsCache {
|
||||
return null;
|
||||
}
|
||||
|
||||
set(hostId: number, data: any): void {
|
||||
set(hostId: number, data: unknown): void {
|
||||
this.cache.set(hostId, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
@@ -297,7 +298,7 @@ interface SSHHostWithCredentials {
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: any[];
|
||||
tunnelConnections: unknown[];
|
||||
statsConfig?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -432,11 +433,11 @@ async function fetchHostById(
|
||||
}
|
||||
|
||||
async function resolveHostCredentials(
|
||||
host: any,
|
||||
host: Record<string, unknown>,
|
||||
userId: string,
|
||||
): Promise<SSHHostWithCredentials | undefined> {
|
||||
try {
|
||||
const baseHost: any = {
|
||||
const baseHost: Record<string, unknown> = {
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
ip: host.ip,
|
||||
@@ -456,7 +457,7 @@ async function resolveHostCredentials(
|
||||
enableFileManager: !!host.enableFileManager,
|
||||
defaultPath: host.defaultPath || "/",
|
||||
tunnelConnections: host.tunnelConnections
|
||||
? JSON.parse(host.tunnelConnections)
|
||||
? JSON.parse(host.tunnelConnections as string)
|
||||
: [],
|
||||
statsConfig: host.statsConfig || undefined,
|
||||
createdAt: host.createdAt,
|
||||
@@ -472,7 +473,7 @@ async function resolveHostCredentials(
|
||||
.from(sshCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(sshCredentials.id, host.credentialId),
|
||||
eq(sshCredentials.id, host.credentialId as number),
|
||||
eq(sshCredentials.userId, userId),
|
||||
),
|
||||
),
|
||||
@@ -512,7 +513,7 @@ async function resolveHostCredentials(
|
||||
addLegacyCredentials(baseHost, host);
|
||||
}
|
||||
|
||||
return baseHost;
|
||||
return baseHost as unknown as SSHHostWithCredentials;
|
||||
} catch (error) {
|
||||
statsLogger.error(
|
||||
`Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
@@ -521,7 +522,10 @@ async function resolveHostCredentials(
|
||||
}
|
||||
}
|
||||
|
||||
function addLegacyCredentials(baseHost: any, host: any): void {
|
||||
function addLegacyCredentials(
|
||||
baseHost: Record<string, unknown>,
|
||||
host: Record<string, unknown>,
|
||||
): void {
|
||||
baseHost.password = host.password || null;
|
||||
baseHost.key = host.key || null;
|
||||
baseHost.keyPassword = host.key_password || host.keyPassword || null;
|
||||
@@ -573,7 +577,7 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
|
||||
if (!host.password) {
|
||||
throw new Error(`No password available for host ${host.ip}`);
|
||||
}
|
||||
(base as any).password = host.password;
|
||||
(base as Record<string, unknown>).password = host.password;
|
||||
} else if (host.authType === "key") {
|
||||
if (!host.key) {
|
||||
throw new Error(`No SSH key available for host ${host.ip}`);
|
||||
@@ -589,10 +593,13 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n");
|
||||
|
||||
(base as any).privateKey = Buffer.from(cleanKey, "utf8");
|
||||
(base as Record<string, unknown>).privateKey = Buffer.from(
|
||||
cleanKey,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
if (host.keyPassword) {
|
||||
(base as any).passphrase = host.keyPassword;
|
||||
(base as Record<string, unknown>).passphrase = host.keyPassword;
|
||||
}
|
||||
} catch (keyError) {
|
||||
statsLogger.error(
|
||||
@@ -724,7 +731,9 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
}> {
|
||||
const cached = metricsCache.get(host.id);
|
||||
if (cached) {
|
||||
return cached;
|
||||
return cached as ReturnType<typeof collectMetrics> extends Promise<infer T>
|
||||
? T
|
||||
: never;
|
||||
}
|
||||
|
||||
return requestQueue.queueRequest(host.id, async () => {
|
||||
@@ -873,7 +882,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
}
|
||||
|
||||
// Collect network interfaces
|
||||
let interfaces: Array<{
|
||||
const interfaces: Array<{
|
||||
name: string;
|
||||
ip: string;
|
||||
state: string;
|
||||
@@ -958,7 +967,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
// Collect process information
|
||||
let totalProcesses: number | null = null;
|
||||
let runningProcesses: number | null = null;
|
||||
let topProcesses: Array<{
|
||||
const topProcesses: Array<{
|
||||
pid: string;
|
||||
user: string;
|
||||
cpu: string;
|
||||
@@ -1145,7 +1154,7 @@ async function pollStatusesOnce(userId?: string): Promise<void> {
|
||||
}
|
||||
|
||||
app.get("/status", async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||
return res.status(401).json({
|
||||
@@ -1166,7 +1175,7 @@ app.get("/status", async (req, res) => {
|
||||
|
||||
app.get("/status/:id", validateHostId, async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||
return res.status(401).json({
|
||||
@@ -1197,7 +1206,7 @@ app.get("/status/:id", validateHostId, async (req, res) => {
|
||||
});
|
||||
|
||||
app.post("/refresh", async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||
return res.status(401).json({
|
||||
@@ -1212,7 +1221,7 @@ app.post("/refresh", async (req, res) => {
|
||||
|
||||
app.get("/metrics/:id", validateHostId, async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||
return res.status(401).json({
|
||||
|
||||
@@ -403,12 +403,19 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedCredentials = {
|
||||
password: credential.password,
|
||||
key:
|
||||
credential.private_key || credential.privateKey || credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
authType: credential.auth_type || credential.authType,
|
||||
password: credential.password as string | undefined,
|
||||
key: (credential.private_key ||
|
||||
credential.privateKey ||
|
||||
credential.key) as string | undefined,
|
||||
keyPassword: (credential.key_password || credential.keyPassword) as
|
||||
| string
|
||||
| undefined,
|
||||
keyType: (credential.key_type || credential.keyType) as
|
||||
| string
|
||||
| undefined,
|
||||
authType: (credential.auth_type || credential.authType) as
|
||||
| string
|
||||
| undefined,
|
||||
};
|
||||
} else {
|
||||
sshLogger.warn(`No credentials found for host ${id}`, {
|
||||
@@ -617,13 +624,18 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
);
|
||||
} else {
|
||||
if (resolvedCredentials.password) {
|
||||
const responses = prompts.map(() => resolvedCredentials.password || "");
|
||||
const responses = prompts.map(
|
||||
() => resolvedCredentials.password || "",
|
||||
);
|
||||
finish(responses);
|
||||
} else {
|
||||
sshLogger.warn("Keyboard-interactive requires password but none available", {
|
||||
operation: "ssh_keyboard_interactive_no_password",
|
||||
hostId: id,
|
||||
});
|
||||
sshLogger.warn(
|
||||
"Keyboard-interactive requires password but none available",
|
||||
{
|
||||
operation: "ssh_keyboard_interactive_no_password",
|
||||
hostId: id,
|
||||
},
|
||||
);
|
||||
finish(prompts.map(() => ""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,12 +515,17 @@ async function connectSSHTunnel(
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedSourceCredentials = {
|
||||
password: credential.password,
|
||||
sshKey:
|
||||
credential.private_key || credential.privateKey || credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
authMethod: credential.auth_type || credential.authType,
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: (credential.private_key ||
|
||||
credential.privateKey ||
|
||||
credential.key) as string | undefined,
|
||||
keyPassword: (credential.key_password || credential.keyPassword) as
|
||||
| string
|
||||
| undefined,
|
||||
keyType: (credential.key_type || credential.keyType) as
|
||||
| string
|
||||
| undefined,
|
||||
authMethod: (credential.auth_type || credential.authType) as string,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -593,12 +598,17 @@ async function connectSSHTunnel(
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedEndpointCredentials = {
|
||||
password: credential.password,
|
||||
sshKey:
|
||||
credential.private_key || credential.privateKey || credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
authMethod: credential.auth_type || credential.authType,
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: (credential.private_key ||
|
||||
credential.privateKey ||
|
||||
credential.key) as string | undefined,
|
||||
keyPassword: (credential.key_password || credential.keyPassword) as
|
||||
| string
|
||||
| undefined,
|
||||
keyType: (credential.key_type || credential.keyType) as
|
||||
| string
|
||||
| undefined,
|
||||
authMethod: (credential.auth_type || credential.authType) as string,
|
||||
};
|
||||
} else {
|
||||
tunnelLogger.warn("No endpoint credentials found in database", {
|
||||
@@ -1031,12 +1041,17 @@ async function killRemoteTunnelByMarker(
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedSourceCredentials = {
|
||||
password: credential.password,
|
||||
sshKey:
|
||||
credential.private_key || credential.privateKey || credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
authMethod: credential.auth_type || credential.authType,
|
||||
password: credential.password as string | undefined,
|
||||
sshKey: (credential.private_key ||
|
||||
credential.privateKey ||
|
||||
credential.key) as string | undefined,
|
||||
keyPassword: (credential.key_password || credential.keyPassword) as
|
||||
| string
|
||||
| undefined,
|
||||
keyType: (credential.key_type || credential.keyType) as
|
||||
| string
|
||||
| undefined,
|
||||
authMethod: (credential.auth_type || credential.authType) as string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class DataCrypto {
|
||||
|
||||
static encryptRecord(
|
||||
tableName: string,
|
||||
record: any,
|
||||
record: Record<string, unknown>,
|
||||
userId: string,
|
||||
userDataKey: Buffer,
|
||||
): any {
|
||||
@@ -24,7 +24,7 @@ class DataCrypto {
|
||||
encryptedRecord[fieldName] = FieldCrypto.encryptField(
|
||||
value as string,
|
||||
userDataKey,
|
||||
recordId,
|
||||
recordId as string,
|
||||
fieldName,
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class DataCrypto {
|
||||
|
||||
static decryptRecord(
|
||||
tableName: string,
|
||||
record: any,
|
||||
record: Record<string, unknown>,
|
||||
userId: string,
|
||||
userDataKey: Buffer,
|
||||
): any {
|
||||
@@ -49,7 +49,7 @@ class DataCrypto {
|
||||
decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue(
|
||||
value as string,
|
||||
userDataKey,
|
||||
recordId,
|
||||
recordId as string,
|
||||
fieldName,
|
||||
);
|
||||
}
|
||||
@@ -60,13 +60,18 @@ class DataCrypto {
|
||||
|
||||
static decryptRecords(
|
||||
tableName: string,
|
||||
records: any[],
|
||||
records: unknown[],
|
||||
userId: string,
|
||||
userDataKey: Buffer,
|
||||
): any[] {
|
||||
): unknown[] {
|
||||
if (!Array.isArray(records)) return records;
|
||||
return records.map((record) =>
|
||||
this.decryptRecord(tableName, record, userId, userDataKey),
|
||||
this.decryptRecord(
|
||||
tableName,
|
||||
record as Record<string, unknown>,
|
||||
userId,
|
||||
userDataKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -386,7 +391,7 @@ class DataCrypto {
|
||||
|
||||
static encryptRecordForUser(
|
||||
tableName: string,
|
||||
record: any,
|
||||
record: Record<string, unknown>,
|
||||
userId: string,
|
||||
): any {
|
||||
const userDataKey = this.validateUserAccess(userId);
|
||||
@@ -395,7 +400,7 @@ class DataCrypto {
|
||||
|
||||
static decryptRecordForUser(
|
||||
tableName: string,
|
||||
record: any,
|
||||
record: Record<string, unknown>,
|
||||
userId: string,
|
||||
): any {
|
||||
const userDataKey = this.validateUserAccess(userId);
|
||||
@@ -404,9 +409,9 @@ class DataCrypto {
|
||||
|
||||
static decryptRecordsForUser(
|
||||
tableName: string,
|
||||
records: any[],
|
||||
records: unknown[],
|
||||
userId: string,
|
||||
): any[] {
|
||||
): unknown[] {
|
||||
const userDataKey = this.validateUserAccess(userId);
|
||||
return this.decryptRecords(tableName, records, userId, userDataKey);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import type { SQLiteTable } from "drizzle-orm/sqlite-core";
|
||||
type TableName = "users" | "ssh_data" | "ssh_credentials";
|
||||
|
||||
class SimpleDBOps {
|
||||
static async insert<T extends Record<string, any>>(
|
||||
static async insert<T extends Record<string, unknown>>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
table: SQLiteTable<any>,
|
||||
tableName: TableName,
|
||||
data: T,
|
||||
@@ -44,8 +45,8 @@ class SimpleDBOps {
|
||||
return decryptedResult as T;
|
||||
}
|
||||
|
||||
static async select<T extends Record<string, any>>(
|
||||
query: any,
|
||||
static async select<T extends Record<string, unknown>>(
|
||||
query: unknown,
|
||||
tableName: TableName,
|
||||
userId: string,
|
||||
): Promise<T[]> {
|
||||
@@ -58,16 +59,16 @@ class SimpleDBOps {
|
||||
|
||||
const decryptedResults = DataCrypto.decryptRecords(
|
||||
tableName,
|
||||
results,
|
||||
results as unknown[],
|
||||
userId,
|
||||
userDataKey,
|
||||
);
|
||||
|
||||
return decryptedResults;
|
||||
return decryptedResults as T[];
|
||||
}
|
||||
|
||||
static async selectOne<T extends Record<string, any>>(
|
||||
query: any,
|
||||
static async selectOne<T extends Record<string, unknown>>(
|
||||
query: unknown,
|
||||
tableName: TableName,
|
||||
userId: string,
|
||||
): Promise<T | undefined> {
|
||||
@@ -81,7 +82,7 @@ class SimpleDBOps {
|
||||
|
||||
const decryptedResult = DataCrypto.decryptRecord(
|
||||
tableName,
|
||||
result,
|
||||
result as Record<string, unknown>,
|
||||
userId,
|
||||
userDataKey,
|
||||
);
|
||||
@@ -89,10 +90,11 @@ class SimpleDBOps {
|
||||
return decryptedResult;
|
||||
}
|
||||
|
||||
static async update<T extends Record<string, any>>(
|
||||
static async update<T extends Record<string, unknown>>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
table: SQLiteTable<any>,
|
||||
tableName: TableName,
|
||||
where: any,
|
||||
where: unknown,
|
||||
data: Partial<T>,
|
||||
userId: string,
|
||||
): Promise<T[]> {
|
||||
@@ -108,7 +110,8 @@ class SimpleDBOps {
|
||||
const result = await getDb()
|
||||
.update(table)
|
||||
.set(encryptedData)
|
||||
.where(where)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.where(where as any)
|
||||
.returning();
|
||||
|
||||
DatabaseSaveTrigger.triggerSave(`update_${tableName}`);
|
||||
@@ -124,12 +127,17 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
static async delete(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
table: SQLiteTable<any>,
|
||||
tableName: TableName,
|
||||
where: any,
|
||||
where: unknown,
|
||||
_userId: string,
|
||||
): Promise<any[]> {
|
||||
const result = await getDb().delete(table).where(where).returning();
|
||||
): Promise<unknown[]> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await getDb()
|
||||
.delete(table)
|
||||
.where(where as any)
|
||||
.returning();
|
||||
|
||||
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
|
||||
|
||||
@@ -145,12 +153,12 @@ class SimpleDBOps {
|
||||
}
|
||||
|
||||
static async selectEncrypted(
|
||||
query: any,
|
||||
query: unknown,
|
||||
_tableName: TableName,
|
||||
): Promise<any[]> {
|
||||
): Promise<unknown[]> {
|
||||
const results = await query;
|
||||
|
||||
return results;
|
||||
return results as unknown[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ interface UserExportData {
|
||||
userId: string;
|
||||
username: string;
|
||||
userData: {
|
||||
sshHosts: any[];
|
||||
sshCredentials: any[];
|
||||
sshHosts: unknown[];
|
||||
sshCredentials: unknown[];
|
||||
fileManagerData: {
|
||||
recent: any[];
|
||||
pinned: any[];
|
||||
shortcuts: any[];
|
||||
recent: unknown[];
|
||||
pinned: unknown[];
|
||||
shortcuts: unknown[];
|
||||
};
|
||||
dismissedAlerts: any[];
|
||||
dismissedAlerts: unknown[];
|
||||
};
|
||||
metadata: {
|
||||
totalRecords: number;
|
||||
@@ -83,7 +83,7 @@ class UserDataExport {
|
||||
)
|
||||
: sshHosts;
|
||||
|
||||
let sshCredentialsData: any[] = [];
|
||||
let sshCredentialsData: unknown[] = [];
|
||||
if (includeCredentials) {
|
||||
const credentials = await getDb()
|
||||
.select()
|
||||
@@ -185,7 +185,10 @@ class UserDataExport {
|
||||
return JSON.stringify(exportData, null, pretty ? 2 : 0);
|
||||
}
|
||||
|
||||
static validateExportData(data: any): { valid: boolean; errors: string[] } {
|
||||
static validateExportData(data: unknown): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
@@ -193,23 +196,26 @@ class UserDataExport {
|
||||
return { valid: false, errors };
|
||||
}
|
||||
|
||||
if (!data.version) {
|
||||
const dataObj = data as Record<string, unknown>;
|
||||
|
||||
if (!dataObj.version) {
|
||||
errors.push("Missing version field");
|
||||
}
|
||||
|
||||
if (!data.userId) {
|
||||
if (!dataObj.userId) {
|
||||
errors.push("Missing userId field");
|
||||
}
|
||||
|
||||
if (!data.userData || typeof data.userData !== "object") {
|
||||
if (!dataObj.userData || typeof dataObj.userData !== "object") {
|
||||
errors.push("Missing or invalid userData field");
|
||||
}
|
||||
|
||||
if (!data.metadata || typeof data.metadata !== "object") {
|
||||
if (!dataObj.metadata || typeof dataObj.metadata !== "object") {
|
||||
errors.push("Missing or invalid metadata field");
|
||||
}
|
||||
|
||||
if (data.userData) {
|
||||
if (dataObj.userData) {
|
||||
const userData = dataObj.userData as Record<string, unknown>;
|
||||
const requiredFields = [
|
||||
"sshHosts",
|
||||
"sshCredentials",
|
||||
@@ -218,23 +224,24 @@ class UserDataExport {
|
||||
];
|
||||
for (const field of requiredFields) {
|
||||
if (
|
||||
!Array.isArray(data.userData[field]) &&
|
||||
!(
|
||||
field === "fileManagerData" &&
|
||||
typeof data.userData[field] === "object"
|
||||
)
|
||||
!Array.isArray(userData[field]) &&
|
||||
!(field === "fileManagerData" && typeof userData[field] === "object")
|
||||
) {
|
||||
errors.push(`Missing or invalid userData.${field} field`);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
data.userData.fileManagerData &&
|
||||
typeof data.userData.fileManagerData === "object"
|
||||
userData.fileManagerData &&
|
||||
typeof userData.fileManagerData === "object"
|
||||
) {
|
||||
const fileManagerData = userData.fileManagerData as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const fmFields = ["recent", "pinned", "shortcuts"];
|
||||
for (const field of fmFields) {
|
||||
if (!Array.isArray(data.userData.fileManagerData[field])) {
|
||||
if (!Array.isArray(fileManagerData[field])) {
|
||||
errors.push(
|
||||
`Missing or invalid userData.fileManagerData.${field} field`,
|
||||
);
|
||||
|
||||
43
src/types/electron.d.ts
vendored
43
src/types/electron.d.ts
vendored
@@ -1,22 +1,49 @@
|
||||
interface ServerConfig {
|
||||
serverUrl?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ConnectionTestResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface DialogOptions {
|
||||
title?: string;
|
||||
defaultPath?: string;
|
||||
buttonLabel?: string;
|
||||
filters?: Array<{ name: string; extensions: string[] }>;
|
||||
properties?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface DialogResult {
|
||||
canceled: boolean;
|
||||
filePath?: string;
|
||||
filePaths?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
getAppVersion: () => Promise<string>;
|
||||
getPlatform: () => Promise<string>;
|
||||
|
||||
getServerConfig: () => Promise<any>;
|
||||
saveServerConfig: (config: any) => Promise<any>;
|
||||
testServerConnection: (serverUrl: string) => Promise<any>;
|
||||
getServerConfig: () => Promise<ServerConfig>;
|
||||
saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>;
|
||||
testServerConnection: (serverUrl: string) => Promise<ConnectionTestResult>;
|
||||
|
||||
showSaveDialog: (options: any) => Promise<any>;
|
||||
showOpenDialog: (options: any) => Promise<any>;
|
||||
showSaveDialog: (options: DialogOptions) => Promise<DialogResult>;
|
||||
showOpenDialog: (options: DialogOptions) => Promise<DialogResult>;
|
||||
|
||||
onUpdateAvailable: (callback: Function) => void;
|
||||
onUpdateDownloaded: (callback: Function) => void;
|
||||
onUpdateAvailable: (callback: () => void) => void;
|
||||
onUpdateDownloaded: (callback: () => void) => void;
|
||||
|
||||
removeAllListeners: (channel: string) => void;
|
||||
isElectron: boolean;
|
||||
isDev: boolean;
|
||||
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
createTempFile: (fileData: {
|
||||
fileName: string;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// This file contains all shared interfaces and types used across the application
|
||||
|
||||
import type { Client } from "ssh2";
|
||||
import type { Request } from "express";
|
||||
|
||||
// ============================================================================
|
||||
// SSH HOST TYPES
|
||||
@@ -58,7 +59,7 @@ export interface SSHHostData {
|
||||
enableTunnel?: boolean;
|
||||
enableFileManager?: boolean;
|
||||
defaultPath?: string;
|
||||
tunnelConnections?: any[];
|
||||
tunnelConnections?: TunnelConnection[];
|
||||
statsConfig?: string;
|
||||
}
|
||||
|
||||
@@ -263,8 +264,8 @@ export interface TabContextTab {
|
||||
| "file_manager"
|
||||
|
|
||||
| "user_profile";
|
||||
title: string;
|
||||
hostConfig?: any;
|
||||
terminalRef?: React.RefObject<any>;
|
||||
hostConfig?: SSHHost;
|
||||
terminalRef?: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -305,7 +306,7 @@ export type KeyType = "rsa" | "ecdsa" | "ed25519";
|
||||
// API RESPONSE TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
export interface ApiResponse<T = unknown> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
@@ -368,13 +369,13 @@ export interface SSHTunnelViewerProps {
|
||||
action: "connect" | "disconnect" | "cancel",
|
||||
host: SSHHost,
|
||||
tunnelIndex: number,
|
||||
) => Promise<any>
|
||||
) => Promise<void>
|
||||
>;
|
||||
onTunnelAction?: (
|
||||
action: "connect" | "disconnect" | "cancel",
|
||||
host: SSHHost,
|
||||
tunnelIndex: number,
|
||||
) => Promise<any>;
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface FileManagerProps {
|
||||
@@ -402,7 +403,7 @@ export interface SSHTunnelObjectProps {
|
||||
action: "connect" | "disconnect" | "cancel",
|
||||
host: SSHHost,
|
||||
tunnelIndex: number,
|
||||
) => Promise<any>;
|
||||
) => Promise<void>;
|
||||
compact?: boolean;
|
||||
bare?: boolean;
|
||||
}
|
||||
@@ -461,3 +462,95 @@ export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
||||
|
||||
export type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
// ============================================================================
|
||||
// EXPRESS REQUEST TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
userId: string;
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GITHUB API TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface GitHubAsset {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
download_count: number;
|
||||
browser_download_url: string;
|
||||
}
|
||||
|
||||
export interface GitHubRelease {
|
||||
id: number;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string;
|
||||
published_at: string;
|
||||
html_url: string;
|
||||
assets: GitHubAsset[];
|
||||
prerelease: boolean;
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
export interface GitHubAPIResponse<T> {
|
||||
data: T;
|
||||
cached: boolean;
|
||||
cache_age?: number;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CACHE TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface CacheEntry<T = unknown> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DATABASE EXPORT/IMPORT TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface ExportSummary {
|
||||
sshHostsImported: number;
|
||||
sshCredentialsImported: number;
|
||||
fileManagerItemsImported: number;
|
||||
dismissedAlertsImported: number;
|
||||
credentialUsageImported: number;
|
||||
settingsImported: number;
|
||||
skippedItems: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
success: boolean;
|
||||
summary: ExportSummary;
|
||||
}
|
||||
|
||||
export interface ExportRequestBody {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ImportRequestBody {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ExportPreviewBody {
|
||||
scope?: string;
|
||||
includeCredentials?: boolean;
|
||||
}
|
||||
|
||||
export interface RestoreRequestBody {
|
||||
backupPath: string;
|
||||
targetPath?: string;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,8 @@ export function AdminSettings({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as any).configuredServerUrl;
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
@@ -127,7 +128,8 @@ export function AdminSettings({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as any).configuredServerUrl;
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
@@ -148,7 +150,8 @@ export function AdminSettings({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as any).configuredServerUrl;
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
@@ -169,7 +172,8 @@ export function AdminSettings({
|
||||
|
||||
const fetchUsers = async () => {
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as any).configuredServerUrl;
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
@@ -234,9 +238,10 @@ export function AdminSettings({
|
||||
try {
|
||||
await updateOIDCConfig(oidcConfig);
|
||||
toast.success(t("admin.oidcConfigurationUpdated"));
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
setOidcError(
|
||||
err?.response?.data?.error || t("admin.failedToUpdateOidcConfig"),
|
||||
(err as { response?: { data?: { error?: string } } })?.response?.data
|
||||
?.error || t("admin.failedToUpdateOidcConfig"),
|
||||
);
|
||||
} finally {
|
||||
setOidcLoading(false);
|
||||
@@ -257,9 +262,10 @@ export function AdminSettings({
|
||||
toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername }));
|
||||
setNewAdminUsername("");
|
||||
fetchUsers();
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
setMakeAdminError(
|
||||
err?.response?.data?.error || t("admin.failedToMakeUserAdmin"),
|
||||
(err as { response?: { data?: { error?: string } } })?.response?.data
|
||||
?.error || t("admin.failedToMakeUserAdmin"),
|
||||
);
|
||||
} finally {
|
||||
setMakeAdminLoading(false);
|
||||
@@ -272,7 +278,7 @@ export function AdminSettings({
|
||||
await removeAdminStatus(username);
|
||||
toast.success(t("admin.adminStatusRemoved", { username }));
|
||||
fetchUsers();
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
toast.error(t("admin.failedToRemoveAdminStatus"));
|
||||
}
|
||||
});
|
||||
@@ -286,7 +292,7 @@ export function AdminSettings({
|
||||
await deleteUser(username);
|
||||
toast.success(t("admin.userDeletedSuccessfully", { username }));
|
||||
fetchUsers();
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
toast.error(t("admin.failedToDeleteUser"));
|
||||
}
|
||||
},
|
||||
@@ -316,7 +322,7 @@ export function AdminSettings({
|
||||
window.location.hostname === "127.0.0.1");
|
||||
|
||||
const apiUrl = isElectron()
|
||||
? `${(window as any).configuredServerUrl}/database/export`
|
||||
? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/export`
|
||||
: isDev
|
||||
? `http://localhost:30001/database/export`
|
||||
: `${window.location.protocol}//${window.location.host}/database/export`;
|
||||
@@ -386,7 +392,7 @@ export function AdminSettings({
|
||||
window.location.hostname === "127.0.0.1");
|
||||
|
||||
const apiUrl = isElectron()
|
||||
? `${(window as any).configuredServerUrl}/database/import`
|
||||
? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/import`
|
||||
: isDev
|
||||
? `http://localhost:30001/database/import`
|
||||
: `${window.location.protocol}//${window.location.host}/database/import`;
|
||||
@@ -713,9 +719,13 @@ export function AdminSettings({
|
||||
try {
|
||||
await disableOIDCConfig();
|
||||
toast.success(t("admin.oidcConfigurationDisabled"));
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
setOidcError(
|
||||
err?.response?.data?.error ||
|
||||
(
|
||||
err as {
|
||||
response?: { data?: { error?: string } };
|
||||
}
|
||||
)?.response?.data?.error ||
|
||||
t("admin.failedToDisableOidcConfig"),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -311,10 +311,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
setFiles(files);
|
||||
clearSelection();
|
||||
initialLoadDoneRef.current = true;
|
||||
} catch (dirError: any) {
|
||||
} catch (dirError: unknown) {
|
||||
console.error("Failed to load initial directory:", dirError);
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("SSH connection failed:", error);
|
||||
handleCloseWithError(
|
||||
t("fileManager.failedToConnect") + ": " + (error.message || error),
|
||||
@@ -353,7 +353,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
|
||||
setFiles(files);
|
||||
clearSelection();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (currentLoadingPathRef.current === path) {
|
||||
console.error("Failed to load directory:", error);
|
||||
|
||||
@@ -535,7 +535,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
t("fileManager.fileUploadedSuccessfully", { name: file.name }),
|
||||
);
|
||||
handleRefreshDirectory();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast.dismiss(progressToast);
|
||||
|
||||
if (
|
||||
@@ -584,7 +584,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
t("fileManager.fileDownloadedSuccessfully", { name: file.name }),
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
@@ -665,7 +665,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
);
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
@@ -775,7 +775,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
component: createWindowComponent,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast.error(
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
@@ -914,7 +914,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(`Failed to ${operation} file ${file.name}:`, error);
|
||||
toast.error(
|
||||
t("fileManager.operationFailed", {
|
||||
@@ -1015,7 +1015,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
if (operation === "cut") {
|
||||
setClipboard(null);
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast.error(
|
||||
`${t("fileManager.pasteFailed")}: ${error.message || t("fileManager.unknownError")}`,
|
||||
);
|
||||
@@ -1050,7 +1050,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
currentHost?.userId?.toString(),
|
||||
);
|
||||
successCount++;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
`Failed to delete copied file ${copiedFile.targetName}:`,
|
||||
error,
|
||||
@@ -1092,7 +1092,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
currentHost?.userId?.toString(),
|
||||
);
|
||||
successCount++;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
`Failed to move back file ${movedFile.targetName}:`,
|
||||
error,
|
||||
@@ -1132,7 +1132,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}
|
||||
|
||||
handleRefreshDirectory();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast.error(
|
||||
`${t("fileManager.undoOperationFailed")}: ${error.message || t("fileManager.unknownError")}`,
|
||||
);
|
||||
@@ -1204,7 +1204,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
|
||||
setCreateIntent(null);
|
||||
handleRefreshDirectory();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Create failed:", error);
|
||||
toast.error(t("fileManager.failedToCreateItem"));
|
||||
}
|
||||
@@ -1233,7 +1233,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
);
|
||||
setEditingFile(null);
|
||||
handleRefreshDirectory();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Rename failed:", error);
|
||||
toast.error(t("fileManager.failedToRenameItem"));
|
||||
}
|
||||
@@ -1269,11 +1269,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
clearSelection();
|
||||
initialLoadDoneRef.current = true;
|
||||
toast.success(t("fileManager.connectedSuccessfully"));
|
||||
} catch (dirError: any) {
|
||||
} catch (dirError: unknown) {
|
||||
console.error("Failed to load initial directory:", dirError);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("TOTP verification failed:", error);
|
||||
toast.error(t("fileManager.totpVerificationFailed"));
|
||||
} finally {
|
||||
@@ -1340,7 +1340,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
movedItems.push(file.name);
|
||||
successCount++;
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(`Failed to move file ${file.name}:`, error);
|
||||
toast.error(
|
||||
t("fileManager.moveFileFailed", { name: file.name }) +
|
||||
@@ -1388,7 +1388,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Drag move operation failed:", error);
|
||||
toast.error(t("fileManager.moveOperationFailed") + ": " + error.message);
|
||||
}
|
||||
@@ -1459,7 +1459,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
await dragToDesktop.dragFilesToDesktop(files);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Drag to desktop failed:", error);
|
||||
toast.error(
|
||||
t("fileManager.dragFailed") +
|
||||
@@ -1554,7 +1554,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
|
||||
try {
|
||||
const pinnedData = await getPinnedFiles(currentHost.id);
|
||||
const pinnedPaths = new Set(pinnedData.map((item: any) => item.path));
|
||||
const pinnedPaths = new Set(
|
||||
pinnedData.map((item: Record<string, unknown>) => item.path),
|
||||
);
|
||||
setPinnedFiles(pinnedPaths);
|
||||
} catch (error) {
|
||||
console.error("Failed to load pinned files:", error);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,21 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
|
||||
interface TabData {
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
terminalRef?: {
|
||||
current?: {
|
||||
fit?: () => void;
|
||||
notifyResize?: () => void;
|
||||
refresh?: () => void;
|
||||
};
|
||||
};
|
||||
hostConfig?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface TerminalViewProps {
|
||||
isTopbarOpen?: boolean;
|
||||
}
|
||||
@@ -25,11 +40,16 @@ interface TerminalViewProps {
|
||||
export function AppView({
|
||||
isTopbarOpen = true,
|
||||
}: TerminalViewProps): React.ReactElement {
|
||||
const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as any;
|
||||
const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as {
|
||||
tabs: TabData[];
|
||||
currentTab: number;
|
||||
allSplitScreenTab: number[];
|
||||
removeTab: (id: number) => void;
|
||||
};
|
||||
const { state: sidebarState } = useSidebar();
|
||||
|
||||
const terminalTabs = tabs.filter(
|
||||
(tab: any) =>
|
||||
(tab: TabData) =>
|
||||
tab.type === "terminal" ||
|
||||
tab.type === "server" ||
|
||||
tab.type === "file_manager",
|
||||
@@ -59,7 +79,7 @@ export function AppView({
|
||||
const splitIds = allSplitScreenTab as number[];
|
||||
visibleIds.push(currentTab, ...splitIds.filter((i) => i !== currentTab));
|
||||
}
|
||||
terminalTabs.forEach((t: any) => {
|
||||
terminalTabs.forEach((t: TabData) => {
|
||||
if (visibleIds.includes(t.id)) {
|
||||
const ref = t.terminalRef?.current;
|
||||
if (ref?.fit) ref.fit();
|
||||
@@ -125,16 +145,16 @@ export function AppView({
|
||||
|
||||
const renderTerminalsLayer = () => {
|
||||
const styles: Record<number, React.CSSProperties> = {};
|
||||
const splitTabs = terminalTabs.filter((tab: any) =>
|
||||
const splitTabs = terminalTabs.filter((tab: TabData) =>
|
||||
allSplitScreenTab.includes(tab.id),
|
||||
);
|
||||
const mainTab = terminalTabs.find((tab: any) => tab.id === currentTab);
|
||||
const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
|
||||
const layoutTabs = [
|
||||
mainTab,
|
||||
...splitTabs.filter(
|
||||
(t: any) => t && t.id !== (mainTab && (mainTab as any).id),
|
||||
(t: TabData) => t && t.id !== (mainTab && (mainTab as TabData).id),
|
||||
),
|
||||
].filter(Boolean) as any[];
|
||||
].filter((t): t is TabData => t !== null && t !== undefined);
|
||||
|
||||
if (allSplitScreenTab.length === 0 && mainTab) {
|
||||
const isFileManagerTab = mainTab.type === "file_manager";
|
||||
@@ -150,7 +170,7 @@ export function AppView({
|
||||
opacity: ready ? 1 : 0,
|
||||
};
|
||||
} else {
|
||||
layoutTabs.forEach((t: any) => {
|
||||
layoutTabs.forEach((t: TabData) => {
|
||||
const rect = panelRects[String(t.id)];
|
||||
const parentRect = containerRef.current?.getBoundingClientRect();
|
||||
if (rect && parentRect) {
|
||||
@@ -171,7 +191,7 @@ export function AppView({
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-[1]">
|
||||
{terminalTabs.map((t: any) => {
|
||||
{terminalTabs.map((t: TabData) => {
|
||||
const hasStyle = !!styles[t.id];
|
||||
const isVisible =
|
||||
hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
|
||||
@@ -241,16 +261,16 @@ export function AppView({
|
||||
};
|
||||
|
||||
const renderSplitOverlays = () => {
|
||||
const splitTabs = terminalTabs.filter((tab: any) =>
|
||||
const splitTabs = terminalTabs.filter((tab: TabData) =>
|
||||
allSplitScreenTab.includes(tab.id),
|
||||
);
|
||||
const mainTab = terminalTabs.find((tab: any) => tab.id === currentTab);
|
||||
const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
|
||||
const layoutTabs = [
|
||||
mainTab,
|
||||
...splitTabs.filter(
|
||||
(t: any) => t && t.id !== (mainTab && (mainTab as any).id),
|
||||
(t: TabData) => t && t.id !== (mainTab && (mainTab as TabData).id),
|
||||
),
|
||||
].filter(Boolean) as any[];
|
||||
].filter((t): t is TabData => t !== null && t !== undefined);
|
||||
if (allSplitScreenTab.length === 0) return null;
|
||||
|
||||
const handleStyle = {
|
||||
@@ -258,13 +278,16 @@ export function AppView({
|
||||
zIndex: 12,
|
||||
background: "var(--color-dark-border)",
|
||||
} as React.CSSProperties;
|
||||
const commonGroupProps = {
|
||||
const commonGroupProps: {
|
||||
onLayout: () => void;
|
||||
onResize: () => void;
|
||||
} = {
|
||||
onLayout: scheduleMeasureAndFit,
|
||||
onResize: scheduleMeasureAndFit,
|
||||
} as any;
|
||||
};
|
||||
|
||||
if (layoutTabs.length === 2) {
|
||||
const [a, b] = layoutTabs as any[];
|
||||
const [a, b] = layoutTabs;
|
||||
return (
|
||||
<div className="absolute inset-0 z-[10] pointer-events-none">
|
||||
<ResizablePrimitive.PanelGroup
|
||||
@@ -316,7 +339,7 @@ export function AppView({
|
||||
);
|
||||
}
|
||||
if (layoutTabs.length === 3) {
|
||||
const [a, b, c] = layoutTabs as any[];
|
||||
const [a, b, c] = layoutTabs;
|
||||
return (
|
||||
<div className="absolute inset-0 z-[10] pointer-events-none">
|
||||
<ResizablePrimitive.PanelGroup
|
||||
@@ -404,7 +427,7 @@ export function AppView({
|
||||
);
|
||||
}
|
||||
if (layoutTabs.length === 4) {
|
||||
const [a, b, c, d] = layoutTabs as any[];
|
||||
const [a, b, c, d] = layoutTabs;
|
||||
return (
|
||||
<div className="absolute inset-0 z-[10] pointer-events-none">
|
||||
<ResizablePrimitive.PanelGroup
|
||||
@@ -529,7 +552,7 @@ export function AppView({
|
||||
return null;
|
||||
};
|
||||
|
||||
const currentTabData = tabs.find((tab: any) => tab.id === currentTab);
|
||||
const currentTabData = tabs.find((tab: TabData) => tab.id === currentTab);
|
||||
const isFileManager = currentTabData?.type === "file_manager";
|
||||
const isSplitScreen = allSplitScreenTab.length > 0;
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ interface SSHHost {
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: any[];
|
||||
tunnelConnections: unknown[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -112,13 +112,19 @@ export function LeftSidebar({
|
||||
setCurrentTab,
|
||||
allSplitScreenTab,
|
||||
updateHostConfig,
|
||||
} = useTabs() as any;
|
||||
} = useTabs() as {
|
||||
tabs: Array<{ id: number; type: string; [key: string]: unknown }>;
|
||||
addTab: (tab: { type: string; [key: string]: unknown }) => number;
|
||||
setCurrentTab: (id: number) => void;
|
||||
allSplitScreenTab: number[];
|
||||
updateHostConfig: (id: number, config: unknown) => void;
|
||||
};
|
||||
const isSplitScreenActive =
|
||||
Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0;
|
||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||
const openSshManagerTab = () => {
|
||||
if (sshManagerTab || isSplitScreenActive) return;
|
||||
const id = addTab({ type: "ssh_manager" } as any);
|
||||
const id = addTab({ type: "ssh_manager" });
|
||||
setCurrentTab(id);
|
||||
};
|
||||
const adminTab = tabList.find((t) => t.type === "admin");
|
||||
@@ -128,7 +134,7 @@ export function LeftSidebar({
|
||||
setCurrentTab(adminTab.id);
|
||||
return;
|
||||
}
|
||||
const id = addTab({ type: "admin" } as any);
|
||||
const id = addTab({ type: "admin" });
|
||||
setCurrentTab(id);
|
||||
};
|
||||
const userProfileTab = tabList.find((t) => t.type === "user_profile");
|
||||
@@ -138,7 +144,7 @@ export function LeftSidebar({
|
||||
setCurrentTab(userProfileTab.id);
|
||||
return;
|
||||
}
|
||||
const id = addTab({ type: "user_profile" } as any);
|
||||
const id = addTab({ type: "user_profile" });
|
||||
setCurrentTab(id);
|
||||
};
|
||||
|
||||
@@ -206,7 +212,7 @@ export function LeftSidebar({
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
setHostsError(t("leftSidebar.failedToLoadHosts"));
|
||||
}
|
||||
}, [updateHostConfig]);
|
||||
@@ -319,9 +325,10 @@ export function LeftSidebar({
|
||||
await deleteAccount(deletePassword);
|
||||
|
||||
handleLogout();
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
setDeleteError(
|
||||
err?.response?.data?.error || t("leftSidebar.failedToDeleteAccount"),
|
||||
(err as { response?: { data?: { error?: string } } })?.response?.data
|
||||
?.error || t("leftSidebar.failedToDeleteAccount"),
|
||||
);
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,18 @@ import { TabDropdown } from "@/ui/Desktop/Navigation/Tabs/TabDropdown.tsx";
|
||||
import { getCookie, setCookie } from "@/ui/main-axios.ts";
|
||||
import { SnippetsSidebar } from "@/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx";
|
||||
|
||||
interface TabData {
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
terminalRef?: {
|
||||
current?: {
|
||||
sendInput?: (data: string) => void;
|
||||
};
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface TopNavbarProps {
|
||||
isTopbarOpen: boolean;
|
||||
setIsTopbarOpen: (open: boolean) => void;
|
||||
@@ -35,7 +47,14 @@ export function TopNavbar({
|
||||
setSplitScreenTab,
|
||||
removeTab,
|
||||
allSplitScreenTab,
|
||||
} = useTabs() as any;
|
||||
} = useTabs() as {
|
||||
tabs: TabData[];
|
||||
currentTab: number;
|
||||
setCurrentTab: (id: number) => void;
|
||||
setSplitScreenTab: (id: number) => void;
|
||||
removeTab: (id: number) => void;
|
||||
allSplitScreenTab: number[];
|
||||
};
|
||||
const leftPosition = state === "collapsed" ? "26px" : "264px";
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -192,7 +211,7 @@ export function TopNavbar({
|
||||
|
||||
if (commandToSend) {
|
||||
selectedTabIds.forEach((tabId) => {
|
||||
const tab = tabs.find((t: any) => t.id === tabId);
|
||||
const tab = tabs.find((t: TabData) => t.id === tabId);
|
||||
if (tab?.terminalRef?.current?.sendInput) {
|
||||
tab.terminalRef.current.sendInput(commandToSend);
|
||||
}
|
||||
@@ -206,7 +225,7 @@ export function TopNavbar({
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
||||
const char = e.key;
|
||||
selectedTabIds.forEach((tabId) => {
|
||||
const tab = tabs.find((t: any) => t.id === tabId);
|
||||
const tab = tabs.find((t: TabData) => t.id === tabId);
|
||||
if (tab?.terminalRef?.current?.sendInput) {
|
||||
tab.terminalRef.current.sendInput(char);
|
||||
}
|
||||
@@ -215,7 +234,7 @@ export function TopNavbar({
|
||||
};
|
||||
|
||||
const handleSnippetExecute = (content: string) => {
|
||||
const tab = tabs.find((t: any) => t.id === currentTab);
|
||||
const tab = tabs.find((t: TabData) => t.id === currentTab);
|
||||
if (tab?.terminalRef?.current?.sendInput) {
|
||||
tab.terminalRef.current.sendInput(content + "\n");
|
||||
}
|
||||
@@ -223,13 +242,13 @@ export function TopNavbar({
|
||||
|
||||
const isSplitScreenActive =
|
||||
Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0;
|
||||
const currentTabObj = tabs.find((t: any) => t.id === currentTab);
|
||||
const currentTabObj = tabs.find((t: TabData) => t.id === currentTab);
|
||||
const currentTabIsHome = currentTabObj?.type === "home";
|
||||
const currentTabIsSshManager = currentTabObj?.type === "ssh_manager";
|
||||
const currentTabIsAdmin = currentTabObj?.type === "admin";
|
||||
const currentTabIsUserProfile = currentTabObj?.type === "user_profile";
|
||||
|
||||
const terminalTabs = tabs.filter((tab: any) => tab.type === "terminal");
|
||||
const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal");
|
||||
|
||||
const updateRightClickCopyPaste = (checked: boolean) => {
|
||||
setCookie("rightClickCopyPaste", checked.toString());
|
||||
@@ -246,7 +265,7 @@ export function TopNavbar({
|
||||
}}
|
||||
>
|
||||
<div className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar">
|
||||
{tabs.map((tab: any) => {
|
||||
{tabs.map((tab: TabData) => {
|
||||
const isActive = tab.id === currentTab;
|
||||
const isSplit =
|
||||
Array.isArray(allSplitScreenTab) &&
|
||||
|
||||
@@ -14,379 +14,412 @@ import { useTranslation } from "react-i18next";
|
||||
import { isElectron, getCookie } from "@/ui/main-axios.ts";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface HostConfig {
|
||||
id?: number;
|
||||
ip: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
key?: string;
|
||||
keyPassword?: string;
|
||||
keyType?: string;
|
||||
authType?: string;
|
||||
credentialId?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface TerminalHandle {
|
||||
disconnect: () => void;
|
||||
fit: () => void;
|
||||
sendInput: (data: string) => void;
|
||||
notifyResize: () => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
interface SSHTerminalProps {
|
||||
hostConfig: any;
|
||||
hostConfig: HostConfig;
|
||||
isVisible: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
{ hostConfig, isVisible },
|
||||
ref,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const { instance: terminal, ref: xtermRef } = useXTerm();
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const webSocketRef = useRef<WebSocket | null>(null);
|
||||
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const wasDisconnectedBySSH = useRef(false);
|
||||
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const isVisibleRef = useRef<boolean>(false);
|
||||
const isConnectingRef = useRef(false);
|
||||
export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
function SSHTerminal({ hostConfig, isVisible }, ref) {
|
||||
const { t } = useTranslation();
|
||||
const { instance: terminal, ref: xtermRef } = useXTerm();
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const webSocketRef = useRef<WebSocket | null>(null);
|
||||
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const wasDisconnectedBySSH = useRef(false);
|
||||
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const isVisibleRef = useRef<boolean>(false);
|
||||
const isConnectingRef = useRef(false);
|
||||
|
||||
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const DEBOUNCE_MS = 140;
|
||||
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const DEBOUNCE_MS = 140;
|
||||
|
||||
useEffect(() => {
|
||||
isVisibleRef.current = isVisible;
|
||||
}, [isVisible]);
|
||||
useEffect(() => {
|
||||
isVisibleRef.current = isVisible;
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const jwtToken = getCookie("jwt");
|
||||
const isAuth = !!(jwtToken && jwtToken.trim() !== "");
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const jwtToken = getCookie("jwt");
|
||||
const isAuth = !!(jwtToken && jwtToken.trim() !== "");
|
||||
|
||||
setIsAuthenticated((prev) => {
|
||||
if (prev !== isAuth) {
|
||||
return isAuth;
|
||||
setIsAuthenticated((prev) => {
|
||||
if (prev !== isAuth) {
|
||||
return isAuth;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
|
||||
const authCheckInterval = setInterval(checkAuth, 5000);
|
||||
|
||||
return () => clearInterval(authCheckInterval);
|
||||
}, []);
|
||||
|
||||
function hardRefresh() {
|
||||
try {
|
||||
if (
|
||||
terminal &&
|
||||
typeof (
|
||||
terminal as { refresh?: (start: number, end: number) => void }
|
||||
).refresh === "function"
|
||||
) {
|
||||
(
|
||||
terminal as { refresh?: (start: number, end: number) => void }
|
||||
).refresh(0, terminal.rows - 1);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
|
||||
const authCheckInterval = setInterval(checkAuth, 5000);
|
||||
|
||||
return () => clearInterval(authCheckInterval);
|
||||
}, []);
|
||||
|
||||
function hardRefresh() {
|
||||
try {
|
||||
if (terminal && typeof (terminal as any).refresh === "function") {
|
||||
(terminal as any).refresh(0, terminal.rows - 1);
|
||||
} catch {
|
||||
// Ignore terminal refresh errors
|
||||
}
|
||||
} catch {
|
||||
// Ignore terminal refresh errors
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNotify(cols: number, rows: number) {
|
||||
if (!(cols > 0 && rows > 0)) return;
|
||||
pendingSizeRef.current = { cols, rows };
|
||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||
notifyTimerRef.current = setTimeout(() => {
|
||||
const next = pendingSizeRef.current;
|
||||
const last = lastSentSizeRef.current;
|
||||
if (!next) return;
|
||||
if (last && last.cols === next.cols && last.rows === next.rows) return;
|
||||
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({ type: "resize", data: next }),
|
||||
function scheduleNotify(cols: number, rows: number) {
|
||||
if (!(cols > 0 && rows > 0)) return;
|
||||
pendingSizeRef.current = { cols, rows };
|
||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||
notifyTimerRef.current = setTimeout(() => {
|
||||
const next = pendingSizeRef.current;
|
||||
const last = lastSentSizeRef.current;
|
||||
if (!next) return;
|
||||
if (last && last.cols === next.cols && last.rows === next.rows) return;
|
||||
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({ type: "resize", data: next }),
|
||||
);
|
||||
lastSentSizeRef.current = next;
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
disconnect: () => {
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
webSocketRef.current?.close();
|
||||
},
|
||||
fit: () => {
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
},
|
||||
sendInput: (data: string) => {
|
||||
if (webSocketRef.current?.readyState === 1) {
|
||||
webSocketRef.current.send(JSON.stringify({ type: "input", data }));
|
||||
}
|
||||
},
|
||||
notifyResize: () => {
|
||||
try {
|
||||
const cols = terminal?.cols ?? undefined;
|
||||
const rows = terminal?.rows ?? undefined;
|
||||
if (typeof cols === "number" && typeof rows === "number") {
|
||||
scheduleNotify(cols, rows);
|
||||
hardRefresh();
|
||||
}
|
||||
} catch {
|
||||
// Ignore resize notification errors
|
||||
}
|
||||
},
|
||||
refresh: () => hardRefresh(),
|
||||
}),
|
||||
[terminal],
|
||||
);
|
||||
|
||||
function handleWindowResize() {
|
||||
if (!isVisibleRef.current) return;
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
}
|
||||
|
||||
function setupWebSocketListeners(
|
||||
ws: WebSocket,
|
||||
cols: number,
|
||||
rows: number,
|
||||
) {
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "connectToHost",
|
||||
data: { cols, rows, hostConfig },
|
||||
}),
|
||||
);
|
||||
lastSentSizeRef.current = next;
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
}
|
||||
terminal.onData((data) => {
|
||||
ws.send(JSON.stringify({ type: "input", data }));
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
disconnect: () => {
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "ping" }));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === "data") {
|
||||
if (typeof msg.data === "string") {
|
||||
terminal.write(msg.data);
|
||||
} else {
|
||||
terminal.write(String(msg.data));
|
||||
}
|
||||
} else if (msg.type === "error")
|
||||
terminal.writeln(`\r\n[${t("terminal.error")}] ${msg.message}`);
|
||||
else if (msg.type === "connected") {
|
||||
isConnectingRef.current = false;
|
||||
} else if (msg.type === "disconnected") {
|
||||
wasDisconnectedBySSH.current = true;
|
||||
isConnectingRef.current = false;
|
||||
terminal.writeln(
|
||||
`\r\n[${msg.message || t("terminal.disconnected")}]`,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Ignore message parsing errors
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
isConnectingRef.current = false;
|
||||
|
||||
if (event.code === 1008) {
|
||||
console.error("WebSocket authentication failed:", event.reason);
|
||||
terminal.writeln(`\r\n[Authentication failed - please re-login]`);
|
||||
|
||||
localStorage.removeItem("jwt");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wasDisconnectedBySSH.current) {
|
||||
terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`);
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener("error", () => {
|
||||
isConnectingRef.current = false;
|
||||
terminal.writeln(`\r\n[${t("terminal.connectionError")}]`);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminal || !xtermRef.current || !hostConfig) return;
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.options = {
|
||||
cursorBlink: false,
|
||||
cursorStyle: "bar",
|
||||
scrollback: 10000,
|
||||
fontSize: 14,
|
||||
fontFamily:
|
||||
'"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
theme: { background: "#09090b", foreground: "#f7f7f7" },
|
||||
allowTransparency: true,
|
||||
convertEol: true,
|
||||
windowsMode: false,
|
||||
macOptionIsMeta: false,
|
||||
macOptionClickForcesSelection: false,
|
||||
rightClickSelectsWord: false,
|
||||
fastScrollModifier: "alt",
|
||||
fastScrollSensitivity: 5,
|
||||
allowProposedApi: true,
|
||||
disableStdin: true,
|
||||
cursorInactiveStyle: "bar",
|
||||
minimumContrastRatio: 1,
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.2,
|
||||
};
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
const clipboardAddon = new ClipboardAddon();
|
||||
const unicode11Addon = new Unicode11Addon();
|
||||
const webLinksAddon = new WebLinksAddon();
|
||||
|
||||
fitAddonRef.current = fitAddon;
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(clipboardAddon);
|
||||
terminal.loadAddon(unicode11Addon);
|
||||
terminal.loadAddon(webLinksAddon);
|
||||
|
||||
terminal.unicode.activeVersion = "11";
|
||||
|
||||
terminal.open(xtermRef.current);
|
||||
|
||||
const textarea = xtermRef.current.querySelector(
|
||||
".xterm-helper-textarea",
|
||||
) as HTMLTextAreaElement | null;
|
||||
if (textarea) {
|
||||
textarea.readOnly = true;
|
||||
textarea.blur();
|
||||
}
|
||||
|
||||
terminal.focus = () => {};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||
resizeTimeout.current = setTimeout(() => {
|
||||
if (!isVisibleRef.current) return;
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
}, 150);
|
||||
});
|
||||
|
||||
resizeObserver.observe(xtermRef.current);
|
||||
|
||||
const readyFonts =
|
||||
(document as { fonts?: { ready?: Promise<unknown> } }).fonts
|
||||
?.ready instanceof Promise
|
||||
? (document as { fonts?: { ready?: Promise<unknown> } }).fonts.ready
|
||||
: Promise.resolve();
|
||||
setVisible(true);
|
||||
|
||||
readyFonts.then(() => {
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
|
||||
const jwtToken = getCookie("jwt");
|
||||
if (!jwtToken || jwtToken.trim() === "") {
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
setConnectionError("Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
const cols = terminal.cols;
|
||||
const rows = terminal.rows;
|
||||
|
||||
const isDev =
|
||||
process.env.NODE_ENV === "development" &&
|
||||
(window.location.port === "3000" ||
|
||||
window.location.port === "5173" ||
|
||||
window.location.port === "");
|
||||
|
||||
const baseWsUrl = isDev
|
||||
? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002`
|
||||
: isElectron()
|
||||
? (() => {
|
||||
const baseUrl =
|
||||
(window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl || "http://127.0.0.1:30001";
|
||||
const wsProtocol = baseUrl.startsWith("https://")
|
||||
? "wss://"
|
||||
: "ws://";
|
||||
const wsHost = baseUrl.replace(/^https?:\/\//, "");
|
||||
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
||||
})()
|
||||
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
|
||||
|
||||
if (isConnectingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isConnectingRef.current = true;
|
||||
|
||||
if (
|
||||
webSocketRef.current &&
|
||||
webSocketRef.current.readyState !== WebSocket.CLOSED
|
||||
) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
|
||||
const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`;
|
||||
|
||||
setIsConnecting(true);
|
||||
setConnectionError(null);
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
webSocketRef.current = ws;
|
||||
wasDisconnectedBySSH.current = false;
|
||||
|
||||
setupWebSocketListeners(ws, cols, rows);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
webSocketRef.current?.close();
|
||||
},
|
||||
fit: () => {
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
},
|
||||
sendInput: (data: string) => {
|
||||
if (webSocketRef.current?.readyState === 1) {
|
||||
webSocketRef.current.send(JSON.stringify({ type: "input", data }));
|
||||
}
|
||||
},
|
||||
notifyResize: () => {
|
||||
try {
|
||||
const cols = terminal?.cols ?? undefined;
|
||||
const rows = terminal?.rows ?? undefined;
|
||||
if (typeof cols === "number" && typeof rows === "number") {
|
||||
scheduleNotify(cols, rows);
|
||||
hardRefresh();
|
||||
}
|
||||
} catch {
|
||||
// Ignore resize notification errors
|
||||
}
|
||||
},
|
||||
refresh: () => hardRefresh(),
|
||||
}),
|
||||
[terminal],
|
||||
);
|
||||
};
|
||||
}, [xtermRef, terminal, hostConfig]);
|
||||
|
||||
function handleWindowResize() {
|
||||
if (!isVisibleRef.current) return;
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
}
|
||||
|
||||
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "connectToHost",
|
||||
data: { cols, rows, hostConfig },
|
||||
}),
|
||||
);
|
||||
terminal.onData((data) => {
|
||||
ws.send(JSON.stringify({ type: "input", data }));
|
||||
});
|
||||
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "ping" }));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === "data") {
|
||||
if (typeof msg.data === "string") {
|
||||
terminal.write(msg.data);
|
||||
} else {
|
||||
terminal.write(String(msg.data));
|
||||
}
|
||||
} else if (msg.type === "error")
|
||||
terminal.writeln(`\r\n[${t("terminal.error")}] ${msg.message}`);
|
||||
else if (msg.type === "connected") {
|
||||
isConnectingRef.current = false;
|
||||
} else if (msg.type === "disconnected") {
|
||||
wasDisconnectedBySSH.current = true;
|
||||
isConnectingRef.current = false;
|
||||
terminal.writeln(
|
||||
`\r\n[${msg.message || t("terminal.disconnected")}]`,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Ignore message parsing errors
|
||||
useEffect(() => {
|
||||
if (isVisible && fitAddonRef.current) {
|
||||
setTimeout(() => {
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}, [isVisible, terminal]);
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
isConnectingRef.current = false;
|
||||
|
||||
if (event.code === 1008) {
|
||||
console.error("WebSocket authentication failed:", event.reason);
|
||||
terminal.writeln(`\r\n[Authentication failed - please re-login]`);
|
||||
|
||||
localStorage.removeItem("jwt");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wasDisconnectedBySSH.current) {
|
||||
terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`);
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener("error", () => {
|
||||
isConnectingRef.current = false;
|
||||
terminal.writeln(`\r\n[${t("terminal.connectionError")}]`);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminal || !xtermRef.current || !hostConfig) return;
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.options = {
|
||||
cursorBlink: false,
|
||||
cursorStyle: "bar",
|
||||
scrollback: 10000,
|
||||
fontSize: 14,
|
||||
fontFamily:
|
||||
'"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
theme: { background: "#09090b", foreground: "#f7f7f7" },
|
||||
allowTransparency: true,
|
||||
convertEol: true,
|
||||
windowsMode: false,
|
||||
macOptionIsMeta: false,
|
||||
macOptionClickForcesSelection: false,
|
||||
rightClickSelectsWord: false,
|
||||
fastScrollModifier: "alt",
|
||||
fastScrollSensitivity: 5,
|
||||
allowProposedApi: true,
|
||||
disableStdin: true,
|
||||
cursorInactiveStyle: "bar",
|
||||
minimumContrastRatio: 1,
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.2,
|
||||
};
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
const clipboardAddon = new ClipboardAddon();
|
||||
const unicode11Addon = new Unicode11Addon();
|
||||
const webLinksAddon = new WebLinksAddon();
|
||||
|
||||
fitAddonRef.current = fitAddon;
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(clipboardAddon);
|
||||
terminal.loadAddon(unicode11Addon);
|
||||
terminal.loadAddon(webLinksAddon);
|
||||
|
||||
terminal.unicode.activeVersion = "11";
|
||||
|
||||
terminal.open(xtermRef.current);
|
||||
|
||||
const textarea = xtermRef.current.querySelector(
|
||||
".xterm-helper-textarea",
|
||||
) as HTMLTextAreaElement | null;
|
||||
if (textarea) {
|
||||
textarea.readOnly = true;
|
||||
textarea.blur();
|
||||
}
|
||||
|
||||
terminal.focus = () => {};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||
resizeTimeout.current = setTimeout(() => {
|
||||
if (!isVisibleRef.current) return;
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
}, 150);
|
||||
});
|
||||
|
||||
resizeObserver.observe(xtermRef.current);
|
||||
|
||||
const readyFonts =
|
||||
(document as any).fonts?.ready instanceof Promise
|
||||
? (document as any).fonts.ready
|
||||
: Promise.resolve();
|
||||
setVisible(true);
|
||||
|
||||
readyFonts.then(() => {
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
|
||||
const jwtToken = getCookie("jwt");
|
||||
if (!jwtToken || jwtToken.trim() === "") {
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
setConnectionError("Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
const cols = terminal.cols;
|
||||
const rows = terminal.rows;
|
||||
|
||||
const isDev =
|
||||
process.env.NODE_ENV === "development" &&
|
||||
(window.location.port === "3000" ||
|
||||
window.location.port === "5173" ||
|
||||
window.location.port === "");
|
||||
|
||||
const baseWsUrl = isDev
|
||||
? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002`
|
||||
: isElectron()
|
||||
? (() => {
|
||||
const baseUrl =
|
||||
(window as any).configuredServerUrl ||
|
||||
"http://127.0.0.1:30001";
|
||||
const wsProtocol = baseUrl.startsWith("https://")
|
||||
? "wss://"
|
||||
: "ws://";
|
||||
const wsHost = baseUrl.replace(/^https?:\/\//, "");
|
||||
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
||||
})()
|
||||
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
|
||||
|
||||
if (isConnectingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isConnectingRef.current = true;
|
||||
|
||||
if (
|
||||
webSocketRef.current &&
|
||||
webSocketRef.current.readyState !== WebSocket.CLOSED
|
||||
) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
|
||||
const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`;
|
||||
|
||||
setIsConnecting(true);
|
||||
setConnectionError(null);
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
webSocketRef.current = ws;
|
||||
wasDisconnectedBySSH.current = false;
|
||||
|
||||
setupWebSocketListeners(ws, cols, rows);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
webSocketRef.current?.close();
|
||||
};
|
||||
}, [xtermRef, terminal, hostConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && fitAddonRef.current) {
|
||||
useEffect(() => {
|
||||
if (!fitAddonRef.current) return;
|
||||
setTimeout(() => {
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
}, 0);
|
||||
}
|
||||
}, [isVisible, terminal]);
|
||||
}, [isVisible, terminal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fitAddonRef.current) return;
|
||||
setTimeout(() => {
|
||||
fitAddonRef.current?.fit();
|
||||
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||
hardRefresh();
|
||||
}, 0);
|
||||
}, [isVisible, terminal]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={xtermRef}
|
||||
className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? "opacity-100" : "opacity-0"} overflow-hidden`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
ref={xtermRef}
|
||||
className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? "opacity-100" : "opacity-0"} overflow-hidden`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = `
|
||||
|
||||
@@ -95,8 +95,22 @@ interface OIDCAuthorize {
|
||||
|
||||
export function isElectron(): boolean {
|
||||
return (
|
||||
(window as any).IS_ELECTRON === true ||
|
||||
(window as any).electronAPI?.isElectron === true
|
||||
(
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).IS_ELECTRON === true ||
|
||||
(
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).electronAPI?.isElectron === true
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,8 +168,8 @@ function createApiInstance(
|
||||
const startTime = performance.now();
|
||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
(config as any).startTime = startTime;
|
||||
(config as any).requestId = requestId;
|
||||
(config as Record<string, unknown>).startTime = startTime;
|
||||
(config as Record<string, unknown>).requestId = requestId;
|
||||
|
||||
const method = config.method?.toUpperCase() || "UNKNOWN";
|
||||
const url = config.url || "UNKNOWN";
|
||||
@@ -189,8 +203,8 @@ function createApiInstance(
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
const endTime = performance.now();
|
||||
const startTime = (response.config as any).startTime;
|
||||
const requestId = (response.config as any).requestId;
|
||||
const startTime = (response.config as Record<string, unknown>).startTime;
|
||||
const requestId = (response.config as Record<string, unknown>).requestId;
|
||||
const responseTime = Math.round(endTime - startTime);
|
||||
|
||||
const method = response.config.method?.toUpperCase() || "UNKNOWN";
|
||||
@@ -227,8 +241,10 @@ function createApiInstance(
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
const endTime = performance.now();
|
||||
const startTime = (error.config as any)?.startTime;
|
||||
const requestId = (error.config as any)?.requestId;
|
||||
const startTime = (error.config as Record<string, unknown> | undefined)
|
||||
?.startTime;
|
||||
const requestId = (error.config as Record<string, unknown> | undefined)
|
||||
?.requestId;
|
||||
const responseTime = startTime
|
||||
? Math.round(endTime - startTime)
|
||||
: undefined;
|
||||
@@ -238,10 +254,11 @@ function createApiInstance(
|
||||
const fullUrl = error.config ? `${error.config.baseURL}${url}` : url;
|
||||
const status = error.response?.status;
|
||||
const message =
|
||||
(error.response?.data as any)?.error ||
|
||||
(error.response?.data as Record<string, unknown>)?.error ||
|
||||
(error as Error).message ||
|
||||
"Unknown error";
|
||||
const errorCode = (error.response?.data as any)?.code || error.code;
|
||||
const errorCode =
|
||||
(error.response?.data as Record<string, unknown>)?.code || error.code;
|
||||
|
||||
const context: LogContext = {
|
||||
requestId,
|
||||
@@ -274,7 +291,8 @@ function createApiInstance(
|
||||
}
|
||||
|
||||
if (status === 401) {
|
||||
const errorCode = (error.response?.data as any)?.code;
|
||||
const errorCode = (error.response?.data as Record<string, unknown>)
|
||||
?.code;
|
||||
const isSessionExpired = errorCode === "SESSION_EXPIRED";
|
||||
|
||||
if (isElectron()) {
|
||||
@@ -337,9 +355,14 @@ export async function getServerConfig(): Promise<ServerConfig | null> {
|
||||
if (!isElectron()) return null;
|
||||
|
||||
try {
|
||||
const result = await (window as any).electronAPI?.invoke(
|
||||
"get-server-config",
|
||||
);
|
||||
const result = await (
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).electronAPI?.invoke("get-server-config");
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Failed to get server config:", error);
|
||||
@@ -351,13 +374,24 @@ export async function saveServerConfig(config: ServerConfig): Promise<boolean> {
|
||||
if (!isElectron()) return false;
|
||||
|
||||
try {
|
||||
const result = await (window as any).electronAPI?.invoke(
|
||||
"save-server-config",
|
||||
config,
|
||||
);
|
||||
const result = await (
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).electronAPI?.invoke("save-server-config", config);
|
||||
if (result?.success) {
|
||||
configuredServerUrl = config.serverUrl;
|
||||
(window as any).configuredServerUrl = configuredServerUrl;
|
||||
(
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).configuredServerUrl = configuredServerUrl;
|
||||
updateApiInstances();
|
||||
return true;
|
||||
}
|
||||
@@ -375,10 +409,14 @@ export async function testServerConnection(
|
||||
return { success: false, error: "Not in Electron environment" };
|
||||
|
||||
try {
|
||||
const result = await (window as any).electronAPI?.invoke(
|
||||
"test-server-connection",
|
||||
serverUrl,
|
||||
);
|
||||
const result = await (
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).electronAPI?.invoke("test-server-connection", serverUrl);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Failed to test server connection:", error);
|
||||
@@ -406,9 +444,14 @@ export async function checkElectronUpdate(): Promise<{
|
||||
return { success: false, error: "Not in Electron environment" };
|
||||
|
||||
try {
|
||||
const result = await (window as any).electronAPI?.invoke(
|
||||
"check-electron-update",
|
||||
);
|
||||
const result = await (
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).electronAPI?.invoke("check-electron-update");
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Failed to check Electron update:", error);
|
||||
@@ -472,7 +515,14 @@ if (isElectron()) {
|
||||
.then((config) => {
|
||||
if (config?.serverUrl) {
|
||||
configuredServerUrl = config.serverUrl;
|
||||
(window as any).configuredServerUrl = configuredServerUrl;
|
||||
(
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).configuredServerUrl = configuredServerUrl;
|
||||
}
|
||||
initializeApiInstances();
|
||||
})
|
||||
@@ -495,7 +545,14 @@ function updateApiInstances() {
|
||||
|
||||
initializeApiInstances();
|
||||
|
||||
(window as any).configuredServerUrl = configuredServerUrl;
|
||||
(
|
||||
window as Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: unknown;
|
||||
configuredServerUrl?: string;
|
||||
}
|
||||
).configuredServerUrl = configuredServerUrl;
|
||||
|
||||
systemLogger.success("All API instances updated successfully", {
|
||||
operation: "api_instance_update_complete",
|
||||
@@ -564,7 +621,7 @@ function handleApiError(error: unknown, operation: string): never {
|
||||
403,
|
||||
code || "ACCESS_DENIED",
|
||||
);
|
||||
(apiError as any).response = error.response;
|
||||
(apiError as ApiError & { response?: unknown }).response = error.response;
|
||||
throw apiError;
|
||||
} else if (status === 404) {
|
||||
apiLogger.warn(`Not found: ${method} ${url}`, errorContext);
|
||||
@@ -788,7 +845,9 @@ export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSSHHost(hostId: number): Promise<any> {
|
||||
export async function deleteSSHHost(
|
||||
hostId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete(`/db/host/${hostId}`);
|
||||
return response.data;
|
||||
@@ -821,7 +880,9 @@ export async function exportSSHHostWithCredentials(
|
||||
// SSH AUTOSTART MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
export async function enableAutoStart(sshConfigId: number): Promise<any> {
|
||||
export async function enableAutoStart(
|
||||
sshConfigId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post("/autostart/enable", {
|
||||
sshConfigId,
|
||||
@@ -832,7 +893,9 @@ export async function enableAutoStart(sshConfigId: number): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function disableAutoStart(sshConfigId: number): Promise<any> {
|
||||
export async function disableAutoStart(
|
||||
sshConfigId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete("/autostart/disable", {
|
||||
data: { sshConfigId },
|
||||
@@ -883,7 +946,9 @@ export async function getTunnelStatusByName(
|
||||
return statuses[tunnelName];
|
||||
}
|
||||
|
||||
export async function connectTunnel(tunnelConfig: TunnelConfig): Promise<any> {
|
||||
export async function connectTunnel(
|
||||
tunnelConfig: TunnelConfig,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await tunnelApi.post("/tunnel/connect", tunnelConfig);
|
||||
return response.data;
|
||||
@@ -892,7 +957,9 @@ export async function connectTunnel(tunnelConfig: TunnelConfig): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectTunnel(tunnelName: string): Promise<any> {
|
||||
export async function disconnectTunnel(
|
||||
tunnelName: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await tunnelApi.post("/tunnel/disconnect", { tunnelName });
|
||||
return response.data;
|
||||
@@ -901,7 +968,9 @@ export async function disconnectTunnel(tunnelName: string): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelTunnel(tunnelName: string): Promise<any> {
|
||||
export async function cancelTunnel(
|
||||
tunnelName: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await tunnelApi.post("/tunnel/cancel", { tunnelName });
|
||||
return response.data;
|
||||
@@ -929,7 +998,7 @@ export async function getFileManagerRecent(
|
||||
|
||||
export async function addFileManagerRecent(
|
||||
file: FileManagerOperation,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post("/file_manager/recent", file);
|
||||
return response.data;
|
||||
@@ -940,7 +1009,7 @@ export async function addFileManagerRecent(
|
||||
|
||||
export async function removeFileManagerRecent(
|
||||
file: FileManagerOperation,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete("/file_manager/recent", {
|
||||
data: file,
|
||||
@@ -966,7 +1035,7 @@ export async function getFileManagerPinned(
|
||||
|
||||
export async function addFileManagerPinned(
|
||||
file: FileManagerOperation,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post("/file_manager/pinned", file);
|
||||
return response.data;
|
||||
@@ -977,7 +1046,7 @@ export async function addFileManagerPinned(
|
||||
|
||||
export async function removeFileManagerPinned(
|
||||
file: FileManagerOperation,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete("/file_manager/pinned", {
|
||||
data: file,
|
||||
@@ -1003,7 +1072,7 @@ export async function getFileManagerShortcuts(
|
||||
|
||||
export async function addFileManagerShortcut(
|
||||
shortcut: FileManagerOperation,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post("/file_manager/shortcuts", shortcut);
|
||||
return response.data;
|
||||
@@ -1014,7 +1083,7 @@ export async function addFileManagerShortcut(
|
||||
|
||||
export async function removeFileManagerShortcut(
|
||||
shortcut: FileManagerOperation,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete("/file_manager/shortcuts", {
|
||||
data: shortcut,
|
||||
@@ -1043,7 +1112,7 @@ export async function connectSSH(
|
||||
credentialId?: number;
|
||||
userId?: string;
|
||||
},
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/connect", {
|
||||
sessionId,
|
||||
@@ -1055,7 +1124,9 @@ export async function connectSSH(
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectSSH(sessionId: string): Promise<any> {
|
||||
export async function disconnectSSH(
|
||||
sessionId: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/disconnect", {
|
||||
sessionId,
|
||||
@@ -1069,7 +1140,7 @@ export async function disconnectSSH(sessionId: string): Promise<any> {
|
||||
export async function verifySSHTOTP(
|
||||
sessionId: string,
|
||||
totpCode: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/connect-totp", {
|
||||
sessionId,
|
||||
@@ -1094,7 +1165,9 @@ export async function getSSHStatus(
|
||||
}
|
||||
}
|
||||
|
||||
export async function keepSSHAlive(sessionId: string): Promise<any> {
|
||||
export async function keepSSHAlive(
|
||||
sessionId: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/keepalive", {
|
||||
sessionId,
|
||||
@@ -1108,7 +1181,7 @@ export async function keepSSHAlive(sessionId: string): Promise<any> {
|
||||
export async function listSSHFiles(
|
||||
sessionId: string,
|
||||
path: string,
|
||||
): Promise<{ files: any[]; path: string }> {
|
||||
): Promise<{ files: unknown[]; path: string }> {
|
||||
try {
|
||||
const response = await fileManagerApi.get("/ssh/listFiles", {
|
||||
params: { sessionId, path },
|
||||
@@ -1143,12 +1216,15 @@ export async function readSSHFile(
|
||||
params: { sessionId, path },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (error.response?.status === 404) {
|
||||
const customError = new Error("File not found");
|
||||
(customError as any).response = error.response;
|
||||
(customError as any).isFileNotFound =
|
||||
error.response.data?.fileNotFound || true;
|
||||
(
|
||||
customError as Error & { response?: unknown; isFileNotFound?: boolean }
|
||||
).response = error.response;
|
||||
(
|
||||
customError as Error & { response?: unknown; isFileNotFound?: boolean }
|
||||
).isFileNotFound = error.response.data?.fileNotFound || true;
|
||||
throw customError;
|
||||
}
|
||||
handleApiError(error, "read SSH file");
|
||||
@@ -1161,7 +1237,7 @@ export async function writeSSHFile(
|
||||
content: string,
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/writeFile", {
|
||||
sessionId,
|
||||
@@ -1192,7 +1268,7 @@ export async function uploadSSHFile(
|
||||
content: string,
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/uploadFile", {
|
||||
sessionId,
|
||||
@@ -1213,7 +1289,7 @@ export async function downloadSSHFile(
|
||||
filePath: string,
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/downloadFile", {
|
||||
sessionId,
|
||||
@@ -1234,7 +1310,7 @@ export async function createSSHFile(
|
||||
content: string = "",
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/createFile", {
|
||||
sessionId,
|
||||
@@ -1256,7 +1332,7 @@ export async function createSSHFolder(
|
||||
folderName: string,
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post("/ssh/createFolder", {
|
||||
sessionId,
|
||||
@@ -1277,7 +1353,7 @@ export async function deleteSSHItem(
|
||||
isDirectory: boolean,
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.delete("/ssh/deleteItem", {
|
||||
data: {
|
||||
@@ -1300,7 +1376,7 @@ export async function copySSHItem(
|
||||
targetDir: string,
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.post(
|
||||
"/ssh/copyItem",
|
||||
@@ -1328,7 +1404,7 @@ export async function renameSSHItem(
|
||||
newName: string,
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.put("/ssh/renameItem", {
|
||||
sessionId,
|
||||
@@ -1350,7 +1426,7 @@ export async function moveSSHItem(
|
||||
newPath: string,
|
||||
hostId?: number,
|
||||
userId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await fileManagerApi.put(
|
||||
"/ssh/moveItem",
|
||||
@@ -1377,7 +1453,9 @@ export async function moveSSHItem(
|
||||
// ============================================================================
|
||||
|
||||
// Recent Files
|
||||
export async function getRecentFiles(hostId: number): Promise<any> {
|
||||
export async function getRecentFiles(
|
||||
hostId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/ssh/file_manager/recent", {
|
||||
params: { hostId },
|
||||
@@ -1393,7 +1471,7 @@ export async function addRecentFile(
|
||||
hostId: number,
|
||||
path: string,
|
||||
name?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/ssh/file_manager/recent", {
|
||||
hostId,
|
||||
@@ -1410,7 +1488,7 @@ export async function addRecentFile(
|
||||
export async function removeRecentFile(
|
||||
hostId: number,
|
||||
path: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/ssh/file_manager/recent", {
|
||||
data: { hostId, path },
|
||||
@@ -1422,7 +1500,9 @@ export async function removeRecentFile(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPinnedFiles(hostId: number): Promise<any> {
|
||||
export async function getPinnedFiles(
|
||||
hostId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/ssh/file_manager/pinned", {
|
||||
params: { hostId },
|
||||
@@ -1438,7 +1518,7 @@ export async function addPinnedFile(
|
||||
hostId: number,
|
||||
path: string,
|
||||
name?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/ssh/file_manager/pinned", {
|
||||
hostId,
|
||||
@@ -1455,7 +1535,7 @@ export async function addPinnedFile(
|
||||
export async function removePinnedFile(
|
||||
hostId: number,
|
||||
path: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/ssh/file_manager/pinned", {
|
||||
data: { hostId, path },
|
||||
@@ -1467,7 +1547,9 @@ export async function removePinnedFile(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFolderShortcuts(hostId: number): Promise<any> {
|
||||
export async function getFolderShortcuts(
|
||||
hostId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/ssh/file_manager/shortcuts", {
|
||||
params: { hostId },
|
||||
@@ -1483,7 +1565,7 @@ export async function addFolderShortcut(
|
||||
hostId: number,
|
||||
path: string,
|
||||
name?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/ssh/file_manager/shortcuts", {
|
||||
hostId,
|
||||
@@ -1500,7 +1582,7 @@ export async function addFolderShortcut(
|
||||
export async function removeFolderShortcut(
|
||||
hostId: number,
|
||||
path: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/ssh/file_manager/shortcuts", {
|
||||
data: { hostId, path },
|
||||
@@ -1552,7 +1634,7 @@ export async function getServerMetricsById(id: number): Promise<ServerMetrics> {
|
||||
export async function registerUser(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/users/create", {
|
||||
username,
|
||||
@@ -1638,11 +1720,11 @@ export async function getPasswordLoginAllowed(): Promise<{ allowed: boolean }> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOIDCConfig(): Promise<any> {
|
||||
export async function getOIDCConfig(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/users/oidc-config");
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.warn(
|
||||
"Failed to fetch OIDC config:",
|
||||
error.response?.data?.error || error.message,
|
||||
@@ -1669,7 +1751,9 @@ export async function getUserCount(): Promise<UserCount> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function initiatePasswordReset(username: string): Promise<any> {
|
||||
export async function initiatePasswordReset(
|
||||
username: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/users/initiate-reset", { username });
|
||||
return response.data;
|
||||
@@ -1681,7 +1765,7 @@ export async function initiatePasswordReset(username: string): Promise<any> {
|
||||
export async function verifyPasswordResetCode(
|
||||
username: string,
|
||||
resetCode: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/users/verify-reset-code", {
|
||||
username,
|
||||
@@ -1697,7 +1781,7 @@ export async function completePasswordReset(
|
||||
username: string,
|
||||
tempToken: string,
|
||||
newPassword: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/users/complete-reset", {
|
||||
username,
|
||||
@@ -1732,7 +1816,9 @@ export async function getUserList(): Promise<{ users: UserInfo[] }> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeUserAdmin(username: string): Promise<any> {
|
||||
export async function makeUserAdmin(
|
||||
username: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/users/make-admin", { username });
|
||||
return response.data;
|
||||
@@ -1741,7 +1827,9 @@ export async function makeUserAdmin(username: string): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeAdminStatus(username: string): Promise<any> {
|
||||
export async function removeAdminStatus(
|
||||
username: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/users/remove-admin", { username });
|
||||
return response.data;
|
||||
@@ -1750,7 +1838,9 @@ export async function removeAdminStatus(username: string): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteUser(username: string): Promise<any> {
|
||||
export async function deleteUser(
|
||||
username: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/users/delete-user", {
|
||||
data: { username },
|
||||
@@ -1761,7 +1851,9 @@ export async function deleteUser(username: string): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAccount(password: string): Promise<any> {
|
||||
export async function deleteAccount(
|
||||
password: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/users/delete-account", {
|
||||
data: { password },
|
||||
@@ -1774,7 +1866,7 @@ export async function deleteAccount(password: string): Promise<any> {
|
||||
|
||||
export async function updateRegistrationAllowed(
|
||||
allowed: boolean,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.patch("/users/registration-allowed", {
|
||||
allowed,
|
||||
@@ -1798,7 +1890,9 @@ export async function updatePasswordLoginAllowed(
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateOIDCConfig(config: any): Promise<any> {
|
||||
export async function updateOIDCConfig(
|
||||
config: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/users/oidc-config", config);
|
||||
return response.data;
|
||||
@@ -1807,7 +1901,7 @@ export async function updateOIDCConfig(config: any): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function disableOIDCConfig(): Promise<any> {
|
||||
export async function disableOIDCConfig(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/users/oidc-config");
|
||||
return response.data;
|
||||
@@ -1893,7 +1987,9 @@ export async function generateBackupCodes(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserAlerts(): Promise<{ alerts: any[] }> {
|
||||
export async function getUserAlerts(): Promise<{
|
||||
alerts: Array<Record<string, unknown>>;
|
||||
}> {
|
||||
try {
|
||||
const response = await authApi.get(`/alerts`);
|
||||
return response.data;
|
||||
@@ -1902,7 +1998,9 @@ export async function getUserAlerts(): Promise<{ alerts: any[] }> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAlert(alertId: string): Promise<any> {
|
||||
export async function dismissAlert(
|
||||
alertId: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/alerts/dismiss", { alertId });
|
||||
return response.data;
|
||||
@@ -1915,7 +2013,9 @@ export async function dismissAlert(alertId: string): Promise<any> {
|
||||
// UPDATES & RELEASES
|
||||
// ============================================================================
|
||||
|
||||
export async function getReleasesRSS(perPage: number = 100): Promise<any> {
|
||||
export async function getReleasesRSS(
|
||||
perPage: number = 100,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get(`/releases/rss?per_page=${perPage}`);
|
||||
return response.data;
|
||||
@@ -1924,7 +2024,7 @@ export async function getReleasesRSS(perPage: number = 100): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVersionInfo(): Promise<any> {
|
||||
export async function getVersionInfo(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/version");
|
||||
return response.data;
|
||||
@@ -1937,7 +2037,7 @@ export async function getVersionInfo(): Promise<any> {
|
||||
// DATABASE HEALTH
|
||||
// ============================================================================
|
||||
|
||||
export async function getDatabaseHealth(): Promise<any> {
|
||||
export async function getDatabaseHealth(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/users/db-health");
|
||||
return response.data;
|
||||
@@ -1950,7 +2050,7 @@ export async function getDatabaseHealth(): Promise<any> {
|
||||
// SSH CREDENTIALS MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
export async function getCredentials(): Promise<any> {
|
||||
export async function getCredentials(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/credentials");
|
||||
return response.data;
|
||||
@@ -1959,7 +2059,9 @@ export async function getCredentials(): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCredentialDetails(credentialId: number): Promise<any> {
|
||||
export async function getCredentialDetails(
|
||||
credentialId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get(`/credentials/${credentialId}`);
|
||||
return response.data;
|
||||
@@ -1968,7 +2070,9 @@ export async function getCredentialDetails(credentialId: number): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCredential(credentialData: any): Promise<any> {
|
||||
export async function createCredential(
|
||||
credentialData: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/credentials", credentialData);
|
||||
return response.data;
|
||||
@@ -1979,8 +2083,8 @@ export async function createCredential(credentialData: any): Promise<any> {
|
||||
|
||||
export async function updateCredential(
|
||||
credentialId: number,
|
||||
credentialData: any,
|
||||
): Promise<any> {
|
||||
credentialData: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.put(
|
||||
`/credentials/${credentialId}`,
|
||||
@@ -1992,7 +2096,9 @@ export async function updateCredential(
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCredential(credentialId: number): Promise<any> {
|
||||
export async function deleteCredential(
|
||||
credentialId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete(`/credentials/${credentialId}`);
|
||||
return response.data;
|
||||
@@ -2001,7 +2107,9 @@ export async function deleteCredential(credentialId: number): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCredentialHosts(credentialId: number): Promise<any> {
|
||||
export async function getCredentialHosts(
|
||||
credentialId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get(`/credentials/${credentialId}/hosts`);
|
||||
return response.data;
|
||||
@@ -2010,7 +2118,7 @@ export async function getCredentialHosts(credentialId: number): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCredentialFolders(): Promise<any> {
|
||||
export async function getCredentialFolders(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/credentials/folders");
|
||||
return response.data;
|
||||
@@ -2019,7 +2127,9 @@ export async function getCredentialFolders(): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSSHHostWithCredentials(hostId: number): Promise<any> {
|
||||
export async function getSSHHostWithCredentials(
|
||||
hostId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.get(
|
||||
`/db/host/${hostId}/with-credentials`,
|
||||
@@ -2033,7 +2143,7 @@ export async function getSSHHostWithCredentials(hostId: number): Promise<any> {
|
||||
export async function applyCredentialToHost(
|
||||
hostId: number,
|
||||
credentialId: number,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post(
|
||||
`/db/host/${hostId}/apply-credential`,
|
||||
@@ -2045,7 +2155,9 @@ export async function applyCredentialToHost(
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCredentialFromHost(hostId: number): Promise<any> {
|
||||
export async function removeCredentialFromHost(
|
||||
hostId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete(`/db/host/${hostId}/credential`);
|
||||
return response.data;
|
||||
@@ -2057,7 +2169,7 @@ export async function removeCredentialFromHost(hostId: number): Promise<any> {
|
||||
export async function migrateHostToCredential(
|
||||
hostId: number,
|
||||
credentialName: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post(
|
||||
`/db/host/${hostId}/migrate-to-credential`,
|
||||
@@ -2073,7 +2185,7 @@ export async function migrateHostToCredential(
|
||||
// SSH FOLDER MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
export async function getFoldersWithStats(): Promise<any> {
|
||||
export async function getFoldersWithStats(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/ssh/db/folders/with-stats");
|
||||
return response.data;
|
||||
@@ -2085,7 +2197,7 @@ export async function getFoldersWithStats(): Promise<any> {
|
||||
export async function renameFolder(
|
||||
oldName: string,
|
||||
newName: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.put("/ssh/folders/rename", {
|
||||
oldName,
|
||||
@@ -2100,7 +2212,7 @@ export async function renameFolder(
|
||||
export async function renameCredentialFolder(
|
||||
oldName: string,
|
||||
newName: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.put("/credentials/folders/rename", {
|
||||
oldName,
|
||||
@@ -2115,7 +2227,7 @@ export async function renameCredentialFolder(
|
||||
export async function detectKeyType(
|
||||
privateKey: string,
|
||||
keyPassword?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/credentials/detect-key-type", {
|
||||
privateKey,
|
||||
@@ -2127,7 +2239,9 @@ export async function detectKeyType(
|
||||
}
|
||||
}
|
||||
|
||||
export async function detectPublicKeyType(publicKey: string): Promise<any> {
|
||||
export async function detectPublicKeyType(
|
||||
publicKey: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/credentials/detect-public-key-type", {
|
||||
publicKey,
|
||||
@@ -2142,7 +2256,7 @@ export async function validateKeyPair(
|
||||
privateKey: string,
|
||||
publicKey: string,
|
||||
keyPassword?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/credentials/validate-key-pair", {
|
||||
privateKey,
|
||||
@@ -2158,7 +2272,7 @@ export async function validateKeyPair(
|
||||
export async function generatePublicKeyFromPrivate(
|
||||
privateKey: string,
|
||||
keyPassword?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/credentials/generate-public-key", {
|
||||
privateKey,
|
||||
@@ -2174,7 +2288,7 @@ export async function generateKeyPair(
|
||||
keyType: "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256",
|
||||
keySize?: number,
|
||||
passphrase?: string,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/credentials/generate-key-pair", {
|
||||
keyType,
|
||||
@@ -2190,7 +2304,7 @@ export async function generateKeyPair(
|
||||
export async function deployCredentialToHost(
|
||||
credentialId: number,
|
||||
targetHostId: number,
|
||||
): Promise<any> {
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post(
|
||||
`/credentials/${credentialId}/deploy-to-host`,
|
||||
@@ -2206,7 +2320,7 @@ export async function deployCredentialToHost(
|
||||
// SNIPPETS API
|
||||
// ============================================================================
|
||||
|
||||
export async function getSnippets(): Promise<any> {
|
||||
export async function getSnippets(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/snippets");
|
||||
return response.data;
|
||||
@@ -2215,7 +2329,9 @@ export async function getSnippets(): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSnippet(snippetData: any): Promise<any> {
|
||||
export async function createSnippet(
|
||||
snippetData: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/snippets", snippetData);
|
||||
return response.data;
|
||||
@@ -2226,8 +2342,8 @@ export async function createSnippet(snippetData: any): Promise<any> {
|
||||
|
||||
export async function updateSnippet(
|
||||
snippetId: number,
|
||||
snippetData: any,
|
||||
): Promise<any> {
|
||||
snippetData: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.put(`/snippets/${snippetId}`, snippetData);
|
||||
return response.data;
|
||||
@@ -2236,7 +2352,9 @@ export async function updateSnippet(
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSnippet(snippetId: number): Promise<any> {
|
||||
export async function deleteSnippet(
|
||||
snippetId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete(`/snippets/${snippetId}`);
|
||||
return response.data;
|
||||
|
||||
Reference in New Issue
Block a user
The type for
terminalRefin theTabContextTabinterface has been changed toReact.RefObject<HTMLElement>, which is incorrect. This ref is used for an imperative handle on theTerminalcomponent, which exposes methods likefit()andsendInput(). UsingHTMLElementloses type safety and could lead to runtime errors.A
TerminalHandleinterface is defined insrc/backend/ssh/terminal.ts. It would be best to move this interface to a shared types file (like this one) and useReact.RefObject<TerminalHandle>here and in other related components.