Raw
  1package web
  2
  3import (
  4	"log/slog"
  5	"net/http"
  6	"strings"
  7
  8	transporthttp "tug/internal/transport/http"
  9	"tug/internal/transport/web/qrsvg"
 10	"tug/internal/transport/web/text"
 11)
 12
 13const (
 14	pageBudget         = 50 * 1024
 15	oneTimeBadgesShown = 10
 16)
 17
 18var drawCode = func(secret Ident) *qrsvg.Symbol {
 19	code, err := qrsvg.Encode(string(secret))
 20	if err != nil {
 21		slog.Error("one-time page drawn without its code", "err", err)
 22		return nil
 23	}
 24	return &code
 25}
 26
 27func invitationMessage(site, expires, link string) string {
 28	s, _ := text.Lookup(text.OneTimeInvitationMessage)
 29	return strings.NewReplacer("{site}", site, "{expires}", expires, "{link}", link).Replace(s)
 30}
 31
 32type oneTimeView struct {
 33	Title      text.Key
 34	Subject    Ident
 35	Secret     Ident
 36	Label      string
 37	Expires    Ident
 38	Message    string
 39	Code       *qrsvg.Symbol
 40	Groups     []Ident
 41	GroupsMore int
 42	Back       string
 43	BackText   text.Key
 44}
 45
 46func shownBadges(groups []Ident) ([]Ident, int) {
 47	if len(groups) <= oneTimeBadgesShown {
 48		return groups, 0
 49	}
 50	return groups[:oneTimeBadgesShown], len(groups) - oneTimeBadgesShown
 51}
 52
 53func newOneTimeView(o OneTime) oneTimeView {
 54	v := oneTimeView{Title: o.Title, Subject: o.Subject, Secret: o.Secret, Label: o.Label, Expires: o.Expires, Message: o.Message, Code: drawCode(o.Secret), Back: o.Back, BackText: o.BackText}
 55	v.Groups, v.GroupsMore = shownBadges(o.Groups)
 56	return v
 57}
 58
 59type OneTimeBatchRow struct {
 60	Label   string
 61	URL     Ident
 62	Message string
 63}
 64
 65type OneTimeBatch struct {
 66	Rows    []OneTimeBatchRow
 67	Expires Ident
 68	Groups  []Ident
 69}
 70
 71type oneTimeBatchRowView struct {
 72	Label string
 73	URL   Ident
 74	Code  *qrsvg.Symbol
 75}
 76
 77type oneTimeBatchView struct {
 78	Rows       []oneTimeBatchRowView
 79	Messages   string
 80	Expires    Ident
 81	Groups     []Ident
 82	GroupsMore int
 83	LeftOut    int
 84}
 85
 86func newOneTimeBatchView(b OneTimeBatch) oneTimeBatchView {
 87	v := oneTimeBatchView{Expires: b.Expires}
 88	v.Groups, v.GroupsMore = shownBadges(b.Groups)
 89	var messages strings.Builder
 90	for _, row := range b.Rows {
 91		v.Rows = append(v.Rows, oneTimeBatchRowView{Label: row.Label, URL: row.URL})
 92		messages.WriteString(row.Label + "\n" + row.Message + "\n\n")
 93	}
 94	v.Messages = messages.String()
 95	return v
 96}
 97
 98func (v oneTimeBatchView) withCodes(codes []*qrsvg.Symbol, keep int) oneTimeBatchView {
 99	rows := make([]oneTimeBatchRowView, len(v.Rows))
100	copy(rows, v.Rows)
101	for i := range rows {
102		rows[i].Code = nil
103		if i < keep && i < len(codes) {
104			rows[i].Code = codes[i]
105		}
106	}
107	v.Rows = rows
108	v.LeftOut = len(rows) - min(keep, len(rows))
109	return v
110}
111
112func (k *Kit) linkedBytes(p page) int {
113	n := 0
114	size := func(path string) int {
115		return len(k.assets.byPath[strings.TrimPrefix(path, transporthttp.SSOAssetPrefix)].body)
116	}
117	if p.Styled {
118		n += size(p.Assets.CSS) + size(p.Assets.Icon)
119	}
120	if p.Script {
121		n += size(p.Assets.JS)
122	}
123	return n
124}
125
126func (k *Kit) adminPage(data any) page {
127	return page{Styled: true, Script: k.enhancement, Assets: k.assets.names, Data: data}
128}
129
130func (k *Kit) RenderOneTimeBatch(w http.ResponseWriter, r *http.Request, b OneTimeBatch) {
131	p := k.newPage(w, r, nil)
132	v := newOneTimeBatchView(b)
133	codes := make([]*qrsvg.Symbol, len(v.Rows))
134	for i, row := range v.Rows {
135		codes[i] = drawCode(row.URL)
136	}
137	for keep := len(codes); ; keep-- {
138		p.Data = v.withCodes(codes, keep)
139		body, err := k.execute("onetime-batch", p)
140		if err != nil {
141			k.renderFailed(w, r, "onetime-batch", err)
142			return
143		}
144		if keep == 0 || len(body)+k.linkedBytes(p) < pageBudget {
145			if keep < len(codes) {
146				slog.Info("batch page drawn without some codes to stay within the page budget", "rows", len(codes), "codes_left_out", len(codes)-keep)
147			}
148			k.write(w, http.StatusOK, body)
149			return
150		}
151	}
152}