mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e8ce6df7 | ||
|
|
3517b2f0ed | ||
|
|
220f5c8676 | ||
|
|
4dfad3efb3 | ||
|
|
e152bacc7c | ||
|
|
8d9c83b0f6 |
No files matched your search
+1
-1
@@ -26,7 +26,7 @@ COPY . .
|
|||||||
RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete
|
RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete
|
||||||
|
|
||||||
RUN npm cache clean --force && \
|
RUN npm cache clean --force && \
|
||||||
NODE_OPTIONS="--max-old-space-size=3072" npm run build
|
TERMIX_RUNTIME_BASE_PATH=true NODE_OPTIONS="--max-old-space-size=3072" npm run build
|
||||||
|
|
||||||
# Stage 3: Build backend
|
# Stage 3: Build backend
|
||||||
FROM deps AS backend-builder
|
FROM deps AS backend-builder
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "26.10.1",
|
"version": "26.10.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"version": "26.10.1",
|
"version": "26.10.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "26.10.1",
|
"version": "26.10.2",
|
||||||
"description": "Self-hosted, plugin-based server management.",
|
"description": "Self-hosted, plugin-based server management.",
|
||||||
"author": "Karmaa",
|
"author": "Karmaa",
|
||||||
"main": "electron/main.cjs",
|
"main": "electron/main.cjs",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"dev:electron": "node scripts/dev.mjs electron",
|
"dev:electron": "node scripts/dev.mjs electron",
|
||||||
"dev:docker": "node scripts/dev.mjs docker",
|
"dev:docker": "node scripts/dev.mjs docker",
|
||||||
"dev:frontend": "vite",
|
"dev:frontend": "vite",
|
||||||
"build": "npm run build:sdk && vite build && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
"build": "npm run build:sdk && vite build && node scripts/stamp-sw-build-info.cjs && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
||||||
"build:backend": "npm run build:sdk && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
"build:backend": "npm run build:sdk && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
||||||
"generate:openapi": "npm run build:sdk && node scripts/generate-openapi.mjs",
|
"generate:openapi": "npm run build:sdk && node scripts/generate-openapi.mjs",
|
||||||
"electron:rebuild": "electron-rebuild -f -o better-sqlite3,@serialport/bindings-cpp,node-pty",
|
"electron:rebuild": "electron-rebuild -f -o better-sqlite3,@serialport/bindings-cpp,node-pty",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
+6
-1
@@ -1,4 +1,9 @@
|
|||||||
const CACHE_NAME = "termix-static-v2";
|
// CACHE_NAME carries the package version so a new release always gets a fresh
|
||||||
|
// cache: the activate handler below deletes every cache whose name differs
|
||||||
|
// from this one, which is what evicts a previous build's entries. Both
|
||||||
|
// placeholders on these lines are stamped by scripts/stamp-sw-build-info.cjs
|
||||||
|
// during `npm run build` - vite copies public/ verbatim and never transforms it.
|
||||||
|
const CACHE_NAME = "termix-static-" + "__TERMIX_CACHE_VERSION__";
|
||||||
const BASE_PATH = "__TERMIX_SW_BASE_PATH__";
|
const BASE_PATH = "__TERMIX_SW_BASE_PATH__";
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
`${BASE_PATH}/favicon.ico`,
|
`${BASE_PATH}/favicon.ico`,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/** Stamp the cache version and static-build base path.
|
||||||
|
* Docker keeps the base-path placeholder for entrypoint.sh to fill at startup.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, "..");
|
||||||
|
const SW_PATH = path.join(ROOT, "dist", "sw.js");
|
||||||
|
|
||||||
|
const VERSION_PLACEHOLDER = "__TERMIX_CACHE_VERSION__";
|
||||||
|
const BASE_PATH_PLACEHOLDER = "__TERMIX_SW_BASE_PATH__";
|
||||||
|
|
||||||
|
const pkg = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(ROOT, "package.json"), "utf8"),
|
||||||
|
);
|
||||||
|
const cacheVersion = String(pkg.version || "0.0.0").replace(/\./g, "-");
|
||||||
|
|
||||||
|
const rawBasePath = process.env.VITE_BASE_PATH || "./";
|
||||||
|
// "./" is what vite emits for a root deployment; cache URLs stay root-relative.
|
||||||
|
const basePath =
|
||||||
|
rawBasePath === "./"
|
||||||
|
? ""
|
||||||
|
: `/${rawBasePath.replace(/^\/+/, "").replace(/\/+$/, "")}`;
|
||||||
|
|
||||||
|
if (!fs.existsSync(SW_PATH)) {
|
||||||
|
console.error(`[stamp-sw] dist/sw.js not found - run vite build first`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sw = fs.readFileSync(SW_PATH, "utf8");
|
||||||
|
const rel = path.relative(ROOT, SW_PATH);
|
||||||
|
const replacements = [
|
||||||
|
[VERSION_PLACEHOLDER, cacheVersion],
|
||||||
|
...(process.env.TERMIX_RUNTIME_BASE_PATH === "true"
|
||||||
|
? []
|
||||||
|
: [[BASE_PATH_PLACEHOLDER, basePath]]),
|
||||||
|
];
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
for (const [placeholder, value] of replacements) {
|
||||||
|
if (sw.includes(placeholder)) {
|
||||||
|
sw = sw.split(placeholder).join(value);
|
||||||
|
changed = true;
|
||||||
|
console.log(
|
||||||
|
`[stamp-sw] ${rel}: ${placeholder} -> ${JSON.stringify(value)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
fs.writeFileSync(SW_PATH, sw);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import {
|
||||||
|
mkdtempSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
rmSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { expect, it } from "vitest";
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[false, "", ""],
|
||||||
|
[false, "/termix/", "/termix"],
|
||||||
|
[true, "", "__TERMIX_SW_BASE_PATH__"],
|
||||||
|
])(
|
||||||
|
"stamps service worker paths (runtime=%s, base=%s)",
|
||||||
|
(runtime, base, expected) => {
|
||||||
|
const dir = mkdtempSync(path.join(tmpdir(), "termix-sw-"));
|
||||||
|
try {
|
||||||
|
mkdirSync(path.join(dir, "scripts"));
|
||||||
|
mkdirSync(path.join(dir, "dist"));
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dir, "scripts/stamp-sw-build-info.cjs"),
|
||||||
|
readFileSync("scripts/stamp-sw-build-info.cjs"),
|
||||||
|
);
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dir, "package.json"),
|
||||||
|
JSON.stringify({ version: "26.10.2" }),
|
||||||
|
);
|
||||||
|
writeFileSync(path.join(dir, "dist/sw.js"), readFileSync("public/sw.js"));
|
||||||
|
execFileSync(
|
||||||
|
process.execPath,
|
||||||
|
[path.join(dir, "scripts/stamp-sw-build-info.cjs")],
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
VITE_BASE_PATH: base,
|
||||||
|
TERMIX_RUNTIME_BASE_PATH: String(runtime),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const sw = readFileSync(path.join(dir, "dist/sw.js"), "utf8");
|
||||||
|
expect(sw).toContain('"26-10-2"');
|
||||||
|
expect(sw).toContain(`const BASE_PATH = "${expected}";`);
|
||||||
|
if (runtime) {
|
||||||
|
expect(sw.replaceAll("__TERMIX_SW_BASE_PATH__", "/termix")).toContain(
|
||||||
|
'const BASE_PATH = "/termix";',
|
||||||
|
);
|
||||||
|
expect(readFileSync("docker/Dockerfile", "utf8")).toContain(
|
||||||
|
"TERMIX_RUNTIME_BASE_PATH=true",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -575,22 +575,21 @@ export function registerHostBulkRoutes(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loaded for both modes: overwrite updates a match, skip leaves it alone.
|
||||||
let existingHostMap: Map<string, { id: number }> | undefined;
|
let existingHostMap: Map<string, { id: number }> | undefined;
|
||||||
const hostRepository = createCurrentHostRepository();
|
const hostRepository = createCurrentHostRepository();
|
||||||
if (overwrite) {
|
try {
|
||||||
try {
|
const allHosts =
|
||||||
const allHosts =
|
await createCurrentHostResolutionRepository().findHostsByUserId(
|
||||||
await createCurrentHostResolutionRepository().findHostsByUserId(
|
userId,
|
||||||
userId,
|
);
|
||||||
);
|
existingHostMap = new Map();
|
||||||
existingHostMap = new Map();
|
for (const h of allHosts) {
|
||||||
for (const h of allHosts) {
|
const key = `${h.ip}:${h.port}:${h.username}`;
|
||||||
const key = `${h.ip}:${h.port}:${h.username}`;
|
existingHostMap.set(key, { id: h.id as number });
|
||||||
existingHostMap.set(key, { id: h.id as number });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
existingHostMap = undefined;
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
existingHostMap = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const knownAuthTypes = listKnownAuthTypes();
|
const knownAuthTypes = listKnownAuthTypes();
|
||||||
@@ -632,6 +631,15 @@ export function registerHostBulkRoutes(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
||||||
|
const existing = existingHostMap?.get(lookupKey);
|
||||||
|
if (existing && !overwrite) {
|
||||||
|
// Hosts that jump through this one still resolve to the kept host.
|
||||||
|
if (exportId !== undefined) importedIds.set(exportId, existing.id);
|
||||||
|
results.skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
effectiveConnectionType === "ssh" &&
|
effectiveConnectionType === "ssh" &&
|
||||||
hostData.authType &&
|
hostData.authType &&
|
||||||
@@ -777,8 +785,6 @@ export function registerHostBulkRoutes(
|
|||||||
hostData.authType === "key" ? hostData.keyType || "auto" : null;
|
hostData.authType === "key" ? hostData.keyType || "auto" : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
|
||||||
const existing = existingHostMap?.get(lookupKey);
|
|
||||||
await withHostDefaults(
|
await withHostDefaults(
|
||||||
userId,
|
userId,
|
||||||
existing?.id ?? null,
|
existing?.id ?? null,
|
||||||
@@ -840,7 +846,7 @@ export function registerHostBulkRoutes(
|
|||||||
notifyImportedHosts(userId, writtenHostIds, createdHostIds);
|
notifyImportedHosts(userId, writtenHostIds, createdHostIds);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
|
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.skipped} skipped, ${results.failed} failed`,
|
||||||
success: results.success,
|
success: results.success,
|
||||||
updated: results.updated,
|
updated: results.updated,
|
||||||
skipped: results.skipped,
|
skipped: results.skipped,
|
||||||
@@ -940,20 +946,18 @@ export function registerHostBulkRoutes(
|
|||||||
|
|
||||||
let existingHostMap: Map<string, { id: number }> | undefined;
|
let existingHostMap: Map<string, { id: number }> | undefined;
|
||||||
const hostRepository = createCurrentHostRepository();
|
const hostRepository = createCurrentHostRepository();
|
||||||
if (overwrite) {
|
try {
|
||||||
try {
|
const allHosts =
|
||||||
const allHosts =
|
await createCurrentHostResolutionRepository().findHostsByUserId(
|
||||||
await createCurrentHostResolutionRepository().findHostsByUserId(
|
userId,
|
||||||
userId,
|
);
|
||||||
);
|
existingHostMap = new Map();
|
||||||
existingHostMap = new Map();
|
for (const h of allHosts) {
|
||||||
for (const h of allHosts) {
|
const key = `${h.ip}:${h.port}:${h.username}`;
|
||||||
const key = `${h.ip}:${h.port}:${h.username}`;
|
existingHostMap.set(key, { id: h.id as number });
|
||||||
existingHostMap.set(key, { id: h.id as number });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
existingHostMap = undefined;
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
existingHostMap = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const writtenHostIds: number[] = [];
|
const writtenHostIds: number[] = [];
|
||||||
@@ -972,6 +976,13 @@ export function registerHostBulkRoutes(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
||||||
|
const existing = existingHostMap?.get(lookupKey);
|
||||||
|
if (existing && !overwrite) {
|
||||||
|
results.skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const sshDataObj: Record<string, unknown> = {
|
const sshDataObj: Record<string, unknown> = {
|
||||||
userId,
|
userId,
|
||||||
connectionType: "ssh",
|
connectionType: "ssh",
|
||||||
@@ -1009,8 +1020,6 @@ export function registerHostBulkRoutes(
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
|
|
||||||
const existing = existingHostMap?.get(lookupKey);
|
|
||||||
await withHostDefaults(
|
await withHostDefaults(
|
||||||
userId,
|
userId,
|
||||||
existing?.id ?? null,
|
existing?.id ?? null,
|
||||||
@@ -1048,7 +1057,7 @@ export function registerHostBulkRoutes(
|
|||||||
notifyImportedHosts(userId, writtenHostIds, createdHostIds);
|
notifyImportedHosts(userId, writtenHostIds, createdHostIds);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
|
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.skipped} skipped, ${results.failed} failed`,
|
||||||
success: results.success,
|
success: results.success,
|
||||||
updated: results.updated,
|
updated: results.updated,
|
||||||
skipped: results.skipped,
|
skipped: results.skipped,
|
||||||
|
|||||||
@@ -69,12 +69,13 @@ beforeEach(() => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mocks.create.mockResolvedValue({ id: 71 });
|
mocks.create.mockResolvedValue({ id: 71 });
|
||||||
mocks.update.mockResolvedValue({ id: 19 });
|
mocks.update.mockResolvedValue({ id: 19 });
|
||||||
mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
mocks.list.mockResolvedValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([false, true])(
|
it.each([false, true])(
|
||||||
"carries plugin host settings through export selection and import (overwrite=%s)",
|
"carries plugin host settings through export selection and import (overwrite=%s)",
|
||||||
async (overwrite) => {
|
async (overwrite) => {
|
||||||
|
if (overwrite) mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
||||||
const payload = buildExportPayload(
|
const payload = buildExportPayload(
|
||||||
{ hosts: [host] },
|
{ hosts: [host] },
|
||||||
null,
|
null,
|
||||||
@@ -171,6 +172,60 @@ it.each([20, "20"])(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("skips hosts that already exist when not overwriting", async () => {
|
||||||
|
mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
||||||
|
const res = await importHosts([host]);
|
||||||
|
expect(res.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ success: 0, updated: 0, skipped: 1, failed: 0 }),
|
||||||
|
);
|
||||||
|
expect(mocks.create).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.update).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.applyPluginSettings).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps a skipped jump host reference to its existing destination ID", async () => {
|
||||||
|
mocks.list.mockResolvedValue([{ ...host, ip: "192.0.2.2", id: 801 }]);
|
||||||
|
const res = await importHosts([
|
||||||
|
{ ...host, exportId: 10, jumpHosts: [{ hostId: 20 }] },
|
||||||
|
{ ...host, ip: "192.0.2.2", exportId: 20 },
|
||||||
|
]);
|
||||||
|
expect(res.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ success: 1, skipped: 1, failed: 0 }),
|
||||||
|
);
|
||||||
|
expect(mocks.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.update).not.toHaveBeenCalled();
|
||||||
|
expect(JSON.parse(mocks.create.mock.calls[0][1].jumpHosts)).toEqual([
|
||||||
|
{ hostId: 801 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[false, { success: 0, updated: 0, skipped: 1 }],
|
||||||
|
[true, { success: 0, updated: 1, skipped: 0 }],
|
||||||
|
])(
|
||||||
|
"SSH config import with an existing host (overwrite=%s)",
|
||||||
|
async (overwrite, counts) => {
|
||||||
|
mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
||||||
|
const res = { json: vi.fn(), status: vi.fn().mockReturnThis() };
|
||||||
|
await sshConfigHandler(
|
||||||
|
{
|
||||||
|
userId: "user-1",
|
||||||
|
body: {
|
||||||
|
content: "Host web\n HostName 192.0.2.1\n User alice\n Port 22\n",
|
||||||
|
overwrite,
|
||||||
|
},
|
||||||
|
} as unknown as Request,
|
||||||
|
res as unknown as Response,
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
expect(res.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ ...counts, failed: 0 }),
|
||||||
|
);
|
||||||
|
expect(mocks.create).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.update).toHaveBeenCalledTimes(overwrite ? 1 : 0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ ...host, jumpHosts: [{ hostId: 20 }] }],
|
[{ ...host, jumpHosts: [{ hostId: 20 }] }],
|
||||||
[{ ...host, jumpHosts: [{ hostId: "20" }] }],
|
[{ ...host, jumpHosts: [{ hostId: "20" }] }],
|
||||||
|
|||||||
Reference in new issue
Block a user