feat(platform): add audit logs and activity feed

This commit is contained in:
Schubert Ferenc 2026-07-03 00:04:44 +02:00
parent 92cb8d1286
commit c816e9869d
34 changed files with 1592 additions and 43 deletions

View file

@ -2,6 +2,7 @@ import logging
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.orm import Session
from starlette.requests import Request
from app.core.rbac import require_permission
from app.db.database import get_db
@ -16,6 +17,7 @@ from app.schemas.customer import (
CustomerResponse,
CustomerUpdate,
)
from app.services.audit_service import sanitize, write_audit_log
logger = logging.getLogger(__name__)
@ -94,42 +96,81 @@ def get_customer(
)
def create_customer(
customer: CustomerCreate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.create")),
):
ensure_unique_customer_number(db, customer.customer_number)
logger.info("customers.create", extra={"actor_user_id": current_user.id})
return CustomerRepository.create(db, customer)
created_customer = CustomerRepository.create(db, customer)
write_audit_log(
db,
action="customers.create",
entity_type="customers",
entity_id=created_customer.id,
entity_label=created_customer.company_name,
actor=current_user,
request=request,
after_data=created_customer,
)
return created_customer
@router.put("/{customer_id}", response_model=CustomerResponse)
def update_customer(
customer_id: int,
customer: CustomerUpdate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.update")),
):
db_customer = get_customer_or_404(db, customer_id)
before_data = sanitize(db_customer)
ensure_unique_customer_number(db, customer.customer_number, customer_id)
logger.info(
"customers.update",
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
)
return CustomerRepository.update(db, db_customer, customer)
updated_customer = CustomerRepository.update(db, db_customer, customer)
write_audit_log(
db,
action="customers.update",
entity_type="customers",
entity_id=updated_customer.id,
entity_label=updated_customer.company_name,
actor=current_user,
request=request,
before_data=before_data,
after_data=updated_customer,
)
return updated_customer
@router.delete("/{customer_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_customer(
customer_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.delete")),
):
db_customer = get_customer_or_404(db, customer_id)
before_data = sanitize(db_customer)
entity_label = db_customer.company_name
logger.info(
"customers.delete",
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
)
CustomerRepository.delete(db, db_customer)
write_audit_log(
db,
action="customers.delete",
entity_type="customers",
entity_id=customer_id,
entity_label=entity_label,
actor=current_user,
request=request,
before_data=before_data,
)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@ -155,15 +196,28 @@ def get_customer_contacts(
def create_customer_contact(
customer_id: int,
contact: CustomerContactCreate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.update")),
):
get_customer_or_404(db, customer_id)
customer = get_customer_or_404(db, customer_id)
logger.info(
"customers.contacts.create",
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
)
return CustomerRepository.create_contact(db, customer_id, contact)
created_contact = CustomerRepository.create_contact(db, customer_id, contact)
write_audit_log(
db,
action="customer_contacts.create",
entity_type="customer_contacts",
entity_id=created_contact.id,
entity_label=f"{created_contact.first_name} {created_contact.last_name}".strip(),
actor=current_user,
request=request,
after_data=created_contact,
metadata={"customer_id": customer_id, "customer_label": customer.company_name},
)
return created_contact
@router.put("/{customer_id}/contacts/{contact_id}", response_model=CustomerContactResponse)
@ -171,11 +225,13 @@ def update_customer_contact(
customer_id: int,
contact_id: int,
contact: CustomerContactUpdate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.update")),
):
get_customer_or_404(db, customer_id)
customer = get_customer_or_404(db, customer_id)
db_contact = get_contact_or_404(db, customer_id, contact_id)
before_data = sanitize(db_contact)
logger.info(
"customers.contacts.update",
extra={
@ -184,18 +240,34 @@ def update_customer_contact(
"target_contact_id": contact_id,
},
)
return CustomerRepository.update_contact(db, db_contact, contact)
updated_contact = CustomerRepository.update_contact(db, db_contact, contact)
write_audit_log(
db,
action="customer_contacts.update",
entity_type="customer_contacts",
entity_id=updated_contact.id,
entity_label=f"{updated_contact.first_name} {updated_contact.last_name}".strip(),
actor=current_user,
request=request,
before_data=before_data,
after_data=updated_contact,
metadata={"customer_id": customer_id, "customer_label": customer.company_name},
)
return updated_contact
@router.delete("/{customer_id}/contacts/{contact_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_customer_contact(
customer_id: int,
contact_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.update")),
):
get_customer_or_404(db, customer_id)
customer = get_customer_or_404(db, customer_id)
db_contact = get_contact_or_404(db, customer_id, contact_id)
before_data = sanitize(db_contact)
entity_label = f"{db_contact.first_name} {db_contact.last_name}".strip()
logger.info(
"customers.contacts.delete",
extra={
@ -205,4 +277,15 @@ def delete_customer_contact(
},
)
CustomerRepository.delete_contact(db, db_contact)
write_audit_log(
db,
action="customer_contacts.delete",
entity_type="customer_contacts",
entity_id=contact_id,
entity_label=entity_label,
actor=current_user,
request=request,
before_data=before_data,
metadata={"customer_id": customer_id, "customer_label": customer.company_name},
)
return Response(status_code=status.HTTP_204_NO_CONTENT)