Files
beardrive/internal/webapp/quota.go
T
Snow LeeandClaude Fable 5 94ef66a4a9 feat(hub): organizations — projects walled by team membership, with invites
Every hub project now belongs to exactly one org (file-backed orgs.json,
same load-at-open + atomic-rewrite discipline as the other registries).
Membership (email -> owner|member) gates every per-project route: the
viewer APIs, uploads, history, blobs, shares management, and the device
sync store proxy; /api/projects lists only your orgs' projects, and
project names are now scoped per org. Public share links (/s/) stay
public by design.

Design choices, per the simplest-consistent rule:
- Migration: a pre-org hub sweeps all org-less projects into a "default"
  org at startup; ALL existing accounts join it (they could all see every
  project before, so anything narrower would lock someone out), oldest
  account as owner. Zero manual steps.
- An account in no org that creates a project gets a fresh org named
  after itself, so nobody is ever blocked from starting to sync.
- Invites are expiring multi-use links (default 7 days): an owner mints
  /#join/<token>, any signed-in account that opens it joins as member.
  The web UI shows the org in a sidebar footer (members on click,
  Invite button for owners).

bdrive init needed no changes: its connect-existing flow lists projects
through the now-filtered API.

Tests: OrgDB + migration units; a 403/access matrix over every
per-project route; invite mint/redeem flow; and a multi-device syncer
test proving a device holding an org-B token can neither pull org A's
files nor push into its store (sync degrades to Offline, never partial).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 17:03:48 -07:00

33 lines
1.3 KiB
Go

package webapp
// QuotaProvider is the seam a managed deployment uses to enforce plan
// limits, exactly like AuthProvider is the seam for identity. The
// open-source server ships only UnlimitedQuota; billing and plan logic live
// outside this repo. Hooks fire on every write path (browser uploads, the
// device sync store proxy) and on seat growth, keyed by org id.
type QuotaProvider interface {
// CheckWrite runs before addedBytes land in the org's storage; a non-nil
// error rejects the write (surfaced to the client as 403).
CheckWrite(org string, addedBytes int64) error
// CheckSeat runs before an invite adds a member; members is the current
// count. A non-nil error rejects the join.
CheckSeat(org string, members int) error
// RecordUsage runs after a write succeeds, for accounting.
RecordUsage(org string, addedBytes int64)
}
// UnlimitedQuota is the open-source default: everything is allowed.
type UnlimitedQuota struct{}
func (UnlimitedQuota) CheckWrite(string, int64) error { return nil }
func (UnlimitedQuota) CheckSeat(string, int) error { return nil }
func (UnlimitedQuota) RecordUsage(string, int64) {}
// quota returns the configured provider, defaulting to unlimited.
func (s *Server) quota() QuotaProvider {
if s.Quota != nil {
return s.Quota
}
return UnlimitedQuota{}
}