Burn after N reads: reads_limit/reads_used, per-viewer 15min dedupe via paste_views, reads_remaining in API+stats pill, raw counts as read (#49)
CI / test (push) Successful in 21s
CI / docker (push) Skipped

This commit is contained in:
2026-09-08 23:54:03 -05:00
parent 127c12c79a
commit d5a47b1a31
5 changed files with 287 additions and 21 deletions
+12 -7
View File
@@ -100,7 +100,7 @@ func expiryString(expiresAt int64) string {
}
}
func (a *apiServer) renderPaste(w http.ResponseWriter, row *PasteRow, justCreated bool, deletionToken string) {
func (a *apiServer) renderPaste(w http.ResponseWriter, row *PasteRow, justCreated bool, deletionToken string, readsRemaining *int) {
lines := strings.Count(row.Content, "\n") + 1
gutter := ""
for i := 1; i <= lines; i++ {
@@ -137,6 +137,9 @@ func (a *apiServer) renderPaste(w http.ResponseWriter, row *PasteRow, justCreate
"ExpiresAt": row.ExpiresAt.Valid,
"ExpiresIn": expIn,
"DeletionToken": deletionToken,
"ReadsLimit": row.ReadsLimit.Valid,
"ReadsLeftN": readsRemaining, // *int: reads remaining after this view
"ReadsTotal": int(row.ReadsLimit.Int64),
"JustCreated": justCreated,
"Host": "this host",
}
@@ -181,7 +184,7 @@ func (a *apiServer) handlePasteView(w http.ResponseWriter, r *http.Request) {
return
}
}
a.renderPaste(w, row, false, "")
a.renderPaste(w, row, false, "", nil)
return
}
renderPage(w, "unlock.html", map[string]any{"Page": "unlock", "ID": row.ID, "Wrong": true, "CreatedAgo": agoString(row.CreatedAt), "CreatedAtUnix": row.CreatedAt})
@@ -195,19 +198,21 @@ func (a *apiServer) handlePasteView(w http.ResponseWriter, r *http.Request) {
}
}
a.store.IncrementViews(row.ID)
justCreated := r.URL.Query().Get("created") == "1"
token := r.URL.Query().Get("token")
if justCreated && token != "" {
// one-time display of the deletion token via the created banner
http.SetCookie(w, &http.Cookie{Name: "tok_" + row.ID, Value: token, Path: "/", MaxAge: 60, HttpOnly: true, SameSite: http.SameSiteLaxMode})
}
// only pass the token to the template right after creation
if justCreated {
a.renderPaste(w, row, true, token)
// #49: burn-after-N-reads budget (per-viewer, 15-minute dedupe window).
// Just-created first render does not count as a read for the creator.
if !justCreated {
rem, _ := a.store.registerRead(row, currentViewerID(r))
a.renderPaste(w, row, false, "", rem)
return
}
a.renderPaste(w, row, false, "")
// only pass the token to the template right after creation
a.renderPaste(w, row, true, token, nil)
}
var _ = strconv.Itoa