~bigbes/sr-ht-compare

ref: a4853d05281d40832e011b6f03a558c7555b997d sr-ht-compare/web/server.go -rw-r--r-- 4.4 KiB
a4853d05 — Eugene Blikh rename module to sourcecraft.dev/bigbes/sr-ht-compare; depend on sourcecraft sr-ht-core 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
// Package web is the HTTP layer of compare.sr.ht. It ports the SourceHut chrome
// (nav/service-switcher, login block, environment banner) to Go html/templates,
// renders the repository landing, compare (base...head) and single-commit pages
// server-side, and embeds a compact JSON payload plus the vendored esbuild
// bundle so the browser renders the diff with @pierre/diffs and @pierre/trees.
//
// The package owns no state of its own: identity comes from the authz cookie
// middleware, authorization from an authz.Authorizer (git.sr.ht GraphQL), and
// git data from gitx over bare repositories on disk. Every request that touches
// a repository authorizes first (a not-found or forbidden repo is a 404, never
// a 403, so private-repo existence never leaks) and only then reads the disk.
//
// # What the cmd layer must wire
//
// Register only installs routes; it assumes the following middleware is already
// applied to the router it is handed, in this order (outermost first):
//
//	chi middleware.RealIP
//	chi middleware.Recoverer
//	chi middleware.Logger        (optional, but recommended)
//	config.Middleware(conf, "compare.sr.ht")   // required: authz + gitx read it
//	authz.Middleware()           // required: never 401s; sets the viewer
//
// config.Middleware must run before authz.Middleware is irrelevant to authz
// itself (it only reads the cookie), but the GraphQL authorizer invoked inside
// handlers needs config.ForContext(ctx) to resolve git.sr.ht's API origin, so
// config.Middleware is mandatory on every request that reaches a handler.
package web

import (
	"fmt"
	"io/fs"
	"net/http"
	"path"
	"regexp"

	"sourcecraft.dev/bigbes/sr-ht-core/config"
	"github.com/vaughan0/go-ini"

	"sourcecraft.dev/bigbes/sr-ht-compare/authz"
)

// hashedCSSRe matches the content-addressed stylesheet name so it can be served
// with an immutable cache lifetime (the hash changes whenever the bytes do).
var hashedCSSRe = regexp.MustCompile(`^main\.min\.[0-9a-f]{6,}\.css$`)

// Server holds the immutable configuration a request handler needs. It is built
// once at startup and is safe for concurrent use.
type Server struct {
	authorizer authz.Authorizer
	reposRoot  string
	conf       ini.File

	siteName      string
	environment   string
	metaOrigin    string
	compareOrigin string
	hubOrigin     string
	cssHref       string

	nav              []navItem
	staticFileServer http.Handler
}

// New assembles a Server from the shared SourceHut config. It reads
// [git.sr.ht] repos, [meta.sr.ht] origin and [compare.sr.ht] origin (all
// required), the [sr.ht] site-name/environment display values, and resolves the
// hashed stylesheet name by globbing the embedded static FS. A missing required
// key is a clear error, not a panic, so the cmd layer can fail startup loudly.
func New(conf ini.File, authorizer authz.Authorizer) (*Server, error) {
	reposRoot, ok := conf.Get("git.sr.ht", "repos")
	if !ok || reposRoot == "" {
		return nil, fmt.Errorf("web: [git.sr.ht] repos is required")
	}
	metaOrigin := config.GetOrigin(conf, "meta.sr.ht", true)
	if metaOrigin == "" {
		return nil, fmt.Errorf("web: [meta.sr.ht] origin is required")
	}
	compareOrigin := config.GetOrigin(conf, "compare.sr.ht", true)
	if compareOrigin == "" {
		return nil, fmt.Errorf("web: [compare.sr.ht] origin is required")
	}

	cssHref, err := resolveCSSHref()
	if err != nil {
		return nil, err
	}

	staticSub, err := fs.Sub(staticFS, "static")
	if err != nil {
		return nil, fmt.Errorf("web: sub static FS: %w", err)
	}

	return &Server{
		authorizer:       authorizer,
		reposRoot:        reposRoot,
		conf:             conf,
		siteName:         config.GetString(conf, "sr.ht", "site-name", "sourcehut"),
		environment:      config.GetString(conf, "sr.ht", "environment", "production"),
		metaOrigin:       metaOrigin,
		compareOrigin:    compareOrigin,
		hubOrigin:        config.GetOrigin(conf, "hub.sr.ht", true),
		cssHref:          cssHref,
		nav:              buildNav(conf),
		staticFileServer: http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))),
	}, nil
}

// resolveCSSHref globs the embedded static FS for the content-addressed
// stylesheet and returns its site-absolute URL.
func resolveCSSHref() (string, error) {
	matches, err := fs.Glob(staticFS, "static/main.min.*.css")
	if err != nil {
		return "", fmt.Errorf("web: glob stylesheet: %w", err)
	}
	if len(matches) == 0 {
		return "", fmt.Errorf("web: no main.min.*.css in embedded static assets")
	}
	return "/static/" + path.Base(matches[0]), nil
}