feat: add stage 0 foundation

This commit is contained in:
hailin
2026-06-30 15:55:46 +08:00
parent 6d373da09f
commit 604f456e03
68 changed files with 4884 additions and 0 deletions

View File

@@ -0,0 +1 @@

26
backend/apps/audit/api.py Normal file
View 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]

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AuditConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.audit"

View 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'),
],
},
),
]

View 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}"

View File

@@ -0,0 +1 @@

View 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"