mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
* refactor(auth): one CLI sign-in flow for every provider (webapp.CLIAuth)
The `bdrive login` surface — /auth/cli, /auth/device/<token>, the approval
page both show, /api/auth/exchange and /api/auth/device/{start,poll} — moves
out of BuiltinAuth into its own type. A provider supplies the two things
that actually differ: who the browser session is, and how a device token is
minted.
Nothing changes for a self-hosted hub; this is the same code behind the same
paths. It moves because the managed hub's provider carries its own copy, and
the copy drifted: months after the OSS flow moved to a single approval link
naming the device, that hub was still printing a four-byte code to retype
into a text box. Sharing the implementation is the only fix that stays fixed.
BuiltinAuth's own grant map now holds just what it should: verification and
password-reset links.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72
* fix(architecture): make the webapp-server diagram parse again
Two mermaid syntax errors, so GitHub rendered the first block as an error
box instead of a diagram:
- the CLIAuth class listed its routes as bare lines, and the `{` in
/auth/device/{token} opens a struct inside a class body — the routes are a
note now, where prose belongs;
- `note for` strings escaped quotes as \" (mermaid has no backslash escapes,
so the string ended early). Pre-existing, in the DirectUploader and
Project notes; both use " now, like the </> already in there.
Checked by parsing every block in architecture/*.md with mermaid 11.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.5 KiB
9.5 KiB
bdrive serve server — class diagram
Source of truth: internal/webapp (server, services, persistence) and
internal/remote (storage backends). Reflects the code as of this commit;
update this file in any PR that changes these types or their relationships.
Server core, sources, and services
classDiagram
direction LR
class Server {
+Source Source
+Volume string
+Root remote.Backend
+Projects *ProjectDB
+Device Identity
+Refresh time.Duration
+Upload UploadConfig
+Auth AuthProvider
+Devices *DeviceRegistry
+Shares *ShareDB
+Reads *ReadLedger
+Dir Directory
+Quota QuotaProvider
+Billing func(email) (plan, url, ok)
+Analytics AnalyticsConfig
+ShareRPM int
-vols per-project volume cache
+Handler() http.Handler
}
class volume {
-source Source
-refresh time.Duration
-snap *snapshot
+snapshot(ctx)
+invalidate()
}
class Source {
<<interface>>
+Files(ctx) map path→FileInfo
+Open(ctx, path, fi) io.ReadCloser
}
class DirSource {
+Dir string
}
class RemoteSource {
+Backend remote.Backend
+Device Identity
+Remove(ctx, path, who, note)
-appendOp(ctx, op)
}
class Uploader {
<<interface>>
+Upload(ctx, path, r, size, who)
}
class DirectUploader {
<<interface>>
+SignBlobPut(ctx, blob, size, ttl)
+HasBlob(ctx, blob)
+Commit(ctx, path, blob, size, who, note)
}
note for DirectUploader "Commit's note is "" for an upload and "restore <path>@<sha8>" for POST /api/p/{id}/restore — which is the upload commit minus the upload: find the historical op for (path, sha), journal a NEW put at its blob. Never rewrites a journal."
note for RemoteSource "Every write ends at appendOp: stamp Seq/Lamport/Time + this server's Identity, append ONE op to journal/<own-device>.jsonl. Commit does that for a put; Remove (POST /api/p/{id}/remove, restore's gates + a snapshot existence check) does it for a delete — the only server path that takes a file away, and itself undone by restoring the DELETED row."
class Backend {
<<interface>>
+Put +Get +List +Exists +Close
}
class PutSigner {
<<interface>>
+SignPut(ctx, key, size, ttl)
}
note for Backend "internal/remote — impls: localBackend (file://), s3Backend, gcsBackend, httpBackend (https:// hub), Prefixed wrapper"
class AuthProvider {
<<interface>>
+CLILoginPath()
+Authenticate(r) User
+Register(mux)
+Accounts() []User
}
class AccountApprover {
<<interface>>
+PendingUsers() +Approve +Deny +SetPolicy +Policy
}
class BuiltinAuth {
+AllowSignup bool
+AllowedDomains
+RequireVerification bool
+RequireApproval bool
+Admins
+InviteValid func(token)
-store AccountRepo
-users, tokens, pending
-cli CLIAuth
}
class CLIAuth {
+Register(mux)
-session func(r) User
-issue func(w, user, device)
-pending map~cliGrant~
}
note for CLIAuth "The paths bdrive login POSTs by name, served the same way for every provider: /auth/cli, /auth/device/<token>, /api/auth/exchange, /api/auth/device/start, /api/auth/device/poll."
class Mailer
class User {
+ID +Email +Name +Admin
}
class Directory {
<<interface>>
+Role(org, email)
+Get +OrgsFor +ListInvites +ValidInvite +ManageURL
+Create +Rename +AddMember +SetRole +RemoveMember
+CreateInvite +RevokeInvite +Redeem
}
class LocalDirectory {
+ManageURL(orgID)
}
class OrgDB {
-repo OrgRepo
-byID, invites
}
class Org {
+ID +Name +Members email→role +Created
}
class OrgInvite {
+Token +Org +Creator +Expires +Uses
}
class ProjectDB {
-repo ProjectRepo
-byID
+Get +Create +Update +Rename +List
+SetCreator +SetDefault
+SetPerm +ClearPerm
}
class Project {
+ID +Name +Org +Created
+Description +Icon
+Creator string
+Default string
+Perms map email→level
}
note for Project "Default == "" means write — the historical behavior, so an upgraded hub needs no migration. SetPerm/ClearPerm refuse to drop the last explicit admin."
class projectPerm {
<<resolver>>
org owner → admin
explicit grant → that level
org member → project Default
otherwise → none
}
note for projectPerm "perms.go — the single authorization ladder. proj(level, h) in server.go is the one choke point: every per-project route declares its level at registration."
class ShareDB {
-repo ShareRepo
-byToken
+Create +Get +Revoke +SetExpiry
}
class Share {
+Token +Project +Path +Creator +Expires
}
class DeviceRegistry {
-repo DeviceRepo
-byID
+Observe(DeviceInfo)
}
class DeviceInfo {
+ID +Name +OS +User +IP +LastSeen
}
class ReadLedger {
-repo ReadRepo
-retention
-byKey, dirty, seen
+Record(...)
+Heat(project, prefix, days)
}
class ReadStat {
+Project +Path +Day +Kind +Actor +Count +Last
}
class HeatEntry {
+Human +Agent +Share +Readers +LastRead
}
class QuotaProvider {
<<interface>>
+CheckWrite(org, bytes)
+CheckSeat(org, members)
+RecordUsage(org, bytes)
}
class UnlimitedQuota
class AnalyticsConfig {
+Key string
+Host string
+Endpoint() string
}
note for AnalyticsConfig "Third managed-deployment seam beside Quota and Billing, but a value rather than an interface — there is nothing to implement, only a project to name. Emitted as /api/config `analytics` when Key is set; empty means the frontend loads no tracker and contacts nobody, which is what a self-hosted hub gets. Endpoint() is exported because the cloud module renders its own loader from the same value."
Server o-- "0..1" Source : single-volume mode
Server o-- "0..1" Backend : Root (hub mode)
Server o-- ProjectDB
Server o-- AuthProvider
Server o-- Directory
Server o-- DeviceRegistry
Server o-- ShareDB
Server o-- ReadLedger
Server o-- QuotaProvider
Server *-- AnalyticsConfig
Server *-- volume : per project, cached
volume o-- Source
Source <|.. DirSource
Source <|.. RemoteSource
Uploader <|-- DirectUploader
DirectUploader <|.. RemoteSource
RemoteSource o-- Backend : Prefixed(Root, projectID)
Backend <|-- PutSigner : optional capability
AuthProvider <|.. BuiltinAuth
AccountApprover <|.. BuiltinAuth
BuiltinAuth *-- CLIAuth : serves bdrive login
BuiltinAuth o-- Mailer : nil → log links
AuthProvider ..> User
Directory <|.. LocalDirectory
LocalDirectory *-- OrgDB : embeds
OrgDB ..> Org
OrgDB ..> OrgInvite
BuiltinAuth ..> OrgDB : InviteValid wiring
ProjectDB ..> Project
Server *-- projectPerm : gates every per-project route
projectPerm ..> Project : Perms + Default
projectPerm ..> Directory : org role
ShareDB ..> Share
DeviceRegistry ..> DeviceInfo
ReadLedger ..> ReadStat
ReadLedger ..> HeatEntry
QuotaProvider <|.. UnlimitedQuota
Metadata persistence (MetaStore)
Service structs keep in-memory maps + logic; every change persists as one record through a typed repo. Blobs and journals never touch this layer.
classDiagram
direction LR
class MetaStore {
<<interface>>
+Accounts() AccountRepo
+Projects() ProjectRepo
+Orgs() OrgRepo
+Shares() ShareRepo
+Devices() DeviceRepo
+Reads() ReadRepo
+Close()
}
class AccountRepo {
<<interface>>
+Load() +PutAccount +DeleteAccount +PutToken +DeleteToken +PutPolicy
}
class ProjectRepo {
<<interface>>
+Load() +Put +Delete
}
class OrgRepo {
<<interface>>
+Load() +PutOrg +DeleteOrg +PutInvite +DeleteInvite
}
class ShareRepo {
<<interface>>
+Load() +Put +Delete
}
class DeviceRepo {
<<interface>>
+Load() +Put
}
class ReadRepo {
<<interface>>
+Load() +PutBatch +DeleteBatch
}
note for ReadRepo "batch-oriented: one flush = one write"
class fileMetaStore {
JSON files, atomic rewrite per change
}
class sqlMetaStore {
one database/sql impl
sqlite (modernc) or postgres (pgx)
+addColumns() idempotent ALTER
}
note for sqlMetaStore "ProjectRepo.Put is transactional over projects + project_perms (same shape as orgs + org_members); addColumns probes the live column set so a running hub gains projects.creator / default_level on restart."
MetaStore <|.. fileMetaStore
MetaStore <|.. sqlMetaStore
MetaStore *-- AccountRepo
MetaStore *-- ProjectRepo
MetaStore *-- OrgRepo
MetaStore *-- ShareRepo
MetaStore *-- DeviceRepo
MetaStore *-- ReadRepo
class BuiltinAuth
class ProjectDB
class OrgDB
class ShareDB
class DeviceRegistry
class ReadLedger
BuiltinAuth o-- AccountRepo
ProjectDB o-- ProjectRepo
OrgDB o-- OrgRepo
ShareDB o-- ShareRepo
DeviceRegistry o-- DeviceRepo
ReadLedger o-- ReadRepo