Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 55m ago 4b8c9e1d
history blame raw 3.9 KiB
1package web
2
3// Templates and static assets are embedded so an instance is still one file.
4//
5// Page templates are split by surface under templates/, one file per destination
6// (issues.html, requests.html, code.html). A page that HTMX can replace in part
7// keeps its fragments in the same file, so a fragment and the element it swaps
8// are read together. Four files hold what more than one surface uses:
9//
10// - chrome.html: the document shell, global bar, repository header, error page
11// - components.html: the label/assignee token input, the commit list, the
12// file tree, and the pager
13// - diff.html: the diff table, including its inline review comments
14// - role-picker.html: the per-account role control the settings surfaces use
15//
16// All page templates are parsed into one namespace at startup (web.New), so a
17// template name must be unique across the whole set: prefix a fragment with the
18// surface it belongs to (issue_, request_, code_, review_) rather than naming it
19// after the element it renders. A fragment that both surfaces render keeps the
20// shared name instead (comment_thread, comment_card, tree_nodes, pager).
21//
22// Static assets are served from /-/static/ and never inlined into a template. The
23// passkey script is a static asset too; it runs under the same CSP as the rest.
24// theme.js is loaded before the stylesheet on purpose: it applies the saved
25// Everforest variant, or the system light/dark medium default, to <html> before
26// the first paint. The account page owns the picker.
27//
28// Scripts vendored beside the assets that load them: htmx.min.js is loaded by the
29// Shell on every page; prism.min.js, prism-manifest.js and the grammars under
30// prism-languages/ are loaded by prism-worker.js, which app.js starts only on a
31// page with a blob to colour, and which imports a grammar only when a blob names
32// that language.
33
34import (
35 "bytes"
36 "crypto/sha256"
37 "embed"
38 "encoding/hex"
39 "io/fs"
40 "net/http"
41 "path"
42 "strings"
43 "time"
44)
45
46//go:embed templates/*.html
47var templateFS embed.FS
48
49//go:embed static/*
50var staticFS embed.FS
51
52// Static serves the embedded stylesheet, favicon, fonts, and scripts. Assets are
53// compiled into the binary, so there is no directory to deploy beside it.
54// Lexend and JetBrains Mono live under static/fonts with their OFL licenses.
55//
56// Each file carries a strong ETag derived from its bytes. An embedded file has
57// no modification time, so without a validator a browser re-downloads the
58// stylesheet and the Prism bundle on every code page; with one it revalidates
59// for a 304. Cache-Control is no-cache rather than immutable because a URL does
60// not change when the binary is upgraded.
61func Static() http.Handler { return embeddedStatic }
62
63var embeddedStatic = newStatic()
64
65func newStatic() http.Handler {
66 sub, err := fs.Sub(staticFS, "static")
67 if err != nil {
68 panic(err)
69 }
70 files := map[string]staticFile{}
71 err = fs.WalkDir(sub, ".", func(p string, d fs.DirEntry, err error) error {
72 if err != nil || d.IsDir() {
73 return err
74 }
75 body, err := fs.ReadFile(sub, p)
76 if err != nil {
77 return err
78 }
79 sum := sha256.Sum256(body)
80 files[p] = staticFile{body: body, etag: `"` + hex.EncodeToString(sum[:12]) + `"`}
81 return nil
82 })
83 if err != nil {
84 panic(err)
85 }
86 return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
87 f, ok := files[strings.TrimPrefix(r.URL.Path, "/-/static/")]
88 if !ok {
89 // No directory listing and no path traversal: the embedded set is the
90 // whole surface, and a miss is a 404.
91 http.NotFound(rw, r)
92 return
93 }
94 rw.Header().Set("ETag", f.etag)
95 rw.Header().Set("Cache-Control", "no-cache")
96 // ServeContent answers If-None-Match from the ETag above and picks the
97 // content type from the name.
98 http.ServeContent(rw, r, path.Base(r.URL.Path), time.Time{}, bytes.NewReader(f.body))
99 })
100}
101
102// staticFile is one embedded asset and the validator that names its bytes.
103type staticFile struct {
104 body []byte
105 etag string
106}
107