mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
feat: rebuild desktop sync as linked server accounts, fix packaged plugin bundles and UI scaling
This commit is contained in:
1 parent
7c02daccf4
commit
6cd9a2846d
242 files changed
+39127
-8558
No files matched your search
@@ -117,5 +117,6 @@
|
||||
}
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"desktop": "server"
|
||||
}
|
||||
@@ -125,5 +125,6 @@
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"dependencies": {
|
||||
"snippets": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"desktop": "server"
|
||||
}
|
||||
@@ -44,7 +44,7 @@ type ConnectErrorResponse = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The interactive-auth branches /ssh/connect can take before a session exists.
|
||||
* The interactive-auth branches /connect can take before a session exists.
|
||||
* Callers switch on these, so they cannot stay behind Record<string, unknown>.
|
||||
*/
|
||||
export interface SSHConnectResult {
|
||||
@@ -106,7 +106,7 @@ export async function connectSSH(
|
||||
): Promise<SSHConnectResult> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/connect",
|
||||
"/connect",
|
||||
{ sessionId, ...config },
|
||||
{ timeout: 120000 },
|
||||
);
|
||||
@@ -156,7 +156,7 @@ export async function disconnectSSH(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/disconnect",
|
||||
"/disconnect",
|
||||
{ sessionId },
|
||||
);
|
||||
return response.data;
|
||||
@@ -173,7 +173,7 @@ export async function verifySSHTOTP(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/connect-totp",
|
||||
"/connect-totp",
|
||||
{ sessionId, totpCode },
|
||||
);
|
||||
return response.data;
|
||||
@@ -187,7 +187,7 @@ export async function verifySSHBrowserSignIn(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/connect-browser-sign-in",
|
||||
"/connect-browser-sign-in",
|
||||
{ sessionId },
|
||||
);
|
||||
return response.data;
|
||||
@@ -201,7 +201,7 @@ export async function getSSHStatus(
|
||||
): Promise<{ connected: boolean }> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).get(
|
||||
"/ssh/status",
|
||||
"/status",
|
||||
{ params: { sessionId } },
|
||||
);
|
||||
return response.data;
|
||||
@@ -215,7 +215,7 @@ export async function keepSSHAlive(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/keepalive",
|
||||
"/keepalive",
|
||||
{ sessionId },
|
||||
);
|
||||
return response.data;
|
||||
@@ -235,7 +235,7 @@ export async function listSSHFiles(
|
||||
async () => {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).get(
|
||||
"/ssh/listFiles",
|
||||
"/listFiles",
|
||||
{ params: { sessionId, path } },
|
||||
);
|
||||
return response.data || { files: [], path };
|
||||
@@ -253,7 +253,7 @@ export async function identifySSHSymlink(
|
||||
): Promise<{ path: string; target: string; type: "directory" | "file" }> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).get(
|
||||
"/ssh/identifySymlink",
|
||||
"/identifySymlink",
|
||||
{ params: { sessionId, path } },
|
||||
);
|
||||
return response.data;
|
||||
@@ -268,7 +268,7 @@ export async function resolveSSHPath(
|
||||
): Promise<string> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).get(
|
||||
"/ssh/resolvePath",
|
||||
"/resolvePath",
|
||||
{ params: { sessionId, path } },
|
||||
);
|
||||
return response.data?.resolvedPath || path;
|
||||
@@ -288,7 +288,7 @@ export async function readSSHFile(
|
||||
async () => {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).get(
|
||||
"/ssh/readFile",
|
||||
"/readFile",
|
||||
{ params: { sessionId, path } },
|
||||
);
|
||||
return response.data;
|
||||
@@ -326,7 +326,7 @@ export async function writeSSHFile(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/writeFile",
|
||||
"/writeFile",
|
||||
{ sessionId, path, content, hostId, userId },
|
||||
);
|
||||
|
||||
@@ -385,7 +385,7 @@ export async function uploadSSHFile(
|
||||
const chunkBlob = file.slice(start, end);
|
||||
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/uploadFileChunk",
|
||||
"/uploadFileChunk",
|
||||
chunkBlob,
|
||||
{
|
||||
params: {
|
||||
@@ -429,7 +429,7 @@ export async function uploadSSHFile(
|
||||
form.append("file", file, fileName);
|
||||
|
||||
const response = await getFileManagerApiForSession(sessionId).postForm(
|
||||
"/ssh/uploadFileStream",
|
||||
"/uploadFileStream",
|
||||
form,
|
||||
{
|
||||
timeout: 0,
|
||||
@@ -470,7 +470,7 @@ export async function downloadSSHFile(
|
||||
): Promise<DownloadedSSHFile> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/downloadFile",
|
||||
"/downloadFile",
|
||||
{
|
||||
sessionId,
|
||||
path: filePath,
|
||||
@@ -496,7 +496,7 @@ export async function downloadSSHFileStream(
|
||||
onProgress?: (event: DownloadProgressEvent) => void,
|
||||
): Promise<void> {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/downloadFileStream",
|
||||
"/downloadFileStream",
|
||||
{ sessionId, path: filePath },
|
||||
{
|
||||
responseType: "blob",
|
||||
@@ -521,7 +521,7 @@ export async function createSSHFile(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/createFile",
|
||||
"/createFile",
|
||||
{ sessionId, path, fileName, content, hostId, userId },
|
||||
);
|
||||
return response.data;
|
||||
@@ -539,7 +539,7 @@ export async function createSSHFolder(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/createFolder",
|
||||
"/createFolder",
|
||||
{ sessionId, path, folderName, hostId, userId },
|
||||
);
|
||||
return response.data;
|
||||
@@ -558,7 +558,7 @@ export async function deleteSSHItem(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).delete(
|
||||
"/ssh/deleteItem",
|
||||
"/deleteItem",
|
||||
{
|
||||
data: {
|
||||
sessionId,
|
||||
@@ -599,7 +599,7 @@ export async function getSSHTrash(sessionId: string): Promise<{
|
||||
}> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).get(
|
||||
"/ssh/trash",
|
||||
"/trash",
|
||||
{ params: { sessionId } },
|
||||
);
|
||||
return response.data;
|
||||
@@ -611,7 +611,7 @@ export async function getSSHTrash(sessionId: string): Promise<{
|
||||
export async function restoreSSHTrashItem(sessionId: string, id: string) {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
`/ssh/trash/${encodeURIComponent(id)}/restore`,
|
||||
`/trash/${encodeURIComponent(id)}/restore`,
|
||||
{ sessionId },
|
||||
);
|
||||
return response.data;
|
||||
@@ -626,7 +626,7 @@ export async function permanentlyDeleteSSHTrashItem(
|
||||
) {
|
||||
try {
|
||||
await getFileManagerApiForSession(sessionId).delete(
|
||||
`/ssh/trash/${encodeURIComponent(id)}`,
|
||||
`/trash/${encodeURIComponent(id)}`,
|
||||
{ data: { sessionId } },
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -636,7 +636,7 @@ export async function permanentlyDeleteSSHTrashItem(
|
||||
|
||||
export async function emptySSHTrash(sessionId: string) {
|
||||
try {
|
||||
await getFileManagerApiForSession(sessionId).delete("/ssh/trash", {
|
||||
await getFileManagerApiForSession(sessionId).delete("/trash", {
|
||||
data: { sessionId },
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -649,7 +649,7 @@ export async function updateSSHTrashRetention(
|
||||
retentionDays: number,
|
||||
) {
|
||||
try {
|
||||
await getFileManagerApiForSession(sessionId).put("/ssh/trash-retention", {
|
||||
await getFileManagerApiForSession(sessionId).put("/trash-retention", {
|
||||
retentionDays,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -687,7 +687,7 @@ export async function copySSHItem(
|
||||
): Promise<CopySSHItemResult> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/copyItem",
|
||||
"/copyItem",
|
||||
{
|
||||
sessionId,
|
||||
sourcePath,
|
||||
@@ -715,7 +715,7 @@ export async function renameSSHItem(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).put(
|
||||
"/ssh/renameItem",
|
||||
"/renameItem",
|
||||
{ sessionId, oldPath, newName, hostId, userId },
|
||||
);
|
||||
invalidateCachedFileContent(sessionId, oldPath);
|
||||
@@ -738,7 +738,7 @@ export async function moveSSHItem(
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await getFileManagerApiForSession(sessionId).put(
|
||||
"/ssh/moveItem",
|
||||
"/moveItem",
|
||||
{
|
||||
sessionId,
|
||||
oldPath,
|
||||
@@ -777,7 +777,7 @@ export async function changeSSHPermissions(
|
||||
});
|
||||
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/changePermissions",
|
||||
"/changePermissions",
|
||||
{ sessionId, path, permissions, hostId, userId },
|
||||
);
|
||||
|
||||
@@ -819,7 +819,7 @@ export async function extractSSHArchive(
|
||||
});
|
||||
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/extractArchive",
|
||||
"/extractArchive",
|
||||
{ sessionId, archivePath, extractPath, hostId, userId },
|
||||
);
|
||||
|
||||
@@ -863,7 +863,7 @@ export async function compressSSHFiles(
|
||||
});
|
||||
|
||||
const response = await getFileManagerApiForSession(sessionId).post(
|
||||
"/ssh/compressFiles",
|
||||
"/compressFiles",
|
||||
{
|
||||
sessionId,
|
||||
paths,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { pluginDir } from "./helpers";
|
||||
|
||||
function read(relative: string): string {
|
||||
return fs.readFileSync(path.join(pluginDir, relative), "utf8");
|
||||
}
|
||||
|
||||
/** Every "/path" the frontend's SSH client sends, without its parameters. */
|
||||
function frontendPaths(): string[] {
|
||||
const source = read("src/frontend/api/ssh-file-operations-api.ts");
|
||||
const paths = new Set<string>();
|
||||
for (const match of source.matchAll(/["`](\/[A-Za-z][\w-]*(?:\/[\w-]+)*)/g)) {
|
||||
paths.add(match[1]);
|
||||
}
|
||||
return [...paths];
|
||||
}
|
||||
|
||||
/** Every route the backend's express app registers. */
|
||||
function backendPaths(): Set<string> {
|
||||
const paths = new Set<string>();
|
||||
const dir = path.join(pluginDir, "src/backend");
|
||||
for (const file of fs
|
||||
.readdirSync(dir)
|
||||
.filter((name) => name.endsWith(".ts"))
|
||||
.map((name) => `src/backend/${name}`)) {
|
||||
for (const match of read(file).matchAll(
|
||||
/app\.(?:get|post|put|delete|patch)\(\s*"(\/[^"]*)"/g,
|
||||
)) {
|
||||
paths.add(match[1].replace(/\/:[\w]+$/, ""));
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
describe("file manager routes", () => {
|
||||
it("serves every path the frontend's SSH client calls", () => {
|
||||
const served = backendPaths();
|
||||
const missing = frontendPaths().filter((p) => !served.has(p));
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -50,7 +50,7 @@ describe("chunked SSH file uploads", () => {
|
||||
expect(fileManagerApiMock.post).toHaveBeenCalledTimes(193);
|
||||
expect(fileManagerApiMock.post).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"/ssh/uploadFileChunk",
|
||||
"/uploadFileChunk",
|
||||
expect.any(Blob),
|
||||
{
|
||||
params: {
|
||||
@@ -65,7 +65,7 @@ describe("chunked SSH file uploads", () => {
|
||||
},
|
||||
);
|
||||
expect(fileManagerApiMock.post).toHaveBeenLastCalledWith(
|
||||
"/ssh/uploadFileChunk",
|
||||
"/uploadFileChunk",
|
||||
expect.any(Blob),
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({ offset: 1.5 * 1024 * 1024 * 1024 }),
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
],
|
||||
"actionSlots": [
|
||||
{ "id": "dashboard.secondaryView", "accepts": ["component"] }
|
||||
]
|
||||
],
|
||||
"syncEntities": ["dashboardServiceLinks", "homepageItems"]
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ export async function activate(ctx: PluginContext) {
|
||||
registerHomepageRoutes(router, repo, ctx);
|
||||
registerOutboundRoutes(router, ctx);
|
||||
|
||||
ctx.sync.registerEntity({ type: "homepageItems", table: items, order: 80 });
|
||||
ctx.sync.registerEntity({
|
||||
type: "homepageItems",
|
||||
table: items,
|
||||
order: 80,
|
||||
// A tile inside a folder points at the folder's row.
|
||||
references: [
|
||||
{
|
||||
field: "folderId",
|
||||
syncField: "folderSyncId",
|
||||
entityType: "homepageItems",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.sync.registerEntity({
|
||||
type: "dashboardServiceLinks",
|
||||
table: serviceLinks,
|
||||
|
||||
@@ -219,7 +219,6 @@ export function registerHomepageRoutes(
|
||||
try {
|
||||
const deleted = await repo.deleteItem(userId, id);
|
||||
if (!deleted) return res.status(404).json({ error: "Not found" });
|
||||
await ctx.sync.recordTombstone(userId, "homepageItems", deleted.syncId);
|
||||
res.json({ message: "Homepage item deleted" });
|
||||
} catch (err) {
|
||||
logError(
|
||||
@@ -506,11 +505,6 @@ export function registerHomepageRoutes(
|
||||
try {
|
||||
const deleted = await repo.deleteServiceLink(userId, id);
|
||||
if (!deleted) return res.status(404).json({ error: "Not found" });
|
||||
await ctx.sync.recordTombstone(
|
||||
userId,
|
||||
"dashboardServiceLinks",
|
||||
deleted.syncId,
|
||||
);
|
||||
res.json({ message: "Service link deleted" });
|
||||
} catch (err) {
|
||||
logError(
|
||||
|
||||
@@ -161,7 +161,9 @@ export function ProcessInspectorCard({ hostId }: { hostId: number | null }) {
|
||||
<span
|
||||
className="truncate"
|
||||
title={`${p.user} · ${p.args}`}
|
||||
style={depth ? { paddingLeft: depth * 12 } : undefined}
|
||||
style={
|
||||
depth ? { paddingLeft: `${(depth * 12) / 14}rem` } : undefined
|
||||
}
|
||||
>
|
||||
{depth > 0 && (
|
||||
<span className="text-muted-foreground/40">└ </span>
|
||||
|
||||
@@ -45,5 +45,6 @@
|
||||
}
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"desktop": "server"
|
||||
}
|
||||
@@ -45,7 +45,8 @@
|
||||
"descriptionKey": "permissions.use.description",
|
||||
"defaultRoles": ["admin", "user"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"syncEntities": ["networkTopology"]
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
Input,
|
||||
PasswordInput,
|
||||
isElectron,
|
||||
linkedServerUrl,
|
||||
resolveConnectionOrigin,
|
||||
useConnectionLog,
|
||||
type ConnectionOrigin,
|
||||
@@ -309,10 +310,7 @@ const GuacamoleAppInner = React.forwardRef<
|
||||
{ defaultRemote: true },
|
||||
);
|
||||
if (resolvedOrigin === "remote") {
|
||||
const remoteConfig = (await window.electronAPI?.invoke?.(
|
||||
"get-remote-sync-config",
|
||||
)) as { serverUrl?: string } | null;
|
||||
if (!remoteConfig?.serverUrl) {
|
||||
if (!(await linkedServerUrl())) {
|
||||
throw new Error(t("errors.remoteServerRequired"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,5 +34,6 @@
|
||||
"icon": "Usb"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"desktop": "local"
|
||||
}
|
||||
@@ -81,6 +81,7 @@
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"syncEntities": ["snippetFolders", "snippets"]
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,8 @@ export async function activate(ctx: PluginContext) {
|
||||
type: "snippets",
|
||||
table: snippetsTable,
|
||||
order: 60,
|
||||
// Host quick actions and keybindings in core point at snippets by this.
|
||||
answersTo: ["commandSnippet"],
|
||||
});
|
||||
|
||||
// The user row survives a password-reset data wipe, so it never triggers
|
||||
@@ -152,9 +154,6 @@ export async function activate(ctx: PluginContext) {
|
||||
if (!userId) throw new Error("snippets.access.remove needs an actor");
|
||||
await requirePermission("delete");
|
||||
const existing = await repo.deleteSnippet(userId, id);
|
||||
if (existing?.syncId) {
|
||||
await ctx.sync.recordTombstone(userId, "snippets", existing.syncId);
|
||||
}
|
||||
return !!existing;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -319,12 +319,7 @@ export function registerSnippetRoutes(
|
||||
|
||||
try {
|
||||
const folderName = decodeURIComponent(name);
|
||||
const deleted = await repo.deleteFolder(userId, folderName);
|
||||
await ctx.sync.recordTombstone(
|
||||
userId,
|
||||
"snippetFolders",
|
||||
deleted?.syncId,
|
||||
);
|
||||
await repo.deleteFolder(userId, folderName);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
logError(
|
||||
@@ -921,7 +916,6 @@ export function registerSnippetRoutes(
|
||||
const existing = await repo.deleteSnippet(userId, parseInt(id, 10));
|
||||
if (!existing)
|
||||
return res.status(404).json({ error: "Snippet not found" });
|
||||
await ctx.sync.recordTombstone(userId, "snippets", existing.syncId);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
logError(ctx, "Failed to delete snippet", err, "snippet_delete_failed");
|
||||
|
||||
@@ -64,5 +64,6 @@
|
||||
}
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"desktop": "server"
|
||||
}
|
||||
@@ -12,6 +12,12 @@ import locales from "../../locales/en.json";
|
||||
|
||||
const manifest = manifestJson as unknown as PluginManifest;
|
||||
|
||||
const linked = vi.hoisted(() => ({ serverUrl: null as string | null }));
|
||||
vi.mock("@/lib/linked-server", () => ({
|
||||
getLinkedSession: async () =>
|
||||
linked.serverUrl ? { serverUrl: linked.serverUrl, token: "t" } : null,
|
||||
}));
|
||||
|
||||
let rendered: RenderedPluginApp | null = null;
|
||||
|
||||
type ElectronWindow = { electronAPI?: unknown };
|
||||
@@ -95,10 +101,10 @@ describe(`${manifest.id} activate`, () => {
|
||||
});
|
||||
|
||||
it("hides the rail item in a standalone desktop app", async () => {
|
||||
linked.serverUrl = null;
|
||||
(window as unknown as ElectronWindow).electronAPI = {
|
||||
isElectron: true,
|
||||
invoke: vi.fn(async () => null),
|
||||
onRemoteSyncStatusChanged: () => () => {},
|
||||
};
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
await waitFor(() =>
|
||||
@@ -108,24 +114,19 @@ describe(`${manifest.id} activate`, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the rail item once the desktop app has a remote server", async () => {
|
||||
let notify = () => {};
|
||||
let serverUrl: string | null = null;
|
||||
it("shows the rail item once the desktop app is linked to a server", async () => {
|
||||
linked.serverUrl = null;
|
||||
(window as unknown as ElectronWindow).electronAPI = {
|
||||
isElectron: true,
|
||||
invoke: vi.fn(async () => (serverUrl ? { serverUrl } : null)),
|
||||
onRemoteSyncStatusChanged: (callback: () => void) => {
|
||||
notify = callback;
|
||||
return () => {};
|
||||
},
|
||||
invoke: vi.fn(async () => null),
|
||||
};
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
await waitFor(() =>
|
||||
expect(rendered!.registered.railItems()[0]?.hidden).toBe(true),
|
||||
);
|
||||
|
||||
serverUrl = "https://termix.example";
|
||||
notify();
|
||||
linked.serverUrl = "https://termix.example";
|
||||
window.dispatchEvent(new CustomEvent("termix:sync-changed"));
|
||||
await waitFor(() =>
|
||||
expect(rendered!.registered.railItems()).toEqual([
|
||||
expect.objectContaining({ id: "termix-id", hidden: false }),
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
}
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"desktop": "server"
|
||||
}
|
||||
@@ -74,7 +74,8 @@
|
||||
"to": "/oidc/callback"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"syncEntities": ["vaultProfiles"]
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
|
||||
@@ -134,7 +134,6 @@ export function createProfileStore(
|
||||
await db.delete(tokensTable).where(eq(tokensTable.profileId, row.id));
|
||||
await db.delete(table).where(eq(table.id, row.id));
|
||||
await ctx.db.persist();
|
||||
await ctx.sync.recordTombstone(row.userId, SYNC_ENTITY, row.syncId);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -124,7 +124,7 @@ describe("profiles", () => {
|
||||
expect((await server.request("GET", "/profiles")).body).toEqual([]);
|
||||
});
|
||||
|
||||
it("records a tombstone and drops cached certificates on delete", async () => {
|
||||
it("drops cached certificates on delete", async () => {
|
||||
server = await startServer();
|
||||
const { id } = await profileOnHost();
|
||||
server.db.sqlite
|
||||
@@ -132,17 +132,9 @@ describe("profiles", () => {
|
||||
"INSERT INTO p_vault_tokens (user_id, profile_id, ssh_cert, private_key, expires_at) VALUES (?, ?, ?, ?, ?)",
|
||||
)
|
||||
.run("user-1", id, "c", "k", "2999-01-01T00:00:00.000Z");
|
||||
const syncId = (
|
||||
server.db.sqlite
|
||||
.prepare("SELECT sync_id FROM p_vault_profiles WHERE id = ?")
|
||||
.get(id) as { sync_id: string }
|
||||
).sync_id;
|
||||
|
||||
await server.request("DELETE", `/profiles/${id}`);
|
||||
|
||||
expect(server.mock.tombstones).toEqual([
|
||||
{ userId: "user-1", entityType: "vaultProfiles", syncId },
|
||||
]);
|
||||
expect(
|
||||
server.db.sqlite
|
||||
.prepare("SELECT COUNT(*) AS n FROM p_vault_tokens")
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
}
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"desktop": "server"
|
||||
}
|
||||
@@ -39,6 +39,7 @@
|
||||
"descriptionKey": "permissions.use.description",
|
||||
"defaultRoles": ["admin", "user"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"syncEntities": ["workspaces"]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user