feat: add stage 0 foundation
This commit is contained in:
1
backend/apps/audit/__init__.py
Normal file
1
backend/apps/audit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
26
backend/apps/audit/api.py
Normal file
26
backend/apps/audit/api.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from ninja import Router, Schema
|
||||
|
||||
from apps.audit.models import OperationAuditLog
|
||||
|
||||
router = Router(tags=["audit"])
|
||||
|
||||
|
||||
class OperationAuditLogOut(Schema):
|
||||
id: int
|
||||
module: str
|
||||
action: str
|
||||
target_type: str
|
||||
target_id: str
|
||||
request_snapshot: dict[str, Any]
|
||||
result: str
|
||||
ip_address: str | None
|
||||
user_agent: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@router.get("/operation-logs", response=list[OperationAuditLogOut])
|
||||
def list_operation_logs(request):
|
||||
return OperationAuditLog.objects.all()[:100]
|
||||
6
backend/apps/audit/apps.py
Normal file
6
backend/apps/audit/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuditConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.audit"
|
||||
61
backend/apps/audit/migrations/0001_initial.py
Normal file
61
backend/apps/audit/migrations/0001_initial.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-30 07:47
|
||||
|
||||
import django.db.models.deletion
|
||||
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='OperationAuditLog',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
('module', models.CharField(max_length=80)),
|
||||
('action', models.CharField(max_length=80)),
|
||||
('target_type', models.CharField(max_length=80)),
|
||||
('target_id', models.CharField(blank=True, max_length=120)),
|
||||
('request_snapshot', models.JSONField(blank=True, default=dict)),
|
||||
('result', models.CharField(max_length=20)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='operation_audit_logs',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [
|
||||
models.Index(fields=['module', 'action'], name='audit_opera_module_8f87af_idx'),
|
||||
models.Index(
|
||||
fields=['target_type', 'target_id'],
|
||||
name='audit_opera_target__560d76_idx',
|
||||
),
|
||||
models.Index(fields=['created_at'], name='audit_opera_created_077b40_idx'),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/apps/audit/migrations/__init__.py
Normal file
0
backend/apps/audit/migrations/__init__.py
Normal file
32
backend/apps/audit/models.py
Normal file
32
backend/apps/audit/models.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class OperationAuditLog(models.Model):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="operation_audit_logs",
|
||||
)
|
||||
module = models.CharField(max_length=80)
|
||||
action = models.CharField(max_length=80)
|
||||
target_type = models.CharField(max_length=80)
|
||||
target_id = models.CharField(max_length=120, blank=True)
|
||||
request_snapshot = models.JSONField(default=dict, blank=True)
|
||||
result = models.CharField(max_length=20)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["module", "action"]),
|
||||
models.Index(fields=["target_type", "target_id"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.module}.{self.action}:{self.result}"
|
||||
1
backend/apps/audit/tests/__init__.py
Normal file
1
backend/apps/audit/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
20
backend/apps/audit/tests/test_audit_api.py
Normal file
20
backend/apps/audit/tests/test_audit_api.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from apps.audit.models import OperationAuditLog
|
||||
|
||||
|
||||
def test_list_operation_logs_returns_recent_logs(client, db):
|
||||
OperationAuditLog.objects.create(
|
||||
module="system",
|
||||
action="health_check",
|
||||
target_type="system",
|
||||
target_id="health",
|
||||
result="success",
|
||||
request_snapshot={"path": "/api/system/health"},
|
||||
)
|
||||
|
||||
response = client.get("/api/audit/operation-logs")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]["module"] == "system"
|
||||
assert payload[0]["action"] == "health_check"
|
||||
Reference in New Issue
Block a user