e94038481e
- File Explorer (Files in the sidebar, /files; also from a workspace's Files tab, portalled so no panel clips it), laid out like GNOME Files: places (Home, workspaces, Trash), Back/Forward/Up, path bar, search, icon grid with image thumbnails or list, labelled New/Upload, selection action bar, hidden files behind Ctrl+H, right-click menus and the usual keys. Make, rename, move, copy (within a workspace, between workspaces, home ↔ workspace), delete into a per-account trash restorable for 7 days, streamed upload of files and whole folders (≤ 500 MB a batch, Replace / Keep both / Skip on a taken name), download a file or a zip of anything. server/lib/file-explorer.js, /api/lanes/:id/fs/*, /api/homes/:who/fs/*. - The user's home: every dot entry at its top stays hidden and refused (also via a symlink, download, zip or as a copy source); an admin may view other users' homes read-only, audited. - write-as learns move/copy (re-run `sudo agenthub server install-system`). - Hardening: a write to a workspace re-checks its owner by the live role; S3 keeps its server.conf if the file stops reading and a tmux call never falls back to the host's server; the AI Proxy gateway marks what it relays and the dashboard refuses it; a NUL in a pane's argv/env is refused. - OpenAPI for the new routes (server/openapi-extra/files.js); docs (API, SERVER-MODE, CLAUDE.md) updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.1 KiB
JavaScript
92 lines
3.1 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 }`.
|
|
* Others' only for an admin, and only those that resolve to a folder.
|
|
*/
|
|
function listHomes(req) {
|
|
const out = [];
|
|
const me = req.user || null;
|
|
if (homeOf(me))
|
|
out.push({ id: "me", label: me?.displayName || me?.username || "", readOnly: false });
|
|
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 };
|