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
}