Files
gw/backend/app/models/audit.py
T

70 lines
2.7 KiB
Python
Raw Normal View History

2026-04-18 07:23:55 +12:00
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]
)