// Package web is the HTTP layer of dolt.sr.ht: the chi router, request // handlers, SourceHut nav/chrome, and the html/template views for the database // dashboard, browse pages, settings and dolt-key management. // // # Dependency injection // // web is deliberately decoupled from the packages that touch Postgres, disk and // the remotesapi. It depends directly only on the pure/committed packages it // renders (core, browse) and authn (for the caller in the request context). // Everything with side effects — the metadata store, the on-disk store manager, // the browse opener, and username resolution against meta — is reached through // SMALL local interfaces declared here and satisfied by thin adapters (see // adapters.go for the production wiring, and the tests for fakes). This keeps // httptest coverage free of Postgres and dolt internals, and lets the Phase-3 // main assemble the real Config without web importing storage/ or remoteapi/. package web import ( "context" "github.com/vaughan0/go-ini" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-dolt/authn" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" "sourcecraft.dev/bigbes/sr-ht-dolt/core" "sourcecraft.dev/bigbes/sr-ht-dolt/db" ) // Config carries everything the router and handlers need. The Phase-3 main // builds one and passes it to Register. type Config struct { // Conf is the shared instance config (the same ini.File every *.sr.ht // service reads). Used to render the nav/chrome and resolve origins. Conf ini.File // ReposRoot is the absolute directory holding the bare NBS stores, one per // database at /~/. Passed to StoreManager.DeleteStore // as the containment root. ReposRoot string // StaticDir is the directory holding built static assets (the hashed // main.min..css and logo.svg). The CSS filename is discovered from it at // Register time; "" falls back to the dev stylesheet /static/main.css. StaticDir string // Stores manages the on-disk NBS chunk stores. Satisfied in production by a // storage-backed adapter (Phase 3); web never imports storage/. Stores StoreManager // Repos is the metadata store (repositories, ACLs, dolt keys). Satisfied in // production by dbAdapter over db.Store; fakes are used in tests. Repos RepoStore // Browse opens read-only handles to bare stores for the browse pages. // Satisfied in production by browseAdapter over browse.Open. Browse BrowseOpener // Users resolves a SourceHut username to its account (for ACL add-by-username), // mirroring the meta profile on first sight. Satisfied in production by a // core-go auth.LookupUser adapter. Users UserResolver // Git resolves the description of the owner's same-named git.sr.ht // repository, so companion databases mirror it (see handleInternalCreate). // nil disables mirroring entirely (tests, instances without git.sr.ht). Git GitDescriber // RepoDiskPath returns the absolute on-disk store dir for owner/name. In // production this is storage.RepoDiskPath bound to ReposRoot. RepoDiskPath func(owner, name string) string } // StoreManager is the on-disk store lifecycle the create/delete handlers drive. // It mirrors the storage package's InitStore/DeleteStore functions and the // Cache.Evict method; web declares it as an interface so it never imports // storage/. type StoreManager interface { // InitStore creates a bare store at absPath and writes an empty repo authored // by ownerName/ownerEmail. On any failure it must leave no partial store. InitStore(ctx context.Context, absPath, ownerName, ownerEmail string) error // DeleteStore removes the store at absPath, refusing anything outside root. DeleteStore(ctx context.Context, root, absPath string) error // Evict closes and drops any memoized served handle for diskPath, so a // recreation at the same path never reuses a stale store. Evict(diskPath string) error } // RepoStore is the subset of db.Store the handlers use. Declaring it as an // interface lets tests inject a fake without Postgres; the production dbAdapter // (adapters.go) is a compile-time-checked implementation over the real store. // Every method takes ctx first; the production adapter reads the request-scoped // *sql.DB from ctx (db.FromContext) so a single adapter value serves all // requests. type RepoStore interface { CreateRepo(ctx context.Context, r *core.Repo) (*core.Repo, error) GetRepoByOwnerAndName(ctx context.Context, ownerUsername, name string) (*core.Repo, error) ListReposByOwner(ctx context.Context, ownerUsername string, viewer *core.Caller) ([]*core.Repo, error) ListReposForDashboard(ctx context.Context, userID int) ([]*core.Repo, error) UpdateRepo(ctx context.Context, id int, description string, visibility core.Visibility) error DeleteRepo(ctx context.Context, id int) error EffectiveAccess(ctx context.Context, userID, repoID int) (*core.AccessMode, error) ListACL(ctx context.Context, repoID int) ([]*db.ACLEntry, error) UpsertACL(ctx context.Context, repoID, userID int, mode core.AccessMode) error DeleteACL(ctx context.Context, repoID, userID int) error InsertKey(ctx context.Context, userID int, kid string, pubkey []byte, comment string) (*db.DoltKey, error) ListKeysByUser(ctx context.Context, userID int) ([]*db.DoltKey, error) DeleteKey(ctx context.Context, id, userID int) error } // BrowseSession is the read-only browse surface a single request uses. It is // exactly the method set of *browse.DB (plus Close), so the production adapter // returns a *browse.DB directly. Fakes implement it for httptest. type BrowseSession interface { Branches(ctx context.Context) ([]browse.Branch, error) Log(ctx context.Context, refStr, fromHash string, limit int) ([]browse.CommitInfo, string, error) Tables(ctx context.Context, refStr string) ([]browse.TableInfo, error) Rows(ctx context.Context, refStr, table string, offset, limit int) (*browse.RowPage, error) CommitSummary(ctx context.Context, hashStr string) (*browse.CommitDiff, error) Close() error } // BrowseOpener opens a BrowseSession over the bare store at diskPath. Open must // be paired with Session.Close by the caller (handlers defer it). type BrowseOpener interface { Open(ctx context.Context, diskPath string) (BrowseSession, error) } // GitDescriber looks up the description of owner's same-named repository on // git.sr.ht. ok=false means it could not be resolved — no git twin, git.sr.ht // unreachable — and the caller must leave the stored description alone; // ("", true) means the twin exists and has no description. type GitDescriber interface { Description(ctx context.Context, owner, name string) (desc string, ok bool) } // UserResolver resolves a username to a core account, mirroring the meta // profile into the local user table on first sight (so the resolved UserID can // be used as an ACL grantee). Returns an error the caller treats as "no such // user" for a permanent miss. type UserResolver interface { LookupUser(ctx context.Context, username string) (*core.Caller, error) } // authContext aliases core-go's auth.AuthContext for brevity in handler // signatures; it is the authenticated caller (nil = anonymous). type authContext = auth.AuthContext // callerOf returns the resolved caller for a request context: the raw // *auth.AuthContext (nil = anonymous) for chrome rendering, and the pure // core.Caller for the access-control matrix. It is the single bridge from the // authn context value to the domain types used throughout the handlers. func callerOf(ctx context.Context) (*auth.AuthContext, *core.Caller) { ac := authn.CallerFromContext(ctx) return ac, authn.AsCoreCaller(ac) }