package storage import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) type AccessLogRepo struct { pool *pgxpool.Pool } func NewAccessLogRepo(db *DB) *AccessLogRepo { return &AccessLogRepo{pool: db.Pool} } type CreateAccessLogParams struct { GuestID uuid.UUID TokenID uuid.UUID Fingerprint map[string]any IPAddress string } func (r *AccessLogRepo) Create(ctx context.Context, p CreateAccessLogParams) (uuid.UUID, error) { var fpJSON []byte if p.Fingerprint != nil { b, err := json.Marshal(p.Fingerprint) if err != nil { return uuid.Nil, fmt.Errorf("marshal fingerprint: %w", err) } fpJSON = b } var ip *string if p.IPAddress != "" { ip = &p.IPAddress } const q = ` INSERT INTO access_logs (guest_id, token_id, fingerprint, ip_address) VALUES ($1, $2, $3, $4::inet) RETURNING id ` var id uuid.UUID err := r.pool.QueryRow(ctx, q, p.GuestID, p.TokenID, fpJSON, ip).Scan(&id) return id, err } type ApplyScoreParams struct { AccessLogID uuid.UUID Score int Reasons []string Flagged bool } // AccessCheckActivity is a scored access-log entry joined with the guest's // name. Used by the activity-history endpoint so dashboards can show // historical security checks (including blocked attempts) even when nobody // was watching the live monitor at the time. type AccessCheckActivity struct { GuestID uuid.UUID GuestName string Score int Reasons []string Flagged bool CreatedAt time.Time } // ListRecentScoredByEvent returns scored access-log entries for an event, // newest first. Unscored entries (someone opened the page but the fraud // engine hasn't replied yet) are excluded — they'd be noise on the feed. func (r *AccessLogRepo) ListRecentScoredByEvent(ctx context.Context, eventID uuid.UUID, limit int) ([]AccessCheckActivity, error) { if limit <= 0 || limit > 200 { limit = 50 } const q = ` SELECT a.guest_id, g.name, a.risk_score, a.risk_reasons, a.flagged, a.created_at FROM access_logs a JOIN guests g ON g.id = a.guest_id WHERE g.event_id = $1 AND a.risk_score IS NOT NULL ORDER BY a.created_at DESC LIMIT $2 ` rows, err := r.pool.Query(ctx, q, eventID, limit) if err != nil { return nil, err } defer rows.Close() var out []AccessCheckActivity for rows.Next() { var ( a AccessCheckActivity reasons []string score int16 ) if err := rows.Scan(&a.GuestID, &a.GuestName, &score, &reasons, &a.Flagged, &a.CreatedAt); err != nil { return nil, err } a.Score = int(score) a.Reasons = reasons out = append(out, a) } return out, rows.Err() } func (r *AccessLogRepo) ApplyScore(ctx context.Context, p ApplyScoreParams) error { const q = ` UPDATE access_logs SET risk_score = $2, risk_reasons = $3, flagged = $4 WHERE id = $1 ` tag, err := r.pool.Exec(ctx, q, p.AccessLogID, p.Score, p.Reasons, p.Flagged) if err != nil { return err } if tag.RowsAffected() == 0 { return errors.New("access_log not found") } return nil }