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