Files
AgentHub/server/lib/homes.js
T
nntrivi2001 1a6b550e89 feat(files): New → Clone repository clones into the open folder
The File Explorer's New menu (and its right-click menu) gains Clone
repository: it clones a URL into the folder it has open — of the user's own
home or of a workspace's own folder — through POST /api/lanes/clone with
that folder as `parent`, so the URL allowlist, the own-folder check and the
folder account's own git identity all apply, and opens a workspace on the
clone. ECREDENTIALS asks for a login, ESSHAUTH points to Settings → Git.
GET /api/homes now gives `me` its absolute `dir`. Keys typed in the dialog
never reach the explorer's shortcuts (Backspace / Delete).
2026-10-19 17:13:26 +07:00

101 lines
3.3 KiB
JavaScript

/**
* @file Which HOME folder a user's File Explorer opens (`/api/homes/*`):
* - S3: their own Linux account's home (`/srv/agenthub/users/<name>`);
* - server mode without S3: their own folder under AGENTHUB_USERS_ROOT;
* - local mode, or an admin with no Linux account of their own: the host
* account's home.
* `me` is always the caller's. An admin may also open another user's home —
* read-only (the routes refuse every write, `EREADONLY`) and written to the
* audit log — the "view other users' homes" option. A home is never a
* workspace (the S3 rule stands); it is browsed with `file-explorer.js`'s
* `inHome`, which keeps every dot entry at its top out of reach.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const auth = require("./auth");
const linux = require("./linux-accounts");
const runAs = require("./run-as");
const { ownFolder } = require("./scope");
const isDir = (p) => {
try {
return fs.statSync(p).isDirectory();
} catch {
return false;
}
};
/** The home of AgentHub user `user` (a users row or req.user), or null. */
function homeOf(user) {
if (!auth.isServerMode()) return os.homedir();
if (!user) return null;
if (linux.s3Enabled()) {
let acct = null;
try {
acct = runAs.accountForUser(user.username);
} catch {
acct = null; // disabled or not provisioned: no home to open
}
if (acct) return acct.home;
return user.role === "admin" ? os.homedir() : null;
}
const own = ownFolder({ user });
if (own) return isDir(own) ? own : null;
return user.role === "admin" ? os.homedir() : null;
}
/**
* The homes `req` may open: `{ id: "me" | <username>, label, readOnly }`,
* `me` with its absolute `dir`.
* Others' only for an admin, and only those that resolve to a folder.
*/
function listHomes(req) {
const out = [];
const me = req.user || null;
const mine = homeOf(me);
// `dir` for the caller's own home only: the Files page clones into it.
if (mine) {
out.push({
id: "me",
label: me?.displayName || me?.username || "",
readOnly: false,
dir: path.resolve(mine),
});
}
if (me?.role === "admin" && auth.isServerMode()) {
for (const u of auth.listUsers()) {
if (u.id === me.id || u.disabled) continue;
const home = homeOf(u);
if (home && home !== homeOf(me)) {
out.push({ id: u.username, label: u.displayName || u.username, readOnly: true });
}
}
}
return out;
}
/**
* `{ dir, readOnly, ownerId }` for home `who` of `req`, or a thrown ENOHOME
* (404) — never another user's for a non-admin.
*/
function resolveHome(req, who) {
const none = () => Object.assign(new Error("no such home"), { code: "ENOHOME" });
const me = req.user || null;
if (who === "me") {
const dir = homeOf(me);
if (!dir) throw none();
return { dir: path.resolve(dir), readOnly: false, ownerId: me?.id ?? null };
}
if (me?.role !== "admin" || !auth.isServerMode()) throw none();
const u = auth.listUsers().find((x) => x.username === String(who) && !x.disabled);
const dir = u && homeOf(u);
if (!dir) throw none();
return { dir: path.resolve(dir), readOnly: true, ownerId: u.id };
}
module.exports = { homeOf, listHomes, resolveHome };