fix error in gitignore
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# audit app
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuditConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.audit"
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.2.17 on 2026-09-15 09:29
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AuditEvent',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('action', models.CharField(db_index=True, max_length=120)),
|
||||
('object_type', models.CharField(db_index=True, max_length=120)),
|
||||
('object_id', models.CharField(blank=True, db_index=True, max_length=120)),
|
||||
('ip', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('before', models.JSONField(blank=True, default=dict)),
|
||||
('after', models.JSONField(blank=True, default=dict)),
|
||||
('metadata', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.db import models
|
||||
|
||||
from apps.core.models import UUIDModel
|
||||
|
||||
|
||||
class AuditEvent(UUIDModel):
|
||||
"""Immutable audit log entry. Records are never updated or deleted."""
|
||||
|
||||
actor = models.ForeignKey("accounts.User", null=True, blank=True, on_delete=models.SET_NULL)
|
||||
action = models.CharField(max_length=120, db_index=True)
|
||||
object_type = models.CharField(max_length=120, db_index=True)
|
||||
object_id = models.CharField(max_length=120, blank=True, db_index=True)
|
||||
ip = models.GenericIPAddressField(null=True, blank=True)
|
||||
before = models.JSONField(default=dict, blank=True)
|
||||
after = models.JSONField(default=dict, blank=True)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.action} {self.object_type}:{self.object_id}"
|
||||
@@ -0,0 +1,14 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.audit.models import AuditEvent
|
||||
|
||||
|
||||
class AuditEventSerializer(serializers.ModelSerializer):
|
||||
actor_name = serializers.CharField(source="actor.username", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AuditEvent
|
||||
fields = [
|
||||
"id", "actor", "actor_name", "action", "object_type", "object_id",
|
||||
"ip", "before", "after", "metadata", "created_at",
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
from apps.audit.models import AuditEvent
|
||||
|
||||
|
||||
def record(actor, action, object_type, object_id="", before=None, after=None, metadata=None, request=None):
|
||||
"""Write an immutable audit event. Never raises to avoid breaking business flows."""
|
||||
ip = None
|
||||
if request is not None:
|
||||
xff = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||
ip = (xff.split(",")[0].strip() if xff else request.META.get("REMOTE_ADDR"))
|
||||
try:
|
||||
AuditEvent.objects.create(
|
||||
actor=actor if actor and actor.is_authenticated else None,
|
||||
action=action,
|
||||
object_type=object_type,
|
||||
object_id=str(object_id),
|
||||
ip=ip,
|
||||
before=before or {},
|
||||
after=after or {},
|
||||
metadata=metadata or {},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from apps.audit.views import AuditEventViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register("", AuditEventViewSet, basename="audit-event")
|
||||
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,12 @@
|
||||
from rest_framework import mixins, viewsets
|
||||
|
||||
from apps.audit.models import AuditEvent
|
||||
from apps.audit.serializers import AuditEventSerializer
|
||||
from apps.rbac.permissions import ActionPermission
|
||||
|
||||
|
||||
class AuditEventViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||
queryset = AuditEvent.objects.select_related("actor").all()
|
||||
serializer_class = AuditEventSerializer
|
||||
permission_classes = [ActionPermission]
|
||||
code_map = {"list": "user.view", "retrieve": "user.view"}
|
||||
Reference in New Issue
Block a user