feat: avatar by link, loaded by the browser; Files nests workspaces under owners' homes
- POST /api/me/avatar/url stores the https link (users.avatar_link) instead of downloading it; GET /api/me/avatar and /api/users/:id/avatar answer 302 to it (no referrer). Link rules: https, no userinfo, <= 2048 chars, no localhost or private/reserved IP literal. Upload and link replace each other. - Settings -> Account first loads the link as an embedded image and refuses one this browser cannot show: a picture behind a login (Mattermost's /api/v4/users/<id>/image) gets no cookie cross-site and 401s. - Files sidebar: each workspace under its owner's home, chevrons remembered per browser (agenthub.files.homesOpen); ownerless ones stay flat.
This commit is contained in:
+2
-2
@@ -112,7 +112,7 @@ An admin (server mode) adds an account with `POST /api/accounts` `{tool: "claude
|
||||
| `POST /api/me/sign-out-others` | `{ended}` — every login of the account but this one ends |
|
||||
| `GET /api/me/avatar` | server mode: the account's own picture (`nosniff`, `sandbox` CSP; the client adds `?v=<avatarAt>`), `404 ENOAVATAR` when it has none |
|
||||
| `PUT /api/me/avatar` (raw image bytes) | the account's picture (PNG, JPEG, GIF or WebP by magic bytes — never SVG), stored as `<data dir>/avatars/u<id>.<ext>`; the `/api/me/account` shape back with `user.avatarAt` set. `400 EBADIMAGE`, `413 ETOOBIG` over 1 MB, `403 ELOCALMODE`. Login cookie only, audited |
|
||||
| `POST /api/me/avatar/url` (`{url}`) | the same picture, downloaded by the SERVER from an `https://` link (no userinfo; every resolved address — an IP literal too — must be public, never loopback/private/link-local/CGNAT/reserved, and the connection is pinned to the vetted address; ≤ 3 redirects, each re-checked; 10 s timeout; ≤ 1 MB read; no cookie sent), then checked like an upload (`server/lib/avatar-url.js`). `400 EBADURL`/`EPRIVATEURL`/`EBADIMAGE`, `413 ETOOBIG`, `502 EFETCH`, `403 ELOCALMODE`. Login cookie only, audited |
|
||||
| `POST /api/me/avatar/url` (`{url}`) | the picture as a LINK: stored as typed (`users.avatar_link`, an `https://` URL with no userinfo, ≤ 2048 chars, not `localhost` or a loopback/private/link-local/reserved IP literal) and NEVER downloaded by the server — `GET /api/me/avatar` and `GET /api/users/:id/avatar` answer `302` to it (`Referrer-Policy: no-referrer`), so the viewer's browser loads it with its own logins and network (a Mattermost `/api/v4/users/<id>/image` link works). Replaces an uploaded picture, and an upload replaces it (`server/lib/avatar-url.js`). `400 EBADURL`/`EPRIVATEURL`, `403 ELOCALMODE`. Login cookie only, audited |
|
||||
| `DELETE /api/me/avatar` | removes it (`avatarAt` back to `null`); deleting the user removes it too |
|
||||
|
||||
The keys are the browser's own localStorage names: `theme`, `agenthub.theme.dark`, `agenthub.theme.light`, `i18nextLng`, `agenthub.terminal.font`, `agenthub.terminal.fontSize`, `agenthub.ui.fontSize`, `agenthub.pet`, `agenthub.pet.size`, `agenthub.pet.names`, `agent-dashboard-sagi-enabled`, `agent-dashboard-sagi-muted`, `agent-monitor-notifications`, `agenthub.defaultTool`, `agenthub.permissionMode` (`bypassPermissions` allowed — set only from Settings → AI tools → Session defaults, behind a confirm), `agenthub.codex.sandbox`, `agenthub.codex.approval`, `agenthub.codex.bypass` (`"true"`/`"false"`: a new Codex session's defaults), `agenthub.tour`, and `agenthub.chatUi` (`"1"` = Claude/Codex sessions open as a chat; read and written on the server only, by `client/src/lib/chatUi.ts`). The request never names an account, and writes are same-origin. An account's save writes only their own row, and under S3 a `theme` of `dark` or `light` also becomes the `theme` in their own `~/.claude/settings.json`, written as them.
|
||||
@@ -290,7 +290,7 @@ POST /api/lanes/:id/fs/upload-batch?dir=&conflict= length + JSON list + bytes
|
||||
GET /api/lanes/:id/fs/zip?p=<path>&p=… application/zip, streamed
|
||||
```
|
||||
|
||||
The workspace **File Explorer** (server mode only — a local dashboard has no **Files** entry, `/files` redirects home, and its buttons are hidden; the routes still answer) — the **Files** page in the sidebar (`/files?lane=<id>&path=<folder>`), laid out like GNOME Files (workspaces as places, Back/Forward, a path bar, icon grid or list, hidden files behind Ctrl+H), and the same manager in a dialog from a workspace's Files tab (the folder button; "Open in File Explorer" on a folder). `?session=` / `?source=` pick the same folder the
|
||||
The workspace **File Explorer** (server mode only — a local dashboard has no **Files** entry, `/files` redirects home, and its buttons are hidden; the routes still answer) — the **Files** page in the sidebar (`/files?lane=<id>&path=<folder>`), laid out like GNOME Files (workspaces as places — each nested under its owner's home, dropped down by that home's chevron (open homes kept per browser in `agenthub.files.homesOpen`), an owner whose home the viewer may not open (an analyst's view) under a row that only folds, one with no owner in a flat Workspaces list — Back/Forward, a path bar, icon grid or list, hidden files behind Ctrl+H), and the same manager in a dialog from a workspace's Files tab (the folder button; "Open in File Explorer" on a folder). `?session=` / `?source=` pick the same folder the
|
||||
git reads do. Paths go through the read path's guards (`lanePath`, the parent
|
||||
`resolveInLane`-confined, the last segment kept as a name so a symlink is acted
|
||||
on as a link); nothing in `.git`, no secret file, never the workspace folder
|
||||
|
||||
+4
-1
@@ -345,7 +345,10 @@ remembered, so the default is always the admin's own account.
|
||||
|
||||
Every signed-in user has **Settings → Account**: their username, role, member
|
||||
since, workspace and session counts and active logins; their own picture
|
||||
(PNG, JPEG, GIF or WebP up to 1 MB, kept in `<data dir>/avatars/`; shown with
|
||||
(PNG, JPEG, GIF or WebP up to 1 MB, kept in `<data dir>/avatars/`, or an
|
||||
https link to one, which the browser loads itself — never downloaded, so a
|
||||
link behind a login (a Mattermost `/api/v4/users/<id>/image`) cannot show and
|
||||
is refused when saved; shown with
|
||||
their name and role at the foot of the sidebar, which opens this page); their
|
||||
own display name and email; **Change password** (the current password is required, the login
|
||||
it is changed from stays and every other one ends); and **Sign out other
|
||||
|
||||
Reference in New Issue
Block a user