~bigbes/sr-ht-dolt

ref: 3843eb55bacf238a627efe60a88763af293d2d7d sr-ht-dolt/web/deps.go -rw-r--r-- 9.0 KiB
3843eb55 — Eugene Blikh remoteapi: prove an empty store takes an unrelated first push 3 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// Package web is the HTTP layer of dolt.sr.ht: the chi router, request
// handlers, and the html/template views for the database dashboard, browse
// pages, settings and dolt-key management.
//
// # The chrome is not ours
//
// The brand, the service switcher, the login block and the environment banner
// come from sourcecraft.dev/bigbes/sr-ht-ecore/chrome, the one copy every
// custom service on this instance draws from. This package builds a single
// chrome.Service at startup (newApp), asks it for a chrome.Page per request
// (app.page), and embeds that Page in each handler's view struct so the shared
// partials find their fields on the dot they are handed. Nothing here rebuilds
// the switcher, re-derives a login URL or re-reads our own origin: the copies
// that used to live in web/chrome.go are what ecore exists to have deleted.
//
// # 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 <ReposRoot>/~<owner>/<name>. Passed to StoreManager.DeleteStore
	// as the containment root.
	ReposRoot string
	// StaticDir is the directory holding built static assets (the hashed
	// main.min.<sha>.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)
	// ListReposForViewer lists every database viewer may be shown, across all
	// owners. It is the enumeration the cross-database ready page is built on:
	// ListReposByOwner asks the same listing question about one owner, and
	// ListReposForDashboard omits every PUBLIC database belonging to somebody
	// else. Listing is not authorization — /ready still asks core.Allowed per
	// database before it opens anything.
	ListReposForViewer(ctx context.Context, 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)
	// TableHash is the content hash of one table at a ref, ok=false when the
	// table does not exist there. It reads no rows, and only the Memory view
	// asks for it: its revision walk uses the hash to skip every commit that did
	// not touch config, so a board never pays for a history walk it does not do.
	TableHash(ctx context.Context, refStr, table string) (string, bool, 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)
}