| 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 |
} |