125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
|
|
"botpontos/internal/adapters/postgres"
|
|
"botpontos/internal/domain"
|
|
)
|
|
|
|
const stateRetentionLastRun = "retention_last_run"
|
|
|
|
// Maintenance é o ciclo de 5 min: falha os envios travados e, uma vez por dia
|
|
// no horário configurado, aplica a retenção de dados (9.10).
|
|
func (s *Service) Maintenance(ctx context.Context) error {
|
|
stuck, err := s.store.FailStuckSending(ctx, s.now().Add(-s.cfg.Jobs.SendingTimeout))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, n := range stuck {
|
|
s.audit(ctx, WorkerActor, &n.UserID, domain.EventInterruptedSending, false,
|
|
map[string]any{"notification_id": n.ID, "type": string(n.Type)})
|
|
}
|
|
return s.applyRetentionIfDue(ctx)
|
|
}
|
|
|
|
func (s *Service) applyRetentionIfDue(ctx context.Context) error {
|
|
now := s.now().In(s.location)
|
|
if now.Hour() != s.cfg.Jobs.RetentionHour {
|
|
return nil
|
|
}
|
|
today := s.TodayDate()
|
|
var last struct {
|
|
Day string `json:"day"`
|
|
}
|
|
if found, err := s.store.GetState(ctx, stateRetentionLastRun, &last); err != nil {
|
|
return err
|
|
} else if found && last.Day == today {
|
|
return nil
|
|
}
|
|
|
|
audit, err := s.store.DeleteAuditBefore(ctx, now.Add(-s.cfg.Jobs.RetentionAudit))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
notificationsCutoff := domain.LocalDay(now.Add(-s.cfg.Jobs.RetentionNotifications), s.location)
|
|
notifications, err := s.store.DeleteNotificationsBefore(ctx, notificationsCutoff)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
days, err := s.store.DeleteUserDaysBefore(ctx, notificationsCutoff)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sessions, err := s.store.DeleteSessionsEndedBefore(ctx, now.Add(-s.cfg.Jobs.RetentionSessions))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
last.Day = today
|
|
if err := s.store.SetState(ctx, stateRetentionLastRun, last); err != nil {
|
|
return err
|
|
}
|
|
s.audit(ctx, WorkerActor, nil, domain.EventRetentionApplied, true, map[string]any{
|
|
"audit_logs": audit, "notifications": notifications, "user_days": days, "web_sessions": sessions,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// RotateKey recifra com a chave atual as credenciais gravadas com outra versão.
|
|
func (s *Service) RotateKey(ctx context.Context) (int, error) {
|
|
creds, err := s.store.ListCredentials(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
current := s.cipher.CurrentVersion()
|
|
rotated := 0
|
|
for _, c := range creds {
|
|
if c.KeyVersion == current {
|
|
continue
|
|
}
|
|
plain, err := s.cipher.Decrypt(c.EncryptedPassword, c.KeyVersion)
|
|
if err != nil {
|
|
return rotated, err
|
|
}
|
|
encrypted, version, err := s.cipher.Encrypt(plain)
|
|
if err != nil {
|
|
return rotated, err
|
|
}
|
|
if err := s.store.UpdateCredentialCipher(ctx, c.UserID, encrypted, version); err != nil {
|
|
return rotated, err
|
|
}
|
|
rotated++
|
|
}
|
|
s.audit(ctx, CLIActor, nil, domain.EventEncryptionKeyRotated, true, map[string]any{"credentials": rotated, "version": current})
|
|
return rotated, nil
|
|
}
|
|
|
|
// Overview reúne os dados do dashboard do painel.
|
|
type Overview struct {
|
|
Counts postgres.Dashboard
|
|
OutageSince *string
|
|
HolidayFallback *string
|
|
}
|
|
|
|
func (s *Service) Overview(ctx context.Context) (Overview, error) {
|
|
var o Overview
|
|
counts, err := s.store.Dashboard(ctx, s.TodayDate())
|
|
if err != nil {
|
|
return o, err
|
|
}
|
|
o.Counts = counts
|
|
var outage outageState
|
|
if found, _ := s.store.GetState(ctx, stateOutage, &outage); found {
|
|
since := outage.Since.Format("2006-01-02T15:04:05Z07:00")
|
|
o.OutageSince = &since
|
|
}
|
|
var fallback struct {
|
|
Day string `json:"day"`
|
|
}
|
|
if found, _ := s.store.GetState(ctx, stateHolidayFallbackNotified, &fallback); found && fallback.Day == s.TodayDate() {
|
|
o.HolidayFallback = &fallback.Day
|
|
}
|
|
return o, nil
|
|
}
|