mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 21:33:41 +00:00
refactor(backend): final cleanup pass
- re-register WebAuthn passkey routes (registration was dropped in the #1054 merge, breaking passkey login) and document all six endpoints - delete utils/simple-db-ops.ts (last caller migrated to DataCrypto) - starter: use the typed serverReady export, collapse the four-way version lookup to env then package.json candidates - add OpenAPI JSDoc to c2s-tunnel-presets endpoints - strip block-divider comment banners
This commit is contained in:
@@ -46,6 +46,22 @@ function validateConfig(config: unknown): config is TunnelConnection[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /c2s-tunnel-presets:
|
||||||
|
* get:
|
||||||
|
* summary: List client tunnel presets
|
||||||
|
* description: Returns the authenticated user's saved client-to-server tunnel presets.
|
||||||
|
* tags:
|
||||||
|
* - Tunnel Presets
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: List of tunnel presets.
|
||||||
|
* 401:
|
||||||
|
* description: Authentication required.
|
||||||
|
* 500:
|
||||||
|
* description: Failed to list presets.
|
||||||
|
*/
|
||||||
router.get(
|
router.get(
|
||||||
"/",
|
"/",
|
||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
@@ -67,6 +83,37 @@ router.get(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /c2s-tunnel-presets:
|
||||||
|
* post:
|
||||||
|
* summary: Create a client tunnel preset
|
||||||
|
* description: Saves a named client-to-server tunnel configuration for the authenticated user.
|
||||||
|
* tags:
|
||||||
|
* - Tunnel Presets
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* config:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Preset created.
|
||||||
|
* 400:
|
||||||
|
* description: Invalid name or config.
|
||||||
|
* 401:
|
||||||
|
* description: Authentication required.
|
||||||
|
* 500:
|
||||||
|
* description: Failed to create preset.
|
||||||
|
*/
|
||||||
router.post(
|
router.post(
|
||||||
"/",
|
"/",
|
||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
@@ -111,6 +158,29 @@ router.post(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /c2s-tunnel-presets/{id}:
|
||||||
|
* put:
|
||||||
|
* summary: Update a client tunnel preset
|
||||||
|
* description: Updates the name or config of one of the authenticated user's tunnel presets.
|
||||||
|
* tags:
|
||||||
|
* - Tunnel Presets
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema: { type: integer }
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Preset updated.
|
||||||
|
* 400:
|
||||||
|
* description: Invalid name or config.
|
||||||
|
* 404:
|
||||||
|
* description: Preset not found.
|
||||||
|
* 500:
|
||||||
|
* description: Failed to update preset.
|
||||||
|
*/
|
||||||
router.put(
|
router.put(
|
||||||
"/:id",
|
"/:id",
|
||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
@@ -171,6 +241,27 @@ router.put(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /c2s-tunnel-presets/{id}:
|
||||||
|
* delete:
|
||||||
|
* summary: Delete a client tunnel preset
|
||||||
|
* description: Deletes one of the authenticated user's tunnel presets.
|
||||||
|
* tags:
|
||||||
|
* - Tunnel Presets
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema: { type: integer }
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Preset deleted.
|
||||||
|
* 404:
|
||||||
|
* description: Preset not found.
|
||||||
|
* 500:
|
||||||
|
* description: Failed to delete preset.
|
||||||
|
*/
|
||||||
router.delete(
|
router.delete(
|
||||||
"/:id",
|
"/:id",
|
||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NextFunction, Request, Response } from "express";
|
import type { NextFunction, Request, Response } from "express";
|
||||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||||
import { SimpleDBOps } from "../../utils/simple-db-ops.js";
|
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||||
|
|
||||||
export function applyHostEnrollmentDefaults(
|
export function applyHostEnrollmentDefaults(
|
||||||
hostData: Record<string, unknown>,
|
hostData: Record<string, unknown>,
|
||||||
@@ -34,7 +34,7 @@ export function requireHostEnrollmentAccessForPath(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SimpleDBOps.isUserDataUnlocked(authReq.userId)) {
|
if (!DataCrypto.canUserAccessData(authReq.userId)) {
|
||||||
res.status(423).json({
|
res.status(423).json({
|
||||||
error: "User data is locked. Sign in before enrolling hosts.",
|
error: "User data is locked. Sign in before enrolling hosts.",
|
||||||
code: "DATA_LOCKED",
|
code: "DATA_LOCKED",
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ const authManager = AuthManager.getInstance();
|
|||||||
const authenticateJWT = authManager.createAuthMiddleware();
|
const authenticateJWT = authManager.createAuthMiddleware();
|
||||||
const requireDataAccess = authManager.createDataAccessMiddleware();
|
const requireDataAccess = authManager.createDataAccessMiddleware();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Proxmox node names are restricted to [a-zA-Z0-9-] by PVE itself,
|
// Proxmox node names are restricted to [a-zA-Z0-9-] by PVE itself,
|
||||||
// but we validate defensively before using in a shell command.
|
// but we validate defensively before using in a shell command.
|
||||||
|
|||||||
@@ -1035,9 +1035,7 @@ router.get(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SNIPPET SHARING
|
// SNIPPET SHARING
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
|
|||||||
@@ -88,10 +88,8 @@ async function getActorUsername(userId: string): Promise<string> {
|
|||||||
return user?.username ?? userId;
|
return user?.username ?? userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Public resolver — UNAUTHENTICATED. Serves authorized_keys (text/plain) or a
|
// Public resolver — UNAUTHENTICATED. Serves authorized_keys (text/plain) or a
|
||||||
// small HTML viewer for browsers, keyed by handle, with optional /<ALGO> filter.
|
// small HTML viewer for browsers, keyed by handle, with optional /<ALGO> filter.
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Never let an intermediary/CDN cache a public resolver feed (a disabled/removed
|
// Never let an intermediary/CDN cache a public resolver feed (a disabled/removed
|
||||||
// key could keep being served after revocation), and keep it out of search
|
// key could keep being served after revocation), and keep it out of search
|
||||||
@@ -276,9 +274,7 @@ router.get("/u/:handle/ca", caResolver);
|
|||||||
router.get("/u/:handle", resolveHandle);
|
router.get("/u/:handle", resolveHandle);
|
||||||
router.get("/u/:handle/:algo", resolveHandle);
|
router.get("/u/:handle/:algo", resolveHandle);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Authenticated management API.
|
// Authenticated management API.
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
@@ -1024,9 +1020,7 @@ router.delete(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Certificate authority — central revocation (rotate) + expiry (validity).
|
// Certificate authority — central revocation (rotate) + expiry (validity).
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type CaRow = {
|
type CaRow = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -132,6 +132,20 @@ export function registerUserWebAuthnRoutes(
|
|||||||
router: Router,
|
router: Router,
|
||||||
{ authenticateJWT, authManager, isNativeAppRequest }: WebAuthnRoutesDeps,
|
{ authenticateJWT, authManager, isNativeAppRequest }: WebAuthnRoutesDeps,
|
||||||
): void {
|
): void {
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /users/webauthn/credentials:
|
||||||
|
* get:
|
||||||
|
* summary: List passkeys
|
||||||
|
* description: Lists the authenticated user's registered passkeys.
|
||||||
|
* tags:
|
||||||
|
* - WebAuthn
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: List of passkeys.
|
||||||
|
* 401:
|
||||||
|
* description: Authentication required.
|
||||||
|
*/
|
||||||
router.get("/webauthn/credentials", authenticateJWT, async (req, res) => {
|
router.get("/webauthn/credentials", authenticateJWT, async (req, res) => {
|
||||||
const userId = (req as AuthenticatedRequest).userId;
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -155,6 +169,22 @@ export function registerUserWebAuthnRoutes(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /users/webauthn/register/options:
|
||||||
|
* post:
|
||||||
|
* summary: Start passkey registration
|
||||||
|
* description: Generates WebAuthn registration options for the authenticated user.
|
||||||
|
* tags:
|
||||||
|
* - WebAuthn
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Registration options and challenge id.
|
||||||
|
* 401:
|
||||||
|
* description: Authentication required.
|
||||||
|
* 404:
|
||||||
|
* description: User not found.
|
||||||
|
*/
|
||||||
router.post(
|
router.post(
|
||||||
"/webauthn/register/options",
|
"/webauthn/register/options",
|
||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
@@ -213,6 +243,22 @@ export function registerUserWebAuthnRoutes(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /users/webauthn/register/verify:
|
||||||
|
* post:
|
||||||
|
* summary: Finish passkey registration
|
||||||
|
* description: Verifies the WebAuthn registration response and stores the passkey.
|
||||||
|
* tags:
|
||||||
|
* - WebAuthn
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Passkey registered.
|
||||||
|
* 400:
|
||||||
|
* description: Registration failed or challenge expired.
|
||||||
|
* 401:
|
||||||
|
* description: Authentication required.
|
||||||
|
*/
|
||||||
router.post(
|
router.post(
|
||||||
"/webauthn/register/verify",
|
"/webauthn/register/verify",
|
||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
@@ -282,6 +328,20 @@ export function registerUserWebAuthnRoutes(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /users/webauthn/authenticate/options:
|
||||||
|
* post:
|
||||||
|
* summary: Start passkey login
|
||||||
|
* description: Generates WebAuthn authentication options, optionally scoped to a username.
|
||||||
|
* tags:
|
||||||
|
* - WebAuthn
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Authentication options and challenge id.
|
||||||
|
* 404:
|
||||||
|
* description: No passkeys found for the user.
|
||||||
|
*/
|
||||||
router.post("/webauthn/authenticate/options", async (req, res) => {
|
router.post("/webauthn/authenticate/options", async (req, res) => {
|
||||||
const origin = getRequestOrigin(req);
|
const origin = getRequestOrigin(req);
|
||||||
const rpID = getRpID(origin);
|
const rpID = getRpID(origin);
|
||||||
@@ -333,6 +393,22 @@ export function registerUserWebAuthnRoutes(
|
|||||||
res.json({ options, challengeId });
|
res.json({ options, challengeId });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /users/webauthn/authenticate/verify:
|
||||||
|
* post:
|
||||||
|
* summary: Finish passkey login
|
||||||
|
* description: Verifies the WebAuthn assertion and issues a session token (or a TOTP challenge).
|
||||||
|
* tags:
|
||||||
|
* - WebAuthn
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Login succeeded or TOTP verification required.
|
||||||
|
* 400:
|
||||||
|
* description: Challenge expired or invalid response.
|
||||||
|
* 401:
|
||||||
|
* description: Passkey not recognized or authentication failed.
|
||||||
|
*/
|
||||||
router.post("/webauthn/authenticate/verify", async (req, res) => {
|
router.post("/webauthn/authenticate/verify", async (req, res) => {
|
||||||
const challenge = takeChallenge(
|
const challenge = takeChallenge(
|
||||||
authenticationChallenges,
|
authenticationChallenges,
|
||||||
@@ -468,6 +544,25 @@ export function registerUserWebAuthnRoutes(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /users/webauthn/credentials/{credentialId}:
|
||||||
|
* delete:
|
||||||
|
* summary: Delete a passkey
|
||||||
|
* description: Removes one of the authenticated user's passkeys.
|
||||||
|
* tags:
|
||||||
|
* - WebAuthn
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: credentialId
|
||||||
|
* required: true
|
||||||
|
* schema: { type: string }
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Passkey deleted.
|
||||||
|
* 401:
|
||||||
|
* description: Authentication required.
|
||||||
|
*/
|
||||||
router.delete(
|
router.delete(
|
||||||
"/webauthn/credentials/:credentialId",
|
"/webauthn/credentials/:credentialId",
|
||||||
authenticateJWT,
|
authenticateJWT,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { registerUserApiKeyRoutes } from "./user-api-key-routes.js";
|
|||||||
import { registerUserSettingsRoutes } from "./user-settings-routes.js";
|
import { registerUserSettingsRoutes } from "./user-settings-routes.js";
|
||||||
import { registerAcmeSSLRoutes } from "./acme-ssl-routes.js";
|
import { registerAcmeSSLRoutes } from "./acme-ssl-routes.js";
|
||||||
import { registerUserTotpRoutes } from "./user-totp-routes.js";
|
import { registerUserTotpRoutes } from "./user-totp-routes.js";
|
||||||
|
import { registerUserWebAuthnRoutes } from "./user-webauthn-routes.js";
|
||||||
import { registerUserSessionRoutes } from "./user-session-routes.js";
|
import { registerUserSessionRoutes } from "./user-session-routes.js";
|
||||||
import { registerUserOidcAccountRoutes } from "./user-oidc-account-routes.js";
|
import { registerUserOidcAccountRoutes } from "./user-oidc-account-routes.js";
|
||||||
import { registerUserPasswordResetRoutes } from "./user-password-reset-routes.js";
|
import { registerUserPasswordResetRoutes } from "./user-password-reset-routes.js";
|
||||||
@@ -2425,6 +2426,12 @@ registerUserTotpRoutes(router, {
|
|||||||
isNativeAppRequest,
|
isNativeAppRequest,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerUserWebAuthnRoutes(router, {
|
||||||
|
authenticateJWT,
|
||||||
|
authManager,
|
||||||
|
isNativeAppRequest,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
* /users/delete-user:
|
* /users/delete-user:
|
||||||
|
|||||||
@@ -54,10 +54,8 @@ interface TmuxSessionOverview extends TmuxSessionSummary {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// SSH connection (lean variant of the per-module pattern used by server-stats
|
// SSH connection (lean variant of the per-module pattern used by server-stats
|
||||||
// and docker; jump hosts and SOCKS5 reuse the shared helpers)
|
// and docker; jump hosts and SOCKS5 reuse the shared helpers)
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function buildSshConfig(host: SSHHost): Promise<ConnectConfig> {
|
async function buildSshConfig(host: SSHHost): Promise<ConnectConfig> {
|
||||||
const base: ConnectConfig = {
|
const base: ConnectConfig = {
|
||||||
@@ -241,9 +239,7 @@ async function withHostConnection<T>(
|
|||||||
return withConnection(getPoolKey(host), connectToHost(host), fn);
|
return withConnection(getPoolKey(host), connectToHost(host), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// tmux queries
|
// tmux queries
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function tmuxAvailable(conn: Client): Promise<boolean> {
|
async function tmuxAvailable(conn: Client): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -326,9 +322,7 @@ async function collectPaneMetrics(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Express app
|
// Express app
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
|
|||||||
+18
-49
@@ -38,58 +38,27 @@ import {
|
|||||||
port: process.env.PORT || 4090,
|
port: process.env.PORT || 4090,
|
||||||
});
|
});
|
||||||
|
|
||||||
let version = "unknown";
|
let version = process.env.VERSION || "unknown";
|
||||||
|
if (version === "unknown") {
|
||||||
const versionSources = [
|
const candidates = [
|
||||||
() => process.env.VERSION,
|
path.join(process.cwd(), "package.json"),
|
||||||
() => {
|
path.join(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
"../../../package.json",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (const packageJsonPath of candidates) {
|
||||||
try {
|
try {
|
||||||
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
||||||
const packageJson = JSON.parse(
|
const packageJson = JSON.parse(
|
||||||
readFileSync(packageJsonPath, "utf-8"),
|
readFileSync(packageJsonPath, "utf-8"),
|
||||||
);
|
);
|
||||||
return packageJson.version;
|
if (packageJson.version) {
|
||||||
|
version = packageJson.version;
|
||||||
|
break;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
// try the next location
|
||||||
}
|
}
|
||||||
},
|
|
||||||
() => {
|
|
||||||
try {
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const packageJsonPath = path.join(
|
|
||||||
path.dirname(__filename),
|
|
||||||
"../../../package.json",
|
|
||||||
);
|
|
||||||
const packageJson = JSON.parse(
|
|
||||||
readFileSync(packageJsonPath, "utf-8"),
|
|
||||||
);
|
|
||||||
return packageJson.version;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
try {
|
|
||||||
const packageJsonPath = path.join("/app", "package.json");
|
|
||||||
const packageJson = JSON.parse(
|
|
||||||
readFileSync(packageJsonPath, "utf-8"),
|
|
||||||
);
|
|
||||||
return packageJson.version;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const getVersion of versionSources) {
|
|
||||||
try {
|
|
||||||
const foundVersion = getVersion();
|
|
||||||
if (foundVersion && foundVersion !== "unknown") {
|
|
||||||
version = foundVersion;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
versionLogger.info(`Termix Backend starting - Version: ${version}`, {
|
versionLogger.info(`Termix Backend starting - Version: ${version}`, {
|
||||||
@@ -152,15 +121,15 @@ import {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const dbServer = await import("./database/database.js");
|
const { serverReady } = await import("./database/database.js");
|
||||||
await (dbServer as unknown as { serverReady: Promise<void> }).serverReady;
|
await serverReady;
|
||||||
await import("./hosts/terminal/index.js");
|
await import("./hosts/terminal/index.js");
|
||||||
await import("./hosts/tunnel/index.js");
|
await import("./hosts/tunnel/index.js");
|
||||||
await import("./hosts/file-manager/index.js");
|
await import("./hosts/file-manager/index.js");
|
||||||
await import("./hosts/metrics/index.js");
|
await import("./hosts/metrics/index.js");
|
||||||
await import("./hosts/docker/index.js");
|
await import("./hosts/docker/index.js");
|
||||||
await import("./hosts/docker/console.js");
|
await import("./hosts/docker/console.js");
|
||||||
await import("./hosts/tmux/index.js"); // --- tmux-monitor ---
|
await import("./hosts/tmux/index.js");
|
||||||
await import("./hosts/serial.js");
|
await import("./hosts/serial.js");
|
||||||
await import("./services/dashboard.js");
|
await import("./services/dashboard.js");
|
||||||
await import("./services/homepage.js");
|
await import("./services/homepage.js");
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ const mocks = vi.hoisted(() => ({
|
|||||||
isUserDataUnlocked: vi.fn(),
|
isUserDataUnlocked: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../../utils/simple-db-ops.js", () => ({
|
vi.mock("../../../utils/data-crypto.js", () => ({
|
||||||
SimpleDBOps: {
|
DataCrypto: {
|
||||||
isUserDataUnlocked: mocks.isUserDataUnlocked,
|
canUserAccessData: mocks.isUserDataUnlocked,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
import { getDb, DatabaseSaveTrigger } from "../database/db/index.js";
|
|
||||||
import { DataCrypto } from "./data-crypto.js";
|
|
||||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
||||||
import type { SQL } from "drizzle-orm";
|
|
||||||
|
|
||||||
type TableName =
|
|
||||||
| "users"
|
|
||||||
| "ssh_data"
|
|
||||||
| "ssh_credentials"
|
|
||||||
| "termix_identity_ca"
|
|
||||||
| "recent_activity"
|
|
||||||
| "socks5_proxy_presets";
|
|
||||||
|
|
||||||
class SimpleDBOps {
|
|
||||||
static async insert<T extends Record<string, unknown>>(
|
|
||||||
table: SQLiteTable,
|
|
||||||
tableName: TableName,
|
|
||||||
data: T,
|
|
||||||
userId: string,
|
|
||||||
): Promise<T> {
|
|
||||||
const userDataKey = DataCrypto.validateUserAccess(userId);
|
|
||||||
|
|
||||||
const tempId = data.id || `temp-${userId}-${Date.now()}`;
|
|
||||||
const dataWithTempId = { ...data, id: tempId };
|
|
||||||
|
|
||||||
const encryptedData = DataCrypto.encryptRecord(
|
|
||||||
tableName,
|
|
||||||
dataWithTempId,
|
|
||||||
userId,
|
|
||||||
userDataKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!data.id) {
|
|
||||||
delete encryptedData.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await getDb()
|
|
||||||
.insert(table)
|
|
||||||
.values(encryptedData)
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
DatabaseSaveTrigger.triggerSave(`insert_${tableName}`);
|
|
||||||
|
|
||||||
const decryptedResult = DataCrypto.decryptRecord(
|
|
||||||
tableName,
|
|
||||||
result[0],
|
|
||||||
userId,
|
|
||||||
userDataKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
return decryptedResult as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async select<T extends Record<string, unknown>>(
|
|
||||||
query: unknown,
|
|
||||||
tableName: TableName,
|
|
||||||
userId: string,
|
|
||||||
): Promise<T[]> {
|
|
||||||
const userDataKey = DataCrypto.getUserDataKey(userId);
|
|
||||||
if (!userDataKey) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await query;
|
|
||||||
|
|
||||||
const decryptedResults = DataCrypto.decryptRecords<T>(
|
|
||||||
tableName,
|
|
||||||
results as T[],
|
|
||||||
userId,
|
|
||||||
userDataKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
return decryptedResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async selectOne<T extends Record<string, unknown>>(
|
|
||||||
query: unknown,
|
|
||||||
tableName: TableName,
|
|
||||||
userId: string,
|
|
||||||
): Promise<T | undefined> {
|
|
||||||
const userDataKey = DataCrypto.getUserDataKey(userId);
|
|
||||||
if (!userDataKey) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await query;
|
|
||||||
if (!result) return undefined;
|
|
||||||
|
|
||||||
const decryptedResult = DataCrypto.decryptRecord<T>(
|
|
||||||
tableName,
|
|
||||||
result as T,
|
|
||||||
userId,
|
|
||||||
userDataKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
return decryptedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update<T extends Record<string, unknown>>(
|
|
||||||
table: SQLiteTable,
|
|
||||||
tableName: TableName,
|
|
||||||
where: unknown,
|
|
||||||
data: Partial<T>,
|
|
||||||
userId: string,
|
|
||||||
): Promise<T[]> {
|
|
||||||
const userDataKey = DataCrypto.validateUserAccess(userId);
|
|
||||||
|
|
||||||
const encryptedData = DataCrypto.encryptRecord(
|
|
||||||
tableName,
|
|
||||||
data,
|
|
||||||
userId,
|
|
||||||
userDataKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await getDb()
|
|
||||||
.update(table)
|
|
||||||
.set(encryptedData)
|
|
||||||
.where(where as SQL | undefined)
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
DatabaseSaveTrigger.triggerSave(`update_${tableName}`);
|
|
||||||
|
|
||||||
const decryptedResults = DataCrypto.decryptRecords(
|
|
||||||
tableName,
|
|
||||||
result,
|
|
||||||
userId,
|
|
||||||
userDataKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
return decryptedResults as T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
static async delete(
|
|
||||||
table: SQLiteTable,
|
|
||||||
tableName: TableName,
|
|
||||||
where: unknown,
|
|
||||||
): Promise<unknown[]> {
|
|
||||||
const result = await getDb()
|
|
||||||
.delete(table)
|
|
||||||
.where(where as SQL | undefined)
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async healthCheck(userId: string): Promise<boolean> {
|
|
||||||
return DataCrypto.canUserAccessData(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
static isUserDataUnlocked(userId: string): boolean {
|
|
||||||
return DataCrypto.getUserDataKey(userId) !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async selectEncrypted(query: unknown): Promise<unknown[]> {
|
|
||||||
const results = await query;
|
|
||||||
|
|
||||||
return results as unknown[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { SimpleDBOps, type TableName };
|
|
||||||
Reference in New Issue
Block a user