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}