Files
beardrive/internal/webapp/directory.go
Snow LeeandClaude Fable 5 e68bd17013 feat(hub): deleting a project or an org purges its files from storage
Deleting a project used to retire only the registry row; its blobs and
journals stayed in the object store forever. Now DELETE /api/projects/{id}
also revokes the project's share links, evicts its cached volume, and
purges everything under its storage prefix through a new optional
remote.Deleter capability (file://, s3://, gs:// — never the https://
client, which must not delete remote objects). The purge is best effort:
registry first, so a storage error leaves orphaned objects (the old
status quo), never a half-deleted project.

Org deletion is new: DELETE /api/orgs/{org} (owners only) drops the org
row first — so an externally managed directory refuses with the usual
409 before anything irreversible happens — then cascades the same
project deletion over everything the org owned. Directory gained
Delete(orgID) so a managed provider has to answer it at compile time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 11:47:52 -07:00

69 lines
3.1 KiB
Go

package webapp
import (
"errors"
"time"
)
// Directory is where a hub's organizations live. The built-in one is this
// package's OrgDB (LocalDirectory); a deployment whose users, orgs and
// memberships are owned by an external identity system supplies its own,
// alongside its AuthProvider.
//
// Two rules shape this interface:
//
// Reads are on the request path. Role is called for every project request,
// including the /store/* sync endpoints a device hits every few seconds with
// a device token that carries no identity claims. An implementation backed by
// a remote system therefore has to answer Role from a local cache, and that
// cache is the implementation's business — the hub does not keep one, does not
// refresh one, and must never be written to from the side. (It used to be:
// the hub owned the mirror and the auth provider poked at it, which is how a
// hub-invented org that the identity system had never heard of could exist.)
//
// Writes are optional. A directory that does not own its data returns
// ErrManagedElsewhere and the handler answers 409 with ManageURL, so the hub
// never needs to know WHY it cannot write — only where the user should go.
type Directory interface {
// ---- reads (request path) ----
Role(orgID, email string) string
Get(orgID string) (Org, bool)
OrgsFor(email string) []Org
ListInvites(orgID string) []OrgInvite
ValidInvite(token string) bool
// ManageURL is where this org is administered: a path within this hub
// when it owns its orgs, an external page when it does not. The client
// follows it and never has to know which kind of hub it is talking to.
ManageURL(orgID string) string
// ---- writes (ErrManagedElsewhere when the directory is read-only) ----
Create(name, ownerEmail string) (Org, error)
Rename(orgID, name string) error
// Delete removes the org and its invites. Its projects are the hub's to
// cascade (registry rows and storage) — the directory knows nothing of
// project storage.
Delete(orgID string) error
AddMember(orgID, email, role string) error
SetRole(orgID, email, role string) error
RemoveMember(orgID, email string) error
CreateInvite(orgID, creator string, ttl time.Duration) (OrgInvite, error)
RevokeInvite(token string) bool
Redeem(token string) (OrgInvite, bool)
RecordInviteUse(token string)
}
// ErrManagedElsewhere is returned by a directory that does not own its
// organizations. Handlers turn it into 409 plus the org's ManageURL — the
// request was well-formed, it is the state of the world that makes it wrong.
var ErrManagedElsewhere = errors.New("this organization is managed outside this hub")
// LocalDirectory is the built-in directory: organizations owned by this hub,
// stored in its own metadata store. This is what every self-hosted install
// runs, and its behavior is exactly OrgDB's — the type exists to add the one
// thing an org store has no opinion about, which is where to send a browser
// to administer an org.
type LocalDirectory struct{ *OrgDB }
// ManageURL is the hub's own org page (a route in the frontend).
func (LocalDirectory) ManageURL(orgID string) string { return "/orgs/" + orgID }