clay-server 2.11.0-beta.10 → 2.11.0-beta.12
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 +207 -109
- package/lib/os-users.js +48 -0
- package/lib/pages.js +22 -28
- package/lib/public/css/messages.css +1 -1
- package/lib/public/modules/admin.js +53 -5
- package/lib/public/modules/terminal.js +104 -1
- package/lib/server.js +42 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -131,8 +131,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
131
131
|
console.log(" --remove <path> Remove a project directory");
|
|
132
132
|
console.log(" --list List all registered projects");
|
|
133
133
|
console.log(" --headless Start daemon and exit immediately (implies --yes)");
|
|
134
|
-
console.log(" --multi-user
|
|
135
|
-
console.log(" --os-users Enable OS-level user isolation (Linux
|
|
134
|
+
console.log(" --multi-user Start in multi-user mode (use with --yes for headless)");
|
|
135
|
+
console.log(" --os-users Enable OS-level user isolation (Linux, requires root + --multi-user)");
|
|
136
136
|
console.log(" --dangerously-skip-permissions");
|
|
137
137
|
console.log(" Bypass all permission prompts");
|
|
138
138
|
process.exit(0);
|
|
@@ -271,59 +271,8 @@ if (listMode) {
|
|
|
271
271
|
return;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
var muResult = enableMultiUser();
|
|
277
|
-
if (muResult.alreadyEnabled && muResult.hasAdmin) {
|
|
278
|
-
console.log("");
|
|
279
|
-
console.log("Multi-user mode is already enabled and an admin account exists.");
|
|
280
|
-
console.log("No changes made.");
|
|
281
|
-
console.log("");
|
|
282
|
-
} else if (muResult.setupCode) {
|
|
283
|
-
console.log("");
|
|
284
|
-
console.log("\x1b[33m⚠ Experimental Feature\x1b[0m");
|
|
285
|
-
console.log("");
|
|
286
|
-
console.log(" Multi-user mode is experimental and may change in future releases.");
|
|
287
|
-
console.log(" Sharing access to AI-powered tools may be subject to your provider's");
|
|
288
|
-
console.log(" terms of service. Please review the applicable usage policies before");
|
|
289
|
-
console.log(" granting access to other users.");
|
|
290
|
-
console.log("");
|
|
291
|
-
console.log("\x1b[32mMulti-user mode enabled.\x1b[0m");
|
|
292
|
-
console.log("");
|
|
293
|
-
console.log("Setup code: \x1b[1m" + muResult.setupCode + "\x1b[0m");
|
|
294
|
-
console.log("");
|
|
295
|
-
console.log("Open Clay in your browser and enter this code to create the admin account.");
|
|
296
|
-
console.log("The code is single-use and will be cleared once the admin is set up.");
|
|
297
|
-
console.log("");
|
|
298
|
-
}
|
|
299
|
-
if (!osUsersMode) {
|
|
300
|
-
process.exit(0);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// --- Handle --os-users validation ---
|
|
305
|
-
if (osUsersMode) {
|
|
306
|
-
if (process.platform !== "linux") {
|
|
307
|
-
console.error("\x1b[31mError: --os-users requires Linux.\x1b[0m");
|
|
308
|
-
console.error("OS-level user isolation depends on setfacl, getent, and uid/gid process spawning.");
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
311
|
-
if (typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
312
|
-
console.error("\x1b[31mError: --os-users requires running as root.\x1b[0m");
|
|
313
|
-
console.error("The daemon must run as root to spawn worker processes as different users.");
|
|
314
|
-
console.error("Use: sudo clay --multi-user --os-users");
|
|
315
|
-
process.exit(1);
|
|
316
|
-
}
|
|
317
|
-
if (!isMultiUser()) {
|
|
318
|
-
console.error("\x1b[31mError: --os-users requires --multi-user mode.\x1b[0m");
|
|
319
|
-
console.error("Enable multi-user mode first: clay --multi-user");
|
|
320
|
-
process.exit(1);
|
|
321
|
-
}
|
|
322
|
-
console.log("\x1b[32mOS-level user isolation enabled.\x1b[0m");
|
|
323
|
-
console.log("Worker processes will run as mapped Linux users.");
|
|
324
|
-
console.log("Linux accounts will be auto-provisioned for existing users on startup.");
|
|
325
|
-
console.log("");
|
|
326
|
-
}
|
|
274
|
+
// --multi-user / --os-users are now handled in the main entry flow (setup wizard or repeat run)
|
|
275
|
+
// Flags are parsed above and applied during forkDaemon()
|
|
327
276
|
|
|
328
277
|
var cwd = process.cwd();
|
|
329
278
|
|
|
@@ -1310,44 +1259,70 @@ function setup(callback) {
|
|
|
1310
1259
|
}
|
|
1311
1260
|
port = p;
|
|
1312
1261
|
log(sym.bar);
|
|
1262
|
+
askMode();
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1313
1266
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
askPin();
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
afterPin(pin);
|
|
1330
|
-
});
|
|
1331
|
-
} else {
|
|
1332
|
-
afterPin(pin);
|
|
1333
|
-
}
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1267
|
+
function askMode() {
|
|
1268
|
+
promptSelect("How will you use Clay?", [
|
|
1269
|
+
{ label: "Just me (single user)", value: "single" },
|
|
1270
|
+
{ label: "Multiple users", value: "multi" },
|
|
1271
|
+
], function (mode) {
|
|
1272
|
+
if (mode === "single") {
|
|
1273
|
+
finishSetup(mode, false);
|
|
1274
|
+
} else {
|
|
1275
|
+
askOsUsers(mode);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1336
1279
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1280
|
+
function askOsUsers(mode) {
|
|
1281
|
+
// Only offer OS user isolation on Linux
|
|
1282
|
+
if (process.platform !== "linux") {
|
|
1283
|
+
finishSetup(mode, false);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
log(sym.bar);
|
|
1287
|
+
promptToggle("Enable OS-level user isolation?", "Run each user's sessions as a separate Linux account", false, function (wantOsUsers) {
|
|
1288
|
+
if (wantOsUsers) {
|
|
1289
|
+
var isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
1290
|
+
if (!isRoot) {
|
|
1291
|
+
// Save config so sudo clay can pick it up
|
|
1292
|
+
var partialConfig = {
|
|
1293
|
+
port: port,
|
|
1294
|
+
host: host,
|
|
1295
|
+
mode: "multi",
|
|
1296
|
+
osUsers: true,
|
|
1297
|
+
setupCompleted: true,
|
|
1298
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1299
|
+
};
|
|
1300
|
+
saveConfig(partialConfig);
|
|
1301
|
+
log(sym.bar);
|
|
1302
|
+
log(sym.warn + " " + a.yellow + "OS user isolation requires root." + a.reset);
|
|
1303
|
+
log(sym.bar + " Run:");
|
|
1304
|
+
log(sym.bar + " " + a.bold + "sudo clay" + a.reset);
|
|
1305
|
+
log(sym.end);
|
|
1306
|
+
log("");
|
|
1307
|
+
process.exit(0);
|
|
1308
|
+
return;
|
|
1345
1309
|
}
|
|
1310
|
+
}
|
|
1311
|
+
finishSetup(mode, wantOsUsers);
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1346
1314
|
|
|
1347
|
-
|
|
1315
|
+
function finishSetup(mode, wantOsUsers) {
|
|
1316
|
+
if (process.platform === "darwin") {
|
|
1317
|
+
log(sym.bar);
|
|
1318
|
+
promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
|
|
1319
|
+
callback(mode, keepAwake, wantOsUsers);
|
|
1348
1320
|
});
|
|
1349
|
-
}
|
|
1321
|
+
} else {
|
|
1322
|
+
callback(mode, false, wantOsUsers);
|
|
1323
|
+
}
|
|
1350
1324
|
}
|
|
1325
|
+
|
|
1351
1326
|
askPort();
|
|
1352
1327
|
});
|
|
1353
1328
|
}
|
|
@@ -1355,7 +1330,7 @@ function setup(callback) {
|
|
|
1355
1330
|
// ==============================
|
|
1356
1331
|
// Fork the daemon process
|
|
1357
1332
|
// ==============================
|
|
1358
|
-
async function forkDaemon(
|
|
1333
|
+
async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
1359
1334
|
var ip = getLocalIP();
|
|
1360
1335
|
var hasTls = false;
|
|
1361
1336
|
|
|
@@ -1434,12 +1409,14 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1434
1409
|
pid: null,
|
|
1435
1410
|
port: port,
|
|
1436
1411
|
host: host,
|
|
1437
|
-
pinHash:
|
|
1412
|
+
pinHash: mode === "multi" && cliPin ? generateAuthToken(cliPin) : null,
|
|
1438
1413
|
tls: hasTls,
|
|
1439
1414
|
debug: debugMode,
|
|
1440
1415
|
keepAwake: keepAwake,
|
|
1441
1416
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1442
|
-
osUsers: osUsersMode,
|
|
1417
|
+
osUsers: wantOsUsers || osUsersMode,
|
|
1418
|
+
mode: mode || "single",
|
|
1419
|
+
setupCompleted: true,
|
|
1443
1420
|
projects: allProjects,
|
|
1444
1421
|
};
|
|
1445
1422
|
|
|
@@ -1481,6 +1458,18 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1481
1458
|
return;
|
|
1482
1459
|
}
|
|
1483
1460
|
|
|
1461
|
+
// Enable multi-user mode if requested
|
|
1462
|
+
if (config.mode === "multi") {
|
|
1463
|
+
var muResult = enableMultiUser();
|
|
1464
|
+
if (muResult.setupCode) {
|
|
1465
|
+
log("");
|
|
1466
|
+
log(sym.done + " " + a.green + "Multi-user mode enabled." + a.reset);
|
|
1467
|
+
log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
|
|
1468
|
+
log(sym.bar + " Open Clay in your browser and enter this code to create the admin account.");
|
|
1469
|
+
log("");
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1484
1473
|
// Headless mode — print status and exit immediately
|
|
1485
1474
|
if (headlessMode) {
|
|
1486
1475
|
var protocol = config.tls ? "https" : "http";
|
|
@@ -1499,7 +1488,7 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1499
1488
|
// ==============================
|
|
1500
1489
|
// Dev mode — foreground daemon with file watching
|
|
1501
1490
|
// ==============================
|
|
1502
|
-
async function devMode(
|
|
1491
|
+
async function devMode(mode, keepAwake, existingPinHash) {
|
|
1503
1492
|
var ip = getLocalIP();
|
|
1504
1493
|
var hasTls = false;
|
|
1505
1494
|
|
|
@@ -1565,11 +1554,13 @@ async function devMode(pin, keepAwake, existingPinHash) {
|
|
|
1565
1554
|
pid: null,
|
|
1566
1555
|
port: port,
|
|
1567
1556
|
host: host,
|
|
1568
|
-
pinHash: existingPinHash ||
|
|
1557
|
+
pinHash: existingPinHash || null,
|
|
1569
1558
|
tls: hasTls,
|
|
1570
1559
|
debug: true,
|
|
1571
1560
|
keepAwake: keepAwake || false,
|
|
1572
1561
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1562
|
+
mode: mode || "single",
|
|
1563
|
+
setupCompleted: true,
|
|
1573
1564
|
projects: allProjects,
|
|
1574
1565
|
};
|
|
1575
1566
|
|
|
@@ -2337,6 +2328,11 @@ function showSettingsMenu(config, ip) {
|
|
|
2337
2328
|
? a.green + "Enabled" + a.reset
|
|
2338
2329
|
: a.dim + "Off" + a.reset;
|
|
2339
2330
|
|
|
2331
|
+
var modeLabel = config.mode === "multi" ? "Multi-user" : "Single user";
|
|
2332
|
+
var modeStatus = config.mode === "multi"
|
|
2333
|
+
? a.clay + modeLabel + a.reset
|
|
2334
|
+
: a.dim + modeLabel + a.reset;
|
|
2335
|
+
log(sym.bar + " Mode " + modeStatus);
|
|
2340
2336
|
log(sym.bar + " PIN " + pinStatus);
|
|
2341
2337
|
log(sym.bar + " Multi-user " + muStatus);
|
|
2342
2338
|
var osUsersStatus = isOsUsers
|
|
@@ -2375,6 +2371,7 @@ function showSettingsMenu(config, ip) {
|
|
|
2375
2371
|
items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
|
|
2376
2372
|
}
|
|
2377
2373
|
items.push({ label: "View logs", value: "logs" });
|
|
2374
|
+
items.push({ label: "Re-run setup wizard", value: "rerun_setup" });
|
|
2378
2375
|
items.push({ label: "Back", value: "back" });
|
|
2379
2376
|
|
|
2380
2377
|
promptSelect("Select", items, function (choice) {
|
|
@@ -2572,6 +2569,70 @@ function showSettingsMenu(config, ip) {
|
|
|
2572
2569
|
});
|
|
2573
2570
|
break;
|
|
2574
2571
|
|
|
2572
|
+
case "rerun_setup":
|
|
2573
|
+
log(sym.bar);
|
|
2574
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Re-run setup wizard?" + a.reset);
|
|
2575
|
+
log(sym.bar);
|
|
2576
|
+
log(sym.bar + " " + a.dim + "This will shut down the running daemon, reset your setup" + a.reset);
|
|
2577
|
+
log(sym.bar + " " + a.dim + "preferences (mode, port), and walk you through the wizard again." + a.reset);
|
|
2578
|
+
log(sym.bar + " " + a.dim + "Your projects and user accounts will be preserved." + a.reset);
|
|
2579
|
+
log(sym.bar);
|
|
2580
|
+
promptSelect("Confirm", [
|
|
2581
|
+
{ label: "Re-run setup wizard", value: "confirm" },
|
|
2582
|
+
{ label: "Cancel", value: "cancel" },
|
|
2583
|
+
], function (confirmChoice) {
|
|
2584
|
+
if (confirmChoice === "confirm") {
|
|
2585
|
+
// Clear setupCompleted so setup() runs fresh
|
|
2586
|
+
var cfg = loadConfig() || {};
|
|
2587
|
+
delete cfg.setupCompleted;
|
|
2588
|
+
delete cfg.mode;
|
|
2589
|
+
cfg.pid = null;
|
|
2590
|
+
saveConfig(cfg);
|
|
2591
|
+
// Shut down the daemon
|
|
2592
|
+
sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
|
|
2593
|
+
clearStaleConfig();
|
|
2594
|
+
// Run the setup wizard, then fork a new daemon
|
|
2595
|
+
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2596
|
+
var rc = loadClayrc();
|
|
2597
|
+
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
2598
|
+
return p.path !== cwd && fs.existsSync(p.path);
|
|
2599
|
+
});
|
|
2600
|
+
if (restorable.length > 0) {
|
|
2601
|
+
promptRestoreProjects(restorable, function (selected) {
|
|
2602
|
+
forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
|
|
2603
|
+
});
|
|
2604
|
+
} else {
|
|
2605
|
+
log(sym.bar);
|
|
2606
|
+
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2607
|
+
log("");
|
|
2608
|
+
forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
}).catch(function () {
|
|
2612
|
+
clearStaleConfig();
|
|
2613
|
+
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2614
|
+
var rc = loadClayrc();
|
|
2615
|
+
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
2616
|
+
return p.path !== cwd && fs.existsSync(p.path);
|
|
2617
|
+
});
|
|
2618
|
+
if (restorable.length > 0) {
|
|
2619
|
+
promptRestoreProjects(restorable, function (selected) {
|
|
2620
|
+
forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
|
|
2621
|
+
});
|
|
2622
|
+
} else {
|
|
2623
|
+
log(sym.bar);
|
|
2624
|
+
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2625
|
+
log("");
|
|
2626
|
+
forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
|
|
2627
|
+
}
|
|
2628
|
+
});
|
|
2629
|
+
});
|
|
2630
|
+
} else {
|
|
2631
|
+
showSettingsMenu(config, ip);
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
break;
|
|
2635
|
+
|
|
2575
2636
|
case "logs":
|
|
2576
2637
|
console.clear();
|
|
2577
2638
|
log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
|
|
@@ -2633,14 +2694,14 @@ var currentVersion = require("../package.json").version;
|
|
|
2633
2694
|
if (devConfig.pid) clearStaleConfig();
|
|
2634
2695
|
devConfig = null;
|
|
2635
2696
|
}
|
|
2636
|
-
// No config — go through setup (disclaimer, port,
|
|
2697
|
+
// No config — go through setup (disclaimer, port, mode, etc.)
|
|
2637
2698
|
if (!devConfig) {
|
|
2638
|
-
setup(function (
|
|
2639
|
-
devMode(
|
|
2699
|
+
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2700
|
+
devMode(mode, keepAwake, null);
|
|
2640
2701
|
});
|
|
2641
2702
|
} else {
|
|
2642
|
-
// Reuse existing
|
|
2643
|
-
await devMode(
|
|
2703
|
+
// Reuse existing config (repeat run)
|
|
2704
|
+
await devMode(devConfig.mode || "single", devConfig.keepAwake || false, devConfig.pinHash || null);
|
|
2644
2705
|
}
|
|
2645
2706
|
return;
|
|
2646
2707
|
}
|
|
@@ -2717,19 +2778,55 @@ var currentVersion = require("../package.json").version;
|
|
|
2717
2778
|
showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
|
|
2718
2779
|
}
|
|
2719
2780
|
} else {
|
|
2720
|
-
// No daemon running —
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2781
|
+
// No daemon running — check for saved config (repeat run)
|
|
2782
|
+
var savedConfig = loadConfig();
|
|
2783
|
+
var isRepeatRun = savedConfig && savedConfig.setupCompleted;
|
|
2784
|
+
|
|
2785
|
+
// --multi-user / --os-users CLI flags set config directly for headless/scripted usage
|
|
2786
|
+
if (multiUserMode) {
|
|
2787
|
+
if (!savedConfig) savedConfig = {};
|
|
2788
|
+
savedConfig.mode = "multi";
|
|
2789
|
+
savedConfig.setupCompleted = true;
|
|
2790
|
+
}
|
|
2791
|
+
if (osUsersMode) {
|
|
2792
|
+
if (!savedConfig) savedConfig = {};
|
|
2793
|
+
savedConfig.osUsers = true;
|
|
2794
|
+
savedConfig.mode = "multi";
|
|
2795
|
+
savedConfig.setupCompleted = true;
|
|
2796
|
+
}
|
|
2797
|
+
isRepeatRun = savedConfig && savedConfig.setupCompleted;
|
|
2798
|
+
|
|
2799
|
+
if (isRepeatRun || autoYes) {
|
|
2800
|
+
// Repeat run or --yes: skip wizard, reuse saved config
|
|
2801
|
+
var savedMode = (savedConfig && savedConfig.mode) || "single";
|
|
2802
|
+
var savedKeepAwake = (savedConfig && savedConfig.keepAwake) || false;
|
|
2803
|
+
var savedOsUsers = (savedConfig && savedConfig.osUsers) || false;
|
|
2804
|
+
|
|
2805
|
+
// os-users requires root
|
|
2806
|
+
if (savedOsUsers && typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
2807
|
+
console.error(a.red + "OS user isolation requires root." + a.reset);
|
|
2808
|
+
console.error("Run: " + a.bold + "sudo clay" + a.reset);
|
|
2809
|
+
process.exit(1);
|
|
2810
|
+
return;
|
|
2727
2811
|
}
|
|
2812
|
+
|
|
2813
|
+
if (savedConfig && savedConfig.port) port = savedConfig.port;
|
|
2814
|
+
if (savedConfig && savedConfig.host) host = savedConfig.host;
|
|
2815
|
+
if (savedConfig && savedConfig.dangerouslySkipPermissions) dangerouslySkipPermissions = true;
|
|
2816
|
+
|
|
2817
|
+
if (autoYes) {
|
|
2818
|
+
console.log(" " + sym.done + " Auto-accepted disclaimer");
|
|
2819
|
+
console.log(" " + sym.done + " Mode: " + savedMode);
|
|
2820
|
+
if (dangerouslySkipPermissions) {
|
|
2821
|
+
console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + a.reset);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2728
2825
|
var autoRc = loadClayrc();
|
|
2729
2826
|
var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
|
|
2730
2827
|
return p.path !== cwd && fs.existsSync(p.path);
|
|
2731
2828
|
});
|
|
2732
|
-
if (autoRestorable.length > 0) {
|
|
2829
|
+
if (autoRestorable.length > 0 && autoYes) {
|
|
2733
2830
|
console.log(" " + sym.done + " Restoring " + autoRestorable.length + " previous project(s)");
|
|
2734
2831
|
}
|
|
2735
2832
|
// Add cwd if it has history in .clayrc, or if there are no other projects to restore
|
|
@@ -2737,9 +2834,10 @@ var currentVersion = require("../package.json").version;
|
|
|
2737
2834
|
return p.path === cwd;
|
|
2738
2835
|
});
|
|
2739
2836
|
var addCwd = cwdInRc || autoRestorable.length === 0;
|
|
2740
|
-
await forkDaemon(
|
|
2837
|
+
await forkDaemon(savedMode, savedKeepAwake, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd, savedOsUsers);
|
|
2741
2838
|
} else {
|
|
2742
|
-
|
|
2839
|
+
// First run: interactive wizard
|
|
2840
|
+
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2743
2841
|
// Check ~/.clayrc for previous projects to restore
|
|
2744
2842
|
var rc = loadClayrc();
|
|
2745
2843
|
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
@@ -2748,13 +2846,13 @@ var currentVersion = require("../package.json").version;
|
|
|
2748
2846
|
|
|
2749
2847
|
if (restorable.length > 0) {
|
|
2750
2848
|
promptRestoreProjects(restorable, function (selected) {
|
|
2751
|
-
forkDaemon(
|
|
2849
|
+
forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
|
|
2752
2850
|
});
|
|
2753
2851
|
} else {
|
|
2754
2852
|
log(sym.bar);
|
|
2755
2853
|
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2756
2854
|
log("");
|
|
2757
|
-
forkDaemon(
|
|
2855
|
+
forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
|
|
2758
2856
|
}
|
|
2759
2857
|
});
|
|
2760
2858
|
}
|
package/lib/os-users.js
CHANGED
|
@@ -174,6 +174,52 @@ function linuxUserExists(username) {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Install Claude CLI for a Linux user account.
|
|
179
|
+
* Downloads and runs the install script, then ensures PATH is configured.
|
|
180
|
+
* Non-fatal: logs errors but does not throw.
|
|
181
|
+
*/
|
|
182
|
+
function installClaudeCli(linuxName) {
|
|
183
|
+
try {
|
|
184
|
+
// Download and run the Claude CLI install script as the target user
|
|
185
|
+
execSync(
|
|
186
|
+
"su - " + linuxName + " -c \"curl -fsSL https://claude.ai/install.sh | bash\"",
|
|
187
|
+
{ encoding: "utf8", timeout: 60000, stdio: "pipe" }
|
|
188
|
+
);
|
|
189
|
+
console.log("[os-users] Claude CLI installed for " + linuxName);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
var msg = (e.stderr || e.message || "").trim();
|
|
192
|
+
console.error("[os-users] Failed to install Claude CLI for " + linuxName + ": " + msg);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Append PATH export to the user's shell config if not already present
|
|
197
|
+
try {
|
|
198
|
+
var home = "/home/" + linuxName;
|
|
199
|
+
var rcFile;
|
|
200
|
+
if (fs.existsSync(home + "/.zshrc")) {
|
|
201
|
+
rcFile = "~/.zshrc";
|
|
202
|
+
} else {
|
|
203
|
+
rcFile = "~/.bashrc";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if PATH export is already present before appending
|
|
207
|
+
var checkCmd = "su - " + linuxName + " -c \"grep -qF '/.local/bin' " + rcFile + "\"";
|
|
208
|
+
try {
|
|
209
|
+
execSync(checkCmd, { encoding: "utf8", timeout: 5000, stdio: "pipe" });
|
|
210
|
+
console.log("[os-users] PATH already configured in " + rcFile + " for " + linuxName);
|
|
211
|
+
} catch (grepErr) {
|
|
212
|
+
// grep returned non-zero, meaning the line is not present; append it
|
|
213
|
+
var appendCmd = "su - " + linuxName + " -c 'echo \"export PATH=\\\"\\$HOME/.local/bin:\\$PATH\\\"\" >> " + rcFile + "'";
|
|
214
|
+
execSync(appendCmd, { encoding: "utf8", timeout: 5000, stdio: "pipe" });
|
|
215
|
+
console.log("[os-users] PATH export appended to " + rcFile + " for " + linuxName);
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
var rcMsg = (e.stderr || e.message || "").trim();
|
|
219
|
+
console.error("[os-users] Failed to configure PATH for " + linuxName + ": " + rcMsg);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
/**
|
|
178
224
|
* Provision a Linux user account for a Clay user.
|
|
179
225
|
* Creates the account via useradd with a home directory.
|
|
@@ -197,6 +243,7 @@ function provisionLinuxUser(clayUsername) {
|
|
|
197
243
|
stdio: "pipe",
|
|
198
244
|
});
|
|
199
245
|
console.log("[os-users] Provisioned Linux user: " + linuxName + " (Clay user: " + clayUsername + ")");
|
|
246
|
+
installClaudeCli(linuxName);
|
|
200
247
|
return { ok: true, linuxUser: linuxName };
|
|
201
248
|
} catch (e) {
|
|
202
249
|
var msg = (e.stderr || e.message || "").trim();
|
|
@@ -296,6 +343,7 @@ module.exports = {
|
|
|
296
343
|
provisionLinuxUser: provisionLinuxUser,
|
|
297
344
|
provisionAllUsers: provisionAllUsers,
|
|
298
345
|
grantAllUsersAccess: grantAllUsersAccess,
|
|
346
|
+
installClaudeCli: installClaudeCli,
|
|
299
347
|
deactivateLinuxUser: deactivateLinuxUser,
|
|
300
348
|
ensureProjectsDir: ensureProjectsDir,
|
|
301
349
|
};
|
package/lib/pages.js
CHANGED
|
@@ -966,39 +966,33 @@ function multiUserLoginPageHtml() {
|
|
|
966
966
|
'.catch(function(){errs[1].textContent="Connection error";btns[1].disabled=false})}' +
|
|
967
967
|
'btns[1].onclick=doLogin;' +
|
|
968
968
|
|
|
969
|
-
// Force PIN change
|
|
969
|
+
// Force PIN change: reuse the same login screen, switch step1 to "Set new PIN" mode
|
|
970
970
|
'function showChangePinOverlay(){' +
|
|
971
|
-
|
|
972
|
-
'
|
|
973
|
-
|
|
974
|
-
'
|
|
975
|
-
'
|
|
976
|
-
'
|
|
977
|
-
'
|
|
978
|
-
'
|
|
979
|
-
|
|
980
|
-
'
|
|
981
|
-
'
|
|
982
|
-
'
|
|
983
|
-
'
|
|
984
|
-
|
|
985
|
-
'
|
|
986
|
-
'
|
|
987
|
-
'
|
|
988
|
-
'var errEl=ov.querySelector("#new-pin-err");' +
|
|
989
|
-
'initPinBoxes("new-pin-boxes","new-pin",function(){if(!saveBtn.disabled)doSavePin()});' +
|
|
990
|
-
'var nb=ov.querySelectorAll(".pin-digit");' +
|
|
991
|
-
'for(var i=0;i<nb.length;i++)nb[i].addEventListener("input",function(){saveBtn.disabled=newPinEl.value.length!==6});' +
|
|
992
|
-
'function doSavePin(){' +
|
|
993
|
-
'saveBtn.disabled=true;errEl.textContent="";' +
|
|
971
|
+
// Hide step dots (no longer a 2-step flow)
|
|
972
|
+
'var bar=document.querySelector(".steps-bar");if(bar)bar.style.display="none";' +
|
|
973
|
+
// Hide step 0 (username), show step 1 (PIN) with new labels
|
|
974
|
+
'steps[0].classList.remove("active");' +
|
|
975
|
+
'steps[1].classList.add("active");' +
|
|
976
|
+
'var h1=steps[1].querySelector("h1");if(h1)h1.textContent="Set your new PIN";' +
|
|
977
|
+
'var sub=steps[1].querySelector(".sub");if(sub)sub.textContent="Your temporary PIN has expired. Please set a new 6-digit PIN to continue.";' +
|
|
978
|
+
'btns[1].textContent="Save PIN";' +
|
|
979
|
+
// Re-init PIN boxes for fresh input
|
|
980
|
+
'resetPin();' +
|
|
981
|
+
'initPinBoxes("pin-boxes","pin",function(){if(!btns[1].disabled)doSaveNewPin()});' +
|
|
982
|
+
'var boxes=document.querySelectorAll(".pin-digit");' +
|
|
983
|
+
'for(var i=0;i<boxes.length;i++)boxes[i].addEventListener("input",function(){btns[1].disabled=pinEl.value.length!==6});' +
|
|
984
|
+
// Override button handler to save new PIN instead of login
|
|
985
|
+
'btns[1].onclick=doSaveNewPin;' +
|
|
986
|
+
'function doSaveNewPin(){' +
|
|
987
|
+
'btns[1].disabled=true;errs[1].textContent="";' +
|
|
994
988
|
'fetch("/api/user/pin",{method:"PUT",headers:{"Content-Type":"application/json"},' +
|
|
995
|
-
'body:JSON.stringify({newPin:
|
|
989
|
+
'body:JSON.stringify({newPin:pinEl.value})})' +
|
|
996
990
|
'.then(function(r){return r.json()})' +
|
|
997
991
|
'.then(function(d){' +
|
|
998
992
|
'if(d.ok){location.reload();return}' +
|
|
999
|
-
'
|
|
1000
|
-
'.catch(function(){
|
|
1001
|
-
'
|
|
993
|
+
'errs[1].textContent=d.error||"Failed to save PIN";btns[1].disabled=false})' +
|
|
994
|
+
'.catch(function(){errs[1].textContent="Connection error";btns[1].disabled=false})}' +
|
|
995
|
+
'}' +
|
|
1002
996
|
|
|
1003
997
|
'</script></div></body></html>';
|
|
1004
998
|
}
|
|
@@ -1472,7 +1472,7 @@ pre.mermaid-error {
|
|
|
1472
1472
|
========================================================================== */
|
|
1473
1473
|
|
|
1474
1474
|
.auth-required-msg {
|
|
1475
|
-
max-width:
|
|
1475
|
+
max-width: 420px;
|
|
1476
1476
|
margin: 12px auto;
|
|
1477
1477
|
padding: 14px 18px;
|
|
1478
1478
|
background: color-mix(in srgb, var(--warning) 8%, var(--bg));
|
|
@@ -24,6 +24,27 @@ function showConfirmDialog(message, onConfirm) {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function showConfirmResetPin(name, onConfirm) {
|
|
28
|
+
var modal = document.createElement("div");
|
|
29
|
+
modal.className = "admin-modal-overlay";
|
|
30
|
+
var html = '<div class="admin-modal">' +
|
|
31
|
+
'<div class="admin-modal-body" style="padding:20px 16px 16px">' +
|
|
32
|
+
'<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">Reset PIN for <strong>' + escapeHtml(name) + '</strong>? A new temporary PIN will be generated and they will need to change it on next login.</p>' +
|
|
33
|
+
'</div>' +
|
|
34
|
+
'<div class="admin-modal-footer">' +
|
|
35
|
+
'<button class="admin-modal-save">Reset PIN</button>' +
|
|
36
|
+
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
37
|
+
'</div></div>';
|
|
38
|
+
modal.innerHTML = html;
|
|
39
|
+
document.body.appendChild(modal);
|
|
40
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
41
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
|
|
42
|
+
modal.querySelector(".admin-modal-save").addEventListener("click", function () {
|
|
43
|
+
modal.remove();
|
|
44
|
+
onConfirm();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
var ctx = null;
|
|
28
49
|
var cachedUsers = [];
|
|
29
50
|
var cachedInvites = [];
|
|
@@ -111,8 +132,13 @@ function renderUsersTab(body) {
|
|
|
111
132
|
html += '</div>';
|
|
112
133
|
html += '<div class="admin-user-meta">' + escapeHtml(u.username) + ' · joined ' + created + '</div>';
|
|
113
134
|
html += '</div>';
|
|
114
|
-
if (!isMe
|
|
115
|
-
html += '<
|
|
135
|
+
if (!isMe) {
|
|
136
|
+
html += '<div style="display:flex;align-items:center;gap:2px">';
|
|
137
|
+
html += '<button class="admin-remove-btn admin-reset-pin-btn" data-user-id="' + u.id + '" title="Reset PIN">' + iconHtml("key-round") + '</button>';
|
|
138
|
+
if (u.role !== "admin") {
|
|
139
|
+
html += '<button class="admin-remove-btn" data-user-id="' + u.id + '" title="Remove user">' + iconHtml("trash-2") + '</button>';
|
|
140
|
+
}
|
|
141
|
+
html += '</div>';
|
|
116
142
|
}
|
|
117
143
|
html += '</div>';
|
|
118
144
|
}
|
|
@@ -129,10 +155,32 @@ function renderUsersTab(body) {
|
|
|
129
155
|
});
|
|
130
156
|
}
|
|
131
157
|
|
|
158
|
+
// Bind reset PIN buttons
|
|
159
|
+
var resetBtns = body.querySelectorAll(".admin-reset-pin-btn");
|
|
160
|
+
for (var j = 0; j < resetBtns.length; j++) {
|
|
161
|
+
resetBtns[j].addEventListener("click", function (e) {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
var userId = this.dataset.userId;
|
|
164
|
+
var user = cachedUsers.find(function (u) { return u.id === userId; });
|
|
165
|
+
var name = user ? (user.displayName || user.username) : "this user";
|
|
166
|
+
showConfirmResetPin(name, function () {
|
|
167
|
+
apiPost("/api/admin/users/" + userId + "/reset-pin").then(function (data) {
|
|
168
|
+
if (data.ok) {
|
|
169
|
+
showTempPinModal({ username: user.username, displayName: user.displayName || user.username }, data.tempPin);
|
|
170
|
+
} else {
|
|
171
|
+
showToast(data.error || "Failed to reset PIN");
|
|
172
|
+
}
|
|
173
|
+
}).catch(function () {
|
|
174
|
+
showToast("Failed to reset PIN");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
// Bind remove buttons
|
|
133
|
-
var removeBtns = body.querySelectorAll(".admin-remove-btn");
|
|
134
|
-
for (var
|
|
135
|
-
removeBtns[
|
|
181
|
+
var removeBtns = body.querySelectorAll(".admin-remove-btn:not(.admin-reset-pin-btn)");
|
|
182
|
+
for (var k = 0; k < removeBtns.length; k++) {
|
|
183
|
+
removeBtns[k].addEventListener("click", function () {
|
|
136
184
|
var userId = this.dataset.userId;
|
|
137
185
|
var user = cachedUsers.find(function (u) { return u.id === userId; });
|
|
138
186
|
var name = user ? (user.displayName || user.username) : "this user";
|
|
@@ -15,6 +15,106 @@ var resizeObserver = null;
|
|
|
15
15
|
var toolbarBound = false;
|
|
16
16
|
var termCtxMenu = null;
|
|
17
17
|
|
|
18
|
+
// --- Multi-line link provider ---
|
|
19
|
+
// xterm's WebLinksAddon only detects URLs on a single line.
|
|
20
|
+
// This provider reconstructs "logical lines" from wrapped buffer lines
|
|
21
|
+
// and detects URLs that span multiple rows.
|
|
22
|
+
function createMultiLineLinkProvider(xterm) {
|
|
23
|
+
var URL_RE = /https?:\/\/[^\s'"\]>)}{]+/g;
|
|
24
|
+
|
|
25
|
+
function getLogicalLine(buffer, y) {
|
|
26
|
+
// Walk backward to find the start of the logical line
|
|
27
|
+
var startY = y;
|
|
28
|
+
while (startY > 0) {
|
|
29
|
+
var line = buffer.getLine(startY);
|
|
30
|
+
if (!line || !line.isWrapped) break;
|
|
31
|
+
startY--;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Walk forward to collect all wrapped continuation lines
|
|
35
|
+
var endY = startY;
|
|
36
|
+
var cols = xterm.cols;
|
|
37
|
+
while (endY < buffer.length - 1) {
|
|
38
|
+
var next = buffer.getLine(endY + 1);
|
|
39
|
+
if (!next || !next.isWrapped) break;
|
|
40
|
+
endY++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build the full logical line text and track row boundaries
|
|
44
|
+
var text = "";
|
|
45
|
+
var rowOffsets = []; // { y, startOffset, length }
|
|
46
|
+
for (var row = startY; row <= endY; row++) {
|
|
47
|
+
var line = buffer.getLine(row);
|
|
48
|
+
if (!line) break;
|
|
49
|
+
var trimRight = (row === endY); // only trim trailing spaces on last row
|
|
50
|
+
var rowText = line.translateToString(trimRight);
|
|
51
|
+
rowOffsets.push({ y: row, startOffset: text.length, length: rowText.length });
|
|
52
|
+
text += rowText;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { text: text, startY: startY, endY: endY, rowOffsets: rowOffsets };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function offsetToBufferPos(rowOffsets, offset) {
|
|
59
|
+
for (var i = 0; i < rowOffsets.length; i++) {
|
|
60
|
+
var ro = rowOffsets[i];
|
|
61
|
+
if (offset < ro.startOffset + ro.length) {
|
|
62
|
+
return { x: offset - ro.startOffset + 1, y: ro.y + 1 }; // 1-based
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Past end, clamp to last row
|
|
66
|
+
var last = rowOffsets[rowOffsets.length - 1];
|
|
67
|
+
return { x: last.length + 1, y: last.y + 1 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
provideLinks: function (y, callback) {
|
|
72
|
+
var buffer = xterm.buffer.active;
|
|
73
|
+
// y is 1-based in provideLinks
|
|
74
|
+
var bufferY = y - 1;
|
|
75
|
+
var logical = getLogicalLine(buffer, bufferY);
|
|
76
|
+
|
|
77
|
+
// Only process if this logical line spans multiple rows
|
|
78
|
+
if (logical.startY === logical.endY) {
|
|
79
|
+
callback(undefined);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Only trigger on the first row of the logical line to avoid duplicates
|
|
84
|
+
if (bufferY !== logical.startY) {
|
|
85
|
+
callback(undefined);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var links = [];
|
|
90
|
+
var match;
|
|
91
|
+
URL_RE.lastIndex = 0;
|
|
92
|
+
while ((match = URL_RE.exec(logical.text)) !== null) {
|
|
93
|
+
var urlStart = match.index;
|
|
94
|
+
var urlEnd = match.index + match[0].length - 1;
|
|
95
|
+
|
|
96
|
+
var startPos = offsetToBufferPos(logical.rowOffsets, urlStart);
|
|
97
|
+
var endPos = offsetToBufferPos(logical.rowOffsets, urlEnd);
|
|
98
|
+
|
|
99
|
+
// Only include if the URL actually spans multiple rows
|
|
100
|
+
if (startPos.y !== endPos.y) {
|
|
101
|
+
(function (url) {
|
|
102
|
+
links.push({
|
|
103
|
+
range: { start: startPos, end: endPos },
|
|
104
|
+
text: url,
|
|
105
|
+
activate: function () {
|
|
106
|
+
window.open(url, "_blank", "noopener");
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
})(match[0]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
callback(links.length > 0 ? links : undefined);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
18
118
|
// --- Init ---
|
|
19
119
|
export function initTerminal(_ctx) {
|
|
20
120
|
ctx = _ctx;
|
|
@@ -225,11 +325,14 @@ function createXtermForTab(tab) {
|
|
|
225
325
|
xterm.loadAddon(fitAddon);
|
|
226
326
|
}
|
|
227
327
|
|
|
228
|
-
// Web links addon: make URLs clickable
|
|
328
|
+
// Web links addon: make URLs clickable (single-line)
|
|
229
329
|
if (typeof WebLinksAddon !== "undefined") {
|
|
230
330
|
xterm.loadAddon(new WebLinksAddon.WebLinksAddon());
|
|
231
331
|
}
|
|
232
332
|
|
|
333
|
+
// Custom multi-line link provider: detect URLs that wrap across lines
|
|
334
|
+
xterm.registerLinkProvider(createMultiLineLinkProvider(xterm));
|
|
335
|
+
|
|
233
336
|
// Create a container div for this tab's terminal
|
|
234
337
|
var bodyEl = document.createElement("div");
|
|
235
338
|
bodyEl.className = "terminal-tab-body";
|
package/lib/server.js
CHANGED
|
@@ -1118,6 +1118,48 @@ function createServer(opts) {
|
|
|
1118
1118
|
return;
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
1121
|
+
// Reset user PIN (admin only) — generates a new temp PIN
|
|
1122
|
+
if (req.method === "POST" && fullUrl.match(/^\/api\/admin\/users\/[^/]+\/reset-pin$/)) {
|
|
1123
|
+
if (!users.isMultiUser()) {
|
|
1124
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1125
|
+
res.end('{"error":"Not found"}');
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
var mu = getMultiUserFromReq(req);
|
|
1129
|
+
if (!mu || mu.role !== "admin") {
|
|
1130
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1131
|
+
res.end('{"error":"Admin access required"}');
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
var urlParts = fullUrl.split("/");
|
|
1135
|
+
var targetUserId = urlParts[4]; // /api/admin/users/{userId}/reset-pin
|
|
1136
|
+
var targetUser = users.findUserById(targetUserId);
|
|
1137
|
+
if (!targetUser) {
|
|
1138
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1139
|
+
res.end('{"error":"User not found"}');
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
var newPin = users.generatePin();
|
|
1143
|
+
var pinResult = users.updateUserPin(targetUserId, newPin);
|
|
1144
|
+
if (pinResult.error) {
|
|
1145
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1146
|
+
res.end(JSON.stringify({ error: pinResult.error }));
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
// Mark as must change on next login
|
|
1150
|
+
var data = users.loadUsers();
|
|
1151
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
1152
|
+
if (data.users[i].id === targetUserId) {
|
|
1153
|
+
data.users[i].mustChangePin = true;
|
|
1154
|
+
users.saveUsers(data);
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1159
|
+
res.end(JSON.stringify({ ok: true, tempPin: newPin }));
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1121
1163
|
// Set Linux user mapping (admin only, OS-level multi-user)
|
|
1122
1164
|
if (req.method === "PUT" && fullUrl.match(/^\/api\/admin\/users\/[^/]+\/linux-user$/)) {
|
|
1123
1165
|
if (!users.isMultiUser()) {
|