coverage~bigbes/sr-ht-doltede3b0bbweb/deps.go

Coverage
100.0% 2/2 statements
Δ
Blob
b6eaae5
Uncovered nothing — every instrumented line ran
1 // Package web is the HTTP layer of dolt.sr.ht: the chi router, request
2 // handlers, and the html/template views for the database dashboard, browse
3 // pages, settings and dolt-key management.
4 //
5 // # The chrome is not ours
6 //
7 // The brand, the service switcher, the login block and the environment banner
8 // come from sourcecraft.dev/bigbes/sr-ht-ecore/chrome, the one copy every
9 // custom service on this instance draws from. This package builds a single
10 // chrome.Service at startup (newApp), asks it for a chrome.Page per request
11 // (app.page), and embeds that Page in each handler's view struct so the shared
12 // partials find their fields on the dot they are handed. Nothing here rebuilds
13 // the switcher, re-derives a login URL or re-reads our own origin: the copies
14 // that used to live in web/chrome.go are what ecore exists to have deleted.
15 //
16 // # Dependency injection
17 //
18 // web is deliberately decoupled from the packages that touch Postgres, disk and
19 // the remotesapi. It depends directly only on the pure/committed packages it
20 // renders (core, browse) and authn (for the caller in the request context).
21 // Everything with side effects — the metadata store, the on-disk store manager,
22 // the browse opener, and username resolution against meta — is reached through
23 // SMALL local interfaces declared here and satisfied by thin adapters (see
24 // adapters.go for the production wiring, and the tests for fakes). This keeps
25 // httptest coverage free of Postgres and dolt internals, and lets the Phase-3
26 // main assemble the real Config without web importing storage/ or remoteapi/.
27 package web
28
29 import (
30 "context"
31
32 "github.com/vaughan0/go-ini"
33
34 "sourcecraft.dev/bigbes/sr-ht-core/auth"
35
36 "sourcecraft.dev/bigbes/sr-ht-dolt/authn"
37 "sourcecraft.dev/bigbes/sr-ht-dolt/browse"
38 "sourcecraft.dev/bigbes/sr-ht-dolt/core"
39 "sourcecraft.dev/bigbes/sr-ht-dolt/db"
40 )
41
42 // Config carries everything the router and handlers need. The Phase-3 main
43 // builds one and passes it to Register.
44 type Config struct {
45 // Conf is the shared instance config (the same ini.File every *.sr.ht
46 // service reads). Used to render the nav/chrome and resolve origins.
47 Conf ini.File
48 // ReposRoot is the absolute directory holding the bare NBS stores, one per
49 // database at <ReposRoot>/~<owner>/<name>. Passed to StoreManager.DeleteStore
50 // as the containment root.
51 ReposRoot string
52 // StaticDir is the directory holding built static assets (the hashed
53 // main.min.<sha>.css and logo.svg). The CSS filename is discovered from it at
54 // Register time; "" falls back to the dev stylesheet /static/main.css.
55 StaticDir string
56
57 // Stores manages the on-disk NBS chunk stores. Satisfied in production by a
58 // storage-backed adapter (Phase 3); web never imports storage/.
59 Stores StoreManager
60 // Repos is the metadata store (repositories, ACLs, dolt keys). Satisfied in
61 // production by dbAdapter over db.Store; fakes are used in tests.
62 Repos RepoStore
63 // Browse opens read-only handles to bare stores for the browse pages.
64 // Satisfied in production by browseAdapter over browse.Open.
65 Browse BrowseOpener
66 // Users resolves a SourceHut username to its account (for ACL add-by-username),
67 // mirroring the meta profile on first sight. Satisfied in production by a
68 // core-go auth.LookupUser adapter.
69 Users UserResolver
70 // Git resolves the description of the owner's same-named git.sr.ht
71 // repository, so companion databases mirror it (see handleInternalCreate).
72 // nil disables mirroring entirely (tests, instances without git.sr.ht).
73 Git GitDescriber
74 // RepoDiskPath returns the absolute on-disk store dir for owner/name. In
75 // production this is storage.RepoDiskPath bound to ReposRoot.
76 RepoDiskPath func(owner, name string) string
77 }
78
79 // StoreManager is the on-disk store lifecycle the create/delete handlers drive.
80 // It mirrors the storage package's InitStore/DeleteStore functions and the
81 // Cache.Evict method; web declares it as an interface so it never imports
82 // storage/.
83 type StoreManager interface {
84 // InitStore creates a bare store at absPath and writes an empty repo authored
85 // by ownerName/ownerEmail. On any failure it must leave no partial store.
86 // That empty repo is an "Initialize data repository" commit, so it is the
87 // opt-in half of creation: see InitEmptyStore for why it is not the default.
88 InitStore(ctx context.Context, absPath, ownerName, ownerEmail string) error
89 // InitEmptyStore creates a bare store at absPath with NO commits and NO
90 // branches, so a client's first push lands as the initial history instead of
91 // being rejected as a non-fast-forward. dolt decides fast-forward on the
92 // client (actions.CanFastForward over the remotesapi), so an initial commit
93 // on our side cannot be forgiven by the server — it can only be not written.
94 // The cost is that a store with no commits cannot be dolt-cloned at all
95 // ("remote at that url contains no Dolt data"), which is why the empty
96 // overview page teaches push rather than clone. On any failure it must leave
97 // no partial store.
98 InitEmptyStore(ctx context.Context, absPath string) error
99 // DeleteStore removes the store at absPath, refusing anything outside root.
100 DeleteStore(ctx context.Context, root, absPath string) error
101 // MoveStore relocates the store at srcPath to dstPath — the on-disk half of
102 // a rename — refusing anything outside root and never overwriting an
103 // existing destination.
104 MoveStore(ctx context.Context, root, srcPath, dstPath string) error
105 // Evict closes and drops any memoized served handle for diskPath, so a
106 // recreation at the same path never reuses a stale store.
107 Evict(diskPath string) error
108 }
109
110 // RepoStore is the subset of db.Store the handlers use. Declaring it as an
111 // interface lets tests inject a fake without Postgres; the production dbAdapter
112 // (adapters.go) is a compile-time-checked implementation over the real store.
113 // Every method takes ctx first; the production adapter reads the request-scoped
114 // *sql.DB from ctx (db.FromContext) so a single adapter value serves all
115 // requests.
116 type RepoStore interface {
117 CreateRepo(ctx context.Context, r *core.Repo) (*core.Repo, error)
118 GetRepoByOwnerAndName(ctx context.Context, ownerUsername, name string) (*core.Repo, error)
119 ListReposByOwner(ctx context.Context, ownerUsername string, viewer *core.Caller) ([]*core.Repo, error)
120 // ListReposForViewer lists every database viewer may be shown, across all
121 // owners. It is the enumeration the cross-database ready page is built on:
122 // ListReposByOwner asks the same listing question about one owner, and
123 // ListReposForDashboard omits every PUBLIC database belonging to somebody
124 // else. Listing is not authorization — /ready still asks core.Allowed per
125 // database before it opens anything.
126 ListReposForViewer(ctx context.Context, viewer *core.Caller) ([]*core.Repo, error)
127 ListReposForDashboard(ctx context.Context, userID int) ([]*core.Repo, error)
128 UpdateRepo(ctx context.Context, id int, description string, visibility core.Visibility) error
129 // RenameRepo moves the row to a new name and on-disk path together; the
130 // store on disk is moved separately by StoreManager.MoveStore.
131 RenameRepo(ctx context.Context, id int, name, path string) error
132 DeleteRepo(ctx context.Context, id int) error
133
134 EffectiveAccess(ctx context.Context, userID, repoID int) (*core.AccessMode, error)
135 ListACL(ctx context.Context, repoID int) ([]*db.ACLEntry, error)
136 UpsertACL(ctx context.Context, repoID, userID int, mode core.AccessMode) error
137 DeleteACL(ctx context.Context, repoID, userID int) error
138
139 InsertKey(ctx context.Context, userID int, kid string, pubkey []byte, comment string) (*db.DoltKey, error)
140 ListKeysByUser(ctx context.Context, userID int) ([]*db.DoltKey, error)
141 DeleteKey(ctx context.Context, id, userID int) error
142 }
143
144 // BrowseSession is the read-only browse surface a single request uses. It is
145 // exactly the method set of *browse.DB (plus Close), so the production adapter
146 // returns a *browse.DB directly. Fakes implement it for httptest.
147 type BrowseSession interface {
148 Branches(ctx context.Context) ([]browse.Branch, error)
149 Log(ctx context.Context, refStr, fromHash string, limit int) ([]browse.CommitInfo, string, error)
150 Tables(ctx context.Context, refStr string) ([]browse.TableInfo, error)
151 // TableHash is the content hash of one table at a ref, ok=false when the
152 // table does not exist there. It reads no rows, and only the Memory view
153 // asks for it: its revision walk uses the hash to skip every commit that did
154 // not touch config, so a board never pays for a history walk it does not do.
155 TableHash(ctx context.Context, refStr, table string) (string, bool, error)
156 Rows(ctx context.Context, refStr, table string, offset, limit int) (*browse.RowPage, error)
157 CommitSummary(ctx context.Context, hashStr string) (*browse.CommitDiff, error)
158 Close() error
159 }
160
161 // BrowseOpener opens a BrowseSession over the bare store at diskPath. Open must
162 // be paired with Session.Close by the caller (handlers defer it).
163 type BrowseOpener interface {
164 Open(ctx context.Context, diskPath string) (BrowseSession, error)
165 }
166
167 // GitDescriber looks up the description of owner's same-named repository on
168 // git.sr.ht. ok=false means it could not be resolved — no git twin, git.sr.ht
169 // unreachable — and the caller must leave the stored description alone;
170 // ("", true) means the twin exists and has no description.
171 type GitDescriber interface {
172 Description(ctx context.Context, owner, name string) (desc string, ok bool)
173 }
174
175 // UserResolver resolves a username to a core account, mirroring the meta
176 // profile into the local user table on first sight (so the resolved UserID can
177 // be used as an ACL grantee). Returns an error the caller treats as "no such
178 // user" for a permanent miss.
179 type UserResolver interface {
180 LookupUser(ctx context.Context, username string) (*core.Caller, error)
181 }
182
183 // authContext aliases core-go's auth.AuthContext for brevity in handler
184 // signatures; it is the authenticated caller (nil = anonymous).
185 type authContext = auth.AuthContext
186
187 // callerOf returns the resolved caller for a request context: the raw
188 // *auth.AuthContext (nil = anonymous) for chrome rendering, and the pure
189 // core.Caller for the access-control matrix. It is the single bridge from the
190 // authn context value to the domain types used throughout the handlers.
191 229 func callerOf(ctx context.Context) (*auth.AuthContext, *core.Caller) {
192 229 ac := authn.CallerFromContext(ctx)
193 229 return ac, authn.AsCoreCaller(ac)
194 229 }