mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
fix: harden collaboration room access (#1332)
* fix: harden collaboration room access * fix: confirm guest link lifecycle changes
This commit is contained in:
@@ -72,6 +72,7 @@ vi.mock("../../../hosts/terminal/session-manager.js", () => ({
|
||||
setRoomShareControl: (...args: unknown[]) => {
|
||||
state.control.push(args);
|
||||
},
|
||||
disconnectShareParticipants: vi.fn(() => 0),
|
||||
},
|
||||
}));
|
||||
vi.mock("../../../hosts/session-sharing/live-sessions.js", () => ({
|
||||
@@ -147,6 +148,16 @@ vi.mock("../../../database/repositories/factory.js", () => ({
|
||||
updateStage: async (roomId: string, stage: Partial<Room>) => {
|
||||
Object.assign(state.rooms.get(roomId)!, stage);
|
||||
},
|
||||
replaceStage: async (
|
||||
roomId: string,
|
||||
expectedShareId: string | null,
|
||||
stage: Partial<Room>,
|
||||
) => {
|
||||
const room = state.rooms.get(roomId)!;
|
||||
if (room.stageShareId !== expectedShareId) return false;
|
||||
Object.assign(room, stage);
|
||||
return true;
|
||||
},
|
||||
clearStage: async (roomId: string) => {
|
||||
Object.assign(state.rooms.get(roomId)!, {
|
||||
presenterUserId: null,
|
||||
@@ -307,6 +318,26 @@ describe("collab room routes", () => {
|
||||
expect((other as { statusCode: number }).statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it("never exposes the guest bearer token in room responses", async () => {
|
||||
const roomId = await createRoom();
|
||||
await invite(roomId, ["alice"]);
|
||||
state.rooms.get(roomId)!.guestLinkToken = "secret-token";
|
||||
|
||||
const list = await as("alice", () => invoke("get", "/rooms"));
|
||||
const listedRoom = (list as Awaited<ReturnType<typeof invoke>>).jsonBody!
|
||||
.rooms as Array<Record<string, unknown>>;
|
||||
expect(listedRoom[0]).not.toHaveProperty("guestLinkToken");
|
||||
expect(listedRoom[0]).toHaveProperty("guestLinkEnabled", true);
|
||||
|
||||
const detail = await as("alice", () =>
|
||||
invoke("get", "/rooms/:id", { params: { id: roomId } }),
|
||||
);
|
||||
const room = (detail as Awaited<ReturnType<typeof invoke>>).jsonBody!
|
||||
.room as Record<string, unknown>;
|
||||
expect(room).not.toHaveProperty("guestLinkToken");
|
||||
expect(room).toHaveProperty("guestLinkEnabled", true);
|
||||
});
|
||||
|
||||
it("rejects an empty or oversized room name", async () => {
|
||||
expect(
|
||||
(await invoke("post", "/rooms", { body: { name: " " } })).statusCode,
|
||||
|
||||
@@ -57,6 +57,8 @@ function makeFakeWs(readyState = 1 /* OPEN */) {
|
||||
return {
|
||||
readyState,
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
} as unknown as import("ws").WebSocket;
|
||||
}
|
||||
const WS_OPEN = 1;
|
||||
@@ -254,6 +256,39 @@ describe("TerminalSessionManager - multiplayer participants", () => {
|
||||
sessionManager.destroySession(id);
|
||||
});
|
||||
|
||||
it("disconnectShareParticipants revokes only the selected share participants", () => {
|
||||
const id = createConnectedSession();
|
||||
const ownerWs = makeFakeWs();
|
||||
const aliceWs = makeFakeWs();
|
||||
const guestWs = makeFakeWs();
|
||||
sessionManager.attachWs(id, "owner-1", ownerWs);
|
||||
const session = sessionManager.joinAsParticipant(id, aliceWs, {
|
||||
userId: "alice",
|
||||
permissionLevel: "read-only",
|
||||
shareId: "stage-share",
|
||||
})!;
|
||||
sessionManager.joinAsParticipant(id, guestWs, {
|
||||
userId: null,
|
||||
permissionLevel: "read-only",
|
||||
shareId: "stage-share",
|
||||
});
|
||||
|
||||
expect(
|
||||
sessionManager.disconnectShareParticipants(id, "stage-share", {
|
||||
userId: "alice",
|
||||
reason: "Removed",
|
||||
}),
|
||||
).toBe(1);
|
||||
expect(sessionManager.getParticipantForWs(session, aliceWs)).toBeNull();
|
||||
expect(sessionManager.getParticipantForWs(session, guestWs)).not.toBeNull();
|
||||
expect(sessionManager.getParticipantForWs(session, ownerWs)?.isOwner).toBe(
|
||||
true,
|
||||
);
|
||||
expect(aliceWs.close).toHaveBeenCalledWith(1008, "Removed");
|
||||
|
||||
sessionManager.destroySession(id);
|
||||
});
|
||||
|
||||
it("joinAsParticipant returns null for a nonexistent or unconnected session", () => {
|
||||
expect(
|
||||
sessionManager.joinAsParticipant("does-not-exist", makeFakeWs(), {
|
||||
|
||||
Reference in New Issue
Block a user