package themis import ( "context" "database/sql" "errors" "fmt" "time" "github.com/rs/zerolog/log" ) type EventType int const ( EventFlush EventType = iota EventClaim EventUnclaim EventAbsence ) func (et EventType) String() string { switch et { case EventFlush: return "FLUSH" case EventClaim: return "CLAIM" case EventUnclaim: return "UNCLAIM" case EventAbsence: return "ABSENT" default: return "" } } func EventTypeFromString(ev string) (EventType, error) { switch ev { case "FLUSH": return EventFlush, nil case "CLAIM": return EventClaim, nil case "UNCLAIM": return EventUnclaim, nil case "ABSENT": return EventAbsence, nil default: return EventType(9999), errors.New("no such event type") } } type AuditableEvent struct { userId string eventType EventType timestamp time.Time err error } // Audit writes to the audit table, returns nothing because it is meant to be // used in a defered statement on functions that write to the database. func (s *Store) Audit(ev *AuditableEvent) { if ev.err == nil { log.Debug().Str("event_type", ev.eventType.String()).Str("userid", ev.userId).Msg("recording audit log") ctx := context.Background() tx, err := s.db.Begin() if err != nil { log.Error().Err(err).Msg("failed to start transaction") } defer tx.Commit() //nolint:errcheck stmt, err := s.db.PrepareContext(ctx, "INSERT INTO audit_log (userid, event_type, ts) VALUES (?, ?, ?)") if err != nil { log.Error().Err(err).Msg("failed to prepare audit log insert") } if _, err := stmt.ExecContext(ctx, ev.userId, ev.eventType.String(), time.Now()); err != nil { log.Error().Err(err).Msg("failed to insert audit log") } } } type AuditEvent struct { Id int UserId string EventType EventType Timestamp time.Time } func (s *Store) LastOf(ctx context.Context, t EventType) (AuditEvent, error) { log.Debug().Str("event_type", t.String()).Msg("finding last audit log") stmt, err := s.db.PrepareContext(ctx, `SELECT id, userid, event_type, ts FROM audit_log WHERE event_type = ? ORDER BY ts DESC LIMIT 1`) if err != nil { return AuditEvent{}, fmt.Errorf("failed to get last event of type %s: %w", t.String(), err) } row := stmt.QueryRowContext(ctx, t.String()) ev := AuditEvent{} var rawEventType string err = row.Scan(&ev.Id, &ev.UserId, &rawEventType, &ev.Timestamp) if err == sql.ErrNoRows { return AuditEvent{}, ErrNever } if err != nil { return AuditEvent{}, fmt.Errorf("failed to scan row: %w", err) } ev.EventType, err = EventTypeFromString(rawEventType) if err != nil { return AuditEvent{}, fmt.Errorf("failed to parse event type %s: %w", rawEventType, err) } return ev, nil }