package web import ( "log/slog" "net/http" "strings" transporthttp "tug/internal/transport/http" "tug/internal/transport/web/qrsvg" "tug/internal/transport/web/text" ) const ( pageBudget = 50 * 1024 oneTimeBadgesShown = 10 ) var drawCode = func(secret Ident) *qrsvg.Symbol { code, err := qrsvg.Encode(string(secret)) if err != nil { slog.Error("one-time page drawn without its code", "err", err) return nil } return &code } func invitationMessage(site, expires, link string) string { s, _ := text.Lookup(text.OneTimeInvitationMessage) return strings.NewReplacer("{site}", site, "{expires}", expires, "{link}", link).Replace(s) } type oneTimeView struct { Title text.Key Subject Ident Secret Ident Label string Expires Ident Message string Code *qrsvg.Symbol Groups []Ident GroupsMore int Back string BackText text.Key } func shownBadges(groups []Ident) ([]Ident, int) { if len(groups) <= oneTimeBadgesShown { return groups, 0 } return groups[:oneTimeBadgesShown], len(groups) - oneTimeBadgesShown } func newOneTimeView(o OneTime) oneTimeView { 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} v.Groups, v.GroupsMore = shownBadges(o.Groups) return v } type OneTimeBatchRow struct { Label string URL Ident Message string } type OneTimeBatch struct { Rows []OneTimeBatchRow Expires Ident Groups []Ident } type oneTimeBatchRowView struct { Label string URL Ident Code *qrsvg.Symbol } type oneTimeBatchView struct { Rows []oneTimeBatchRowView Messages string Expires Ident Groups []Ident GroupsMore int LeftOut int } func newOneTimeBatchView(b OneTimeBatch) oneTimeBatchView { v := oneTimeBatchView{Expires: b.Expires} v.Groups, v.GroupsMore = shownBadges(b.Groups) var messages strings.Builder for _, row := range b.Rows { v.Rows = append(v.Rows, oneTimeBatchRowView{Label: row.Label, URL: row.URL}) messages.WriteString(row.Label + "\n" + row.Message + "\n\n") } v.Messages = messages.String() return v } func (v oneTimeBatchView) withCodes(codes []*qrsvg.Symbol, keep int) oneTimeBatchView { rows := make([]oneTimeBatchRowView, len(v.Rows)) copy(rows, v.Rows) for i := range rows { rows[i].Code = nil if i < keep && i < len(codes) { rows[i].Code = codes[i] } } v.Rows = rows v.LeftOut = len(rows) - min(keep, len(rows)) return v } func (k *Kit) linkedBytes(p page) int { n := 0 size := func(path string) int { return len(k.assets.byPath[strings.TrimPrefix(path, transporthttp.SSOAssetPrefix)].body) } if p.Styled { n += size(p.Assets.CSS) + size(p.Assets.Icon) } if p.Script { n += size(p.Assets.JS) } return n } func (k *Kit) adminPage(data any) page { return page{Styled: true, Script: k.enhancement, Assets: k.assets.names, Data: data} } func (k *Kit) RenderOneTimeBatch(w http.ResponseWriter, r *http.Request, b OneTimeBatch) { p := k.newPage(w, r, nil) v := newOneTimeBatchView(b) codes := make([]*qrsvg.Symbol, len(v.Rows)) for i, row := range v.Rows { codes[i] = drawCode(row.URL) } for keep := len(codes); ; keep-- { p.Data = v.withCodes(codes, keep) body, err := k.execute("onetime-batch", p) if err != nil { k.renderFailed(w, r, "onetime-batch", err) return } if keep == 0 || len(body)+k.linkedBytes(p) < pageBudget { if keep < len(codes) { slog.Info("batch page drawn without some codes to stay within the page budget", "rows", len(codes), "codes_left_out", len(codes)-keep) } k.write(w, http.StatusOK, body) return } } }