~bigbes/sr-ht-compare

ref: 2ce47ce90a72fbe1f33bfd2ffb9eb76ea363cf42 sr-ht-compare/web/server.go -rw-r--r-- 5.3 KiB
2ce47ce9 — bigbes ci: cache Go module and build dirs via cacher 13 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
// 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"

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

	"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$`)

// hashedBundleRe matches the content-addressed frontend bundle. Like the
// stylesheet it carries a content hash in its name so a deploy busts the browser
// cache (a stale bundle.js is why the file tree can render blank after an
// upgrade); a matching name is served immutable.
var hashedBundleRe = regexp.MustCompile(`^bundle\.[0-9a-f]{6,}\.js$`)

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

	bundleHref, err := resolveBundleHref()
	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,
		bundleHref:       bundleHref,
		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
}

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