fix(server-mode): close the gaps the review of workspace sharing found
- A Terminal tab a collaborator opens records them (dashboard_runs.started_by): its pane token acts as them — and only while still shared with them — never as the workspace owner (userFromPaneToken). - POST /api/run always starts the lane's AI tool: a collaborator gets ECOLLABSTART whatever the body says (shell: true no longer slips past). - A collaborator disabled since no longer locks the share list: kept on it (shown disabled) or taken off; only a new addition must be enabled. - The workspace home tells a collaborator to open an existing session and keeps Start disabled instead of failing on every submit.
This commit is contained in:
+14
-5
@@ -1416,11 +1416,12 @@ function laneShares(laneId) {
|
||||
return require("../db")
|
||||
.db.prepare(
|
||||
`SELECT u.id, u.username, COALESCE(NULLIF(u.display_name, ''), u.username) AS name,
|
||||
s.shared_at AS sharedAt
|
||||
s.shared_at AS sharedAt, u.disabled
|
||||
FROM lane_shares s JOIN users u ON u.id = s.user_id
|
||||
WHERE s.lane_id = ? ORDER BY name COLLATE NOCASE`
|
||||
)
|
||||
.all(Number(laneId));
|
||||
.all(Number(laneId))
|
||||
.map((u) => ({ ...u, disabled: Boolean(u.disabled) }));
|
||||
}
|
||||
|
||||
router.get("/:id/shares", (req, res) => {
|
||||
@@ -1455,10 +1456,13 @@ router.put("/:id/shares", sameOriginGuard, (req, res) => {
|
||||
}
|
||||
const { db } = require("../db");
|
||||
const wanted = [...new Set(raw)];
|
||||
const before = new Set(laneShares(lane.id).map((u) => u.id));
|
||||
const user = db.prepare("SELECT id, disabled FROM users WHERE id = ?");
|
||||
for (const id of wanted) {
|
||||
const u = user.get(id);
|
||||
if (!u || u.disabled) {
|
||||
// A user already on the list may since have been disabled: keeping them
|
||||
// must not block every other change (it grants nothing while disabled).
|
||||
if (!u || (u.disabled && !before.has(id))) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: { code: "EBADUSER", message: `no enabled user ${id}` } });
|
||||
@@ -1469,7 +1473,6 @@ router.put("/:id/shares", sameOriginGuard, (req, res) => {
|
||||
.json({ error: { code: "EBADUSER", message: "a workspace is not shared with its owner" } });
|
||||
}
|
||||
}
|
||||
const before = new Set(laneShares(lane.id).map((u) => u.id));
|
||||
const added = wanted.filter((id) => !before.has(id));
|
||||
const removed = [...before].filter((id) => !wanted.includes(id));
|
||||
const actor = req.user?.id ?? null;
|
||||
@@ -3497,7 +3500,13 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
if (picked === null) return { allBusy: true };
|
||||
slot = picked;
|
||||
if (isLive(current.id, slot)) return { conflict: true };
|
||||
startedRunId = runs.spawnShell({ laneId: current.id, slot, cwd: current.cwd }).id;
|
||||
startedRunId = runs.spawnShell({
|
||||
laneId: current.id,
|
||||
slot,
|
||||
cwd: current.cwd,
|
||||
// A user it is shared with: their pane token acts as them.
|
||||
startedBy: req.user?.id ?? null,
|
||||
}).id;
|
||||
return {};
|
||||
});
|
||||
if (shellOutcome.missing) {
|
||||
|
||||
@@ -618,8 +618,9 @@ router.post("/", async (req, res) => {
|
||||
if (!Number.isInteger(laneId)) {
|
||||
return res.status(400).json({ error: { code: "EBADLANE", message: "laneId is required" } });
|
||||
}
|
||||
// A workspace shared with this user: no AI session of theirs yet (sharing phase 1).
|
||||
if (lanesLib.getLane(laneId) && !scope.isLaneOwner(req, laneId) && body.shell !== true) {
|
||||
// A workspace shared with this user: no AI session of theirs yet (sharing
|
||||
// phase 1). This route always starts the lane's AI tool — no shell here.
|
||||
if (lanesLib.getLane(laneId) && !scope.isLaneOwner(req, laneId)) {
|
||||
return require("./lanes").collabStart(res);
|
||||
}
|
||||
let cwd;
|
||||
|
||||
Reference in New Issue
Block a user