b54493f785
BE-3 observability: expose the worker's Prometheus metrics on :6060/metrics (apns/fcm/asynq histograms + a new cache_ops_total counter were recorded all along but never scraped — which is why those dashboard panels read empty); add the worker containerPort, the vmagent worker scrape job, and two additive NetworkPolicies. Instrument cache Get/Set hit/miss. BE-2 retention: three periodic Asynq cleanup crons mirroring the reminder-log cleanup — notifications (90d), webhook dedup log (180d), audit_log (365d). BE-1 GDPR data export: POST /api/auth/export/ enqueues a low-priority Asynq job that gathers all of the user's data (owned residences + their tasks/contractors/ documents/share-codes, plus profile/notifications/prefs/push-tokens/subscription/ audit log), zips one JSON file per category, and emails it as an attachment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
2.5 KiB
Go
95 lines
2.5 KiB
Go
package services
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/rs/zerolog/log"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/treytartt/honeydue-api/internal/models"
|
|
)
|
|
|
|
// Audit event type constants
|
|
const (
|
|
AuditEventLogin = "auth.login"
|
|
AuditEventLoginFailed = "auth.login_failed"
|
|
AuditEventRegister = "auth.register"
|
|
AuditEventLogout = "auth.logout"
|
|
AuditEventPasswordReset = "auth.password_reset"
|
|
AuditEventPasswordChanged = "auth.password_changed"
|
|
AuditEventAccountDeleted = "auth.account_deleted"
|
|
AuditEventDataExport = "auth.data_export_requested"
|
|
)
|
|
|
|
// AuditService handles audit logging for security-relevant events.
|
|
// It writes audit log entries asynchronously via a buffered channel to avoid
|
|
// blocking request handlers.
|
|
type AuditService struct {
|
|
db *gorm.DB
|
|
logChan chan *models.AuditLog
|
|
done chan struct{}
|
|
stopOnce sync.Once
|
|
}
|
|
|
|
// NewAuditService creates a new audit service with a buffered channel for async writes.
|
|
// Call Stop() when shutting down to flush remaining entries.
|
|
func NewAuditService(db *gorm.DB) *AuditService {
|
|
s := &AuditService{
|
|
db: db,
|
|
logChan: make(chan *models.AuditLog, 256),
|
|
done: make(chan struct{}),
|
|
}
|
|
go s.processLogs()
|
|
return s
|
|
}
|
|
|
|
// processLogs drains the log channel and writes entries to the database.
|
|
func (s *AuditService) processLogs() {
|
|
defer close(s.done)
|
|
for entry := range s.logChan {
|
|
if err := s.db.Create(entry).Error; err != nil {
|
|
log.Error().Err(err).
|
|
Str("event_type", entry.EventType).
|
|
Msg("Failed to write audit log entry")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop closes the log channel and waits for all pending entries to be written.
|
|
// It is safe to call Stop multiple times.
|
|
func (s *AuditService) Stop() {
|
|
s.stopOnce.Do(func() {
|
|
close(s.logChan)
|
|
})
|
|
<-s.done
|
|
}
|
|
|
|
// LogEvent records an audit event. It extracts the client IP and User-Agent from
|
|
// the Echo context and sends the entry to the background writer. If the channel
|
|
// is full the entry is dropped and an error is logged (non-blocking).
|
|
func (s *AuditService) LogEvent(c echo.Context, userID *uint, eventType string, details map[string]interface{}) {
|
|
var ip, ua string
|
|
if c != nil {
|
|
ip = c.RealIP()
|
|
ua = c.Request().Header.Get("User-Agent")
|
|
}
|
|
|
|
entry := &models.AuditLog{
|
|
UserID: userID,
|
|
EventType: eventType,
|
|
IPAddress: ip,
|
|
UserAgent: ua,
|
|
Details: models.JSONMap(details),
|
|
}
|
|
|
|
select {
|
|
case s.logChan <- entry:
|
|
// sent
|
|
default:
|
|
log.Warn().
|
|
Str("event_type", eventType).
|
|
Msg("Audit log channel full, dropping entry")
|
|
}
|
|
}
|