mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
fix(session-sharing): use the linked server for desktop meetings and links (#1507)
This commit is contained in:
1 parent
9801cc7fc7
commit
ec7c2c2835
15 files changed
+621
-210
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import http from "node:http";
|
||||
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import express, { type Router } from "express";
|
||||
@@ -85,7 +86,20 @@ export async function startServer(
|
||||
settings?: Record<string, unknown>;
|
||||
} = {},
|
||||
): Promise<TestServer> {
|
||||
const db = await createTestDb(pluginDir);
|
||||
const db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => {
|
||||
sqlite.exec("ALTER TABLE ssh_data ADD COLUMN sync_id TEXT");
|
||||
},
|
||||
});
|
||||
const refs = await db.database.refs();
|
||||
db.database.refs = async () =>
|
||||
({
|
||||
...refs,
|
||||
hosts: sqliteTable("ssh_data", {
|
||||
id: integer("id").primaryKey(),
|
||||
syncId: text("sync_id"),
|
||||
}),
|
||||
}) as Awaited<ReturnType<typeof db.database.refs>>;
|
||||
for (const user of options.users ?? ["alice", "bob", "carol"]) {
|
||||
db.sqlite
|
||||
.prepare("INSERT INTO users (id, username, is_admin) VALUES (?, ?, ?)")
|
||||
|
||||
@@ -28,6 +28,28 @@ async function present(s: TestServer, roomId: string, user = "alice") {
|
||||
}
|
||||
|
||||
describe("collab rooms", () => {
|
||||
it("resolves a synced host to the server ID only with connect access", async () => {
|
||||
server = await startServer({ hostIds: [41, 42], accessibleHostIds: [42] });
|
||||
server.db.sqlite.exec("UPDATE ssh_data SET sync_id = 'shared-host'");
|
||||
expect(await server.request("GET", "/meeting-host/shared-host")).toEqual({
|
||||
status: 200,
|
||||
body: { id: 42 },
|
||||
});
|
||||
server.db.sqlite.exec(
|
||||
"UPDATE ssh_data SET sync_id = 'private-host' WHERE id = 41",
|
||||
);
|
||||
expect(
|
||||
(await server.request("GET", "/meeting-host/private-host")).status,
|
||||
).toBe(404);
|
||||
expect(
|
||||
(await server.request("GET", "/meeting-host/missing-host")).status,
|
||||
).toBe(404);
|
||||
expect(
|
||||
(await server.request("GET", "/meeting-host/shared-host", { user: null }))
|
||||
.status,
|
||||
).toBe(401);
|
||||
});
|
||||
|
||||
it("creates a room with the creator as host and hides the guest token", async () => {
|
||||
server = await startServer();
|
||||
const res = await server.request("POST", "/rooms", {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginApiClient, TermixApp } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
connectMeetings,
|
||||
meetingGuestUrl,
|
||||
} from "../../src/frontend/meeting-backend";
|
||||
|
||||
function client(): PluginApiClient {
|
||||
return Object.fromEntries(
|
||||
["get", "post", "put", "patch", "delete"].map((method) => [
|
||||
method,
|
||||
vi.fn(async () => ({ data: { rooms: [] } })),
|
||||
]),
|
||||
) as unknown as PluginApiClient;
|
||||
}
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("meeting backend", () => {
|
||||
it("routes rooms, invites and host resolution to the linked server and refuses a later switch", async () => {
|
||||
const local = client();
|
||||
const remote = client();
|
||||
let url: string | null = "https://termix.example/base/";
|
||||
const app = {
|
||||
desktop: { available: true, remoteServerUrl: async () => url },
|
||||
apiFor: (origin: string) => (origin === "remote" ? remote : local),
|
||||
t: (key: string) => key,
|
||||
} as unknown as TermixApp;
|
||||
const backend = await connectMeetings(app);
|
||||
expect(backend.origin).toBe("remote");
|
||||
await backend.api.listCollabRooms();
|
||||
await backend.api.createCollabRoom("Standup", true);
|
||||
await backend.api.getDirectory();
|
||||
await backend.api.resolveHost("stable-host-id");
|
||||
await backend.api.presentCollabStage("room", {
|
||||
hostId: 99,
|
||||
sessionId: "remote-session",
|
||||
protocol: "ssh",
|
||||
});
|
||||
expect(local.get).not.toHaveBeenCalled();
|
||||
expect(local.post).not.toHaveBeenCalled();
|
||||
expect(remote.get).toHaveBeenCalledWith("/meeting-host/stable-host-id");
|
||||
expect(remote.post).toHaveBeenCalledWith("/rooms/room/present", {
|
||||
hostId: 99,
|
||||
sessionId: "remote-session",
|
||||
protocol: "ssh",
|
||||
});
|
||||
expect(meetingGuestUrl(backend.publicUrl, "a+b&c")).toBe(
|
||||
"https://termix.example/base/?view=collab-guest&token=a%2Bb%26c",
|
||||
);
|
||||
url = "https://other.example";
|
||||
await expect(backend.api.deleteCollabRoom("room")).rejects.toThrow(
|
||||
"collab.serverChanged",
|
||||
);
|
||||
expect(remote.delete).not.toHaveBeenCalled();
|
||||
url = null;
|
||||
await expect(backend.api.getCollabRoom("room")).rejects.toThrow(
|
||||
"collab.serverChanged",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps standalone rooms local without generating a file URL", async () => {
|
||||
const local = client();
|
||||
const backend = await connectMeetings({
|
||||
desktop: { available: true, remoteServerUrl: async () => null },
|
||||
apiFor: () => local,
|
||||
t: (key: string) => key,
|
||||
} as unknown as TermixApp);
|
||||
expect(backend.origin).toBe("local");
|
||||
await backend.api.listCollabRooms();
|
||||
expect(local.get).toHaveBeenCalledWith("/rooms");
|
||||
expect(meetingGuestUrl(backend.publicUrl, "token")).toBeNull();
|
||||
expect(meetingGuestUrl("file:///app/index.html", "token")).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves web deployment paths while replacing old query and fragment values", () => {
|
||||
expect(
|
||||
meetingGuestUrl("https://termix.example/app/?view=other#old", "token"),
|
||||
).toBe("https://termix.example/app/?view=collab-guest&token=token");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { pluginWsUrl } = vi.hoisted(() => ({ pluginWsUrl: vi.fn() }));
|
||||
vi.mock("@termix/plugin-sdk/ui", () => ({ pluginWsUrl }));
|
||||
import { wsTargetForPath } from "../../src/frontend/shared";
|
||||
|
||||
describe("meeting event transport", () => {
|
||||
it("preserves remote authentication protocols and event query parameters", async () => {
|
||||
pluginWsUrl.mockResolvedValue({
|
||||
url: "wss://server/base/plugin-ws/ssh-terminal/terminal",
|
||||
protocols: ["jwt", "session-token"],
|
||||
});
|
||||
expect(
|
||||
await wsTargetForPath(
|
||||
"/plugin-ws/ssh-terminal/terminal?room=abc",
|
||||
"remote",
|
||||
),
|
||||
).toEqual({
|
||||
url: "wss://server/base/plugin-ws/ssh-terminal/terminal?room=abc",
|
||||
protocols: ["jwt", "session-token"],
|
||||
});
|
||||
expect(pluginWsUrl).toHaveBeenCalledWith("ssh-terminal", "/terminal", {
|
||||
origin: "remote",
|
||||
});
|
||||
expect(
|
||||
await wsTargetForPath("https://another-server/ws", "remote"),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user