59 lines
2.3 KiB
Python
59 lines
2.3 KiB
Python
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy.dialects.mysql import INTEGER
|
|
|
|
from app import db
|
|
|
|
|
|
class AuditLog(db.Model):
|
|
"""
|
|
Server-side audit trail for create, edit, and delete actions.
|
|
|
|
Captures who did what, when, and on which resource.
|
|
Sensitive field values are never logged — only resource IDs and action types.
|
|
"""
|
|
__tablename__ = 'audit_logs'
|
|
|
|
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
|
|
user_id = db.Column(INTEGER(unsigned=True), nullable=False, index=True)
|
|
action = db.Column(db.String(64), nullable=False) # e.g. "vault_item.create"
|
|
resource_type = db.Column(db.String(64), nullable=False) # e.g. "vault_item"
|
|
resource_id = db.Column(INTEGER(unsigned=True), nullable=True) # FK to the affected row
|
|
detail = db.Column(db.String(512), nullable=True) # human-readable summary (no secrets)
|
|
ip_address = db.Column(db.String(45), nullable=True) # IPv4 or IPv6
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc).replace(tzinfo=None), nullable=False, index=True)
|
|
|
|
@classmethod
|
|
def log(cls, user_id: int, action: str, resource_type: str,
|
|
resource_id: int = None, detail: str = None, ip_address: str = None):
|
|
"""
|
|
Convenience factory that creates and flushes an audit entry.
|
|
Caller is responsible for calling db.session.commit() (or the surrounding
|
|
request handler will do so together with the business-logic commit).
|
|
"""
|
|
entry = cls(
|
|
user_id=user_id,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
resource_id=resource_id,
|
|
detail=detail,
|
|
ip_address=ip_address,
|
|
)
|
|
db.session.add(entry)
|
|
return entry
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'user_id': self.user_id,
|
|
'action': self.action,
|
|
'resource_type': self.resource_type,
|
|
'resource_id': self.resource_id,
|
|
'detail': self.detail,
|
|
'ip_address': self.ip_address,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
def __repr__(self):
|
|
return f'<AuditLog {self.action} by user={self.user_id} on {self.resource_type}#{self.resource_id}>'
|