mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
main
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
94091a8795 |
fix(hub): device binding is a provider contract, not a BuiltinAuth field (#147)
ownJournal refuses a journal write unless the device id is bound to the caller's account, for EVERY provider — it asks only whether s.Devices is nil. The only thing that creates that binding is DeviceRegistry.Bind, whose only caller is BuiltinAuth.finishLogin, and that hook was wired behind `if a, ok := s.Auth.(*BuiltinAuth); ok`. So a hub running a managed AuthProvider — the deployment the seam exists for — bound nothing, ever, and refused every journal push from every device forever. Everything around it read healthy: /api/auth/me answered, project permissions said write, and blobs (content-addressed, so ownerless) uploaded fine. Only the journal PUT died. Signing in again could not help, because signing in was the step that was supposed to bind. UseDeviceBinder moves the hook onto the AuthProvider interface — a breaking change for an out-of-tree provider, deliberately, so one that ignores a precondition of a gate the hub enforces for it does not compile. The hub cannot bind on the provider's behalf: a bind must be reachable only from a completed authentication, and Authenticate reports who a request is, never which credential class it presented. A device token still cannot reach a bind; no new door was added, and every /store/* door still creates nothing. Bind also reported success for a row the store had refused — observeLocked logged the write failure and swallowed it — so a login could hand back a token whose every push was then denied, with nothing in the hub explaining why. It now propagates and the login fails honestly. And a hub that refuses a journal write while holding no binding at all logs that its provider is not calling the binder — the sentence that would have ended this investigation on day one instead of day two. Tested by driving the real binary against a hub with a managed provider (cli_provider_e2e_test.go), which is the configuration no test in this repo covered and the reason this shipped: every existing test used BuiltinAuth, where the wiring happened to work. Both directions are pinned — a provider that binds pushes, one that ignores the binder reproduces the reported symptom exactly. Claude-Session: https://claude.ai/code/session_01GSHsQU4pBCzKkPyPeXSwTm Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dfef5720df |
webapp: organizations behind a Directory seam
The hub already abstracted authentication — AuthProvider, with BuiltinAuth as the built-in implementation — and then reached around that seam three times: Accounts() was declared on neither interface, admin.go type-asserted *BuiltinAuth (five handlers silently degraded to 404/empty under any other provider), and organizations were not on the seam at all. That last gap had teeth. A deployment whose identities come from elsewhere had no way to own its orgs, so the code that did own them wrote into the hub's OrgDB from the side — and nothing stopped the hub from inventing an org that the identity system had never heard of. One did: a hub-created org held every project while the mirrored one sat empty, and no sync path could see the difference. Directory (directory.go) is where organizations live now. LocalDirectory wraps today's OrgDB unchanged — same last-owner protection, same normEmail, same "o-"+randHex(4) ids, same file/SQL persistence — so a self-hosted hub behaves exactly as before. A deployment whose orgs are owned elsewhere implements the same interface, returns ErrManagedElsewhere from the write half, and the handlers answer 409 with ManageURL. The hub never learns why a write was refused, only where to send the user. Two rules shape the interface. Reads are on the request path: Role runs on every project request, including the /store/* endpoints a device hits every few seconds with a token that carries no identity claims, so an implementation backed by a remote system answers from its own cache — and that cache is its business, not the hub's. Writes are optional, because "this hub owns its orgs" is a deployment fact, not a code path. - Server.Orgs *OrgDB becomes Server.Dir Directory: 28 call sites, 8 nil-checks, one writeDirErr helper for the 409 translation. - /api/orgs gains manage_url per org — the destination of the account menu's Settings entry. The client follows a link and never branches on which kind of hub it is talking to. - Org administration becomes a real route, /orgs/<id>, retiring one of the two URL-less panels CLAUDE.md grandfathers. When a directory's ManageURL is not hub-local, the SPA fallback redirects there instead — so a hub that cannot administer its orgs cannot paint a console whose every control 409s. - Accounts() moves onto AuthProvider. admin.go's type assertion becomes an optional AccountApprover, and a provider without one now answers 503 rather than an empty approval queue: "no queue here" and "queue is empty" are different answers and only one of them was true. Two reviews drove the rest. The architecture review caught a browser page load that could delete org members (a display read ran the full membership reconcile, and a 200 with an empty user list evicted everyone), one write site that escaped the 409 translation, and a webhook that could wedge an event stream behind an unappliable event. The design review, over eight rounds, caught the org page rendering live controls on a hub that cannot use them, a share link made unrevokable by a long filename, nine keyboard tab stops parked off-screen behind a closed drawer, and — five separate times — a fix of mine that looked right in the source and did nothing in the browser. Conformance tests run both a writable and a read-only implementation against one contract; the seat, prune, and out-of-order regressions each have a test written to fail against the old code. |
||
|
|
baad9e6dc2 |
feat(hub): signup gating, admin lifecycle, and onboarding UX
Makes a self-hosted hub safe to expose on a public URL and operable without hand-editing JSON — addressing the blocker/major findings from the persona usability evaluations. Signup gating (config auth block, all optional): - allowed_domains: signup email must match (e.g. only @runbear.io) - require_verification: email-link activation before sign-in (reuses mailer) - require_approval: hub admins approve new accounts (admins list) - brand shown on the sign-in page; allow_signup:false already hid Sign up Accounts carry a Status (active/unverified/pending); non-active accounts cannot authenticate. Admin lifecycle (endpoints + web UI): - org: rename, member role change, member remove (last-owner guarded), invite list + revoke - project: create (web), rename, delete (from the org panel) - hub admins: approve/deny pending signups (sidebar bell + panel) - org-wide public-share audit with revoke UX: onboarding empty-state (explains invites, paste-invite + create-project) instead of a blank sidebar; visible "Search ⌘K" button; toasts replace blocking alert(); responsive layout with an off-canvas sidebar; joining via #join now survives a logged-out click (token carried through login). Web uploads are attributed to the signed-in account, not the server. Login/signup are rate-limited per IP. Tests: domain/verification/approval gates, auth rate limit, org+project lifecycle, owner-only guards, invite→join→role→remove over HTTP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
2d7f2f8bfa |
feat: auth, move-proof projects, interactive init/login, web history
Authentication (previous phase, now landed together with its follow-ups): - Email+password+name accounts behind an AuthProvider interface; the OSS server ships BuiltinAuth only (file-backed auth.json: bcrypt password hashes + SHA-256 token digests, plaintext never stored; server-owned /auth/* pages; managed deployments can swap in another provider). - bdrive login: loopback-callback browser flow (sign-up on the page, the terminal finishes itself) with a device-code fallback for headless machines; long-lived revocable device tokens in settings.json. - Password reset via plain SMTP (stdlib) with a log-link fallback when no SMTP is configured. Move-proof projects: - .bdrive is now a directory; config.json carries a stable mount id. The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed by that id — never the folder path — so renames/moves are free. - The daemon re-reads the project config each tick and exits cleanly (propagating nothing) when its folder vanishes; the registry self-heals and the next bdrive command at the new location resumes with zero spurious changes. bdrive init is the front door (mnt/umnt removed; bdrive stop pauses): - Interactive on a TTY (create new / connect existing project from the server's list; whole folder / shared subfolder via the include list), full flag bypass (--name/--project/--shared/--yes), never prompts without a TTY. Runs the login flow first when there is no session. Default server: beardrive.ai (config.DefaultServer). Web history (revert-ready): - Hubs now always require auth; journal ops carry the signed-in account (user/user_name) alongside the git/OS fallback author. - File-backed device registry: per-device name, OS, account, and the public IP the server observed, joined into history at read time. - GET /api/p/<id>/history?path=|prefix= (newest first) and GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained forever, so the next phase's revert is re-putting an old blob. - UI: History button (file versions or project feed), per-folder history shortcut, view/download of any past version. Tests: auth flows (callback, device-code, reset single-use, persistence, gating), history API + device registry, folder-move survival, registry self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |