Files
gw/backend/app/models/audit.py
T
ponzischeme89 6d44e05de4 v1
2026-04-18 07:23:55 +12:00

70 lines
2.7 KiB
Python

import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, Index, String, Text, JSON, func
from sqlalchemy import Uuid
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, UUIDMixin
class AuditLog(Base, UUIDMixin):
"""Immutable record of member activity and application errors."""
__tablename__ = "audit_logs"
__table_args__ = (
Index("ix_audit_logs_timestamp", "timestamp"),
Index("ix_audit_logs_member_id", "member_id"),
Index("ix_audit_logs_action_type", "action_type"),
Index("ix_audit_logs_status", "status"),
)
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
# Nullable FK — SET NULL if member is deleted so the log is preserved.
member_id: Mapped[Optional[uuid.UUID]] = mapped_column(
Uuid(as_uuid=True),
ForeignKey("members.id", ondelete="SET NULL"),
nullable=True,
)
# Denormalised for readability after member deletion.
member_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# One of: login, logout, page_visit, booking_created, booking_cancelled,
# profile_updated, onboarding_updated, contract_signed,
# account_claimed, message_read, error
action_type: Mapped[str] = mapped_column(String(64), nullable=False)
# Identifies the page / feature area, e.g. "members/dashboard"
area: Mapped[str] = mapped_column(String(255), nullable=False)
# Human-readable one-liner
description: Mapped[str] = mapped_column(String(500), nullable=False)
# success | warning | error
status: Mapped[str] = mapped_column(String(16), nullable=False, default="success")
# Optional related booking — SET NULL if booking is deleted.
booking_id: Mapped[Optional[uuid.UUID]] = mapped_column(
Uuid(as_uuid=True),
ForeignKey("bookings.id", ondelete="SET NULL"),
nullable=True,
)
# Error detail — populated for action_type='error' records.
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
error_detail: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
# Request metadata
ip_address: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
user_agent: Mapped[Optional[str]] = mapped_column(String(512), nullable=True)
# Catch-all JSON for any extra context (e.g. booking service_type)
extra: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
member: Mapped[Optional["Member"]] = relationship( # type: ignore[name-defined]
"Member", foreign_keys=[member_id]
)