feat(audit): add Audit Log module — coverage, metrics endpoint, indexes

- UserStatusView, EventModerationView, ReviewModerationView,
  PartnerKYCReviewView: each state change now emits _audit_log()
  inside the same transaction.atomic() block so the log stays
  consistent with DB state on partial failure
- AuditLogMetricsView: GET /api/v1/rbac/audit-log/metrics/ returns
  total/today/week/distinct_users/by_action_group; 60 s cache with
  ?nocache=1 bypass
- AuditLogListView: free-text search (Q over action/target/user),
  page_size bounded to [1, 200]
- accounts.User.ALL_MODULES += 'audit-log';
  StaffProfile.SCOPE_TO_MODULE['audit'] = 'audit-log'
- Migration 0005: composite indexes (action,-created_at) and
  (target_type,target_id) on AuditLog
- admin_api/tests.py: 11 tests covering list shape, search,
  page bounds, metrics shape+nocache, suspend/ban/reinstate
  audit emission

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 12:39:38 +05:30
parent 9cde886bd4
commit 2c60a82704
7 changed files with 633 additions and 51 deletions

View File

@@ -130,7 +130,7 @@ class StaffProfile(models.Model):
def get_allowed_modules(self):
scopes = self.get_effective_scopes()
if '*' in scopes:
return ['dashboard', 'partners', 'events', 'ad-control', 'users', 'reviews', 'contributions', 'leads', 'financials', 'settings']
return ['dashboard', 'partners', 'events', 'ad-control', 'users', 'reviews', 'contributions', 'leads', 'financials', 'audit-log', 'settings']
SCOPE_TO_MODULE = {
'users': 'users',
'events': 'events',
@@ -141,6 +141,7 @@ class StaffProfile(models.Model):
'ads': 'ad-control',
'contributions': 'contributions',
'leads': 'leads',
'audit': 'audit-log',
}
modules = {'dashboard'}
for scope in scopes:
@@ -179,6 +180,12 @@ class AuditLog(models.Model):
class Meta:
ordering = ['-created_at']
indexes = [
# Fast filter-by-action ordered by time (audit log page default view)
models.Index(fields=['action', '-created_at'], name='auditlog_action_time_idx'),
# Fast "related entries for this target" lookups in the detail panel
models.Index(fields=['target_type', 'target_id'], name='auditlog_target_idx'),
]
def __str__(self):
return f"{self.action} by {self.user} at {self.created_at}"