clay-server 2.22.0-beta.2 → 2.22.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +1 -0
- package/lib/daemon.js +16 -29
- package/lib/project.js +9 -0
- package/lib/public/app.js +55 -30
- package/lib/public/css/icon-strip.css +107 -0
- package/lib/public/modules/sidebar.js +169 -0
- package/lib/server.js +31 -12
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1546,6 +1546,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1546
1546
|
builtinCert: hasBuiltinCert,
|
|
1547
1547
|
mkcertDetected: mkcertDetected,
|
|
1548
1548
|
debug: debugMode,
|
|
1549
|
+
headless: headlessMode,
|
|
1549
1550
|
keepAwake: keepAwake,
|
|
1550
1551
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1551
1552
|
osUsers: wantOsUsers || osUsersMode,
|
package/lib/daemon.js
CHANGED
|
@@ -98,14 +98,6 @@ if (config.tls) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
var caRoot = null;
|
|
101
|
-
try {
|
|
102
|
-
var { execSync } = require("child_process");
|
|
103
|
-
caRoot = path.join(
|
|
104
|
-
execSync("mkcert -CAROOT", { encoding: "utf8", stdio: "pipe" }).trim(),
|
|
105
|
-
"rootCA.pem"
|
|
106
|
-
);
|
|
107
|
-
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
108
|
-
} catch (e) {}
|
|
109
101
|
|
|
110
102
|
// --- Resolve LAN IP for share URL ---
|
|
111
103
|
var os2 = require("os");
|
|
@@ -160,11 +152,10 @@ var relay = createServer({
|
|
|
160
152
|
var slugs = config.projects.map(function (p) { return p.slug; });
|
|
161
153
|
var slug = generateSlug(absPath, slugs);
|
|
162
154
|
relay.addProject(absPath, slug);
|
|
163
|
-
var projectEntry = { path: absPath, slug: slug, addedAt: Date.now() };
|
|
164
|
-
//
|
|
165
|
-
if (wsUser && wsUser.id
|
|
155
|
+
var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
156
|
+
// The user who adds a project always becomes the owner
|
|
157
|
+
if (wsUser && wsUser.id) {
|
|
166
158
|
projectEntry.ownerId = wsUser.id;
|
|
167
|
-
projectEntry.visibility = "private";
|
|
168
159
|
}
|
|
169
160
|
config.projects.push(projectEntry);
|
|
170
161
|
// Remove from removedProjects if present
|
|
@@ -237,15 +228,10 @@ var relay = createServer({
|
|
|
237
228
|
try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (ce) {}
|
|
238
229
|
return { ok: false, error: "Failed to create project: " + e.message };
|
|
239
230
|
}
|
|
240
|
-
// Register project
|
|
241
|
-
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
|
|
231
|
+
// Register project - creator always becomes owner, default private
|
|
232
|
+
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
242
233
|
if (wsUser && wsUser.id) {
|
|
243
|
-
|
|
244
|
-
projectEntry.ownerId = wsUser.id;
|
|
245
|
-
}
|
|
246
|
-
if (wsUser.role !== "admin") {
|
|
247
|
-
projectEntry.visibility = "private";
|
|
248
|
-
}
|
|
234
|
+
projectEntry.ownerId = wsUser.id;
|
|
249
235
|
}
|
|
250
236
|
relay.addProject(targetDir, slug);
|
|
251
237
|
config.projects.push(projectEntry);
|
|
@@ -312,15 +298,11 @@ var relay = createServer({
|
|
|
312
298
|
execSync("chown -R " + wsUser.linuxUser + ":" + wsUser.linuxUser + " " + JSON.stringify(targetDir));
|
|
313
299
|
} catch (e) {}
|
|
314
300
|
}
|
|
315
|
-
// Register project
|
|
316
|
-
|
|
301
|
+
// Register project - creator always becomes owner
|
|
302
|
+
// Creator always becomes owner, default private
|
|
303
|
+
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
|
|
317
304
|
if (wsUser && wsUser.id) {
|
|
318
|
-
|
|
319
|
-
projectEntry.ownerId = wsUser.id;
|
|
320
|
-
}
|
|
321
|
-
if (wsUser.role !== "admin") {
|
|
322
|
-
projectEntry.visibility = "private";
|
|
323
|
-
}
|
|
305
|
+
projectEntry.ownerId = wsUser.id;
|
|
324
306
|
}
|
|
325
307
|
relay.addProject(targetDir, slug);
|
|
326
308
|
config.projects.push(projectEntry);
|
|
@@ -665,6 +647,7 @@ var relay = createServer({
|
|
|
665
647
|
port: config.port,
|
|
666
648
|
tls: !!tlsOptions,
|
|
667
649
|
debug: !!config.debug,
|
|
650
|
+
headless: !!config.headless,
|
|
668
651
|
keepAwake: !!config.keepAwake,
|
|
669
652
|
autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
|
|
670
653
|
pinEnabled: !!config.pinHash,
|
|
@@ -1041,7 +1024,7 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1041
1024
|
var slugs = config.projects.map(function (p) { return p.slug; });
|
|
1042
1025
|
var slug = generateSlug(absPath, slugs);
|
|
1043
1026
|
relay.addProject(absPath, slug);
|
|
1044
|
-
config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
|
|
1027
|
+
config.projects.push({ path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" });
|
|
1045
1028
|
saveConfig(config);
|
|
1046
1029
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
1047
1030
|
console.log("[daemon] Added project:", slug, "→", absPath);
|
|
@@ -1192,6 +1175,10 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1192
1175
|
return { ok: true };
|
|
1193
1176
|
|
|
1194
1177
|
case "update": {
|
|
1178
|
+
if (config.headless) {
|
|
1179
|
+
console.log("[daemon] Update & restart requested via IPC — blocked (headless mode)");
|
|
1180
|
+
return { ok: false, error: "Auto-update is disabled in headless mode" };
|
|
1181
|
+
}
|
|
1195
1182
|
console.log("[daemon] Update & restart requested via IPC");
|
|
1196
1183
|
|
|
1197
1184
|
// Dev mode (config.debug): just exit with code 120, cli.js dev watcher respawns daemon
|
package/lib/project.js
CHANGED
|
@@ -1171,6 +1171,15 @@ function createProjectContext(opts) {
|
|
|
1171
1171
|
setTimeout(function() { resumeLoop(); }, 500);
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
+
// Auto-assign owner if project has none and a user connects (e.g. IPC-added projects)
|
|
1175
|
+
if (!projectOwnerId && ws._clayUser && ws._clayUser.id && !isMate) {
|
|
1176
|
+
projectOwnerId = ws._clayUser.id;
|
|
1177
|
+
if (opts.onProjectOwnerChanged) {
|
|
1178
|
+
opts.onProjectOwnerChanged(slug, projectOwnerId);
|
|
1179
|
+
}
|
|
1180
|
+
console.log("[project] Auto-assigned owner for " + slug + ": " + projectOwnerId);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1174
1183
|
// Send cached state
|
|
1175
1184
|
var _userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1176
1185
|
var _filteredProjects = getProjectList(_userId);
|
package/lib/public/app.js
CHANGED
|
@@ -2212,6 +2212,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
|
|
|
2212
2212
|
sendBtn.disabled = false;
|
|
2213
2213
|
setSendBtnMode("send");
|
|
2214
2214
|
connectOverlay.classList.add("hidden");
|
|
2215
|
+
// Hide update banner on reconnect; server will re-send update_available if still needed
|
|
2216
|
+
var updPill = $("update-pill-wrap");
|
|
2217
|
+
if (updPill) updPill.classList.add("hidden");
|
|
2215
2218
|
stopVerbCycle();
|
|
2216
2219
|
} else if (status === "processing") {
|
|
2217
2220
|
if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
|
|
@@ -4024,6 +4027,48 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
|
|
|
4024
4027
|
if (savedMainWs === this) return;
|
|
4025
4028
|
};
|
|
4026
4029
|
|
|
4030
|
+
function showUpdateAvailable(msg) {
|
|
4031
|
+
var updatePillWrap = $("update-pill-wrap");
|
|
4032
|
+
var updateVersion = $("update-version");
|
|
4033
|
+
if (updatePillWrap && updateVersion && msg.version) {
|
|
4034
|
+
updateVersion.textContent = "v" + msg.version;
|
|
4035
|
+
updatePillWrap.classList.remove("hidden");
|
|
4036
|
+
var updPill = $("update-pill");
|
|
4037
|
+
var updResetBtn = $("update-now");
|
|
4038
|
+
if (isHeadlessMode) {
|
|
4039
|
+
// In headless mode, hide auto-update button and show manual guide only
|
|
4040
|
+
if (updPill) updPill.innerHTML = '<i data-lucide="arrow-up-circle"></i> <span id="update-version">v' + msg.version + '</span> available. Update manually';
|
|
4041
|
+
if (updResetBtn) updResetBtn.style.display = "none";
|
|
4042
|
+
} else {
|
|
4043
|
+
// Reset button state (may be stuck on "Updating..." after restart)
|
|
4044
|
+
if (updResetBtn) {
|
|
4045
|
+
updResetBtn.innerHTML = '<i data-lucide="download"></i> Update now';
|
|
4046
|
+
updResetBtn.disabled = false;
|
|
4047
|
+
updResetBtn.style.display = "";
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
// Update manual command based on version (beta vs stable)
|
|
4051
|
+
var updManualCmd = $("update-manual-cmd");
|
|
4052
|
+
if (updManualCmd) {
|
|
4053
|
+
var updTag = msg.version.indexOf("-beta") !== -1 ? "beta" : "latest";
|
|
4054
|
+
updManualCmd.textContent = "npx clay-server@" + updTag;
|
|
4055
|
+
}
|
|
4056
|
+
refreshIcons();
|
|
4057
|
+
}
|
|
4058
|
+
// Update the settings check-for-updates button
|
|
4059
|
+
var settingsUpdBtn = $("settings-update-check");
|
|
4060
|
+
if (settingsUpdBtn && msg.version) {
|
|
4061
|
+
settingsUpdBtn.innerHTML = "";
|
|
4062
|
+
var ic = document.createElement("i");
|
|
4063
|
+
ic.setAttribute("data-lucide", "arrow-up-circle");
|
|
4064
|
+
settingsUpdBtn.appendChild(ic);
|
|
4065
|
+
settingsUpdBtn.appendChild(document.createTextNode(" Update available (v" + msg.version + ")"));
|
|
4066
|
+
settingsUpdBtn.classList.add("settings-btn-update-available");
|
|
4067
|
+
settingsUpdBtn.disabled = false;
|
|
4068
|
+
refreshIcons();
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4027
4072
|
ws.onmessage = function (event) {
|
|
4028
4073
|
// If this WS is stashed while in mate DM, only allow skill_installed through
|
|
4029
4074
|
if (savedMainWs === this) {
|
|
@@ -4173,36 +4218,14 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
|
|
|
4173
4218
|
break;
|
|
4174
4219
|
|
|
4175
4220
|
case "update_available":
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
updResetBtn.innerHTML = '<i data-lucide="download"></i> Update now';
|
|
4185
|
-
updResetBtn.disabled = false;
|
|
4186
|
-
}
|
|
4187
|
-
// Update manual command based on version (beta vs stable)
|
|
4188
|
-
var updManualCmd = $("update-manual-cmd");
|
|
4189
|
-
if (updManualCmd) {
|
|
4190
|
-
var updTag = msg.version.indexOf("-beta") !== -1 ? "beta" : "latest";
|
|
4191
|
-
updManualCmd.textContent = "npx clay-server@" + updTag;
|
|
4192
|
-
}
|
|
4193
|
-
refreshIcons();
|
|
4194
|
-
}
|
|
4195
|
-
// Update the settings check-for-updates button
|
|
4196
|
-
var settingsUpdBtn = $("settings-update-check");
|
|
4197
|
-
if (settingsUpdBtn && msg.version) {
|
|
4198
|
-
settingsUpdBtn.innerHTML = "";
|
|
4199
|
-
var ic = document.createElement("i");
|
|
4200
|
-
ic.setAttribute("data-lucide", "arrow-up-circle");
|
|
4201
|
-
settingsUpdBtn.appendChild(ic);
|
|
4202
|
-
settingsUpdBtn.appendChild(document.createTextNode(" Update available (v" + msg.version + ")"));
|
|
4203
|
-
settingsUpdBtn.classList.add("settings-btn-update-available");
|
|
4204
|
-
settingsUpdBtn.disabled = false;
|
|
4205
|
-
refreshIcons();
|
|
4221
|
+
// In multi-user mode, only show update UI to admins
|
|
4222
|
+
if (isMultiUserMode) {
|
|
4223
|
+
checkAdminAccess().then(function (isAdmin) {
|
|
4224
|
+
if (!isAdmin) return;
|
|
4225
|
+
showUpdateAvailable(msg);
|
|
4226
|
+
});
|
|
4227
|
+
} else {
|
|
4228
|
+
showUpdateAvailable(msg);
|
|
4206
4229
|
}
|
|
4207
4230
|
break;
|
|
4208
4231
|
|
|
@@ -5147,6 +5170,7 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
|
|
|
5147
5170
|
break;
|
|
5148
5171
|
|
|
5149
5172
|
case "daemon_config":
|
|
5173
|
+
if (msg.config && msg.config.headless) isHeadlessMode = true;
|
|
5150
5174
|
updateDaemonConfig(msg.config);
|
|
5151
5175
|
break;
|
|
5152
5176
|
|
|
@@ -5634,6 +5658,7 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
|
|
|
5634
5658
|
|
|
5635
5659
|
// --- Admin (multi-user mode) ---
|
|
5636
5660
|
var isMultiUserMode = false;
|
|
5661
|
+
var isHeadlessMode = false;
|
|
5637
5662
|
var myUserId = null;
|
|
5638
5663
|
initAdmin({
|
|
5639
5664
|
get projectList() { return cachedProjects; },
|
|
@@ -498,6 +498,113 @@
|
|
|
498
498
|
.project-ctx-item.project-ctx-delete { color: var(--error); }
|
|
499
499
|
.project-ctx-item.project-ctx-delete:hover { background: var(--error-8); }
|
|
500
500
|
|
|
501
|
+
/* --- Project Access Popover --- */
|
|
502
|
+
.project-access-popover {
|
|
503
|
+
position: fixed;
|
|
504
|
+
background: var(--sidebar-bg);
|
|
505
|
+
border: 1px solid var(--border);
|
|
506
|
+
border-radius: 12px;
|
|
507
|
+
padding: 0;
|
|
508
|
+
width: 260px;
|
|
509
|
+
box-shadow: 0 8px 30px rgba(var(--shadow-rgb), 0.35);
|
|
510
|
+
z-index: 9999;
|
|
511
|
+
animation: ctxMenuAppear 0.12s ease-out;
|
|
512
|
+
overflow: hidden;
|
|
513
|
+
}
|
|
514
|
+
.project-access-header {
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: space-between;
|
|
518
|
+
padding: 12px 14px 8px;
|
|
519
|
+
border-bottom: 1px solid var(--border);
|
|
520
|
+
}
|
|
521
|
+
.project-access-title {
|
|
522
|
+
font-size: 13px;
|
|
523
|
+
font-weight: 600;
|
|
524
|
+
color: var(--text);
|
|
525
|
+
}
|
|
526
|
+
.project-access-close {
|
|
527
|
+
background: none;
|
|
528
|
+
border: none;
|
|
529
|
+
color: var(--text-secondary);
|
|
530
|
+
font-size: 18px;
|
|
531
|
+
cursor: pointer;
|
|
532
|
+
padding: 0 2px;
|
|
533
|
+
line-height: 1;
|
|
534
|
+
}
|
|
535
|
+
.project-access-close:hover { color: var(--text); }
|
|
536
|
+
.project-access-section {
|
|
537
|
+
padding: 10px 14px;
|
|
538
|
+
}
|
|
539
|
+
.project-access-label {
|
|
540
|
+
display: block;
|
|
541
|
+
font-size: 11px;
|
|
542
|
+
font-weight: 600;
|
|
543
|
+
color: var(--text-secondary);
|
|
544
|
+
text-transform: uppercase;
|
|
545
|
+
letter-spacing: 0.04em;
|
|
546
|
+
margin-bottom: 6px;
|
|
547
|
+
}
|
|
548
|
+
.project-access-vis-row {
|
|
549
|
+
display: flex;
|
|
550
|
+
gap: 6px;
|
|
551
|
+
}
|
|
552
|
+
.project-access-vis-btn {
|
|
553
|
+
flex: 1;
|
|
554
|
+
display: flex;
|
|
555
|
+
align-items: center;
|
|
556
|
+
justify-content: center;
|
|
557
|
+
gap: 6px;
|
|
558
|
+
padding: 7px 0;
|
|
559
|
+
font-size: 12px;
|
|
560
|
+
font-family: inherit;
|
|
561
|
+
color: var(--text-secondary);
|
|
562
|
+
background: rgba(var(--overlay-rgb), 0.04);
|
|
563
|
+
border: 1px solid var(--border);
|
|
564
|
+
border-radius: 8px;
|
|
565
|
+
cursor: pointer;
|
|
566
|
+
transition: all 0.15s;
|
|
567
|
+
}
|
|
568
|
+
.project-access-vis-btn .lucide { width: 14px; height: 14px; }
|
|
569
|
+
.project-access-vis-btn:hover { background: rgba(var(--overlay-rgb), 0.08); }
|
|
570
|
+
.project-access-vis-btn.active {
|
|
571
|
+
background: var(--accent-8, rgba(99, 102, 241, 0.08));
|
|
572
|
+
border-color: var(--accent, #6366f1);
|
|
573
|
+
color: var(--accent, #6366f1);
|
|
574
|
+
font-weight: 500;
|
|
575
|
+
}
|
|
576
|
+
.project-access-user-list {
|
|
577
|
+
max-height: 200px;
|
|
578
|
+
overflow-y: auto;
|
|
579
|
+
}
|
|
580
|
+
.project-access-user-item {
|
|
581
|
+
display: flex;
|
|
582
|
+
align-items: center;
|
|
583
|
+
gap: 8px;
|
|
584
|
+
padding: 5px 0;
|
|
585
|
+
font-size: 13px;
|
|
586
|
+
color: var(--text);
|
|
587
|
+
cursor: pointer;
|
|
588
|
+
}
|
|
589
|
+
.project-access-user-item input[type="checkbox"] {
|
|
590
|
+
accent-color: var(--accent, #6366f1);
|
|
591
|
+
width: 15px;
|
|
592
|
+
height: 15px;
|
|
593
|
+
cursor: pointer;
|
|
594
|
+
}
|
|
595
|
+
.project-access-user-item:hover { color: var(--text); }
|
|
596
|
+
.project-access-empty {
|
|
597
|
+
font-size: 12px;
|
|
598
|
+
color: var(--text-tertiary);
|
|
599
|
+
padding: 8px 0;
|
|
600
|
+
}
|
|
601
|
+
.project-access-loading {
|
|
602
|
+
padding: 20px 14px;
|
|
603
|
+
text-align: center;
|
|
604
|
+
font-size: 12px;
|
|
605
|
+
color: var(--text-secondary);
|
|
606
|
+
}
|
|
607
|
+
|
|
501
608
|
/* --- Emoji picker popover --- */
|
|
502
609
|
.emoji-picker {
|
|
503
610
|
position: fixed;
|
|
@@ -2672,6 +2672,158 @@ var EMOJI_CATEGORIES = [
|
|
|
2672
2672
|
]},
|
|
2673
2673
|
];
|
|
2674
2674
|
|
|
2675
|
+
// --- Project Access Popover ---
|
|
2676
|
+
var projectAccessPopover = null;
|
|
2677
|
+
|
|
2678
|
+
function closeProjectAccessPopover() {
|
|
2679
|
+
if (projectAccessPopover) {
|
|
2680
|
+
projectAccessPopover.remove();
|
|
2681
|
+
projectAccessPopover = null;
|
|
2682
|
+
document.removeEventListener("click", closeAccessOnOutside);
|
|
2683
|
+
document.removeEventListener("keydown", closeAccessOnEscape);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
function closeAccessOnOutside(e) {
|
|
2688
|
+
if (projectAccessPopover && !projectAccessPopover.contains(e.target)) closeProjectAccessPopover();
|
|
2689
|
+
}
|
|
2690
|
+
function closeAccessOnEscape(e) {
|
|
2691
|
+
if (e.key === "Escape") closeProjectAccessPopover();
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
function showProjectAccessPopover(anchorEl, slug) {
|
|
2695
|
+
closeProjectAccessPopover();
|
|
2696
|
+
|
|
2697
|
+
var popover = document.createElement("div");
|
|
2698
|
+
popover.className = "project-access-popover";
|
|
2699
|
+
popover.innerHTML = '<div class="project-access-loading">Loading...</div>';
|
|
2700
|
+
popover.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
2701
|
+
document.body.appendChild(popover);
|
|
2702
|
+
projectAccessPopover = popover;
|
|
2703
|
+
|
|
2704
|
+
// Position near anchor
|
|
2705
|
+
requestAnimationFrame(function () {
|
|
2706
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
2707
|
+
popover.style.position = "fixed";
|
|
2708
|
+
popover.style.left = (rect.right + 8) + "px";
|
|
2709
|
+
popover.style.top = rect.top + "px";
|
|
2710
|
+
popover.style.zIndex = "9999";
|
|
2711
|
+
var popRect = popover.getBoundingClientRect();
|
|
2712
|
+
if (popRect.right > window.innerWidth - 8) {
|
|
2713
|
+
popover.style.left = (rect.left - popRect.width - 8) + "px";
|
|
2714
|
+
}
|
|
2715
|
+
if (popRect.bottom > window.innerHeight - 8) {
|
|
2716
|
+
popover.style.top = (window.innerHeight - popRect.height - 8) + "px";
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
|
|
2720
|
+
setTimeout(function () {
|
|
2721
|
+
document.addEventListener("click", closeAccessOnOutside);
|
|
2722
|
+
document.addEventListener("keydown", closeAccessOnEscape);
|
|
2723
|
+
}, 0);
|
|
2724
|
+
|
|
2725
|
+
// Fetch access info and user list in parallel
|
|
2726
|
+
Promise.all([
|
|
2727
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/access").then(function (r) { return r.json(); }),
|
|
2728
|
+
fetch("/api/admin/users").then(function (r) { return r.json(); }),
|
|
2729
|
+
]).then(function (results) {
|
|
2730
|
+
var access = results[0];
|
|
2731
|
+
var usersData = results[1];
|
|
2732
|
+
if (access.error || usersData.error) {
|
|
2733
|
+
popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
renderAccessPopover(popover, slug, access, usersData.users || []);
|
|
2737
|
+
}).catch(function () {
|
|
2738
|
+
popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
function renderAccessPopover(popover, slug, access, allUsers) {
|
|
2743
|
+
var visibility = access.visibility || "public";
|
|
2744
|
+
var allowedUsers = access.allowedUsers || [];
|
|
2745
|
+
var ownerId = access.ownerId;
|
|
2746
|
+
|
|
2747
|
+
// Filter out the owner from the user list (owner always has access)
|
|
2748
|
+
var selectableUsers = allUsers.filter(function (u) { return u.id !== ownerId; });
|
|
2749
|
+
|
|
2750
|
+
var html = '';
|
|
2751
|
+
html += '<div class="project-access-header">';
|
|
2752
|
+
html += '<span class="project-access-title">Project Access</span>';
|
|
2753
|
+
html += '<button class="project-access-close">×</button>';
|
|
2754
|
+
html += '</div>';
|
|
2755
|
+
|
|
2756
|
+
// Visibility toggle
|
|
2757
|
+
html += '<div class="project-access-section">';
|
|
2758
|
+
html += '<label class="project-access-label">Visibility</label>';
|
|
2759
|
+
html += '<div class="project-access-vis-row">';
|
|
2760
|
+
html += '<button class="project-access-vis-btn' + (visibility === "private" ? ' active' : '') + '" data-vis="private">';
|
|
2761
|
+
html += iconHtml("lock") + ' Private';
|
|
2762
|
+
html += '</button>';
|
|
2763
|
+
html += '<button class="project-access-vis-btn' + (visibility === "public" ? ' active' : '') + '" data-vis="public">';
|
|
2764
|
+
html += iconHtml("globe") + ' Public';
|
|
2765
|
+
html += '</button>';
|
|
2766
|
+
html += '</div>';
|
|
2767
|
+
html += '</div>';
|
|
2768
|
+
|
|
2769
|
+
// Allowed users (only when private)
|
|
2770
|
+
html += '<div class="project-access-section project-access-users-section"' + (visibility !== "private" ? ' style="display:none"' : '') + '>';
|
|
2771
|
+
html += '<label class="project-access-label">Allowed Users</label>';
|
|
2772
|
+
html += '<div class="project-access-user-list">';
|
|
2773
|
+
for (var i = 0; i < selectableUsers.length; i++) {
|
|
2774
|
+
var u = selectableUsers[i];
|
|
2775
|
+
var checked = allowedUsers.indexOf(u.id) !== -1 ? " checked" : "";
|
|
2776
|
+
html += '<label class="project-access-user-item">';
|
|
2777
|
+
html += '<input type="checkbox" data-uid="' + u.id + '"' + checked + '>';
|
|
2778
|
+
html += '<span>' + escapeHtml(u.displayName || u.username || u.id) + '</span>';
|
|
2779
|
+
html += '</label>';
|
|
2780
|
+
}
|
|
2781
|
+
if (selectableUsers.length === 0) {
|
|
2782
|
+
html += '<div class="project-access-empty">No other users</div>';
|
|
2783
|
+
}
|
|
2784
|
+
html += '</div>';
|
|
2785
|
+
html += '</div>';
|
|
2786
|
+
|
|
2787
|
+
popover.innerHTML = html;
|
|
2788
|
+
refreshIcons();
|
|
2789
|
+
|
|
2790
|
+
// Close button
|
|
2791
|
+
popover.querySelector(".project-access-close").addEventListener("click", function () {
|
|
2792
|
+
closeProjectAccessPopover();
|
|
2793
|
+
});
|
|
2794
|
+
|
|
2795
|
+
// Visibility toggle
|
|
2796
|
+
popover.querySelectorAll(".project-access-vis-btn").forEach(function (btn) {
|
|
2797
|
+
btn.addEventListener("click", function () {
|
|
2798
|
+
var newVis = btn.dataset.vis;
|
|
2799
|
+
popover.querySelectorAll(".project-access-vis-btn").forEach(function (b) { b.classList.remove("active"); });
|
|
2800
|
+
btn.classList.add("active");
|
|
2801
|
+
var usersSection = popover.querySelector(".project-access-users-section");
|
|
2802
|
+
if (usersSection) usersSection.style.display = newVis === "private" ? "" : "none";
|
|
2803
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/visibility", {
|
|
2804
|
+
method: "PUT",
|
|
2805
|
+
headers: { "Content-Type": "application/json" },
|
|
2806
|
+
body: JSON.stringify({ visibility: newVis }),
|
|
2807
|
+
});
|
|
2808
|
+
});
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
// User checkboxes
|
|
2812
|
+
popover.querySelectorAll('.project-access-user-item input[type="checkbox"]').forEach(function (cb) {
|
|
2813
|
+
cb.addEventListener("change", function () {
|
|
2814
|
+
var selected = [];
|
|
2815
|
+
popover.querySelectorAll('.project-access-user-item input[type="checkbox"]:checked').forEach(function (c) {
|
|
2816
|
+
selected.push(c.dataset.uid);
|
|
2817
|
+
});
|
|
2818
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/users", {
|
|
2819
|
+
method: "PUT",
|
|
2820
|
+
headers: { "Content-Type": "application/json" },
|
|
2821
|
+
body: JSON.stringify({ allowedUsers: selected }),
|
|
2822
|
+
});
|
|
2823
|
+
});
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2675
2827
|
function closeProjectCtxMenu() {
|
|
2676
2828
|
if (projectCtxMenu) {
|
|
2677
2829
|
projectCtxMenu.remove();
|
|
@@ -2797,6 +2949,23 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
2797
2949
|
});
|
|
2798
2950
|
menu.appendChild(shareItem);
|
|
2799
2951
|
|
|
2952
|
+
// --- Manage Access (owner or admin, multi-user only) ---
|
|
2953
|
+
if (ctx.multiUser && slug.indexOf("--") === -1) {
|
|
2954
|
+
var isProjectOwner = ctx.myUserId && ctx.projectOwnerId && ctx.myUserId === ctx.projectOwnerId;
|
|
2955
|
+
var isAdmin = ctx.permissions && ctx.permissions.projectSettings !== false;
|
|
2956
|
+
if (isProjectOwner || isAdmin) {
|
|
2957
|
+
var accessItem = document.createElement("button");
|
|
2958
|
+
accessItem.className = "project-ctx-item";
|
|
2959
|
+
accessItem.innerHTML = iconHtml("users") + " <span>Manage Access</span>";
|
|
2960
|
+
accessItem.addEventListener("click", function (e) {
|
|
2961
|
+
e.stopPropagation();
|
|
2962
|
+
closeProjectCtxMenu();
|
|
2963
|
+
showProjectAccessPopover(anchorEl, slug);
|
|
2964
|
+
});
|
|
2965
|
+
menu.appendChild(accessItem);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2800
2969
|
if (!ctx.permissions || ctx.permissions.deleteProject !== false) {
|
|
2801
2970
|
// --- Separator ---
|
|
2802
2971
|
var sep = document.createElement("div");
|
package/lib/server.js
CHANGED
|
@@ -1512,13 +1512,23 @@ function createServer(opts) {
|
|
|
1512
1512
|
return;
|
|
1513
1513
|
}
|
|
1514
1514
|
var mu = getMultiUserFromReq(req);
|
|
1515
|
-
if (!mu
|
|
1516
|
-
res.writeHead(
|
|
1517
|
-
res.end('{"error":"
|
|
1515
|
+
if (!mu) {
|
|
1516
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1517
|
+
res.end('{"error":"Authentication required"}');
|
|
1518
1518
|
return;
|
|
1519
1519
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1520
|
+
// Admins get full user list; project owners get limited list (id, displayName, username)
|
|
1521
|
+
if (mu.role === "admin") {
|
|
1522
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1523
|
+
res.end(JSON.stringify({ users: users.getAllUsers() }));
|
|
1524
|
+
} else {
|
|
1525
|
+
var allU = users.getAllUsers();
|
|
1526
|
+
var safeUsers = allU.map(function (u) {
|
|
1527
|
+
return { id: u.id, displayName: u.displayName, username: u.username };
|
|
1528
|
+
});
|
|
1529
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1530
|
+
res.end(JSON.stringify({ users: safeUsers }));
|
|
1531
|
+
}
|
|
1522
1532
|
return;
|
|
1523
1533
|
}
|
|
1524
1534
|
|
|
@@ -1990,9 +2000,12 @@ function createServer(opts) {
|
|
|
1990
2000
|
return;
|
|
1991
2001
|
}
|
|
1992
2002
|
var mu = getMultiUserFromReq(req);
|
|
1993
|
-
|
|
2003
|
+
var _visSlug = fullUrl.split("/")[4];
|
|
2004
|
+
var _visAccess = onGetProjectAccess ? onGetProjectAccess(_visSlug) : null;
|
|
2005
|
+
var _isOwner = mu && _visAccess && _visAccess.ownerId && mu.id === _visAccess.ownerId;
|
|
2006
|
+
if (!mu || (mu.role !== "admin" && !_isOwner)) {
|
|
1994
2007
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1995
|
-
res.end('{"error":"Admin access required"}');
|
|
2008
|
+
res.end('{"error":"Admin or project owner access required"}');
|
|
1996
2009
|
return;
|
|
1997
2010
|
}
|
|
1998
2011
|
var projSlug = fullUrl.split("/")[4];
|
|
@@ -2092,9 +2105,12 @@ function createServer(opts) {
|
|
|
2092
2105
|
return;
|
|
2093
2106
|
}
|
|
2094
2107
|
var mu = getMultiUserFromReq(req);
|
|
2095
|
-
|
|
2108
|
+
var _usrSlug = fullUrl.split("/")[4];
|
|
2109
|
+
var _usrAccess = onGetProjectAccess ? onGetProjectAccess(_usrSlug) : null;
|
|
2110
|
+
var _isOwnerU = mu && _usrAccess && _usrAccess.ownerId && mu.id === _usrAccess.ownerId;
|
|
2111
|
+
if (!mu || (mu.role !== "admin" && !_isOwnerU)) {
|
|
2096
2112
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2097
|
-
res.end('{"error":"Admin access required"}');
|
|
2113
|
+
res.end('{"error":"Admin or project owner access required"}');
|
|
2098
2114
|
return;
|
|
2099
2115
|
}
|
|
2100
2116
|
var projSlug = fullUrl.split("/")[4];
|
|
@@ -2134,7 +2150,7 @@ function createServer(opts) {
|
|
|
2134
2150
|
return;
|
|
2135
2151
|
}
|
|
2136
2152
|
|
|
2137
|
-
// Get project access info (admin
|
|
2153
|
+
// Get project access info (admin or project owner)
|
|
2138
2154
|
if (req.method === "GET" && /^\/api\/admin\/projects\/[a-z0-9_-]+\/access$/.test(fullUrl)) {
|
|
2139
2155
|
if (!users.isMultiUser()) {
|
|
2140
2156
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -2142,9 +2158,12 @@ function createServer(opts) {
|
|
|
2142
2158
|
return;
|
|
2143
2159
|
}
|
|
2144
2160
|
var mu = getMultiUserFromReq(req);
|
|
2145
|
-
|
|
2161
|
+
var _accSlug = fullUrl.split("/")[4];
|
|
2162
|
+
var _accAccess = onGetProjectAccess ? onGetProjectAccess(_accSlug) : null;
|
|
2163
|
+
var _isOwnerA = mu && _accAccess && _accAccess.ownerId && mu.id === _accAccess.ownerId;
|
|
2164
|
+
if (!mu || (mu.role !== "admin" && !_isOwnerA)) {
|
|
2146
2165
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2147
|
-
res.end('{"error":"Admin access required"}');
|
|
2166
|
+
res.end('{"error":"Admin or project owner access required"}');
|
|
2148
2167
|
return;
|
|
2149
2168
|
}
|
|
2150
2169
|
var projSlug = fullUrl.split("/")[4];
|