~bigbes/sr-ht-dolt

ref: ba34443357cb2471337ae8faa7dee369bdbcae69 sr-ht-dolt/web/router.go -rw-r--r-- 4.4 KiB
ba344433 — Eugene Blikh feat(remoteapi): auto-create databases on first push to own namespace 30 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
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
}

// 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 {
	if cfg.Repos == nil || cfg.Stores == nil || cfg.Browse == nil ||
		cfg.Users == nil || cfg.RepoDiskPath == nil {
		return fmt.Errorf("web: Register requires Repos, Stores, Browse, Users and RepoDiskPath")
	}

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

	a := &app{
		cfg:       cfg,
		templates: templates,
		styleHref: discoverStyleHref(cfg.StaticDir),
	}

	r.Get("/", a.handleIndex)
	r.Get("/create", a.handleCreateForm)
	r.Post("/create", a.handleCreate)

	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}/settings", a.handleSettings)
	r.Post("/~{user}/{db}/settings", a.handleSettingsPost)

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

	return nil
}

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