feat: add visitor identity cookie and database reset job
Three connected features for the deployment-strategy demo: 1. Visitor identity cookie (v3). A new visitors table backs a first-party gb_visitor cookie: middleware on /api checks the cookie's UUID against the database on every request and regenerates + persists it when the id is missing, malformed, or unknown (fresh browser, pruned identity, or a reset database). The identity remembers the name a visitor signed with (pre-filled next time, used for unsigned posts), replaces the standalone reaction cookie for reaction de-dupe, and feeds a live "N visiting" counter (identities seen in the last 5 minutes) shown on the wall, in /api/info, the admin overview, and the presence/tick SSE events. The admin console gains Visitors/Visiting tiles and a visitors table, plus a GET /api/me endpoint and GET /api/admin/visitors. 2. guestbook reset command. Wipes entries (including soft-deleted rows), reactions and visitor identities under the schema advisory lock, with --dry-run, --hard (also reset wall settings) and --reseed flags; meant to run as a one-off Miabi Job. After a reset every outstanding cookie is invalidated, so browsers get a fresh identity on their next request. 3. cleanup learns --prune-visitors (default 30 days): the cron job now also forgets idle identities together with their reactions. Docs (README, DEMO) updated: act 8 covers reset, the visitor-identity talking point is in act 3, and the API tables list the new endpoints. Akili-Task: tsk_ptdi5ufndrzwlljk4v5g Akili-Agent: goldfish
This commit is contained in:
+5
-37
@@ -1,12 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jkaninda/okapi"
|
||||
"gorm.io/gorm"
|
||||
@@ -14,37 +11,8 @@ import (
|
||||
|
||||
// Reactions let visitors cheer a signature without an account: one tap adds,
|
||||
// tapping again removes, and the tally streams to every open tab over SSE.
|
||||
// A first-party cookie identifies the browser so a count can't be inflated by
|
||||
// hammering the endpoint.
|
||||
|
||||
const (
|
||||
reactionCookie = "gb_reactor"
|
||||
reactionCookieTTL = 365 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// reactorID returns the stable, anonymous id of this browser, setting the
|
||||
// cookie on first sight.
|
||||
func reactorID(c *okapi.Context) string {
|
||||
if id, err := c.Cookie(reactionCookie); err == nil && len(id) >= 8 && len(id) <= 64 {
|
||||
return id
|
||||
}
|
||||
buf := make([]byte, 16)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return ""
|
||||
}
|
||||
id := hex.EncodeToString(buf)
|
||||
r := c.Request()
|
||||
http.SetCookie(c.ResponseWriter(), &http.Cookie{
|
||||
Name: reactionCookie,
|
||||
Value: id,
|
||||
Path: "/",
|
||||
MaxAge: int(reactionCookieTTL.Seconds()),
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
return id
|
||||
}
|
||||
// The visitor identity cookie (see visitors.go) identifies the browser, so a
|
||||
// count can't be inflated by hammering the endpoint.
|
||||
|
||||
// parseReactionRequest validates the {id} path param and the ?emoji= query.
|
||||
func parseReactionRequest(c *okapi.Context) (uint64, string, error) {
|
||||
@@ -69,7 +37,7 @@ func (h *Handler) React(c *okapi.Context) error {
|
||||
if _, err := h.store.Get(ctx, id); errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.JSON(http.StatusNotFound, okapi.M{"error": "entry not found"})
|
||||
}
|
||||
who := reactorID(c)
|
||||
who := visitorFromContext(c).ID
|
||||
if who == "" {
|
||||
return c.JSON(http.StatusInternalServerError, okapi.M{"error": "could not identify the browser"})
|
||||
}
|
||||
@@ -88,7 +56,7 @@ func (h *Handler) Unreact(c *okapi.Context) error {
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, okapi.M{"error": err.Error()})
|
||||
}
|
||||
who := reactorID(c)
|
||||
who := visitorFromContext(c).ID
|
||||
if who == "" {
|
||||
return c.JSON(http.StatusInternalServerError, okapi.M{"error": "could not identify the browser"})
|
||||
}
|
||||
@@ -101,7 +69,7 @@ func (h *Handler) Unreact(c *okapi.Context) error {
|
||||
}
|
||||
|
||||
// countRequests is middleware that tallies the HTTP requests this replica has
|
||||
// answered since it started. The number lands in /healthz, /api/info and the
|
||||
// answered since boot. The number lands in /healthz, /api/info and the
|
||||
// admin replica table: it grows per replica under load-balancing and drops
|
||||
// back to ~0 when a rollout replaces a container, which makes recreate and
|
||||
// rolling updates visible from the outside.
|
||||
|
||||
Reference in New Issue
Block a user