From e358c433f02613081dc0b93d1e25f2b3e200466d Mon Sep 17 00:00:00 2001 From: Jenic Rycr Date: Tue, 23 Dec 2025 07:50:00 -0500 Subject: [PATCH] feat: allow audit log retention to be controlled by env variable (#1158) --- backend/internal/common/env_config.go | 26 +++++++++------- backend/internal/common/env_config_test.go | 35 ++++++++++++++++++++++ backend/internal/job/db_cleanup_job.go | 7 +++-- frontend/messages/en.json | 4 +-- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 9d25652e..cb81eb0e 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -68,6 +68,7 @@ type EnvConfigSchema struct { TracingEnabled bool `env:"TRACING_ENABLED"` LogJSON bool `env:"LOG_JSON"` TrustProxy bool `env:"TRUST_PROXY"` + AuditLogRetentionDays int `env:"AUDIT_LOG_RETENTION_DAYS"` AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"` AllowDowngrade bool `env:"ALLOW_DOWNGRADE"` InternalAppURL string `env:"INTERNAL_APP_URL"` @@ -85,16 +86,17 @@ func init() { func defaultConfig() EnvConfigSchema { return EnvConfigSchema{ - AppEnv: AppEnvProduction, - LogLevel: "info", - DbProvider: "sqlite", - FileBackend: "filesystem", - KeysPath: "data/keys", - AppURL: AppUrl, - Port: "1411", - Host: "0.0.0.0", - GeoLiteDBPath: "data/GeoLite2-City.mmdb", - GeoLiteDBUrl: MaxMindGeoLiteCityUrl, + AppEnv: AppEnvProduction, + LogLevel: "info", + DbProvider: "sqlite", + FileBackend: "filesystem", + KeysPath: "data/keys", + AuditLogRetentionDays: 90, + AppURL: AppUrl, + Port: "1411", + Host: "0.0.0.0", + GeoLiteDBPath: "data/GeoLite2-City.mmdb", + GeoLiteDBUrl: MaxMindGeoLiteCityUrl, } } @@ -214,6 +216,10 @@ func validateEnvConfig(config *EnvConfigSchema) error { } + if config.AuditLogRetentionDays <= 0 { + return errors.New("AUDIT_LOG_RETENTION_DAYS must be greater than 0") + } + return nil } diff --git a/backend/internal/common/env_config_test.go b/backend/internal/common/env_config_test.go index 3353be05..bfd8a887 100644 --- a/backend/internal/common/env_config_test.go +++ b/backend/internal/common/env_config_test.go @@ -187,6 +187,41 @@ func TestParseEnvConfig(t *testing.T) { assert.False(t, EnvConfig.AnalyticsDisabled) }) + t.Run("should default audit log retention days to 90", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("DB_PROVIDER", "sqlite") + t.Setenv("DB_CONNECTION_STRING", "file:test.db") + t.Setenv("APP_URL", "http://localhost:3000") + + err := parseEnvConfig() + require.NoError(t, err) + assert.Equal(t, 90, EnvConfig.AuditLogRetentionDays) + }) + + t.Run("should parse audit log retention days override", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("DB_PROVIDER", "sqlite") + t.Setenv("DB_CONNECTION_STRING", "file:test.db") + t.Setenv("APP_URL", "http://localhost:3000") + t.Setenv("AUDIT_LOG_RETENTION_DAYS", "365") + + err := parseEnvConfig() + require.NoError(t, err) + assert.Equal(t, 365, EnvConfig.AuditLogRetentionDays) + }) + + t.Run("should fail when AUDIT_LOG_RETENTION_DAYS is non-positive", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("DB_PROVIDER", "sqlite") + t.Setenv("DB_CONNECTION_STRING", "file:test.db") + t.Setenv("APP_URL", "http://localhost:3000") + t.Setenv("AUDIT_LOG_RETENTION_DAYS", "0") + + err := parseEnvConfig() + require.Error(t, err) + assert.ErrorContains(t, err, "AUDIT_LOG_RETENTION_DAYS must be greater than 0") + }) + t.Run("should parse string environment variables correctly", func(t *testing.T) { EnvConfig = defaultConfig() t.Setenv("DB_PROVIDER", "postgres") diff --git a/backend/internal/job/db_cleanup_job.go b/backend/internal/job/db_cleanup_job.go index 3e89e886..3566f858 100644 --- a/backend/internal/job/db_cleanup_job.go +++ b/backend/internal/job/db_cleanup_job.go @@ -10,6 +10,7 @@ import ( "github.com/go-co-op/gocron/v2" "gorm.io/gorm" + "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/model" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" ) @@ -119,11 +120,13 @@ func (j *DbCleanupJobs) clearReauthenticationTokens(ctx context.Context) error { return nil } -// ClearAuditLogs deletes audit logs older than 90 days +// ClearAuditLogs deletes audit logs older than the configured retention window func (j *DbCleanupJobs) clearAuditLogs(ctx context.Context) error { + cutoff := time.Now().AddDate(0, 0, -common.EnvConfig.AuditLogRetentionDays) + st := j.db. WithContext(ctx). - Delete(&model.AuditLog{}, "created_at < ?", datatype.DateTime(time.Now().AddDate(0, 0, -90))) + Delete(&model.AuditLog{}, "created_at < ?", datatype.DateTime(cutoff)) if st.Error != nil { return fmt.Errorf("failed to delete old audit logs: %w", st.Error) } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 1bf2716e..f933cb49 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -95,7 +95,7 @@ "settings": "Settings", "update_pocket_id": "Update Pocket ID", "powered_by": "Powered by", - "see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.", + "see_your_recent_account_activities": "See your account activities within the configured retention period.", "time": "Time", "event": "Event", "approximate_location": "Approximate Location", @@ -328,7 +328,7 @@ "all_clients": "All Clients", "all_locations": "All Locations", "global_audit_log": "Global Audit Log", - "see_all_account_activities_from_the_last_3_months": "See all user activity for the last 3 months.", + "see_all_recent_account_activities": "View the account activities of all users during the set retention period.", "token_sign_in": "Token Sign In", "client_authorization": "Client Authorization", "new_client_authorization": "New Client Authorization",