~bigbes/sr-ht-dolt

ref: 2ee66fb89c83da809c295f2b8bb52f630e55f629 sr-ht-dolt/web/router.go -rw-r--r-- 5.6 KiB
2ee66fb8 — Eugene Blikh feat(web/beads): epic view with subtask rollup, tabbed Comments/History 29 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
package web

import (
	"fmt"
	"net/http"

	"github.com/go-chi/chi/v5"

	"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)

// app bundles the parsed templates, the discovered stylesheet href and the
// injected config. Handlers are methods on *app so they share this state
// without a global.
type app struct {
	cfg       Config
	templates templateSet
	styleHref string
	// views is a snapshot of the global registeredViews taken at Register time.
	// Handlers read this (never the global) so tests can inject their own set.
	views []View
}

// Register mounts every dolt.sr.ht web route onto r. The caller (the Phase-3
// main) installs the config/database/cookie middleware upstream on the router
// group it passes here, then calls Register with the assembled Config.
//
// It parses templates and discovers the stylesheet once, at registration time,
// so a broken template fails startup loudly rather than a request later. A
// parse failure returns an error the caller must surface.
func Register(r chi.Router, cfg Config) error {
	a, err := newApp(cfg)
	if err != nil {
		return err
	}
	a.mount(r)
	return nil
}

// newApp validates cfg, parses templates and snapshots the view registry into a
// ready *app. Register uses it; tests build an *app directly so they can inspect
// and override its fields (e.g. app.views) before mounting.
func newApp(cfg Config) (*app, error) {
	if cfg.Repos == nil || cfg.Stores == nil || cfg.Browse == nil ||
		cfg.Users == nil || cfg.RepoDiskPath == nil {
		return nil, fmt.Errorf("web: Register requires Repos, Stores, Browse, Users and RepoDiskPath")
	}

	templates, err := loadTemplates()
	if err != nil {
		return nil, err
	}

	return &app{
		cfg:       cfg,
		templates: templates,
		styleHref: discoverStyleHref(cfg.StaticDir),
		// Snapshot the registry so all handlers see a stable set and tests can
		// override it per-app without mutating the global.
		views: append([]View{}, registeredViews...),
	}, nil
}

// mount installs every dolt.sr.ht web route onto r. Split from Register so tests
// can mount an *app they retain a handle to.
func (a *app) mount(r chi.Router) {
	r.Get("/", a.handleIndex)
	r.Get("/create", a.handleCreateForm)
	r.Post("/create", a.handleCreate)

	// Service-to-service companion provisioning (git.sr.ht post-update hook).
	// Guarded by internal-network + network-key auth, not the cookie/CSRF the
	// browser routes use.
	r.With(internalAuthGuard).Post("/internal/repos", a.handleInternalCreate)

	r.Get("/settings/keys", a.handleKeys)
	r.Post("/settings/keys", a.handleKeysPost)

	r.Get("/~{user}", a.handleUser)
	r.Get("/~{user}/{db}", a.handleOverview)
	r.Get("/~{user}/{db}/log", a.handleLog)
	r.Get("/~{user}/{db}/commit/{hash}", a.handleCommit)
	r.Get("/~{user}/{db}/tree/{ref}", a.handleTree)
	r.Get("/~{user}/{db}/table/{ref}/{table}", a.handleTable)
	r.Get("/~{user}/{db}/view/{view}", a.handleView)
	r.Get("/~{user}/{db}/settings", a.handleSettings)
	r.Post("/~{user}/{db}/settings", a.handleSettingsPost)

	r.Handle("/static/*", httpStaticHandler(a.cfg.StaticDir))
}

// --- shared response helpers -------------------------------------------------

// notFound renders the 404 page. Used both for genuinely missing repos and to
// hide the existence of PRIVATE repos the caller may not browse.
func (a *app) notFound(w http.ResponseWriter, r *http.Request) {
	view := struct {
		basePage
	}{basePage: a.newBasePage(r, "Not found — "+serviceName)}
	a.render(w, http.StatusNotFound, "404.html", view)
}

// forbidden renders the 403 page for a denied but non-hidden request.
func (a *app) forbidden(w http.ResponseWriter, r *http.Request, msg string) {
	view := struct {
		basePage
		Message string
	}{basePage: a.newBasePage(r, "Forbidden — "+serviceName), Message: msg}
	a.render(w, http.StatusForbidden, "403.html", view)
}

// redirectLogin sends an unauthenticated caller to meta's login, returning them
// to the current URL afterwards.
func (a *app) redirectLogin(w http.ResponseWriter, r *http.Request) {
	http.Redirect(w, r, a.newBasePage(r, "").LoginURL, http.StatusSeeOther)
}

// loadRepoForBrowse loads the repo named by the {user}/{db} URL params and
// enforces read (OpBrowse) authorization. On any denial it writes the response
// (404 for hidden PRIVATE repos, 403 otherwise) and returns ok=false. On
// success it returns the repo, the (possibly nil) caller and the caller's ACL
// grant for reuse by the handler.
func (a *app) loadRepoForBrowse(w http.ResponseWriter, r *http.Request) (repo *core.Repo, caller *core.Caller, aclMode *core.AccessMode, ok bool) {
	owner := chi.URLParam(r, "user")
	name := chi.URLParam(r, "db")

	_, caller = callerOf(r.Context())

	repo, err := a.cfg.Repos.GetRepoByOwnerAndName(r.Context(), owner, name)
	if err != nil {
		// A missing repo is reported as not found regardless of the caller.
		a.notFound(w, r)
		return nil, nil, nil, false
	}

	aclMode = a.effectiveACL(r, caller, repo)
	if !core.Allowed(caller, repo, aclMode, core.OpBrowse) {
		if core.NotFoundForPrivate(caller, repo, aclMode) {
			a.notFound(w, r)
		} else {
			a.forbidden(w, r, "You do not have access to this database.")
		}
		return nil, nil, nil, false
	}
	return repo, caller, aclMode, true
}

// effectiveACL resolves the caller's ACL grant on repo, or nil for an anonymous
// caller or a caller with no grant. A lookup error degrades to nil (no grant):
// access then falls back to visibility, which never over-grants.
func (a *app) effectiveACL(r *http.Request, caller *core.Caller, repo *core.Repo) *core.AccessMode {
	if caller == nil {
		return nil
	}
	mode, err := a.cfg.Repos.EffectiveAccess(r.Context(), caller.UserID, repo.ID)
	if err != nil {
		return nil
	}
	return mode
}