feat: collaboration rooms with switchable presenter (#1328)

* feat: add collaboration rooms with switchable presenter

Rooms are a group of members watching one stage - the live SSH/RDP/VNC
session the current presenter shares. Any member can take over the
stage; the host can invite, force-stop and end the meeting. Stages
reuse session_shares (new room share type), so gating, recording,
expiry and the global sharing toggle all apply unchanged.

* feat: add stage control handoff to collaboration rooms

The presenter or host can grant any member write access to the live
stage and take it back; members can raise a hand to ask. SSH flips the
participant's permission on the live gate; RDP/VNC re-mint the viewer's
join token. Control clears on every stage switch.

* feat: guest links, role invites and invite awareness for collab rooms

- Anonymous guest link per room (host toggles/rotates), followed by
  polling the public resolve endpoint; SSH guests join over the terminal
  WS with roomGuestToken, guac guests get read-only join tokens
- Invite by role (expands to current members, snapshot semantics)
- Toast when a room you were invited to appears
- Stale stages are cleared lazily when the presenter is gone
- Telnet presenting, expired-tab fallback, documented single-instance
  and guac-kick limits
- Tests for the collab routes, room hub, share access and control flip

* fix: keep remote desktop collaboration read-only
This commit is contained in:
ZacharyZcR
2026-08-25 00:56:04 +08:00
committed by GitHub
parent d35458f78b
commit 81d79cc89b
50 changed files with 56698 additions and 81 deletions
+34
View File
@@ -0,0 +1,34 @@
CREATE TABLE `collab_room_members` (
`id` int AUTO_INCREMENT NOT NULL,
`room_id` varchar(255) NOT NULL,
`user_id` varchar(255) NOT NULL,
`room_role` text NOT NULL DEFAULT ('member'),
`added_by` varchar(255),
`created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP),
CONSTRAINT `collab_room_members_id` PRIMARY KEY(`id`),
CONSTRAINT `idx_collab_room_members_room_user` UNIQUE(`room_id`,`user_id`)
);
--> statement-breakpoint
CREATE TABLE `collab_rooms` (
`id` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`owner_user_id` varchar(255) NOT NULL,
`persistent` boolean NOT NULL DEFAULT false,
`presenter_user_id` varchar(255),
`stage_protocol` text,
`stage_host_id` int,
`stage_share_id` varchar(255),
`created_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP),
`ended_at` text,
CONSTRAINT `collab_rooms_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
ALTER TABLE `collab_room_members` ADD CONSTRAINT `collab_room_members_room_id_collab_rooms_id_fk` FOREIGN KEY (`room_id`) REFERENCES `collab_rooms`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `collab_room_members` ADD CONSTRAINT `collab_room_members_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `collab_room_members` ADD CONSTRAINT `collab_room_members_added_by_users_id_fk` FOREIGN KEY (`added_by`) REFERENCES `users`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `collab_rooms` ADD CONSTRAINT `collab_rooms_owner_user_id_users_id_fk` FOREIGN KEY (`owner_user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `collab_rooms` ADD CONSTRAINT `collab_rooms_presenter_user_id_users_id_fk` FOREIGN KEY (`presenter_user_id`) REFERENCES `users`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `collab_rooms` ADD CONSTRAINT `collab_rooms_stage_host_id_ssh_data_id_fk` FOREIGN KEY (`stage_host_id`) REFERENCES `ssh_data`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `collab_rooms` ADD CONSTRAINT `collab_rooms_stage_share_id_session_shares_id_fk` FOREIGN KEY (`stage_share_id`) REFERENCES `session_shares`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE INDEX `idx_collab_room_members_user` ON `collab_room_members` (`user_id`);--> statement-breakpoint
CREATE INDEX `idx_collab_rooms_owner` ON `collab_rooms` (`owner_user_id`);
+2
View File
@@ -0,0 +1,2 @@
ALTER TABLE `collab_rooms` ADD `guest_link_token` varchar(255);--> statement-breakpoint
ALTER TABLE `collab_rooms` ADD CONSTRAINT `idx_collab_rooms_guest_token` UNIQUE(`guest_link_token`);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14
View File
@@ -106,6 +106,20 @@
"when": 1786757023790,
"tag": "0014_bitter_nextwave",
"breakpoints": true
},
{
"idx": 15,
"version": "5",
"when": 1787576997080,
"tag": "0015_red_cobalt_man",
"breakpoints": true
},
{
"idx": 16,
"version": "5",
"when": 1787581983770,
"tag": "0016_unusual_tyrannus",
"breakpoints": true
}
]
}
+32
View File
@@ -0,0 +1,32 @@
CREATE TABLE "collab_room_members" (
"id" serial PRIMARY KEY NOT NULL,
"room_id" varchar(255) NOT NULL,
"user_id" varchar(255) NOT NULL,
"room_role" text DEFAULT 'member' NOT NULL,
"added_by" varchar(255),
"created_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL
);
--> statement-breakpoint
CREATE TABLE "collab_rooms" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"owner_user_id" varchar(255) NOT NULL,
"persistent" boolean DEFAULT false NOT NULL,
"presenter_user_id" varchar(255),
"stage_protocol" text,
"stage_host_id" integer,
"stage_share_id" varchar(255),
"created_at" varchar(255) DEFAULT CURRENT_TIMESTAMP NOT NULL,
"ended_at" text
);
--> statement-breakpoint
ALTER TABLE "collab_room_members" ADD CONSTRAINT "collab_room_members_room_id_collab_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."collab_rooms"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "collab_room_members" ADD CONSTRAINT "collab_room_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "collab_room_members" ADD CONSTRAINT "collab_room_members_added_by_users_id_fk" FOREIGN KEY ("added_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "collab_rooms" ADD CONSTRAINT "collab_rooms_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "collab_rooms" ADD CONSTRAINT "collab_rooms_presenter_user_id_users_id_fk" FOREIGN KEY ("presenter_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "collab_rooms" ADD CONSTRAINT "collab_rooms_stage_host_id_ssh_data_id_fk" FOREIGN KEY ("stage_host_id") REFERENCES "public"."ssh_data"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "collab_rooms" ADD CONSTRAINT "collab_rooms_stage_share_id_session_shares_id_fk" FOREIGN KEY ("stage_share_id") REFERENCES "public"."session_shares"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "idx_collab_room_members_room_user" ON "collab_room_members" USING btree ("room_id","user_id");--> statement-breakpoint
CREATE INDEX "idx_collab_room_members_user" ON "collab_room_members" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_collab_rooms_owner" ON "collab_rooms" USING btree ("owner_user_id");
@@ -0,0 +1,2 @@
ALTER TABLE "collab_rooms" ADD COLUMN "guest_link_token" varchar(255);--> statement-breakpoint
CREATE UNIQUE INDEX "idx_collab_rooms_guest_token" ON "collab_rooms" USING btree ("guest_link_token");
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14
View File
@@ -106,6 +106,20 @@
"when": 1786757021444,
"tag": "0014_unusual_maelstrom",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1787576994553,
"tag": "0015_early_spitfire",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1787581981251,
"tag": "0016_slippery_anita_blake",
"breakpoints": true
}
]
}
@@ -0,0 +1,32 @@
CREATE TABLE `collab_room_members` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`room_id` text NOT NULL,
`user_id` text NOT NULL,
`room_role` text DEFAULT 'member' NOT NULL,
`added_by` text,
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (`room_id`) REFERENCES `collab_rooms`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`added_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE UNIQUE INDEX `idx_collab_room_members_room_user` ON `collab_room_members` (`room_id`,`user_id`);--> statement-breakpoint
CREATE INDEX `idx_collab_room_members_user` ON `collab_room_members` (`user_id`);--> statement-breakpoint
CREATE TABLE `collab_rooms` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`owner_user_id` text NOT NULL,
`persistent` integer DEFAULT false NOT NULL,
`presenter_user_id` text,
`stage_protocol` text,
`stage_host_id` integer,
`stage_share_id` text,
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
`ended_at` text,
FOREIGN KEY (`owner_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`presenter_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`stage_host_id`) REFERENCES `ssh_data`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`stage_share_id`) REFERENCES `session_shares`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE INDEX `idx_collab_rooms_owner` ON `collab_rooms` (`owner_user_id`);
+2
View File
@@ -0,0 +1,2 @@
ALTER TABLE `collab_rooms` ADD `guest_link_token` text;--> statement-breakpoint
CREATE UNIQUE INDEX `idx_collab_rooms_guest_token` ON `collab_rooms` (`guest_link_token`);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14
View File
@@ -78,6 +78,20 @@
"when": 1786757019248,
"tag": "0010_mean_queen_noir",
"breakpoints": true
},
{
"idx": 11,
"version": "6",
"when": 1787576992255,
"tag": "0011_wakeful_titanium_man",
"breakpoints": true
},
{
"idx": 12,
"version": "6",
"when": 1787581978969,
"tag": "0012_yellow_firedrake",
"breakpoints": true
}
]
}