feat(store): throttled per-day StampLogin + login_events delete cascade
StampLogin appends one login_events row per user per day via an atomic INSERT ... SELECT ... WHERE NOT EXISTS, run through withUser so the throttle probe is itself RLS-scoped to the caller. DeleteUser now deletes login_events explicitly (no FK = no cascade — the summary_actions footgun, repeated). Extends the two-user RLS isolation proof and the delete-account proof to cover login_events, and adds throttle / new-day / user-scoping tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// StampLogin records that the user was active today, throttled to one row per
|
||||
// user per day. It is the read-side counterpart to SetAction: the middleware
|
||||
// calls it on every authenticated request, but the append happens at most once a
|
||||
// day so login_events stays small and the signal clean (one row = one active
|
||||
// day, not one request).
|
||||
//
|
||||
// The check-and-insert is a single atomic statement: the INSERT ... SELECT ...
|
||||
// WHERE NOT EXISTS only writes when no row for this user has seen_at in today
|
||||
// (date_trunc('day', NOW()), server timezone). It runs through withUser, so the
|
||||
// NOT EXISTS probe is itself RLS-scoped to the calling user via the
|
||||
// tapir.current_user_id GUC — one user's stamp can never be suppressed or
|
||||
// triggered by another user's rows. The explicit user_id predicate also keeps the
|
||||
// probe on the (user_id, seen_at) index.
|
||||
//
|
||||
// A unique constraint is deliberately not used: under concurrent same-day
|
||||
// requests the worst case is two rows for one day, which the gate query collapses
|
||||
// to a single week bucket anyway — not worth a write-blocking constraint.
|
||||
func (s *Store) StampLogin(ctx context.Context, userID string) error {
|
||||
if err := s.withUser(ctx, userID, func(tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`INSERT INTO login_events (user_id)
|
||||
SELECT $1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM login_events
|
||||
WHERE user_id = $1 AND seen_at >= date_trunc('day', NOW())
|
||||
)`, userID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("store: stamp login: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user