Production hardening: security, resilience, observability, and compliance
Password complexity: custom validator requiring uppercase, lowercase, digit (min 8 chars)
Token expiry: 90-day token lifetime with refresh endpoint (60-90 day renewal window)
Health check: /api/health/ now pings Postgres + Redis, returns 503 on failure
Audit logging: async audit_log table for auth events (login, register, delete, etc.)
Circuit breaker: APNs/FCM push sends wrapped with 5-failure threshold, 30s recovery
FK indexes: 27 missing foreign key indexes across all tables (migration 017)
CSP header: default-src 'none'; frame-ancestors 'none'
Gzip compression: level 5 with media endpoint skipper
Prometheus metrics: /metrics endpoint using existing monitoring service
External timeouts: 15s push, 30s SMTP, context timeouts on all external calls
Migrations: 016 (token created_at), 017 (FK indexes), 018 (audit_log)
Tests: circuit breaker (15), audit service (8), token refresh (7), health (4),
middleware expiry (5), validator (new)
This commit is contained in:
93
internal/services/audit_service.go
Normal file
93
internal/services/audit_service.go
Normal file
@@ -0,0 +1,93 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user