Explorar o código

Add ISO 27001 compliant audit logging (#53)

* add audit log to server actions

* translations

* translations

* loader

* signin audit

* test

* migration

* audit info dialog

* make the entire audit block fire-and-forget
Bernt Christian Egeland hai 6 meses
pai
achega
7b558e2837
Modificáronse 80 ficheiros con 2494 adicións e 213 borrados
  1. 86 0
      messages/de/audit.json
  2. 3 0
      messages/de/dashboard.json
  3. 4 2
      messages/de/navigation.json
  4. 86 0
      messages/en/audit.json
  5. 3 0
      messages/en/dashboard.json
  6. 4 2
      messages/en/navigation.json
  7. 86 0
      messages/es/audit.json
  8. 12 9
      messages/es/dashboard.json
  9. 4 2
      messages/es/navigation.json
  10. 86 0
      messages/fr/audit.json
  11. 34 31
      messages/fr/dashboard.json
  12. 4 2
      messages/fr/navigation.json
  13. 86 0
      messages/it/audit.json
  14. 3 0
      messages/it/dashboard.json
  15. 4 2
      messages/it/navigation.json
  16. 86 0
      messages/nb/audit.json
  17. 3 0
      messages/nb/dashboard.json
  18. 4 2
      messages/nb/navigation.json
  19. 86 0
      messages/nl/audit.json
  20. 3 0
      messages/nl/dashboard.json
  21. 4 2
      messages/nl/navigation.json
  22. 86 0
      messages/pl/audit.json
  23. 3 0
      messages/pl/dashboard.json
  24. 4 2
      messages/pl/navigation.json
  25. 86 0
      messages/pt-BR/audit.json
  26. 3 0
      messages/pt-BR/dashboard.json
  27. 4 2
      messages/pt-BR/navigation.json
  28. 86 0
      messages/tr/audit.json
  29. 3 0
      messages/tr/dashboard.json
  30. 4 2
      messages/tr/navigation.json
  31. 31 0
      prisma/migrations/20260311184346_audit_log/migration.sql
  32. 25 0
      prisma/schema.prisma
  33. 2 0
      src/__tests__/multitenancy/vehicle-service-isolation.test.ts
  34. 288 0
      src/app/(authenticated)/audit-log/audit-log-client.tsx
  35. 55 0
      src/app/(authenticated)/audit-log/page.tsx
  36. 72 0
      src/app/(authenticated)/dashboard-client.tsx
  37. 5 1
      src/app/(authenticated)/page.tsx
  38. 86 19
      src/app/api/public/auth/[...all]/route.ts
  39. 2 0
      src/components/app-sidebar.tsx
  40. 1 1
      src/components/data-table-pagination.tsx
  41. 1 0
      src/components/page-header.tsx
  42. 1 0
      src/cronTasks.ts
  43. 109 0
      src/features/audit/Actions/auditActions.ts
  44. 31 4
      src/features/billing/Actions/recurringInvoiceActions.ts
  45. 31 3
      src/features/custom-fields/Actions/customFieldActions.ts
  46. 31 3
      src/features/customers/Actions/customerActions.ts
  47. 33 6
      src/features/email/Actions/emailActions.ts
  48. 33 5
      src/features/inspections/Actions/inspectionActions.ts
  49. 11 2
      src/features/inspections/Actions/quoteRequestActions.ts
  50. 31 3
      src/features/inspections/Actions/templateActions.ts
  51. 32 5
      src/features/inventory/Actions/inventoryActions.ts
  52. 20 0
      src/features/onboarding/Actions/createOnboardingOrg.ts
  53. 22 4
      src/features/payments/Actions/paymentActions.ts
  54. 8 2
      src/features/portal/Actions/portalActions.ts
  55. 53 7
      src/features/quotes/Actions/quoteActions.ts
  56. 7 0
      src/features/settings/Actions/deleteContent.ts
  57. 16 2
      src/features/settings/Actions/invoiceLayoutActions.ts
  58. 8 1
      src/features/sms/Actions/smsActions.ts
  59. 12 0
      src/features/subscription/Actions/subscriptionActions.ts
  60. 11 2
      src/features/team/Actions/cancelInvitation.ts
  61. 8 0
      src/features/team/Actions/createNewOrganization.ts
  62. 10 1
      src/features/team/Actions/createRole.ts
  63. 11 2
      src/features/team/Actions/deleteRole.ts
  64. 10 2
      src/features/team/Actions/sendInvitation.ts
  65. 42 7
      src/features/team/Actions/teamActions.ts
  66. 10 1
      src/features/team/Actions/updateRole.ts
  67. 8 1
      src/features/vehicles/Actions/archiveVehicle.ts
  68. 7 0
      src/features/vehicles/Actions/createDraftServiceRecord.ts
  69. 21 2
      src/features/vehicles/Actions/noteActions.ts
  70. 21 2
      src/features/vehicles/Actions/reminderActions.ts
  71. 42 5
      src/features/vehicles/Actions/serviceActions.ts
  72. 8 1
      src/features/vehicles/Actions/unarchiveVehicle.ts
  73. 50 8
      src/features/vehicles/Actions/vehicleActions.ts
  74. 23 2
      src/features/workboard/Actions/technicianActions.ts
  75. 2 0
      src/i18n/request.ts
  76. 2 1
      src/instrumentation.ts
  77. 37 0
      src/lib/audit.ts
  78. 17 0
      src/lib/auth.ts
  79. 32 0
      src/lib/cron/cleanup-audit-logs.ts
  80. 95 48
      src/lib/with-auth.ts

+ 86 - 0
messages/de/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Aktivitätsprotokoll durchsuchen...",
+  "allActions": "Alle Aktionen",
+  "allEntities": "Alle Einträge",
+  "allUsers": "Alle Benutzer",
+  "timestamp": "Zeitstempel",
+  "user": "Benutzer",
+  "action": "Aktion",
+  "details": "Details",
+  "entityId": "Eintrags-ID",
+  "noLogsFound": "Keine Protokolleinträge gefunden",
+  "unknownUser": "Unbekannt",
+  "logDetails": "Protokolldetails",
+  "entity": "Eintrag",
+  "ipAddress": "IP-Adresse",
+  "userAgentLabel": "User-Agent",
+  "metadata": "Metadaten",
+  "actions": {
+    "vehicle_create": "Fahrzeug erstellt",
+    "vehicle_update": "Fahrzeug aktualisiert",
+    "vehicle_delete": "Fahrzeug gelöscht",
+    "vehicle_archive": "Fahrzeug archiviert",
+    "vehicle_unarchive": "Fahrzeug wiederhergestellt",
+    "service_create": "Serviceeintrag erstellt",
+    "service_update": "Serviceeintrag aktualisiert",
+    "service_status": "Servicestatus geändert",
+    "service_delete": "Serviceeintrag gelöscht",
+    "quote_create": "Angebot erstellt",
+    "quote_update": "Angebot aktualisiert",
+    "quote_status": "Angebotsstatus geändert",
+    "quote_delete": "Angebot gelöscht",
+    "quote_convert": "Angebot konvertiert",
+    "inspection_create": "Inspektion erstellt",
+    "inspection_complete": "Inspektion abgeschlossen",
+    "inspection_delete": "Inspektion gelöscht",
+    "payment_create": "Zahlung erfasst",
+    "payment_delete": "Zahlung gelöscht",
+    "customer_create": "Kunde erstellt",
+    "customer_update": "Kunde aktualisiert",
+    "customer_delete": "Kunde gelöscht",
+    "inventory_create": "Lagerteil erstellt",
+    "inventory_update": "Lagerteil aktualisiert",
+    "inventory_delete": "Lagerteil gelöscht",
+    "team_invite": "Teammitglied eingeladen",
+    "team_sendInvitation": "Einladung gesendet",
+    "team_cancelInvitation": "Einladung storniert",
+    "team_updateRole": "Mitgliedsrolle geändert",
+    "team_removeMember": "Teammitglied entfernt",
+    "role_create": "Rolle erstellt",
+    "role_update": "Rolle aktualisiert",
+    "role_delete": "Rolle gelöscht",
+    "technician_create": "Techniker erstellt",
+    "technician_update": "Techniker aktualisiert",
+    "technician_delete": "Techniker gelöscht",
+    "recurringInvoice_create": "Wiederkehrende Rechnung erstellt",
+    "recurringInvoice_update": "Wiederkehrende Rechnung aktualisiert",
+    "recurringInvoice_delete": "Wiederkehrende Rechnung gelöscht",
+    "email_sendQuote": "Angebots-E-Mail gesendet",
+    "email_sendInvoice": "Rechnungs-E-Mail gesendet",
+    "email_sendInspection": "Inspektions-E-Mail gesendet",
+    "sms_send": "SMS gesendet",
+    "note_create": "Notiz erstellt",
+    "note_delete": "Notiz gelöscht",
+    "reminder_create": "Erinnerung erstellt",
+    "reminder_delete": "Erinnerung gelöscht",
+    "organization_create": "Organisation erstellt",
+    "subscription_cancel": "Abonnement gekündigt",
+    "subscription_resume": "Abonnement fortgesetzt",
+    "settings_deleteContent": "Inhalt gelöscht",
+    "settings_updateInvoiceLayout": "Rechnungslayout aktualisiert",
+    "settings_updateQuoteLayout": "Angebotslayout aktualisiert",
+    "settings_updatePortalSlug": "Portal-URL aktualisiert",
+    "inspectionTemplate_create": "Inspektionsvorlage erstellt",
+    "inspectionTemplate_update": "Inspektionsvorlage aktualisiert",
+    "inspectionTemplate_delete": "Inspektionsvorlage gelöscht",
+    "customField_create": "Benutzerdefiniertes Feld erstellt",
+    "customField_update": "Benutzerdefiniertes Feld aktualisiert",
+    "customField_delete": "Benutzerdefiniertes Feld gelöscht",
+    "quoteRequest_update": "Angebotsanfrage aktualisiert",
+    "auth_login": "Benutzer angemeldet",
+    "auth_loginFailed": "Fehlgeschlagener Anmeldeversuch",
+    "auth_register": "Benutzer registriert",
+    "auth_passwordReset": "Passwort-Zurücksetzung angefordert",
+    "auth_permissionDenied": "Zugriff verweigert"
+  }
+}

+ 3 - 0
messages/de/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Letzte Aktivitäten",
+  "noRecentActivity": "Keine aktuellen Aktivitäten",
+  "unknownUser": "Benutzer",
   "stats": {
     "activeJobs": "Aktive Aufträge",
     "pending": "Ausstehend",

+ 4 - 2
messages/de/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Wartung",
     "customerPortal": "Kundenportal",
     "reminders": "Erinnerungen",
-    "allReminders": "Alle Erinnerungen"
+    "allReminders": "Alle Erinnerungen",
+    "auditLog": "Aktivitätsprotokoll"
   },
   "search": "Suchen...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Unternehmen erstellen",
     "lightMode": "Heller Modus",
     "darkMode": "Dunkler Modus",
-    "signOut": "Abmelden"
+    "signOut": "Abmelden",
+    "auditLog": "Aktivitätsprotokoll"
   }
 }

+ 86 - 0
messages/en/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Search audit logs...",
+  "allActions": "All actions",
+  "allEntities": "All entities",
+  "allUsers": "All users",
+  "timestamp": "Timestamp",
+  "user": "User",
+  "action": "Action",
+  "details": "Details",
+  "entityId": "Entity ID",
+  "noLogsFound": "No audit logs found",
+  "unknownUser": "Unknown",
+  "logDetails": "Log Details",
+  "entity": "Entity",
+  "ipAddress": "IP Address",
+  "userAgentLabel": "User Agent",
+  "metadata": "Metadata",
+  "actions": {
+    "vehicle_create": "Created Vehicle",
+    "vehicle_update": "Updated Vehicle",
+    "vehicle_delete": "Deleted Vehicle",
+    "vehicle_archive": "Archived Vehicle",
+    "vehicle_unarchive": "Unarchived Vehicle",
+    "service_create": "Created Service Record",
+    "service_update": "Updated Service Record",
+    "service_status": "Changed Service Status",
+    "service_delete": "Deleted Service Record",
+    "quote_create": "Created Quote",
+    "quote_update": "Updated Quote",
+    "quote_status": "Changed Quote Status",
+    "quote_delete": "Deleted Quote",
+    "quote_convert": "Converted Quote",
+    "inspection_create": "Created Inspection",
+    "inspection_complete": "Completed Inspection",
+    "inspection_delete": "Deleted Inspection",
+    "payment_create": "Recorded Payment",
+    "payment_delete": "Deleted Payment",
+    "customer_create": "Created Customer",
+    "customer_update": "Updated Customer",
+    "customer_delete": "Deleted Customer",
+    "inventory_create": "Created Inventory Part",
+    "inventory_update": "Updated Inventory Part",
+    "inventory_delete": "Deleted Inventory Part",
+    "team_invite": "Invited Team Member",
+    "team_sendInvitation": "Sent Invitation",
+    "team_cancelInvitation": "Cancelled Invitation",
+    "team_updateRole": "Changed Member Role",
+    "team_removeMember": "Removed Team Member",
+    "role_create": "Created Role",
+    "role_update": "Updated Role",
+    "role_delete": "Deleted Role",
+    "technician_create": "Created Technician",
+    "technician_update": "Updated Technician",
+    "technician_delete": "Deleted Technician",
+    "recurringInvoice_create": "Created Recurring Invoice",
+    "recurringInvoice_update": "Updated Recurring Invoice",
+    "recurringInvoice_delete": "Deleted Recurring Invoice",
+    "email_sendQuote": "Sent Quote Email",
+    "email_sendInvoice": "Sent Invoice Email",
+    "email_sendInspection": "Sent Inspection Email",
+    "sms_send": "Sent SMS",
+    "note_create": "Created Note",
+    "note_delete": "Deleted Note",
+    "reminder_create": "Created Reminder",
+    "reminder_delete": "Deleted Reminder",
+    "organization_create": "Created Organization",
+    "subscription_cancel": "Cancelled Subscription",
+    "subscription_resume": "Resumed Subscription",
+    "settings_deleteContent": "Deleted Content",
+    "settings_updateInvoiceLayout": "Updated Invoice Layout",
+    "settings_updateQuoteLayout": "Updated Quote Layout",
+    "settings_updatePortalSlug": "Updated Portal Slug",
+    "inspectionTemplate_create": "Created Inspection Template",
+    "inspectionTemplate_update": "Updated Inspection Template",
+    "inspectionTemplate_delete": "Deleted Inspection Template",
+    "customField_create": "Created Custom Field",
+    "customField_update": "Updated Custom Field",
+    "customField_delete": "Deleted Custom Field",
+    "quoteRequest_update": "Updated Quote Request",
+    "auth_login": "User Logged In",
+    "auth_loginFailed": "Failed Login Attempt",
+    "auth_register": "User Registered",
+    "auth_passwordReset": "Password Reset Requested",
+    "auth_permissionDenied": "Permission Denied"
+  }
+}

+ 3 - 0
messages/en/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Recent Activity",
+  "noRecentActivity": "No recent activity",
+  "unknownUser": "User",
   "stats": {
     "activeJobs": "Active Jobs",
     "pending": "Pending",

+ 4 - 2
messages/en/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Maintenance",
     "customerPortal": "Customer Portal",
     "reminders": "Reminders",
-    "allReminders": "All Reminders"
+    "allReminders": "All Reminders",
+    "auditLog": "Audit Log"
   },
   "search": "Search...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Create Company",
     "lightMode": "Light Mode",
     "darkMode": "Dark Mode",
-    "signOut": "Sign Out"
+    "signOut": "Sign Out",
+    "auditLog": "Audit Log"
   }
 }

+ 86 - 0
messages/es/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Buscar en el registro de auditoría...",
+  "allActions": "Todas las acciones",
+  "allEntities": "Todas las entidades",
+  "allUsers": "Todos los usuarios",
+  "timestamp": "Fecha y hora",
+  "user": "Usuario",
+  "action": "Acción",
+  "details": "Detalles",
+  "entityId": "ID de entidad",
+  "noLogsFound": "No se encontraron registros de auditoría",
+  "unknownUser": "Desconocido",
+  "logDetails": "Detalles del registro",
+  "entity": "Entidad",
+  "ipAddress": "Dirección IP",
+  "userAgentLabel": "Agente de usuario",
+  "metadata": "Metadatos",
+  "actions": {
+    "vehicle_create": "Vehículo creado",
+    "vehicle_update": "Vehículo actualizado",
+    "vehicle_delete": "Vehículo eliminado",
+    "vehicle_archive": "Vehículo archivado",
+    "vehicle_unarchive": "Vehículo desarchivado",
+    "service_create": "Registro de servicio creado",
+    "service_update": "Registro de servicio actualizado",
+    "service_status": "Estado del servicio cambiado",
+    "service_delete": "Registro de servicio eliminado",
+    "quote_create": "Presupuesto creado",
+    "quote_update": "Presupuesto actualizado",
+    "quote_status": "Estado del presupuesto cambiado",
+    "quote_delete": "Presupuesto eliminado",
+    "quote_convert": "Presupuesto convertido",
+    "inspection_create": "Inspección creada",
+    "inspection_complete": "Inspección completada",
+    "inspection_delete": "Inspección eliminada",
+    "payment_create": "Pago registrado",
+    "payment_delete": "Pago eliminado",
+    "customer_create": "Cliente creado",
+    "customer_update": "Cliente actualizado",
+    "customer_delete": "Cliente eliminado",
+    "inventory_create": "Pieza de inventario creada",
+    "inventory_update": "Pieza de inventario actualizada",
+    "inventory_delete": "Pieza de inventario eliminada",
+    "team_invite": "Miembro del equipo invitado",
+    "team_sendInvitation": "Invitación enviada",
+    "team_cancelInvitation": "Invitación cancelada",
+    "team_updateRole": "Rol del miembro cambiado",
+    "team_removeMember": "Miembro del equipo eliminado",
+    "role_create": "Rol creado",
+    "role_update": "Rol actualizado",
+    "role_delete": "Rol eliminado",
+    "technician_create": "Técnico creado",
+    "technician_update": "Técnico actualizado",
+    "technician_delete": "Técnico eliminado",
+    "recurringInvoice_create": "Factura recurrente creada",
+    "recurringInvoice_update": "Factura recurrente actualizada",
+    "recurringInvoice_delete": "Factura recurrente eliminada",
+    "email_sendQuote": "Correo de presupuesto enviado",
+    "email_sendInvoice": "Correo de factura enviado",
+    "email_sendInspection": "Correo de inspección enviado",
+    "sms_send": "SMS enviado",
+    "note_create": "Nota creada",
+    "note_delete": "Nota eliminada",
+    "reminder_create": "Recordatorio creado",
+    "reminder_delete": "Recordatorio eliminado",
+    "organization_create": "Organización creada",
+    "subscription_cancel": "Suscripción cancelada",
+    "subscription_resume": "Suscripción reanudada",
+    "settings_deleteContent": "Contenido eliminado",
+    "settings_updateInvoiceLayout": "Diseño de factura actualizado",
+    "settings_updateQuoteLayout": "Diseño de presupuesto actualizado",
+    "settings_updatePortalSlug": "URL del portal actualizada",
+    "inspectionTemplate_create": "Plantilla de inspección creada",
+    "inspectionTemplate_update": "Plantilla de inspección actualizada",
+    "inspectionTemplate_delete": "Plantilla de inspección eliminada",
+    "customField_create": "Campo personalizado creado",
+    "customField_update": "Campo personalizado actualizado",
+    "customField_delete": "Campo personalizado eliminado",
+    "quoteRequest_update": "Solicitud de presupuesto actualizada",
+    "auth_login": "Usuario conectado",
+    "auth_loginFailed": "Intento de inicio de sesión fallido",
+    "auth_register": "Usuario registrado",
+    "auth_passwordReset": "Restablecimiento de contraseña solicitado",
+    "auth_permissionDenied": "Permiso denegado"
+  }
+}

+ 12 - 9
messages/es/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Actividad reciente",
+  "noRecentActivity": "Sin actividad reciente",
+  "unknownUser": "Usuario",
   "stats": {
     "activeJobs": "Trabajos activos",
     "pending": "Pendientes",
@@ -23,9 +26,9 @@
     "title": "Mantenimiento previsto pendiente",
     "description": "Basado en el historial de servicio y el kilometraje diario estimado",
     "overdue": "Vencido",
-    "approaching": "Pr\u00f3ximo",
+    "approaching": "Próximo",
     "estimated": "Est. {mileage} {unit}",
-    "sinceLastService": "{mileage} {unit} desde el \u00faltimo servicio",
+    "sinceLastService": "{mileage} {unit} desde el último servicio",
     "certainty": "{percent}% de certeza",
     "active": "Activos",
     "dismissedTab": "Descartados",
@@ -36,7 +39,7 @@
   "reminders": {
     "title": "Recordatorios pendientes",
     "overdue": "Vencidos",
-    "dueSoon": "Pr\u00f3ximamente",
+    "dueSoon": "Próximamente",
     "noData": "No hay recordatorios próximos."
   },
   "messages": {
@@ -76,19 +79,19 @@
   "recentCompleted": {
     "title": "Completados recientemente",
     "date": "Fecha",
-    "vehicle": "Veh\u00edculo",
+    "vehicle": "Vehículo",
     "customer": "Cliente",
-    "serviceTitle": "T\u00edtulo",
+    "serviceTitle": "Título",
     "total": "Total",
-    "empty": "A\u00fan no hay servicios completados"
+    "empty": "Aún no hay servicios completados"
   },
   "activeJobsTable": {
     "title": "Trabajos activos",
-    "vehicle": "Veh\u00edculo",
+    "vehicle": "Vehículo",
     "customer": "Cliente",
-    "serviceTitle": "T\u00edtulo",
+    "serviceTitle": "Título",
     "status": "Estado",
-    "tech": "T\u00e9cnico",
+    "tech": "Técnico",
     "empty": "No hay trabajos activos"
   },
   "relativeTime": {

+ 4 - 2
messages/es/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Mantenimiento",
     "customerPortal": "Portal del cliente",
     "reminders": "Recordatorios",
-    "allReminders": "Todos los recordatorios"
+    "allReminders": "Todos los recordatorios",
+    "auditLog": "Registro de auditoría"
   },
   "search": "Buscar...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Crear empresa",
     "lightMode": "Modo claro",
     "darkMode": "Modo oscuro",
-    "signOut": "Cerrar sesión"
+    "signOut": "Cerrar sesión",
+    "auditLog": "Registro de auditoría"
   }
 }

+ 86 - 0
messages/fr/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Rechercher dans le journal d'audit...",
+  "allActions": "Toutes les actions",
+  "allEntities": "Toutes les entités",
+  "allUsers": "Tous les utilisateurs",
+  "timestamp": "Horodatage",
+  "user": "Utilisateur",
+  "action": "Action",
+  "details": "Détails",
+  "entityId": "ID de l'entité",
+  "noLogsFound": "Aucun journal d'audit trouvé",
+  "unknownUser": "Inconnu",
+  "logDetails": "Détails du journal",
+  "entity": "Entité",
+  "ipAddress": "Adresse IP",
+  "userAgentLabel": "Agent utilisateur",
+  "metadata": "Métadonnées",
+  "actions": {
+    "vehicle_create": "Véhicule créé",
+    "vehicle_update": "Véhicule mis à jour",
+    "vehicle_delete": "Véhicule supprimé",
+    "vehicle_archive": "Véhicule archivé",
+    "vehicle_unarchive": "Véhicule désarchivé",
+    "service_create": "Fiche de service créée",
+    "service_update": "Fiche de service mise à jour",
+    "service_status": "Statut du service modifié",
+    "service_delete": "Fiche de service supprimée",
+    "quote_create": "Devis créé",
+    "quote_update": "Devis mis à jour",
+    "quote_status": "Statut du devis modifié",
+    "quote_delete": "Devis supprimé",
+    "quote_convert": "Devis converti",
+    "inspection_create": "Inspection créée",
+    "inspection_complete": "Inspection terminée",
+    "inspection_delete": "Inspection supprimée",
+    "payment_create": "Paiement enregistré",
+    "payment_delete": "Paiement supprimé",
+    "customer_create": "Client créé",
+    "customer_update": "Client mis à jour",
+    "customer_delete": "Client supprimé",
+    "inventory_create": "Pièce d'inventaire créée",
+    "inventory_update": "Pièce d'inventaire mise à jour",
+    "inventory_delete": "Pièce d'inventaire supprimée",
+    "team_invite": "Membre d'équipe invité",
+    "team_sendInvitation": "Invitation envoyée",
+    "team_cancelInvitation": "Invitation annulée",
+    "team_updateRole": "Rôle du membre modifié",
+    "team_removeMember": "Membre d'équipe supprimé",
+    "role_create": "Rôle créé",
+    "role_update": "Rôle mis à jour",
+    "role_delete": "Rôle supprimé",
+    "technician_create": "Technicien créé",
+    "technician_update": "Technicien mis à jour",
+    "technician_delete": "Technicien supprimé",
+    "recurringInvoice_create": "Facture récurrente créée",
+    "recurringInvoice_update": "Facture récurrente mise à jour",
+    "recurringInvoice_delete": "Facture récurrente supprimée",
+    "email_sendQuote": "E-mail de devis envoyé",
+    "email_sendInvoice": "E-mail de facture envoyé",
+    "email_sendInspection": "E-mail d'inspection envoyé",
+    "sms_send": "SMS envoyé",
+    "note_create": "Note créée",
+    "note_delete": "Note supprimée",
+    "reminder_create": "Rappel créé",
+    "reminder_delete": "Rappel supprimé",
+    "organization_create": "Organisation créée",
+    "subscription_cancel": "Abonnement annulé",
+    "subscription_resume": "Abonnement repris",
+    "settings_deleteContent": "Contenu supprimé",
+    "settings_updateInvoiceLayout": "Mise en page de facture mise à jour",
+    "settings_updateQuoteLayout": "Mise en page de devis mise à jour",
+    "settings_updatePortalSlug": "URL du portail mise à jour",
+    "inspectionTemplate_create": "Modèle d'inspection créé",
+    "inspectionTemplate_update": "Modèle d'inspection mis à jour",
+    "inspectionTemplate_delete": "Modèle d'inspection supprimé",
+    "customField_create": "Champ personnalisé créé",
+    "customField_update": "Champ personnalisé mis à jour",
+    "customField_delete": "Champ personnalisé supprimé",
+    "quoteRequest_update": "Demande de devis mise à jour",
+    "auth_login": "Utilisateur connecté",
+    "auth_loginFailed": "Tentative de connexion échouée",
+    "auth_register": "Utilisateur inscrit",
+    "auth_passwordReset": "Réinitialisation du mot de passe demandée",
+    "auth_permissionDenied": "Permission refusée"
+  }
+}

+ 34 - 31
messages/fr/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Activité récente",
+  "noRecentActivity": "Aucune activité récente",
+  "unknownUser": "Utilisateur",
   "stats": {
     "activeJobs": "Travaux en cours",
     "pending": "En attente",
@@ -9,24 +12,24 @@
   "viewAll": "Tout voir",
   "showCards": "Afficher les cartes",
   "cards": {
-    "maintenance": "Maintenance pr\u00e9vue",
-    "reminders": "Rappels \u00e0 venir",
+    "maintenance": "Maintenance prévue",
+    "reminders": "Rappels à venir",
     "inspections": "Inspections",
     "quoteRequests": "Demandes de devis",
-    "quoteResponses": "R\u00e9ponses aux devis",
-    "sms": "Messages r\u00e9cents",
-    "notifications": "Notifications r\u00e9centes",
-    "recentCompleted": "Termin\u00e9s r\u00e9cemment",
+    "quoteResponses": "Réponses aux devis",
+    "sms": "Messages récents",
+    "notifications": "Notifications récentes",
+    "recentCompleted": "Terminés récemment",
     "activeJobs": "Travaux en cours"
   },
   "maintenance": {
-    "title": "Maintenance pr\u00e9vue \u00e0 effectuer",
-    "description": "Bas\u00e9 sur l\u2019historique d\u2019entretien et le kilom\u00e9trage quotidien estim\u00e9",
+    "title": "Maintenance prévue à effectuer",
+    "description": "Basé sur l’historique d’entretien et le kilométrage quotidien estimé",
     "overdue": "En retard",
     "approaching": "Imminent",
     "estimated": "Est. {mileage} {unit}",
     "sinceLastService": "{mileage} {unit} depuis le dernier entretien",
-    "certainty": "{percent}\u00a0% de certitude",
+    "certainty": "{percent} % de certitude",
     "active": "Actifs",
     "dismissedTab": "Ignorés",
     "noActive": "Aucun véhicule n'a besoin d'entretien actuellement.",
@@ -34,57 +37,57 @@
     "restore": "Restaurer"
   },
   "reminders": {
-    "title": "Rappels \u00e0 venir",
+    "title": "Rappels à venir",
     "overdue": "En retard",
-    "dueSoon": "Bient\u00f4t d\u00fb",
+    "dueSoon": "Bientôt dû",
     "noData": "Aucun rappel à venir."
   },
   "messages": {
-    "title": "Messages r\u00e9cents",
+    "title": "Messages récents",
     "viewAll": "Tout afficher",
-    "you": "Vous\u00a0: ",
+    "you": "Vous : ",
     "noData": "Aucun message récent."
   },
   "notifications": {
-    "title": "Notifications r\u00e9centes",
+    "title": "Notifications récentes",
     "noData": "Aucune notification récente."
   },
   "inspections": {
     "title": "Inspections",
     "inProgress": "En cours",
-    "completed": "Termin\u00e9",
-    "itemCount": "{inspected}/{total} \u00e9l\u00e9ments",
+    "completed": "Terminé",
+    "itemCount": "{inspected}/{total} éléments",
     "noData": "Aucune inspection."
   },
   "quoteRequests": {
     "title": "Demandes de devis",
-    "description": "Clients demandant des devis \u00e0 partir d\u2019inspections partag\u00e9es",
+    "description": "Clients demandant des devis à partir d’inspections partagées",
     "pending": "En attente",
-    "createQuote": "Cr\u00e9er un devis",
-    "itemsRequested": "{count, plural, one {# \u00e9l\u00e9ment demand\u00e9} other {# \u00e9l\u00e9ments demand\u00e9s}}",
+    "createQuote": "Créer un devis",
+    "itemsRequested": "{count, plural, one {# élément demandé} other {# éléments demandés}}",
     "noData": "Aucune demande de devis en attente."
   },
   "quoteResponses": {
-    "title": "R\u00e9ponses des clients aux devis",
-    "description": "Devis que les clients ont accept\u00e9s ou pour lesquels des modifications ont \u00e9t\u00e9 demand\u00e9es",
-    "accepted": "Accept\u00e9",
-    "changesRequested": "Modifications demand\u00e9es",
+    "title": "Réponses des clients aux devis",
+    "description": "Devis que les clients ont acceptés ou pour lesquels des modifications ont été demandées",
+    "accepted": "Accepté",
+    "changesRequested": "Modifications demandées",
     "convertToWorkOrder": "Convertir en ordre de travail",
     "viewQuote": "Voir le devis",
     "noData": "Aucune réponse client."
   },
   "recentCompleted": {
-    "title": "Termin\u00e9s r\u00e9cemment",
+    "title": "Terminés récemment",
     "date": "Date",
-    "vehicle": "V\u00e9hicule",
+    "vehicle": "Véhicule",
     "customer": "Client",
     "serviceTitle": "Titre",
     "total": "Total",
-    "empty": "Aucun entretien termin\u00e9 pour le moment"
+    "empty": "Aucun entretien terminé pour le moment"
   },
   "activeJobsTable": {
     "title": "Travaux en cours",
-    "vehicle": "V\u00e9hicule",
+    "vehicle": "Véhicule",
     "customer": "Client",
     "serviceTitle": "Titre",
     "status": "Statut",
@@ -93,9 +96,9 @@
   },
   "relativeTime": {
     "now": "maintenant",
-    "minutes": "{count}\u00a0min",
-    "hours": "{count}\u00a0h",
-    "days": "{count}\u00a0j"
+    "minutes": "{count} min",
+    "hours": "{count} h",
+    "days": "{count} j"
   },
-  "error": "\u00c9chec du chargement du tableau de bord"
+  "error": "Échec du chargement du tableau de bord"
 }

+ 4 - 2
messages/fr/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Maintenance",
     "customerPortal": "Portail client",
     "reminders": "Rappels",
-    "allReminders": "Tous les rappels"
+    "allReminders": "Tous les rappels",
+    "auditLog": "Journal d'audit"
   },
   "search": "Rechercher\u2026",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Créer l'entreprise",
     "lightMode": "Mode clair",
     "darkMode": "Mode sombre",
-    "signOut": "Se déconnecter"
+    "signOut": "Se déconnecter",
+    "auditLog": "Journal d'audit"
   }
 }

+ 86 - 0
messages/it/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Cerca nel registro attività...",
+  "allActions": "Tutte le azioni",
+  "allEntities": "Tutte le entità",
+  "allUsers": "Tutti gli utenti",
+  "timestamp": "Data e ora",
+  "user": "Utente",
+  "action": "Azione",
+  "details": "Dettagli",
+  "entityId": "ID entità",
+  "noLogsFound": "Nessun registro di attività trovato",
+  "unknownUser": "Sconosciuto",
+  "logDetails": "Dettagli del registro",
+  "entity": "Entità",
+  "ipAddress": "Indirizzo IP",
+  "userAgentLabel": "User Agent",
+  "metadata": "Metadati",
+  "actions": {
+    "vehicle_create": "Veicolo creato",
+    "vehicle_update": "Veicolo aggiornato",
+    "vehicle_delete": "Veicolo eliminato",
+    "vehicle_archive": "Veicolo archiviato",
+    "vehicle_unarchive": "Veicolo ripristinato",
+    "service_create": "Scheda di servizio creata",
+    "service_update": "Scheda di servizio aggiornata",
+    "service_status": "Stato del servizio modificato",
+    "service_delete": "Scheda di servizio eliminata",
+    "quote_create": "Preventivo creato",
+    "quote_update": "Preventivo aggiornato",
+    "quote_status": "Stato del preventivo modificato",
+    "quote_delete": "Preventivo eliminato",
+    "quote_convert": "Preventivo convertito",
+    "inspection_create": "Ispezione creata",
+    "inspection_complete": "Ispezione completata",
+    "inspection_delete": "Ispezione eliminata",
+    "payment_create": "Pagamento registrato",
+    "payment_delete": "Pagamento eliminato",
+    "customer_create": "Cliente creato",
+    "customer_update": "Cliente aggiornato",
+    "customer_delete": "Cliente eliminato",
+    "inventory_create": "Pezzo di inventario creato",
+    "inventory_update": "Pezzo di inventario aggiornato",
+    "inventory_delete": "Pezzo di inventario eliminato",
+    "team_invite": "Membro del team invitato",
+    "team_sendInvitation": "Invito inviato",
+    "team_cancelInvitation": "Invito annullato",
+    "team_updateRole": "Ruolo del membro modificato",
+    "team_removeMember": "Membro del team rimosso",
+    "role_create": "Ruolo creato",
+    "role_update": "Ruolo aggiornato",
+    "role_delete": "Ruolo eliminato",
+    "technician_create": "Tecnico creato",
+    "technician_update": "Tecnico aggiornato",
+    "technician_delete": "Tecnico eliminato",
+    "recurringInvoice_create": "Fattura ricorrente creata",
+    "recurringInvoice_update": "Fattura ricorrente aggiornata",
+    "recurringInvoice_delete": "Fattura ricorrente eliminata",
+    "email_sendQuote": "E-mail preventivo inviata",
+    "email_sendInvoice": "E-mail fattura inviata",
+    "email_sendInspection": "E-mail ispezione inviata",
+    "sms_send": "SMS inviato",
+    "note_create": "Nota creata",
+    "note_delete": "Nota eliminata",
+    "reminder_create": "Promemoria creato",
+    "reminder_delete": "Promemoria eliminato",
+    "organization_create": "Organizzazione creata",
+    "subscription_cancel": "Abbonamento annullato",
+    "subscription_resume": "Abbonamento ripreso",
+    "settings_deleteContent": "Contenuto eliminato",
+    "settings_updateInvoiceLayout": "Layout fattura aggiornato",
+    "settings_updateQuoteLayout": "Layout preventivo aggiornato",
+    "settings_updatePortalSlug": "URL portale aggiornato",
+    "inspectionTemplate_create": "Modello di ispezione creato",
+    "inspectionTemplate_update": "Modello di ispezione aggiornato",
+    "inspectionTemplate_delete": "Modello di ispezione eliminato",
+    "customField_create": "Campo personalizzato creato",
+    "customField_update": "Campo personalizzato aggiornato",
+    "customField_delete": "Campo personalizzato eliminato",
+    "quoteRequest_update": "Richiesta di preventivo aggiornata",
+    "auth_login": "Utente connesso",
+    "auth_loginFailed": "Tentativo di accesso fallito",
+    "auth_register": "Utente registrato",
+    "auth_passwordReset": "Reimpostazione password richiesta",
+    "auth_permissionDenied": "Permesso negato"
+  }
+}

+ 3 - 0
messages/it/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Attività recente",
+  "noRecentActivity": "Nessuna attività recente",
+  "unknownUser": "Utente",
   "stats": {
     "activeJobs": "Lavori attivi",
     "pending": "In attesa",

+ 4 - 2
messages/it/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Manutenzione",
     "customerPortal": "Portale clienti",
     "reminders": "Promemoria",
-    "allReminders": "Tutti i promemoria"
+    "allReminders": "Tutti i promemoria",
+    "auditLog": "Registro attività"
   },
   "search": "Cerca...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Crea azienda",
     "lightMode": "Modalità chiara",
     "darkMode": "Modalità scura",
-    "signOut": "Esci"
+    "signOut": "Esci",
+    "auditLog": "Registro attività"
   }
 }

+ 86 - 0
messages/nb/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Søk i aktivitetslogg...",
+  "allActions": "Alle handlinger",
+  "allEntities": "Alle enheter",
+  "allUsers": "Alle brukere",
+  "timestamp": "Tidspunkt",
+  "user": "Bruker",
+  "action": "Handling",
+  "details": "Detaljer",
+  "entityId": "Enhets-ID",
+  "noLogsFound": "Ingen aktivitetslogger funnet",
+  "unknownUser": "Ukjent",
+  "logDetails": "Loggdetaljer",
+  "entity": "Enhet",
+  "ipAddress": "IP-adresse",
+  "userAgentLabel": "Nettleser",
+  "metadata": "Metadata",
+  "actions": {
+    "vehicle_create": "Opprettet kjøretøy",
+    "vehicle_update": "Oppdaterte kjøretøy",
+    "vehicle_delete": "Slettet kjøretøy",
+    "vehicle_archive": "Arkiverte kjøretøy",
+    "vehicle_unarchive": "Gjenopprettet kjøretøy",
+    "service_create": "Opprettet servicepost",
+    "service_update": "Oppdaterte servicepost",
+    "service_status": "Endret servicestatus",
+    "service_delete": "Slettet servicepost",
+    "quote_create": "Opprettet tilbud",
+    "quote_update": "Oppdaterte tilbud",
+    "quote_status": "Endret tilbudsstatus",
+    "quote_delete": "Slettet tilbud",
+    "quote_convert": "Konverterte tilbud",
+    "inspection_create": "Opprettet inspeksjon",
+    "inspection_complete": "Fullførte inspeksjon",
+    "inspection_delete": "Slettet inspeksjon",
+    "payment_create": "Registrerte betaling",
+    "payment_delete": "Slettet betaling",
+    "customer_create": "Opprettet kunde",
+    "customer_update": "Oppdaterte kunde",
+    "customer_delete": "Slettet kunde",
+    "inventory_create": "Opprettet lagerdel",
+    "inventory_update": "Oppdaterte lagerdel",
+    "inventory_delete": "Slettet lagerdel",
+    "team_invite": "Inviterte teammedlem",
+    "team_sendInvitation": "Sendte invitasjon",
+    "team_cancelInvitation": "Kansellerte invitasjon",
+    "team_updateRole": "Endret medlemsrolle",
+    "team_removeMember": "Fjernet teammedlem",
+    "role_create": "Opprettet rolle",
+    "role_update": "Oppdaterte rolle",
+    "role_delete": "Slettet rolle",
+    "technician_create": "Opprettet tekniker",
+    "technician_update": "Oppdaterte tekniker",
+    "technician_delete": "Slettet tekniker",
+    "recurringInvoice_create": "Opprettet gjentakende faktura",
+    "recurringInvoice_update": "Oppdaterte gjentakende faktura",
+    "recurringInvoice_delete": "Slettet gjentakende faktura",
+    "email_sendQuote": "Sendte tilbuds-e-post",
+    "email_sendInvoice": "Sendte faktura-e-post",
+    "email_sendInspection": "Sendte inspeksjons-e-post",
+    "sms_send": "Sendte SMS",
+    "note_create": "Opprettet notat",
+    "note_delete": "Slettet notat",
+    "reminder_create": "Opprettet påminnelse",
+    "reminder_delete": "Slettet påminnelse",
+    "organization_create": "Opprettet organisasjon",
+    "subscription_cancel": "Kansellerte abonnement",
+    "subscription_resume": "Gjenopptok abonnement",
+    "settings_deleteContent": "Slettet innhold",
+    "settings_updateInvoiceLayout": "Oppdaterte fakturalayout",
+    "settings_updateQuoteLayout": "Oppdaterte tilbudslayout",
+    "settings_updatePortalSlug": "Oppdaterte portal-URL",
+    "inspectionTemplate_create": "Opprettet inspeksjonsmal",
+    "inspectionTemplate_update": "Oppdaterte inspeksjonsmal",
+    "inspectionTemplate_delete": "Slettet inspeksjonsmal",
+    "customField_create": "Opprettet egendefinert felt",
+    "customField_update": "Oppdaterte egendefinert felt",
+    "customField_delete": "Slettet egendefinert felt",
+    "quoteRequest_update": "Oppdaterte tilbudsforespørsel",
+    "auth_login": "Bruker logget inn",
+    "auth_loginFailed": "Mislykket påloggingsforsøk",
+    "auth_register": "Bruker registrert",
+    "auth_passwordReset": "Passord-tilbakestilling forespurt",
+    "auth_permissionDenied": "Tilgang nektet"
+  }
+}

+ 3 - 0
messages/nb/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Nylig aktivitet",
+  "noRecentActivity": "Ingen nylig aktivitet",
+  "unknownUser": "Bruker",
   "stats": {
     "activeJobs": "Aktive jobber",
     "pending": "Ventende",

+ 4 - 2
messages/nb/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Vedlikehold",
     "customerPortal": "Kundeportal",
     "reminders": "Påminnelser",
-    "allReminders": "Alle påminnelser"
+    "allReminders": "Alle påminnelser",
+    "auditLog": "Aktivitetslogg"
   },
   "search": "Søk...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Opprett firma",
     "lightMode": "Lyst modus",
     "darkMode": "Mørkt modus",
-    "signOut": "Logg ut"
+    "signOut": "Logg ut",
+    "auditLog": "Aktivitetslogg"
   }
 }

+ 86 - 0
messages/nl/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Zoeken in activiteitenlog...",
+  "allActions": "Alle acties",
+  "allEntities": "Alle entiteiten",
+  "allUsers": "Alle gebruikers",
+  "timestamp": "Tijdstip",
+  "user": "Gebruiker",
+  "action": "Actie",
+  "details": "Details",
+  "entityId": "Entiteits-ID",
+  "noLogsFound": "Geen activiteitenlogs gevonden",
+  "unknownUser": "Onbekend",
+  "logDetails": "Logdetails",
+  "entity": "Entiteit",
+  "ipAddress": "IP-adres",
+  "userAgentLabel": "User-Agent",
+  "metadata": "Metadata",
+  "actions": {
+    "vehicle_create": "Voertuig aangemaakt",
+    "vehicle_update": "Voertuig bijgewerkt",
+    "vehicle_delete": "Voertuig verwijderd",
+    "vehicle_archive": "Voertuig gearchiveerd",
+    "vehicle_unarchive": "Voertuig hersteld",
+    "service_create": "Servicerecord aangemaakt",
+    "service_update": "Servicerecord bijgewerkt",
+    "service_status": "Servicestatus gewijzigd",
+    "service_delete": "Servicerecord verwijderd",
+    "quote_create": "Offerte aangemaakt",
+    "quote_update": "Offerte bijgewerkt",
+    "quote_status": "Offertestatus gewijzigd",
+    "quote_delete": "Offerte verwijderd",
+    "quote_convert": "Offerte geconverteerd",
+    "inspection_create": "Inspectie aangemaakt",
+    "inspection_complete": "Inspectie voltooid",
+    "inspection_delete": "Inspectie verwijderd",
+    "payment_create": "Betaling geregistreerd",
+    "payment_delete": "Betaling verwijderd",
+    "customer_create": "Klant aangemaakt",
+    "customer_update": "Klant bijgewerkt",
+    "customer_delete": "Klant verwijderd",
+    "inventory_create": "Inventarisonderdeel aangemaakt",
+    "inventory_update": "Inventarisonderdeel bijgewerkt",
+    "inventory_delete": "Inventarisonderdeel verwijderd",
+    "team_invite": "Teamlid uitgenodigd",
+    "team_sendInvitation": "Uitnodiging verzonden",
+    "team_cancelInvitation": "Uitnodiging geannuleerd",
+    "team_updateRole": "Lidrol gewijzigd",
+    "team_removeMember": "Teamlid verwijderd",
+    "role_create": "Rol aangemaakt",
+    "role_update": "Rol bijgewerkt",
+    "role_delete": "Rol verwijderd",
+    "technician_create": "Technicus aangemaakt",
+    "technician_update": "Technicus bijgewerkt",
+    "technician_delete": "Technicus verwijderd",
+    "recurringInvoice_create": "Terugkerende factuur aangemaakt",
+    "recurringInvoice_update": "Terugkerende factuur bijgewerkt",
+    "recurringInvoice_delete": "Terugkerende factuur verwijderd",
+    "email_sendQuote": "Offerte-e-mail verzonden",
+    "email_sendInvoice": "Factuur-e-mail verzonden",
+    "email_sendInspection": "Inspectie-e-mail verzonden",
+    "sms_send": "SMS verzonden",
+    "note_create": "Notitie aangemaakt",
+    "note_delete": "Notitie verwijderd",
+    "reminder_create": "Herinnering aangemaakt",
+    "reminder_delete": "Herinnering verwijderd",
+    "organization_create": "Organisatie aangemaakt",
+    "subscription_cancel": "Abonnement opgezegd",
+    "subscription_resume": "Abonnement hervat",
+    "settings_deleteContent": "Inhoud verwijderd",
+    "settings_updateInvoiceLayout": "Factuurlay-out bijgewerkt",
+    "settings_updateQuoteLayout": "Offertelay-out bijgewerkt",
+    "settings_updatePortalSlug": "Portal-URL bijgewerkt",
+    "inspectionTemplate_create": "Inspectiesjabloon aangemaakt",
+    "inspectionTemplate_update": "Inspectiesjabloon bijgewerkt",
+    "inspectionTemplate_delete": "Inspectiesjabloon verwijderd",
+    "customField_create": "Aangepast veld aangemaakt",
+    "customField_update": "Aangepast veld bijgewerkt",
+    "customField_delete": "Aangepast veld verwijderd",
+    "quoteRequest_update": "Offerteaanvraag bijgewerkt",
+    "auth_login": "Gebruiker ingelogd",
+    "auth_loginFailed": "Mislukte inlogpoging",
+    "auth_register": "Gebruiker geregistreerd",
+    "auth_passwordReset": "Wachtwoord reset aangevraagd",
+    "auth_permissionDenied": "Toegang geweigerd"
+  }
+}

+ 3 - 0
messages/nl/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Recente activiteit",
+  "noRecentActivity": "Geen recente activiteit",
+  "unknownUser": "Gebruiker",
   "stats": {
     "activeJobs": "Actieve opdrachten",
     "pending": "In afwachting",

+ 4 - 2
messages/nl/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Onderhoud",
     "customerPortal": "Klantenportaal",
     "reminders": "Herinneringen",
-    "allReminders": "Alle herinneringen"
+    "allReminders": "Alle herinneringen",
+    "auditLog": "Activiteitenlog"
   },
   "search": "Zoeken...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Bedrijf aanmaken",
     "lightMode": "Lichte modus",
     "darkMode": "Donkere modus",
-    "signOut": "Uitloggen"
+    "signOut": "Uitloggen",
+    "auditLog": "Activiteitenlog"
   }
 }

+ 86 - 0
messages/pl/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Szukaj w dzienniku audytu...",
+  "allActions": "Wszystkie akcje",
+  "allEntities": "Wszystkie encje",
+  "allUsers": "Wszyscy użytkownicy",
+  "timestamp": "Data i godzina",
+  "user": "Użytkownik",
+  "action": "Akcja",
+  "details": "Szczegóły",
+  "entityId": "ID encji",
+  "noLogsFound": "Nie znaleziono wpisów w dzienniku audytu",
+  "unknownUser": "Nieznany",
+  "logDetails": "Szczegóły wpisu",
+  "entity": "Encja",
+  "ipAddress": "Adres IP",
+  "userAgentLabel": "Przeglądarka",
+  "metadata": "Metadane",
+  "actions": {
+    "vehicle_create": "Utworzono pojazd",
+    "vehicle_update": "Zaktualizowano pojazd",
+    "vehicle_delete": "Usunięto pojazd",
+    "vehicle_archive": "Zarchiwizowano pojazd",
+    "vehicle_unarchive": "Przywrócono pojazd",
+    "service_create": "Utworzono wpis serwisowy",
+    "service_update": "Zaktualizowano wpis serwisowy",
+    "service_status": "Zmieniono status serwisu",
+    "service_delete": "Usunięto wpis serwisowy",
+    "quote_create": "Utworzono wycenę",
+    "quote_update": "Zaktualizowano wycenę",
+    "quote_status": "Zmieniono status wyceny",
+    "quote_delete": "Usunięto wycenę",
+    "quote_convert": "Przekonwertowano wycenę",
+    "inspection_create": "Utworzono inspekcję",
+    "inspection_complete": "Ukończono inspekcję",
+    "inspection_delete": "Usunięto inspekcję",
+    "payment_create": "Zarejestrowano płatność",
+    "payment_delete": "Usunięto płatność",
+    "customer_create": "Utworzono klienta",
+    "customer_update": "Zaktualizowano klienta",
+    "customer_delete": "Usunięto klienta",
+    "inventory_create": "Utworzono część magazynową",
+    "inventory_update": "Zaktualizowano część magazynową",
+    "inventory_delete": "Usunięto część magazynową",
+    "team_invite": "Zaproszono członka zespołu",
+    "team_sendInvitation": "Wysłano zaproszenie",
+    "team_cancelInvitation": "Anulowano zaproszenie",
+    "team_updateRole": "Zmieniono rolę członka",
+    "team_removeMember": "Usunięto członka zespołu",
+    "role_create": "Utworzono rolę",
+    "role_update": "Zaktualizowano rolę",
+    "role_delete": "Usunięto rolę",
+    "technician_create": "Utworzono technika",
+    "technician_update": "Zaktualizowano technika",
+    "technician_delete": "Usunięto technika",
+    "recurringInvoice_create": "Utworzono fakturę cykliczną",
+    "recurringInvoice_update": "Zaktualizowano fakturę cykliczną",
+    "recurringInvoice_delete": "Usunięto fakturę cykliczną",
+    "email_sendQuote": "Wysłano e-mail z wyceną",
+    "email_sendInvoice": "Wysłano e-mail z fakturą",
+    "email_sendInspection": "Wysłano e-mail z inspekcją",
+    "sms_send": "Wysłano SMS",
+    "note_create": "Utworzono notatkę",
+    "note_delete": "Usunięto notatkę",
+    "reminder_create": "Utworzono przypomnienie",
+    "reminder_delete": "Usunięto przypomnienie",
+    "organization_create": "Utworzono organizację",
+    "subscription_cancel": "Anulowano subskrypcję",
+    "subscription_resume": "Wznowiono subskrypcję",
+    "settings_deleteContent": "Usunięto zawartość",
+    "settings_updateInvoiceLayout": "Zaktualizowano układ faktury",
+    "settings_updateQuoteLayout": "Zaktualizowano układ wyceny",
+    "settings_updatePortalSlug": "Zaktualizowano URL portalu",
+    "inspectionTemplate_create": "Utworzono szablon inspekcji",
+    "inspectionTemplate_update": "Zaktualizowano szablon inspekcji",
+    "inspectionTemplate_delete": "Usunięto szablon inspekcji",
+    "customField_create": "Utworzono pole niestandardowe",
+    "customField_update": "Zaktualizowano pole niestandardowe",
+    "customField_delete": "Usunięto pole niestandardowe",
+    "quoteRequest_update": "Zaktualizowano zapytanie o wycenę",
+    "auth_login": "Użytkownik zalogowany",
+    "auth_loginFailed": "Nieudana próba logowania",
+    "auth_register": "Użytkownik zarejestrowany",
+    "auth_passwordReset": "Żądanie resetowania hasła",
+    "auth_permissionDenied": "Odmowa dostępu"
+  }
+}

+ 3 - 0
messages/pl/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Ostatnia aktywność",
+  "noRecentActivity": "Brak ostatniej aktywności",
+  "unknownUser": "Użytkownik",
   "stats": {
     "activeJobs": "Aktywne zlecenia",
     "pending": "Oczekujące",

+ 4 - 2
messages/pl/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Konserwacja",
     "customerPortal": "Portal klienta",
     "reminders": "Przypomnienia",
-    "allReminders": "Wszystkie przypomnienia"
+    "allReminders": "Wszystkie przypomnienia",
+    "auditLog": "Dziennik audytu"
   },
   "search": "Szukaj...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Utwórz firmę",
     "lightMode": "Tryb jasny",
     "darkMode": "Tryb ciemny",
-    "signOut": "Wyloguj się"
+    "signOut": "Wyloguj się",
+    "auditLog": "Dziennik audytu"
   }
 }

+ 86 - 0
messages/pt-BR/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Pesquisar no registro de auditoria...",
+  "allActions": "Todas as ações",
+  "allEntities": "Todas as entidades",
+  "allUsers": "Todos os usuários",
+  "timestamp": "Data e hora",
+  "user": "Usuário",
+  "action": "Ação",
+  "details": "Detalhes",
+  "entityId": "ID da entidade",
+  "noLogsFound": "Nenhum registro de auditoria encontrado",
+  "unknownUser": "Desconhecido",
+  "logDetails": "Detalhes do registro",
+  "entity": "Entidade",
+  "ipAddress": "Endereço IP",
+  "userAgentLabel": "Agente do usuário",
+  "metadata": "Metadados",
+  "actions": {
+    "vehicle_create": "Veículo criado",
+    "vehicle_update": "Veículo atualizado",
+    "vehicle_delete": "Veículo excluído",
+    "vehicle_archive": "Veículo arquivado",
+    "vehicle_unarchive": "Veículo desarquivado",
+    "service_create": "Registro de serviço criado",
+    "service_update": "Registro de serviço atualizado",
+    "service_status": "Status do serviço alterado",
+    "service_delete": "Registro de serviço excluído",
+    "quote_create": "Orçamento criado",
+    "quote_update": "Orçamento atualizado",
+    "quote_status": "Status do orçamento alterado",
+    "quote_delete": "Orçamento excluído",
+    "quote_convert": "Orçamento convertido",
+    "inspection_create": "Inspeção criada",
+    "inspection_complete": "Inspeção concluída",
+    "inspection_delete": "Inspeção excluída",
+    "payment_create": "Pagamento registrado",
+    "payment_delete": "Pagamento excluído",
+    "customer_create": "Cliente criado",
+    "customer_update": "Cliente atualizado",
+    "customer_delete": "Cliente excluído",
+    "inventory_create": "Peça de inventário criada",
+    "inventory_update": "Peça de inventário atualizada",
+    "inventory_delete": "Peça de inventário excluída",
+    "team_invite": "Membro da equipe convidado",
+    "team_sendInvitation": "Convite enviado",
+    "team_cancelInvitation": "Convite cancelado",
+    "team_updateRole": "Função do membro alterada",
+    "team_removeMember": "Membro da equipe removido",
+    "role_create": "Função criada",
+    "role_update": "Função atualizada",
+    "role_delete": "Função excluída",
+    "technician_create": "Técnico criado",
+    "technician_update": "Técnico atualizado",
+    "technician_delete": "Técnico excluído",
+    "recurringInvoice_create": "Fatura recorrente criada",
+    "recurringInvoice_update": "Fatura recorrente atualizada",
+    "recurringInvoice_delete": "Fatura recorrente excluída",
+    "email_sendQuote": "E-mail de orçamento enviado",
+    "email_sendInvoice": "E-mail de fatura enviado",
+    "email_sendInspection": "E-mail de inspeção enviado",
+    "sms_send": "SMS enviado",
+    "note_create": "Nota criada",
+    "note_delete": "Nota excluída",
+    "reminder_create": "Lembrete criado",
+    "reminder_delete": "Lembrete excluído",
+    "organization_create": "Organização criada",
+    "subscription_cancel": "Assinatura cancelada",
+    "subscription_resume": "Assinatura retomada",
+    "settings_deleteContent": "Conteúdo excluído",
+    "settings_updateInvoiceLayout": "Layout da fatura atualizado",
+    "settings_updateQuoteLayout": "Layout do orçamento atualizado",
+    "settings_updatePortalSlug": "URL do portal atualizada",
+    "inspectionTemplate_create": "Modelo de inspeção criado",
+    "inspectionTemplate_update": "Modelo de inspeção atualizado",
+    "inspectionTemplate_delete": "Modelo de inspeção excluído",
+    "customField_create": "Campo personalizado criado",
+    "customField_update": "Campo personalizado atualizado",
+    "customField_delete": "Campo personalizado excluído",
+    "quoteRequest_update": "Solicitação de orçamento atualizada",
+    "auth_login": "Usuário conectado",
+    "auth_loginFailed": "Tentativa de login falhada",
+    "auth_register": "Usuário registrado",
+    "auth_passwordReset": "Redefinição de senha solicitada",
+    "auth_permissionDenied": "Permissão negada"
+  }
+}

+ 3 - 0
messages/pt-BR/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Atividade recente",
+  "noRecentActivity": "Nenhuma atividade recente",
+  "unknownUser": "Usuário",
   "stats": {
     "activeJobs": "Trabalhos Ativos",
     "pending": "Pendentes",

+ 4 - 2
messages/pt-BR/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Manutenção",
     "customerPortal": "Portal do Cliente",
     "reminders": "Lembretes",
-    "allReminders": "Todos os lembretes"
+    "allReminders": "Todos os lembretes",
+    "auditLog": "Registro de auditoria"
   },
   "search": "Buscar...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Criar Empresa",
     "lightMode": "Modo Claro",
     "darkMode": "Modo Escuro",
-    "signOut": "Sair"
+    "signOut": "Sair",
+    "auditLog": "Registro de auditoria"
   }
 }

+ 86 - 0
messages/tr/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Denetim günlüğünde ara...",
+  "allActions": "Tüm eylemler",
+  "allEntities": "Tüm varlıklar",
+  "allUsers": "Tüm kullanıcılar",
+  "timestamp": "Zaman damgası",
+  "user": "Kullanıcı",
+  "action": "Eylem",
+  "details": "Ayrıntılar",
+  "entityId": "Varlık ID",
+  "noLogsFound": "Denetim günlüğü bulunamadı",
+  "unknownUser": "Bilinmeyen",
+  "logDetails": "Günlük Detayları",
+  "entity": "Varlık",
+  "ipAddress": "IP Adresi",
+  "userAgentLabel": "Tarayıcı",
+  "metadata": "Meta veriler",
+  "actions": {
+    "vehicle_create": "Araç oluşturuldu",
+    "vehicle_update": "Araç güncellendi",
+    "vehicle_delete": "Araç silindi",
+    "vehicle_archive": "Araç arşivlendi",
+    "vehicle_unarchive": "Araç arşivden çıkarıldı",
+    "service_create": "Servis kaydı oluşturuldu",
+    "service_update": "Servis kaydı güncellendi",
+    "service_status": "Servis durumu değiştirildi",
+    "service_delete": "Servis kaydı silindi",
+    "quote_create": "Teklif oluşturuldu",
+    "quote_update": "Teklif güncellendi",
+    "quote_status": "Teklif durumu değiştirildi",
+    "quote_delete": "Teklif silindi",
+    "quote_convert": "Teklif dönüştürüldü",
+    "inspection_create": "Muayene oluşturuldu",
+    "inspection_complete": "Muayene tamamlandı",
+    "inspection_delete": "Muayene silindi",
+    "payment_create": "Ödeme kaydedildi",
+    "payment_delete": "Ödeme silindi",
+    "customer_create": "Müşteri oluşturuldu",
+    "customer_update": "Müşteri güncellendi",
+    "customer_delete": "Müşteri silindi",
+    "inventory_create": "Envanter parçası oluşturuldu",
+    "inventory_update": "Envanter parçası güncellendi",
+    "inventory_delete": "Envanter parçası silindi",
+    "team_invite": "Ekip üyesi davet edildi",
+    "team_sendInvitation": "Davet gönderildi",
+    "team_cancelInvitation": "Davet iptal edildi",
+    "team_updateRole": "Üye rolü değiştirildi",
+    "team_removeMember": "Ekip üyesi kaldırıldı",
+    "role_create": "Rol oluşturuldu",
+    "role_update": "Rol güncellendi",
+    "role_delete": "Rol silindi",
+    "technician_create": "Teknisyen oluşturuldu",
+    "technician_update": "Teknisyen güncellendi",
+    "technician_delete": "Teknisyen silindi",
+    "recurringInvoice_create": "Tekrarlayan fatura oluşturuldu",
+    "recurringInvoice_update": "Tekrarlayan fatura güncellendi",
+    "recurringInvoice_delete": "Tekrarlayan fatura silindi",
+    "email_sendQuote": "Teklif e-postası gönderildi",
+    "email_sendInvoice": "Fatura e-postası gönderildi",
+    "email_sendInspection": "Muayene e-postası gönderildi",
+    "sms_send": "SMS gönderildi",
+    "note_create": "Not oluşturuldu",
+    "note_delete": "Not silindi",
+    "reminder_create": "Hatırlatma oluşturuldu",
+    "reminder_delete": "Hatırlatma silindi",
+    "organization_create": "Organizasyon oluşturuldu",
+    "subscription_cancel": "Abonelik iptal edildi",
+    "subscription_resume": "Abonelik devam ettirildi",
+    "settings_deleteContent": "İçerik silindi",
+    "settings_updateInvoiceLayout": "Fatura düzeni güncellendi",
+    "settings_updateQuoteLayout": "Teklif düzeni güncellendi",
+    "settings_updatePortalSlug": "Portal URL güncellendi",
+    "inspectionTemplate_create": "Muayene şablonu oluşturuldu",
+    "inspectionTemplate_update": "Muayene şablonu güncellendi",
+    "inspectionTemplate_delete": "Muayene şablonu silindi",
+    "customField_create": "Özel alan oluşturuldu",
+    "customField_update": "Özel alan güncellendi",
+    "customField_delete": "Özel alan silindi",
+    "quoteRequest_update": "Teklif talebi güncellendi",
+    "auth_login": "Kullanıcı giriş yaptı",
+    "auth_loginFailed": "Başarısız giriş denemesi",
+    "auth_register": "Kullanıcı kaydoldu",
+    "auth_passwordReset": "Şifre sıfırlama talep edildi",
+    "auth_permissionDenied": "İzin reddedildi"
+  }
+}

+ 3 - 0
messages/tr/dashboard.json

@@ -1,4 +1,7 @@
 {
+  "recentActivity": "Son aktivite",
+  "noRecentActivity": "Son aktivite yok",
+  "unknownUser": "Kullanıcı",
   "stats": {
     "activeJobs": "Aktif İşler",
     "pending": "Beklemede",

+ 4 - 2
messages/tr/navigation.json

@@ -48,7 +48,8 @@
     "maintenance": "Bakım",
     "customerPortal": "Müşteri Portalı",
     "reminders": "Hatırlatmalar",
-    "allReminders": "Tüm hatırlatmalar"
+    "allReminders": "Tüm hatırlatmalar",
+    "auditLog": "Denetim günlüğü"
   },
   "search": "Ara...",
   "shortcut": "Ctrl+K",
@@ -83,6 +84,7 @@
     "createCompany": "Şirket Oluştur",
     "lightMode": "Açık Mod",
     "darkMode": "Koyu Mod",
-    "signOut": "Çıkış Yap"
+    "signOut": "Çıkış Yap",
+    "auditLog": "Denetim günlüğü"
   }
 }

+ 31 - 0
prisma/migrations/20260311184346_audit_log/migration.sql

@@ -0,0 +1,31 @@
+-- CreateTable
+CREATE TABLE "audit_logs" (
+    "id" TEXT NOT NULL,
+    "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "action" TEXT NOT NULL,
+    "entity" TEXT,
+    "entityId" TEXT,
+    "message" TEXT,
+    "metadata" JSONB,
+    "ip" TEXT,
+    "userAgent" TEXT,
+    "userId" TEXT,
+    "organizationId" TEXT,
+
+    CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "audit_logs_organizationId_timestamp_idx" ON "audit_logs"("organizationId", "timestamp" DESC);
+
+-- CreateIndex
+CREATE INDEX "audit_logs_userId_timestamp_idx" ON "audit_logs"("userId", "timestamp" DESC);
+
+-- CreateIndex
+CREATE INDEX "audit_logs_action_timestamp_idx" ON "audit_logs"("action", "timestamp" DESC);
+
+-- AddForeignKey
+ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 25 - 0
prisma/schema.prisma

@@ -33,6 +33,7 @@ model User {
   twoFactor              TwoFactor?
   passkeys               Passkey[]
   organizationMembers    OrganizationMember[]
+  auditLogs              AuditLog[]
 
   @@map("users")
 }
@@ -552,6 +553,7 @@ model Organization {
   inspectionTemplates    InspectionTemplate[]
   smsMessages            SmsMessage[]
   technicians            Technician[]
+  auditLogs              AuditLog[]
 
   @@map("organizations")
 }
@@ -903,6 +905,29 @@ model Technician {
   @@map("technicians")
 }
 
+model AuditLog {
+  id              String   @id @default(cuid())
+  timestamp       DateTime @default(now())
+  action          String
+  entity          String?
+  entityId        String?
+  message         String?
+  metadata        Json?
+  ip              String?
+  userAgent       String?
+
+  userId          String?
+  user            User?    @relation(fields: [userId], references: [id], onDelete: SetNull)
+
+  organizationId  String?
+  organization    Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
+
+  @@index([organizationId, timestamp(sort: Desc)])
+  @@index([userId, timestamp(sort: Desc)])
+  @@index([action, timestamp(sort: Desc)])
+  @@map("audit_logs")
+}
+
 model CustomerSession {
   id        String   @id @default(cuid())
   token     String   @unique

+ 2 - 0
src/__tests__/multitenancy/vehicle-service-isolation.test.ts

@@ -170,6 +170,7 @@ describe("updateVehicle — cross-org isolation", () => {
 
   it("update query always includes organizationId to prevent cross-org writes", async () => {
     setupOrgAOwner();
+    vi.mocked(db.vehicle.findFirst).mockResolvedValue(ORG_A_VEHICLE as any);
     vi.mocked(db.vehicle.updateMany).mockResolvedValue({ count: 1 } as any);
 
     await updateVehicle({ id: "veh-a", make: "Toyota" });
@@ -183,6 +184,7 @@ describe("updateVehicle — cross-org isolation", () => {
 
   it("successfully updates a vehicle belonging to the caller's org", async () => {
     setupOrgAOwner();
+    vi.mocked(db.vehicle.findFirst).mockResolvedValue(ORG_A_VEHICLE as any);
     vi.mocked(db.vehicle.updateMany).mockResolvedValue({ count: 1 } as any);
 
     const result = await updateVehicle({ id: "veh-a", make: "Toyota", model: "Updated" });

+ 288 - 0
src/app/(authenticated)/audit-log/audit-log-client.tsx

@@ -0,0 +1,288 @@
+"use client";
+
+import { useCallback, useState, useTransition } from "react";
+import { useRouter, usePathname, useSearchParams } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+  DialogDescription,
+} from "@/components/ui/dialog";
+import { DataTablePagination } from "@/components/data-table-pagination";
+import { useFormatDate } from "@/lib/use-format-date";
+import { cn } from "@/lib/utils";
+import { Search, Loader2 } from "lucide-react";
+
+type AuditLogData = {
+  logs: {
+    id: string;
+    timestamp: Date;
+    action: string;
+    entity: string | null;
+    entityId: string | null;
+    message: string | null;
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    metadata: any;
+    ip: string | null;
+    userAgent: string | null;
+    user: { id: string; name: string | null; email: string | null } | null;
+  }[];
+  total: number;
+  page: number;
+  pageSize: number;
+  totalPages: number;
+  filters: {
+    actions: string[];
+    entities: string[];
+    users: { id: string; name: string | null; email: string | null }[];
+  };
+};
+
+function getActionColor(action: string): string {
+  if (action.includes("delete") || action.includes("remove")) return "destructive";
+  if (action.includes("create") || action.includes("invite") || action.includes("send")) return "default";
+  if (action.includes("update") || action.includes("status") || action.includes("complete")) return "secondary";
+  return "outline";
+}
+
+export function AuditLogClient({
+  data,
+  search,
+  actionFilter,
+  entityFilter,
+  userFilter,
+}: {
+  data: AuditLogData;
+  search: string;
+  actionFilter: string;
+  entityFilter: string;
+  userFilter: string;
+}) {
+  const router = useRouter();
+  const pathname = usePathname();
+  const searchParams = useSearchParams();
+  const [isPending, startTransition] = useTransition();
+  const [selectedLog, setSelectedLog] = useState<AuditLogData["logs"][number] | null>(null);
+  const { formatDateTime } = useFormatDate();
+  const t = useTranslations("audit");
+
+  const getActionLabel = (action: string) => {
+    try {
+      return t(`actions.${action.replace(".", "_")}`);
+    } catch {
+      return action;
+    }
+  };
+
+  const navigate = useCallback(
+    (params: Record<string, string | number | undefined>) => {
+      const newParams = new URLSearchParams(searchParams.toString());
+      for (const [key, value] of Object.entries(params)) {
+        if (value === undefined || value === "" || value === "all") {
+          newParams.delete(key);
+        } else {
+          newParams.set(key, String(value));
+        }
+      }
+      if (!("page" in params) && ("search" in params || "action" in params || "entity" in params || "userId" in params)) {
+        newParams.delete("page");
+      }
+      startTransition(() => {
+        router.push(`${pathname}?${newParams.toString()}`);
+      });
+    },
+    [router, pathname, searchParams],
+  );
+
+  return (
+    <div className="space-y-4">
+      {/* Filters */}
+      <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
+        <div className="relative flex-1">
+          <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
+          <Input
+            placeholder={t("searchPlaceholder")}
+            defaultValue={search}
+            onChange={(e) => {
+              const value = e.target.value;
+              if (value === search) return;
+              navigate({ search: value || undefined });
+            }}
+            className="pl-9 pr-9"
+          />
+          {isPending && <Loader2 className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 animate-spin text-muted-foreground" />}
+        </div>
+        <div className="flex gap-2">
+          <Select value={actionFilter} onValueChange={(v) => navigate({ action: v })}>
+            <SelectTrigger className="w-[180px]">
+              <SelectValue placeholder={t("allActions")} />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="all">{t("allActions")}</SelectItem>
+              {data.filters.actions.map((a) => (
+                <SelectItem key={a} value={a}>
+                  {getActionLabel(a)}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          <Select value={entityFilter} onValueChange={(v) => navigate({ entity: v })}>
+            <SelectTrigger className="w-[160px]">
+              <SelectValue placeholder={t("allEntities")} />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="all">{t("allEntities")}</SelectItem>
+              {data.filters.entities.map((e) => (
+                <SelectItem key={e} value={e}>
+                  {e}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          <Select value={userFilter} onValueChange={(v) => navigate({ userId: v })}>
+            <SelectTrigger className="w-[160px]">
+              <SelectValue placeholder={t("allUsers")} />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="all">{t("allUsers")}</SelectItem>
+              {data.filters.users.map((u) => (
+                <SelectItem key={u.id} value={u.id}>
+                  {u.name || u.email || u.id.substring(0, 8)}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+        </div>
+      </div>
+
+      {/* Table */}
+      <div className="rounded-md border">
+        <Table>
+          <TableHeader>
+            <TableRow>
+              <TableHead className="w-[170px]">{t("timestamp")}</TableHead>
+              <TableHead className="w-[140px]">{t("user")}</TableHead>
+              <TableHead className="w-[200px]">{t("action")}</TableHead>
+              <TableHead className="hidden md:table-cell">{t("details")}</TableHead>
+              <TableHead className="hidden lg:table-cell w-[100px]">{t("entityId")}</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {data.logs.length === 0 ? (
+              <TableRow>
+                <TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
+                  {t("noLogsFound")}
+                </TableCell>
+              </TableRow>
+            ) : (
+              data.logs.map((log) => (
+                <TableRow key={log.id} className="cursor-pointer" onClick={() => setSelectedLog(log)}>
+                  <TableCell className="text-xs text-muted-foreground whitespace-nowrap" suppressHydrationWarning>
+                    {formatDateTime(log.timestamp)}
+                  </TableCell>
+                  <TableCell className="text-sm">
+                    {log.user?.name || log.user?.email || t("unknownUser")}
+                  </TableCell>
+                  <TableCell>
+                    <Badge variant={getActionColor(log.action) as "default" | "secondary" | "destructive" | "outline"}>
+                      {getActionLabel(log.action)}
+                    </Badge>
+                  </TableCell>
+                  <TableCell className="hidden md:table-cell text-sm text-muted-foreground truncate max-w-[300px]">
+                    {log.message}
+                  </TableCell>
+                  <TableCell className="hidden lg:table-cell font-mono text-xs text-muted-foreground">
+                    {log.entityId?.substring(0, 8)}
+                  </TableCell>
+                </TableRow>
+              ))
+            )}
+          </TableBody>
+        </Table>
+      </div>
+
+      {/* Pagination */}
+      <DataTablePagination
+        total={data.total}
+        page={data.page}
+        pageSize={data.pageSize}
+        totalPages={data.totalPages}
+        onNavigate={navigate}
+      />
+
+      {/* Detail Dialog */}
+      <Dialog open={!!selectedLog} onOpenChange={(open) => !open && setSelectedLog(null)}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>{t("logDetails")}</DialogTitle>
+            <DialogDescription>
+              {selectedLog && getActionLabel(selectedLog.action)}
+            </DialogDescription>
+          </DialogHeader>
+          {selectedLog && (
+            <div className="grid gap-3 text-sm">
+              <DetailRow label={t("timestamp")} value={formatDateTime(selectedLog.timestamp)} suppressHydrationWarning />
+              <DetailRow label={t("user")} value={selectedLog.user?.name || selectedLog.user?.email || t("unknownUser")} />
+              <DetailRow label={t("action")} value={getActionLabel(selectedLog.action)} />
+              {selectedLog.message && <DetailRow label={t("details")} value={selectedLog.message} />}
+              {selectedLog.entity && <DetailRow label={t("entity")} value={selectedLog.entity} />}
+              {selectedLog.entityId && <DetailRow label={t("entityId")} value={selectedLog.entityId} mono />}
+              {selectedLog.ip && <DetailRow label={t("ipAddress")} value={selectedLog.ip} mono />}
+              {selectedLog.userAgent && <DetailRow label={t("userAgentLabel")} value={selectedLog.userAgent} className="break-all" />}
+              {selectedLog.metadata && Object.keys(selectedLog.metadata).length > 0 && (
+                <div>
+                  <span className="font-medium text-muted-foreground">{t("metadata")}</span>
+                  <pre className="mt-1 rounded-md bg-muted p-2 text-xs overflow-x-auto">
+                    {JSON.stringify(selectedLog.metadata, null, 2)}
+                  </pre>
+                </div>
+              )}
+            </div>
+          )}
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+}
+
+function DetailRow({
+  label,
+  value,
+  mono,
+  className,
+  suppressHydrationWarning,
+}: {
+  label: string;
+  value: string;
+  mono?: boolean;
+  className?: string;
+  suppressHydrationWarning?: boolean;
+}) {
+  return (
+    <div className="flex flex-col gap-0.5">
+      <span className="font-medium text-muted-foreground">{label}</span>
+      <span className={cn(mono ? "font-mono text-xs" : "", className)} suppressHydrationWarning={suppressHydrationWarning}>
+        {value}
+      </span>
+    </div>
+  );
+}

+ 55 - 0
src/app/(authenticated)/audit-log/page.tsx

@@ -0,0 +1,55 @@
+import { getAuditLogsPaginated } from "@/features/audit/Actions/auditActions";
+import { PageHeader } from "@/components/page-header";
+import { AuditLogClient } from "./audit-log-client";
+
+export default async function AuditLogPage({
+  searchParams,
+}: {
+  searchParams: Promise<{
+    page?: string;
+    pageSize?: string;
+    search?: string;
+    action?: string;
+    entity?: string;
+    userId?: string;
+  }>;
+}) {
+  const params = await searchParams;
+
+  const result = await getAuditLogsPaginated({
+    page: params.page ? parseInt(params.page) : 1,
+    pageSize: params.pageSize ? parseInt(params.pageSize) : 25,
+    search: params.search,
+    action: params.action,
+    entity: params.entity,
+    userId: params.userId,
+  });
+
+  if (!result.success || !result.data) {
+    return (
+      <>
+        <PageHeader />
+        <div className="flex h-[50vh] items-center justify-center">
+          <p className="text-muted-foreground">
+            {result.error || "Failed to load audit logs"}
+          </p>
+        </div>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <PageHeader />
+      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+        <AuditLogClient
+          data={result.data}
+          search={params.search || ""}
+          actionFilter={params.action || "all"}
+          entityFilter={params.entity || "all"}
+          userFilter={params.userId || "all"}
+        />
+      </div>
+    </>
+  );
+}

+ 72 - 0
src/app/(authenticated)/dashboard-client.tsx

@@ -200,6 +200,7 @@ export function DashboardClient({
   smsThreads = [],
   smsEnabled = false,
   notifications = [],
+  recentAuditLogs = [],
 }: {
   stats: DashboardStats;
   currencyCode?: string;
@@ -214,8 +215,20 @@ export function DashboardClient({
   smsThreads?: SmsThread[];
   smsEnabled?: boolean;
   notifications?: DashboardNotification[];
+  recentAuditLogs?: {
+    id: string;
+    timestamp: string | Date;
+    action: string;
+    entity: string | null;
+    entityId: string | null;
+    message: string | null;
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    metadata?: any;
+    user: { id: string; name: string | null; email: string | null } | null;
+  }[];
 }) {
   const t = useTranslations("dashboard");
+  const tAudit = useTranslations("audit");
   const distUnit = unitSystem === "metric" ? "km" : "mi";
   const router = useRouter();
   const { formatDate } = useFormatDate();
@@ -1156,6 +1169,65 @@ export function DashboardClient({
             </CardContent>
           </Card>
         )}
+        {/* Recent Activity (Audit Logs) */}
+        <Card className="border-0 shadow-sm">
+          <CardHeader className="pb-1">
+            <div className="flex items-center justify-between">
+              <CardTitle className="flex items-center gap-2 text-base">
+                <ClipboardList className="h-4 w-4" />
+                {t("recentActivity")}
+              </CardTitle>
+              <Button
+                variant="ghost"
+                size="sm"
+                className="h-7 text-xs gap-1"
+                onClick={() => router.push("/audit-log")}
+              >
+                {t("viewAll")}
+                <ArrowRight className="h-3 w-3" />
+              </Button>
+            </div>
+          </CardHeader>
+          <CardContent className="p-0">
+            {recentAuditLogs.length === 0 ? (
+              <p className="px-5 py-4 text-xs text-muted-foreground">{t("noRecentActivity")}</p>
+            ) : (
+              <div className="divide-y">
+                {recentAuditLogs.slice(0, 5).map((log) => {
+                  const userLabel = log.user?.name ?? log.user?.email ?? t("unknownUser");
+                  const meta = (log as unknown as { metadata?: Record<string, unknown> }).metadata || {};
+                  const vehicleDisplay = typeof meta["vehicleDisplay"] === "string" ? (meta["vehicleDisplay"] as string) : undefined;
+                  const quoteNumber = typeof meta["quoteNumber"] === "string" ? (meta["quoteNumber"] as string) : undefined;
+                  const actionKey = log.action.replace(".", "_");
+                  let friendlyAction: string;
+                  try {
+                    friendlyAction = tAudit(`actions.${actionKey}`);
+                  } catch {
+                    friendlyAction = log.action;
+                  }
+                  const entityLabel = vehicleDisplay ?? quoteNumber ?? log.entityId?.substring(0, 8);
+                  return (
+                    <div key={log.id} className="px-5 py-3 text-sm flex items-center justify-between">
+                      <div className="min-w-0">
+                        <p className="truncate">
+                          <span className="font-medium">{userLabel}</span>{" "}
+                          {friendlyAction}
+                          {entityLabel ? <> — <span className="text-muted-foreground">{entityLabel}</span></> : null}
+                        </p>
+                        {log.message && (
+                          <p className="text-xs text-muted-foreground truncate">{log.message}</p>
+                        )}
+                      </div>
+                      <div className="shrink-0 ml-3 text-xs text-muted-foreground">
+                        {formatDate(new Date(log.timestamp))}
+                      </div>
+                    </div>
+                  );
+                })}
+              </div>
+            )}
+          </CardContent>
+        </Card>
       </div>
     </div>
   );

+ 5 - 1
src/app/(authenticated)/page.tsx

@@ -10,6 +10,7 @@ import { getAuthContext } from "@/lib/get-auth-context";
 import { getFeatures } from "@/lib/features";
 import { getRecentSmsThreads } from "@/features/sms/Actions/smsActions";
 import { getNotifications } from "@/features/notifications/Actions/notificationActions";
+import { getRecentAuditLogs } from "@/features/audit/Actions/auditActions";
 import { DashboardClient } from "./dashboard-client";
 import { PageHeader } from "@/components/page-header";
 
@@ -18,7 +19,7 @@ export default async function DashboardPage() {
   const features = auth ? await getFeatures(auth.organizationId) : null;
   const smsEnabled = features?.sms ?? false;
 
-  const [result, settingsResult, remindersResult, maintenanceResult, dismissedMaintenanceResult, inProgressResult, completedResult, quoteRequestsResult, quoteResponsesResult, smsResult, notificationsResult] = await Promise.all([
+  const [result, settingsResult, remindersResult, maintenanceResult, dismissedMaintenanceResult, inProgressResult, completedResult, quoteRequestsResult, quoteResponsesResult, smsResult, notificationsResult, auditLogsResult] = await Promise.all([
     getDashboardStats(),
     getSettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.UNIT_SYSTEM]),
     getUpcomingReminders(),
@@ -30,6 +31,7 @@ export default async function DashboardPage() {
     getQuoteResponses(),
     smsEnabled ? getRecentSmsThreads(0, 5) : Promise.resolve(null),
     getNotifications(),
+    getRecentAuditLogs(10),
   ]);
 
   if (!result.success || !result.data) {
@@ -51,6 +53,7 @@ export default async function DashboardPage() {
   const unitSystem = (settings[SETTING_KEYS.UNIT_SYSTEM] || "imperial") as "metric" | "imperial";
   const smsThreads = smsResult && smsResult.success && smsResult.data ? smsResult.data.threads : [];
   const notifications = notificationsResult.success && notificationsResult.data ? notificationsResult.data.notifications : [];
+  const recentAuditLogs = auditLogsResult.success && auditLogsResult.data ? auditLogsResult.data : [];
 
   return (
     <>
@@ -70,6 +73,7 @@ export default async function DashboardPage() {
           smsThreads={smsThreads}
           smsEnabled={smsEnabled}
           notifications={notifications}
+          recentAuditLogs={recentAuditLogs}
         />
       </div>
     </>

+ 86 - 19
src/app/api/public/auth/[...all]/route.ts

@@ -1,30 +1,97 @@
-import { auth } from "@/lib/auth";
-import { rateLimit } from "@/lib/rate-limit";
-import { toNextJsHandler } from "better-auth/next-js";
+import { auth } from '@/lib/auth'
+import { rateLimit } from '@/lib/rate-limit'
+import { toNextJsHandler } from 'better-auth/next-js'
+import { db } from '@/lib/db'
+import { logAudit } from '@/lib/audit'
 
-const { POST: authPOST, GET } = toNextJsHandler(auth);
+const { POST: authPOST, GET } = toNextJsHandler(auth)
 
 // Path prefixes that need stricter rate limits.
 // Better-auth registers sub-paths like /sign-in/email, /sign-up/email,
 // /two-factor/verify-totp, etc., so we match by prefix.
 const strictPrefixes: { prefix: string; limit: number; windowMs: number }[] = [
-  { prefix: "/api/public/auth/sign-in", limit: 10, windowMs: 60_000 },
-  { prefix: "/api/public/auth/two-factor/verify", limit: 10, windowMs: 60_000 },
-  { prefix: "/api/public/auth/sign-up", limit: 5, windowMs: 60_000 },
-  { prefix: "/api/public/auth/request-password-reset", limit: 5, windowMs: 60_000 },
-  { prefix: "/api/public/auth/reset-password", limit: 5, windowMs: 60_000 },
-  { prefix: "/api/public/auth/passkey", limit: 10, windowMs: 60_000 },
-];
+  { prefix: '/api/public/auth/sign-in', limit: 10, windowMs: 60_000 },
+  { prefix: '/api/public/auth/two-factor/verify', limit: 10, windowMs: 60_000 },
+  { prefix: '/api/public/auth/sign-up', limit: 5, windowMs: 60_000 },
+  { prefix: '/api/public/auth/request-password-reset', limit: 5, windowMs: 60_000 },
+  { prefix: '/api/public/auth/reset-password', limit: 5, windowMs: 60_000 },
+  { prefix: '/api/public/auth/passkey', limit: 10, windowMs: 60_000 },
+]
 
-const defaultConfig = { limit: 30, windowMs: 60_000 };
+const authAuditPrefixes = [
+  '/api/public/auth/sign-in',
+  '/api/public/auth/two-factor/verify',
+  '/api/public/auth/passkey',
+]
+
+const defaultConfig = { limit: 30, windowMs: 60_000 }
+
+function getRequestIp(request: Request): string | null {
+  return (
+    request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
+    request.headers.get('x-real-ip') ||
+    null
+  )
+}
 
 async function POST(request: Request) {
-  const { pathname } = new URL(request.url);
-  const config =
-    strictPrefixes.find((p) => pathname.startsWith(p.prefix)) ?? defaultConfig;
-  const limited = rateLimit(request, config);
-  if (limited) return limited;
-  return authPOST(request);
+  const { pathname } = new URL(request.url)
+  const config = strictPrefixes.find((p) => pathname.startsWith(p.prefix)) ?? defaultConfig
+  const limited = rateLimit(request, config)
+  if (limited) return limited
+
+  const isAuthAttempt = authAuditPrefixes.some((p) => pathname.startsWith(p))
+
+  if (isAuthAttempt) {
+    // Clone body before better-auth consumes it
+    const cloned = request.clone()
+    const response = await authPOST(request)
+
+    // Log failed authentication attempts (fire-and-forget to avoid timing side-channels)
+    if (!response.ok) {
+      const ip = getRequestIp(cloned)
+      const userAgent = cloned.headers.get('user-agent')
+      const status = response.status
+      void (async () => {
+        try {
+          const body = await cloned.json().catch(() => null)
+          const rawEmail = body?.email
+          // Sanitize: must be a string, cap length to prevent log pollution
+          const email =
+            typeof rawEmail === 'string' && rawEmail.length <= 255
+              ? rawEmail
+              : 'unknown'
+          // Try to find user to attach userId
+          const user =
+            email !== 'unknown'
+              ? await db.user.findFirst({ where: { email }, select: { id: true } })
+              : null
+          const membership = user
+            ? await db.organizationMember.findFirst({
+                where: { userId: user.id },
+                select: { organizationId: true },
+              })
+            : null
+          await logAudit(
+            { userId: user?.id ?? '', organizationId: membership?.organizationId ?? '' },
+            {
+              action: 'auth.loginFailed',
+              message: `Failed login attempt for ${email}`,
+              metadata: { email, statusCode: status, path: pathname },
+              ip,
+              userAgent,
+            }
+          )
+        } catch {
+          /* best-effort */
+        }
+      })()
+    }
+
+    return response
+  }
+
+  return authPOST(request)
 }
 
-export { GET, POST };
+export { GET, POST }

+ 2 - 0
src/components/app-sidebar.tsx

@@ -48,6 +48,7 @@ import {
   ClipboardList,
   Columns3,
   FileText,
+  History,
   Globe,
   MessageSquare,
   LayoutDashboard,
@@ -126,6 +127,7 @@ export function AppSidebar({
     { titleKey: 'sidebar.billing' as const, url: '/billing', icon: Receipt },
     { titleKey: 'sidebar.inventory' as const, url: '/inventory', icon: Package },
     ...(showReports ? [{ titleKey: 'sidebar.reports' as const, url: '/reports', icon: BarChart3 }] : []),
+    { titleKey: 'sidebar.auditLog' as const, url: '/audit-log', icon: History },
   ]
 
   const closeMobileSidebar = () => {

+ 1 - 1
src/components/data-table-pagination.tsx

@@ -50,7 +50,7 @@ export function DataTablePagination({
           </SelectTrigger>
           <SelectContent>
             <SelectItem value="10">10</SelectItem>
-            <SelectItem value="20">20</SelectItem>
+            <SelectItem value="25">25</SelectItem>
             <SelectItem value="50">50</SelectItem>
           </SelectContent>
         </Select>

+ 1 - 0
src/components/page-header.tsx

@@ -75,6 +75,7 @@ const breadcrumbMap: Record<string, BreadcrumbSegment[]> = {
   '/settings/subscription': [{ key: 'settings', href: '/settings' }, { key: 'subscription' }],
   '/settings/maintenance': [{ key: 'settings', href: '/settings' }, { key: 'maintenance' }],
   '/settings/customer-portal': [{ key: 'settings', href: '/settings' }, { key: 'customerPortal' }],
+  '/audit-log': [{ key: 'auditLog' }],
 }
 
 export function PageHeader() {

+ 1 - 0
src/cronTasks.ts

@@ -2,3 +2,4 @@ export { checkSubscriptions } from './lib/cron/check-subscriptions'
 export { checkLicenses } from './lib/cron/check-licenses'
 export { processRecurringInvoices } from './lib/cron/recurring-invoices'
 export { cleanupPortalSessions } from './lib/cron/cleanup-portal-sessions'
+export { cleanupAuditLogs } from './lib/cron/cleanup-audit-logs'

+ 109 - 0
src/features/audit/Actions/auditActions.ts

@@ -0,0 +1,109 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+
+export async function getRecentAuditLogs(limit = 10) {
+  return withAuth(async ({ organizationId }) => {
+    const logs = await db.auditLog.findMany({
+      where: { organizationId },
+      include: { user: { select: { id: true, name: true, email: true } } },
+      orderBy: { timestamp: "desc" },
+      take: limit,
+    });
+    return logs;
+  });
+}
+
+export async function getAuditLogsPaginated(params: {
+  page?: number;
+  pageSize?: number;
+  search?: string;
+  action?: string;
+  entity?: string;
+  userId?: string;
+}) {
+  return withAuth(async ({ organizationId }) => {
+    const page = params.page || 1;
+    const pageSize = params.pageSize || 25;
+    const skip = (page - 1) * pageSize;
+
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const where: any = { organizationId };
+
+    if (params.search) {
+      where.OR = [
+        { action: { contains: params.search, mode: "insensitive" } },
+        { entity: { contains: params.search, mode: "insensitive" } },
+        { message: { contains: params.search, mode: "insensitive" } },
+        { entityId: { contains: params.search, mode: "insensitive" } },
+        { user: { name: { contains: params.search, mode: "insensitive" } } },
+        { user: { email: { contains: params.search, mode: "insensitive" } } },
+      ];
+    }
+
+    if (params.action && params.action !== "all") {
+      where.action = params.action;
+    }
+
+    if (params.entity && params.entity !== "all") {
+      where.entity = params.entity;
+    }
+
+    if (params.userId && params.userId !== "all") {
+      where.userId = params.userId;
+    }
+
+    const [logs, total] = await Promise.all([
+      db.auditLog.findMany({
+        where,
+        include: { user: { select: { id: true, name: true, email: true } } },
+        orderBy: { timestamp: "desc" },
+        skip,
+        take: pageSize,
+      }),
+      db.auditLog.count({ where }),
+    ]);
+
+    // Fetch distinct filter values for dropdowns
+    const [actionValues, entityValues, userValues] = await Promise.all([
+      db.auditLog.findMany({
+        where: { organizationId },
+        select: { action: true },
+        distinct: ["action"],
+        orderBy: { action: "asc" },
+      }),
+      db.auditLog.findMany({
+        where: { organizationId, entity: { not: null } },
+        select: { entity: true },
+        distinct: ["entity"],
+        orderBy: { entity: "asc" },
+      }),
+      db.auditLog.findMany({
+        where: { organizationId, userId: { not: null } },
+        select: { userId: true, user: { select: { name: true, email: true } } },
+        distinct: ["userId"],
+      }),
+    ]);
+
+    return {
+      logs,
+      total,
+      page,
+      pageSize,
+      totalPages: Math.ceil(total / pageSize),
+      filters: {
+        actions: actionValues.map((a) => a.action),
+        entities: entityValues.map((e) => e.entity).filter(Boolean) as string[],
+        users: userValues
+          .filter((u) => u.userId && u.user)
+          .map((u) => ({
+            id: u.userId!,
+            name: u.user!.name,
+            email: u.user!.email,
+          })),
+      },
+    };
+  });
+}
+

+ 31 - 4
src/features/billing/Actions/recurringInvoiceActions.ts

@@ -78,7 +78,16 @@ export async function createRecurringInvoice(input: CreateRecurringInvoiceInput)
 
     revalidatePath("/billing/recurring");
     return invoice;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.BILLING }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.BILLING }],
+    audit: ({ result }) => ({
+      action: "recurringInvoice.create",
+      entity: "RecurringInvoice",
+      entityId: result.id,
+      message: `Created recurring invoice "${result.title}"`,
+      metadata: { recurringInvoiceId: result.id },
+    }),
+  });
 }
 
 export async function updateRecurringInvoice(input: UpdateRecurringInvoiceInput) {
@@ -145,7 +154,16 @@ export async function updateRecurringInvoice(input: UpdateRecurringInvoiceInput)
 
     revalidatePath("/billing/recurring");
     return updated;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.BILLING }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.BILLING }],
+    audit: ({ result }) => ({
+      action: "recurringInvoice.update",
+      entity: "RecurringInvoice",
+      entityId: result.id,
+      message: `Updated recurring invoice "${result.title}"`,
+      metadata: { recurringInvoiceId: result.id },
+    }),
+  });
 }
 
 export async function deleteRecurringInvoice(id: string) {
@@ -158,8 +176,17 @@ export async function deleteRecurringInvoice(id: string) {
     await db.recurringInvoice.delete({ where: { id } });
 
     revalidatePath("/billing/recurring");
-    return { deleted: true };
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.BILLING }] });
+    return { deleted: true, recurringInvoiceId: id };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.BILLING }],
+    audit: ({ result }) => ({
+      action: "recurringInvoice.delete",
+      entity: "RecurringInvoice",
+      entityId: result.recurringInvoiceId,
+      message: `Deleted recurring invoice ${result.recurringInvoiceId}`,
+      metadata: { recurringInvoiceId: result.recurringInvoiceId },
+    }),
+  });
 }
 
 export async function toggleRecurringInvoice(id: string) {

+ 31 - 3
src/features/custom-fields/Actions/customFieldActions.ts

@@ -37,7 +37,16 @@ export async function createFieldDefinition(input: unknown) {
 
     revalidatePath("/settings/custom-fields");
     return field;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "customField.create",
+      entity: "CustomFieldDefinition",
+      entityId: result.id,
+      message: `Created custom field "${result.name}"`,
+      metadata: { fieldId: result.id, fieldName: result.name, entityType: result.entityType },
+    }),
+  });
 }
 
 export async function updateFieldDefinition(input: unknown) {
@@ -57,7 +66,16 @@ export async function updateFieldDefinition(input: unknown) {
 
     revalidatePath("/settings/custom-fields");
     return field;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "customField.update",
+      entity: "CustomFieldDefinition",
+      entityId: result.id,
+      message: `Updated custom field "${result.name}"`,
+      metadata: { fieldId: result.id, fieldName: result.name },
+    }),
+  });
 }
 
 export async function deleteFieldDefinition(fieldId: string) {
@@ -73,7 +91,17 @@ export async function deleteFieldDefinition(fieldId: string) {
     ]);
 
     revalidatePath("/settings/custom-fields");
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }] });
+    return { fieldId, fieldName: field.name };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "customField.delete",
+      entity: "CustomFieldDefinition",
+      entityId: result.fieldId,
+      message: `Deleted custom field "${result.fieldName}"`,
+      metadata: { fieldId: result.fieldId, fieldName: result.fieldName },
+    }),
+  });
 }
 
 export async function getCustomFieldValues(entityId: string, entityType: string) {

+ 31 - 3
src/features/customers/Actions/customerActions.ts

@@ -66,7 +66,16 @@ export async function createCustomer(input: unknown) {
     });
     revalidatePath("/customers");
     return customer;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.CUSTOMERS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.CUSTOMERS }],
+    audit: ({ result }) => ({
+      action: "customer.create",
+      entity: "Customer",
+      entityId: result.id,
+      message: `Created customer ${result.name}`,
+      metadata: { customerId: result.id },
+    }),
+  });
 }
 
 export async function updateCustomer(input: unknown) {
@@ -86,7 +95,16 @@ export async function updateCustomer(input: unknown) {
     revalidatePath("/customers");
     revalidatePath(`/customers/${id}`);
     return { id };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.CUSTOMERS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.CUSTOMERS }],
+    audit: ({ result }) => ({
+      action: "customer.update",
+      entity: "Customer",
+      entityId: result.id,
+      message: `Updated customer ${result.id}`,
+      metadata: { customerId: result.id },
+    }),
+  });
 }
 
 export async function deleteCustomer(customerId: string) {
@@ -94,7 +112,17 @@ export async function deleteCustomer(customerId: string) {
     const result = await db.customer.deleteMany({ where: { id: customerId, organizationId } });
     if (result.count === 0) throw new Error("Customer not found");
     revalidatePath("/customers");
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.CUSTOMERS }] });
+    return { customerId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.CUSTOMERS }],
+    audit: ({ result }) => ({
+      action: "customer.delete",
+      entity: "Customer",
+      entityId: result.customerId,
+      message: `Deleted customer ${result.customerId}`,
+      metadata: { customerId: result.customerId },
+    }),
+  });
 }
 
 export async function deleteCustomers(customerIds: string[]) {

+ 33 - 6
src/features/email/Actions/emailActions.ts

@@ -156,8 +156,17 @@ export async function sendQuoteEmail(input: {
       });
     }
 
-    return { sent: true };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.QUOTES }] });
+    return { sent: true, quoteId, recipientEmail };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.QUOTES }],
+    audit: ({ result }) => ({
+      action: "email.sendQuote",
+      entity: "Quote",
+      entityId: result.quoteId,
+      message: `Sent quote email to ${result.recipientEmail}`,
+      metadata: { quoteId: result.quoteId, recipientEmail: result.recipientEmail },
+    }),
+  });
 }
 
 export async function sendNotificationEmail(input: {
@@ -283,8 +292,17 @@ export async function sendInvoiceEmail(input: {
       ],
     });
 
-    return { sent: true };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
+    return { sent: true, serviceRecordId, recipientEmail };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }],
+    audit: ({ result }) => ({
+      action: "email.sendInvoice",
+      entity: "ServiceRecord",
+      entityId: result.serviceRecordId,
+      message: `Sent invoice email to ${result.recipientEmail}`,
+      metadata: { serviceRecordId: result.serviceRecordId, recipientEmail: result.recipientEmail },
+    }),
+  });
 }
 
 export async function sendInspectionEmail(input: {
@@ -391,6 +409,15 @@ export async function sendInspectionEmail(input: {
       ],
     });
 
-    return { sent: true };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
+    return { sent: true, inspectionId, recipientEmail };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }],
+    audit: ({ result }) => ({
+      action: "email.sendInspection",
+      entity: "Inspection",
+      entityId: result.inspectionId,
+      message: `Sent inspection email to ${result.recipientEmail}`,
+      metadata: { inspectionId: result.inspectionId, recipientEmail: result.recipientEmail },
+    }),
+  });
 }

+ 33 - 5
src/features/inspections/Actions/inspectionActions.ts

@@ -176,8 +176,17 @@ export async function createInspection(input: unknown) {
 
     revalidatePath("/inspections");
     revalidatePath(`/vehicles/${data.vehicleId}`);
-    return inspection;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INSPECTIONS }] });
+    return { ...inspection, vehicleId: data.vehicleId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INSPECTIONS }],
+    audit: ({ result }) => ({
+      action: "inspection.create",
+      entity: "Inspection",
+      entityId: result.id,
+      message: `Created inspection ${result.id}`,
+      metadata: { inspectionId: result.id, vehicleId: result.vehicleId },
+    }),
+  });
 }
 
 export async function updateInspectionItem(itemId: string, input: unknown) {
@@ -224,8 +233,17 @@ export async function completeInspection(id: string) {
     revalidatePath("/inspections");
     revalidatePath(`/inspections/${id}`);
     revalidatePath(`/vehicles/${inspection.vehicleId}`);
-    return { success: true };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }] });
+    return { success: true, inspectionId: id };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }],
+    audit: ({ result }) => ({
+      action: "inspection.complete",
+      entity: "Inspection",
+      entityId: result.inspectionId,
+      message: `Completed inspection ${result.inspectionId}`,
+      metadata: { inspectionId: result.inspectionId },
+    }),
+  });
 }
 
 export async function deleteInspection(id: string) {
@@ -238,5 +256,15 @@ export async function deleteInspection(id: string) {
     await db.inspection.deleteMany({ where: { id, organizationId } });
     revalidatePath("/inspections");
     revalidatePath(`/vehicles/${inspection.vehicleId}`);
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.INSPECTIONS }] });
+    return { inspectionId: id };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.INSPECTIONS }],
+    audit: ({ result }) => ({
+      action: "inspection.delete",
+      entity: "Inspection",
+      entityId: result.inspectionId,
+      message: `Deleted inspection ${result.inspectionId}`,
+      metadata: { inspectionId: result.inspectionId },
+    }),
+  });
 }

+ 11 - 2
src/features/inspections/Actions/quoteRequestActions.ts

@@ -134,6 +134,15 @@ export async function updateQuoteRequestStatus(id: string, status: "quoted" | "d
       data: { status },
     });
 
-    return { success: true };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }] });
+    return { success: true, requestId: id, status };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }],
+    audit: ({ result }) => ({
+      action: "quoteRequest.update",
+      entity: "InspectionQuoteRequest",
+      entityId: result.requestId,
+      message: `Updated quote request status to ${result.status}`,
+      metadata: { requestId: result.requestId, status: result.status },
+    }),
+  });
 }

+ 31 - 3
src/features/inspections/Actions/templateActions.ts

@@ -96,7 +96,16 @@ export async function createTemplate(input: unknown) {
 
     revalidatePath("/settings/inspections");
     return template;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INSPECTIONS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INSPECTIONS }],
+    audit: ({ result }) => ({
+      action: "inspectionTemplate.create",
+      entity: "InspectionTemplate",
+      entityId: result.id,
+      message: `Created inspection template "${result.name}"`,
+      metadata: { templateId: result.id, templateName: result.name },
+    }),
+  });
 }
 
 export async function updateTemplate(input: unknown) {
@@ -151,7 +160,16 @@ export async function updateTemplate(input: unknown) {
 
     revalidatePath("/settings/inspections");
     return template;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }],
+    audit: ({ result }) => ({
+      action: "inspectionTemplate.update",
+      entity: "InspectionTemplate",
+      entityId: result.id,
+      message: `Updated inspection template "${result.name}"`,
+      metadata: { templateId: result.id, templateName: result.name },
+    }),
+  });
 }
 
 export async function deleteTemplate(id: string) {
@@ -171,7 +189,17 @@ export async function deleteTemplate(id: string) {
     await db.inspectionTemplate.delete({ where: { id } });
 
     revalidatePath("/settings/templates");
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.INSPECTIONS }] });
+    return { templateId: id, templateName: template.name };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.INSPECTIONS }],
+    audit: ({ result }) => ({
+      action: "inspectionTemplate.delete",
+      entity: "InspectionTemplate",
+      entityId: result.templateId,
+      message: `Deleted inspection template "${result.templateName}"`,
+      metadata: { templateId: result.templateId, templateName: result.templateName },
+    }),
+  });
 }
 
 async function seedDefaultTemplateForOrg(organizationId: string) {

+ 32 - 5
src/features/inventory/Actions/inventoryActions.ts

@@ -91,7 +91,16 @@ export async function createInventoryPart(input: unknown) {
     });
     revalidatePath("/inventory");
     return part;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INVENTORY }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INVENTORY }],
+    audit: ({ result }) => ({
+      action: "inventory.create",
+      entity: "InventoryPart",
+      entityId: result.id,
+      message: `Created inventory part "${result.name}"`,
+      metadata: { partId: result.id },
+    }),
+  });
 }
 
 export async function updateInventoryPart(input: unknown) {
@@ -130,8 +139,17 @@ export async function updateInventoryPart(input: unknown) {
     });
     if (result.count === 0) throw new Error("Part not found");
     revalidatePath("/inventory");
-    return { updated: true };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INVENTORY }] });
+    return { updated: true, partId: id };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INVENTORY }],
+    audit: ({ result }) => ({
+      action: "inventory.update",
+      entity: "InventoryPart",
+      entityId: result.partId,
+      message: `Updated inventory part ${result.partId}`,
+      metadata: { partId: result.partId },
+    }),
+  });
 }
 
 export async function deleteInventoryPart(partId: string) {
@@ -155,8 +173,17 @@ export async function deleteInventoryPart(partId: string) {
     }
 
     revalidatePath("/inventory");
-    return { deleted: true };
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.INVENTORY }] });
+    return { deleted: true, partId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.INVENTORY }],
+    audit: ({ result }) => ({
+      action: "inventory.delete",
+      entity: "InventoryPart",
+      entityId: result.partId,
+      message: `Deleted inventory part ${result.partId}`,
+      metadata: { partId: result.partId },
+    }),
+  });
 }
 
 export async function adjustInventoryStock(input: unknown) {

+ 20 - 0
src/features/onboarding/Actions/createOnboardingOrg.ts

@@ -3,6 +3,7 @@
 import { headers, cookies } from "next/headers";
 import { auth } from "@/lib/auth";
 import { db } from "@/lib/db";
+import { logAudit } from "@/lib/audit";
 import { onboardingSchema } from "../Schema/onboardingSchema";
 import type { ActionResult } from "@/lib/with-auth";
 
@@ -44,6 +45,25 @@ export async function createOnboardingOrg(
       sameSite: "lax",
     });
 
+    // Audit: log registration + org creation (first org = new user onboarding)
+    const h = await headers();
+    const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim() || h.get("x-real-ip") || null;
+    const userAgent = h.get("user-agent") || null;
+    const ctx = { userId: session.user.id, organizationId: org.id };
+    logAudit(ctx, {
+      action: "auth.register",
+      message: `New user registered: ${session.user.email}`,
+      ip, userAgent,
+    }).catch(() => { /* best-effort */ });
+    logAudit(ctx, {
+      action: "organization.create",
+      entity: "Organization",
+      entityId: org.id,
+      message: `Created organization: ${data.workshopName}`,
+      metadata: { organizationName: data.workshopName },
+      ip, userAgent,
+    }).catch(() => { /* best-effort */ });
+
     return { success: true, data: { organizationId: org.id } };
   } catch (error) {
     const message =

+ 22 - 4
src/features/payments/Actions/paymentActions.ts

@@ -28,8 +28,17 @@ export async function createPayment(input: unknown) {
     });
 
     revalidatePath(`/vehicles/${serviceRecord.vehicleId}/service/${data.serviceRecordId}`);
-    return payment;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.BILLING }] });
+    return { ...payment, serviceRecordId: data.serviceRecordId, amount: data.amount, method: data.method };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.BILLING }],
+    audit: ({ result }) => ({
+      action: "payment.create",
+      entity: "Payment",
+      entityId: result.id,
+      message: `Recorded payment ${result.amount} for service ${result.serviceRecordId}`,
+      metadata: { paymentId: result.id, serviceRecordId: result.serviceRecordId, amount: result.amount, method: result.method },
+    }),
+  });
 }
 
 export async function deletePayment(paymentId: string) {
@@ -44,6 +53,15 @@ export async function deletePayment(paymentId: string) {
 
     const { vehicleId, id: serviceId } = payment.serviceRecord;
     revalidatePath(`/vehicles/${vehicleId}/service/${serviceId}`);
-    return { deleted: true };
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.BILLING }] });
+    return { deleted: true, paymentId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.BILLING }],
+    audit: ({ result }) => ({
+      action: "payment.delete",
+      entity: "Payment",
+      entityId: result.paymentId,
+      message: `Deleted payment ${result.paymentId}`,
+      metadata: { paymentId: result.paymentId },
+    }),
+  });
 }

+ 8 - 2
src/features/portal/Actions/portalActions.ts

@@ -364,7 +364,7 @@ export async function updatePortalSlug(
           data: { portalSlug: null },
         });
         revalidatePath("/settings/customer-portal");
-        return null;
+        return { slug: null };
       }
 
       const normalized = slug.trim().toLowerCase();
@@ -395,12 +395,18 @@ export async function updatePortalSlug(
       });
 
       revalidatePath("/settings/customer-portal");
-      return null;
+      return { slug: normalized || null };
     },
     {
       requiredPermissions: [
         { action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS },
       ],
+      audit: ({ result }) => result ? ({
+        action: "settings.updatePortalSlug",
+        entity: "Organization",
+        message: result.slug ? `Set portal slug to "${result.slug}"` : "Cleared portal slug",
+        metadata: { slug: result.slug },
+      }) : null,
     },
   );
 }

+ 53 - 7
src/features/quotes/Actions/quoteActions.ts

@@ -149,7 +149,16 @@ export async function createQuote(input: unknown) {
 
     revalidatePath("/quotes");
     return quote;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.QUOTES }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.QUOTES }],
+    audit: ({ result }) => ({
+      action: "quote.create",
+      entity: "Quote",
+      entityId: result.id,
+      message: `Created quote ${result.quoteNumber || result.id}`,
+      metadata: { quoteId: result.id },
+    }),
+  });
 }
 
 export async function updateQuote(input: unknown) {
@@ -196,7 +205,16 @@ export async function updateQuote(input: unknown) {
     revalidatePath("/quotes");
     revalidatePath(`/quotes/${id}`);
     return quote;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.QUOTES }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.QUOTES }],
+    audit: ({ result }) => ({
+      action: "quote.update",
+      entity: "Quote",
+      entityId: result.id,
+      message: `Updated quote ${result.id}`,
+      metadata: { quoteId: result.id },
+    }),
+  });
 }
 
 export async function updateQuoteStatus(quoteId: string, status: string) {
@@ -213,8 +231,17 @@ export async function updateQuoteStatus(quoteId: string, status: string) {
 
     revalidatePath("/quotes");
     revalidatePath(`/quotes/${quoteId}`);
-    return { success: true };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.QUOTES }] });
+    return { success: true, quoteId, status };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.QUOTES }],
+    audit: ({ result }) => ({
+      action: "quote.status",
+      entity: "Quote",
+      entityId: result.quoteId,
+      message: `Changed quote status to ${result.status}`,
+      metadata: { quoteId: result.quoteId, status: result.status },
+    }),
+  });
 }
 
 export async function deleteQuote(quoteId: string) {
@@ -226,7 +253,17 @@ export async function deleteQuote(quoteId: string) {
 
     await db.quote.deleteMany({ where: { id: quoteId, organizationId } });
     revalidatePath("/quotes");
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.QUOTES }] });
+    return { quoteId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.QUOTES }],
+    audit: ({ result }) => ({
+      action: "quote.delete",
+      entity: "Quote",
+      entityId: result.quoteId,
+      message: `Deleted quote ${result.quoteId}`,
+      metadata: { quoteId: result.quoteId },
+    }),
+  });
 }
 
 export async function convertQuoteToServiceRecord(quoteId: string, vehicleId: string) {
@@ -362,6 +399,15 @@ export async function convertQuoteToServiceRecord(quoteId: string, vehicleId: st
     revalidatePath("/quotes");
     revalidatePath("/work-orders");
     revalidatePath(`/vehicles/${vehicleId}`);
-    return record;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.SERVICES }] });
+    return { ...record, convertedFromQuoteId: quoteId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.SERVICES }],
+    audit: ({ result }) => ({
+      action: "quote.convert",
+      entity: "Quote",
+      entityId: result.convertedFromQuoteId,
+      message: `Converted quote ${result.convertedFromQuoteId} to service record ${result.id}`,
+      metadata: { quoteId: result.convertedFromQuoteId, serviceRecordId: result.id },
+    }),
+  });
 }

+ 7 - 0
src/features/settings/Actions/deleteContent.ts

@@ -102,6 +102,13 @@ export async function deleteContent(input: unknown) {
     revalidatePath("/settings/account");
 
     return { deleted };
+  }, {
+    audit: ({ result }) => ({
+      action: "settings.deleteContent",
+      entity: "Organization",
+      message: `Deleted all data: ${result.deleted.join(", ")}`,
+      metadata: { deleted: result.deleted },
+    }),
   });
 }
 

+ 16 - 2
src/features/settings/Actions/invoiceLayoutActions.ts

@@ -55,7 +55,14 @@ export async function saveInvoiceLayoutConfig(config: InvoiceLayoutConfig) {
 
     revalidatePath("/settings/templates");
     return validated;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }],
+    audit: () => ({
+      action: "settings.updateInvoiceLayout",
+      entity: "AppSetting",
+      message: "Updated invoice layout configuration",
+    }),
+  });
 }
 
 export async function getQuoteLayoutConfig() {
@@ -101,5 +108,12 @@ export async function saveQuoteLayoutConfig(config: InvoiceLayoutConfig) {
 
     revalidatePath("/settings/templates");
     return validated;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SETTINGS }],
+    audit: () => ({
+      action: "settings.updateQuoteLayout",
+      entity: "AppSetting",
+      message: "Updated quote layout configuration",
+    }),
+  });
 }

+ 8 - 1
src/features/sms/Actions/smsActions.ts

@@ -58,7 +58,7 @@ export async function sendSmsToCustomer(input: {
           },
         });
 
-        return { id: message.id, status: "sent" as const };
+        return { id: message.id, status: "sent" as const, customerPhone: customer.phone, customerName: customer.name };
       } catch (error) {
         const errorMessage =
           error instanceof Error ? error.message : "Unknown error";
@@ -75,6 +75,13 @@ export async function sendSmsToCustomer(input: {
       requiredPermissions: [
         { action: PermissionAction.UPDATE, subject: PermissionSubject.CUSTOMERS },
       ],
+      audit: ({ result }) => ({
+        action: "sms.send",
+        entity: "SmsMessage",
+        entityId: result.id,
+        message: `Sent SMS to ${result.customerName} (${result.customerPhone})`,
+        metadata: { messageId: result.id },
+      }),
     },
   );
 }

+ 12 - 0
src/features/subscription/Actions/subscriptionActions.ts

@@ -26,6 +26,12 @@ export async function cancelSubscription() {
     });
 
     return { cancelAtPeriodEnd: true };
+  }, {
+    audit: () => ({
+      action: "subscription.cancel",
+      entity: "Subscription",
+      message: "Cancelled subscription (end of period)",
+    }),
   });
 }
 
@@ -51,5 +57,11 @@ export async function resumeSubscription() {
     });
 
     return { cancelAtPeriodEnd: false };
+  }, {
+    audit: () => ({
+      action: "subscription.resume",
+      entity: "Subscription",
+      message: "Resumed subscription",
+    }),
   });
 }

+ 11 - 2
src/features/team/Actions/cancelInvitation.ts

@@ -28,6 +28,15 @@ export async function cancelInvitation(input: unknown) {
     });
 
     revalidatePath("/settings/team");
-    return { cancelled: true };
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+    return { cancelled: true, invitationId: data.invitationId, email: invitation.email };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "team.cancelInvitation",
+      entity: "TeamInvitation",
+      entityId: result.invitationId,
+      message: `Cancelled invitation for ${result.email}`,
+      metadata: { invitationId: result.invitationId, email: result.email },
+    }),
+  });
 }

+ 8 - 0
src/features/team/Actions/createNewOrganization.ts

@@ -51,5 +51,13 @@ export async function createNewOrganization(input: unknown) {
 
     revalidatePath("/");
     return org;
+  }, {
+    audit: ({ result }) => ({
+      action: "organization.create",
+      entity: "Organization",
+      entityId: result.id,
+      message: `Created organization "${result.name}"`,
+      metadata: { organizationId: result.id },
+    }),
   });
 }

+ 10 - 1
src/features/team/Actions/createRole.ts

@@ -31,5 +31,14 @@ export async function createRole(input: unknown) {
 
     revalidatePath("/settings/team");
     return created;
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "role.create",
+      entity: "Role",
+      entityId: result.id,
+      message: `Created role "${result.name}"`,
+      metadata: { roleId: result.id, roleName: result.name },
+    }),
+  });
 }

+ 11 - 2
src/features/team/Actions/deleteRole.ts

@@ -19,6 +19,15 @@ export async function deleteRole(roleId: string) {
     await db.role.delete({ where: { id: roleId } });
 
     revalidatePath("/settings/team");
-    return { deleted: true };
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+    return { deleted: true, roleId, roleName: existing.name };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "role.delete",
+      entity: "Role",
+      entityId: result.roleId,
+      message: `Deleted role "${result.roleName}"`,
+      metadata: { roleId: result.roleId, roleName: result.roleName },
+    }),
+  });
 }

+ 10 - 2
src/features/team/Actions/sendInvitation.ts

@@ -100,6 +100,14 @@ export async function sendInvitation(input: unknown) {
     }
 
     revalidatePath("/settings/team");
-    return { invited: true, pending: true };
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+    return { invited: true, pending: true, email: data.email, role: data.role };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "team.sendInvitation",
+      entity: "TeamInvitation",
+      message: `Sent invitation to ${result.email} as ${result.role}`,
+      metadata: { email: result.email, role: result.role },
+    }),
+  });
 }

+ 42 - 7
src/features/team/Actions/teamActions.ts

@@ -100,7 +100,16 @@ export async function createOrganization(input: unknown) {
 
     revalidatePath("/settings/team");
     return org;
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "organization.create",
+      entity: "Organization",
+      entityId: result.id,
+      message: `Created organization "${result.name}"`,
+      metadata: { organizationId: result.id },
+    }),
+  });
 }
 
 export async function inviteMember(input: unknown) {
@@ -146,8 +155,16 @@ export async function inviteMember(input: unknown) {
     });
 
     revalidatePath("/settings/team");
-    return { invited: true };
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+    return { invited: true, email: data.email, role: data.role };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => result.invited ? ({
+      action: "team.invite",
+      entity: "OrganizationMember",
+      message: `Invited ${result.email} as ${result.role}`,
+      metadata: { email: result.email, role: result.role },
+    }) : null,
+  });
 }
 
 export async function updateMemberRole(input: unknown) {
@@ -172,8 +189,17 @@ export async function updateMemberRole(input: unknown) {
     });
 
     revalidatePath("/settings/team");
-    return { updated: true };
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+    return { updated: true, memberId: data.memberId, role: data.role };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "team.updateRole",
+      entity: "OrganizationMember",
+      entityId: result.memberId,
+      message: `Changed member role to ${result.role}`,
+      metadata: { memberId: result.memberId, role: result.role },
+    }),
+  });
 }
 
 export async function removeMember(memberId: string) {
@@ -194,6 +220,15 @@ export async function removeMember(memberId: string) {
     await db.organizationMember.delete({ where: { id: memberId } });
 
     revalidatePath("/settings/team");
-    return { removed: true };
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+    return { removed: true, memberId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "team.removeMember",
+      entity: "OrganizationMember",
+      entityId: result.memberId,
+      message: `Removed team member ${result.memberId}`,
+      metadata: { memberId: result.memberId },
+    }),
+  });
 }

+ 10 - 1
src/features/team/Actions/updateRole.ts

@@ -43,5 +43,14 @@ export async function updateRole(input: unknown) {
 
     revalidatePath("/settings/team");
     return updated;
-  }, { requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.MANAGE, subject: PermissionSubject.SETTINGS }],
+    audit: ({ result }) => ({
+      action: "role.update",
+      entity: "Role",
+      entityId: result.id,
+      message: `Updated role "${result.name}"`,
+      metadata: { roleId: result.id, roleName: result.name },
+    }),
+  });
 }

+ 8 - 1
src/features/vehicles/Actions/archiveVehicle.ts

@@ -17,12 +17,19 @@ export async function archiveVehicle(vehicleId: string, reason?: string) {
       revalidatePath("/vehicles");
       revalidatePath(`/vehicles/${vehicleId}`);
 
-      return { success: true as const };
+      return { success: true as const, vehicleId };
     },
     {
       requiredPermissions: [
         { action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES },
       ],
+      audit: ({ result }) => ({
+        action: "vehicle.archive",
+        entity: "Vehicle",
+        entityId: result.vehicleId,
+        message: `Archived vehicle ${result.vehicleId}`,
+        metadata: { vehicleId: result.vehicleId },
+      }),
     }
   );
 }

+ 7 - 0
src/features/vehicles/Actions/createDraftServiceRecord.ts

@@ -137,6 +137,13 @@ export async function createDraftServiceRecord(
           subject: PermissionSubject.SERVICES,
         },
       ],
+      audit: ({ result }) => ({
+        action: "service.create",
+        entity: "ServiceRecord",
+        entityId: result.id,
+        message: `Created draft service record ${result.invoiceNumber || result.id}`,
+        metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
+      }),
     },
   );
 }

+ 21 - 2
src/features/vehicles/Actions/noteActions.ts

@@ -51,7 +51,16 @@ export async function createNote(input: unknown) {
     const note = await db.note.create({ data });
     revalidatePath(`/vehicles/${data.vehicleId}`);
     return note;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }],
+    audit: ({ result }) => ({
+      action: "note.create",
+      entity: "Note",
+      entityId: result.id,
+      message: `Created note on vehicle ${result.vehicleId}`,
+      metadata: { noteId: result.id, vehicleId: result.vehicleId },
+    }),
+  });
 }
 
 export async function updateNote(input: unknown) {
@@ -90,5 +99,15 @@ export async function deleteNote(noteId: string) {
 
     await db.note.delete({ where: { id: noteId } });
     revalidatePath(`/vehicles/${note.vehicleId}`);
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }] });
+    return { noteId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }],
+    audit: ({ result }) => ({
+      action: "note.delete",
+      entity: "Note",
+      entityId: result.noteId,
+      message: `Deleted note ${result.noteId}`,
+      metadata: { noteId: result.noteId },
+    }),
+  });
 }

+ 21 - 2
src/features/vehicles/Actions/reminderActions.ts

@@ -47,7 +47,16 @@ export async function createReminder(input: unknown) {
     revalidatePath("/");
     revalidatePath("/reminders");
     return reminder;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }],
+    audit: ({ result }) => ({
+      action: "reminder.create",
+      entity: "Reminder",
+      entityId: result.id,
+      message: `Created reminder "${result.title}"`,
+      metadata: { reminderId: result.id, vehicleId: result.vehicleId },
+    }),
+  });
 }
 
 export async function updateReminder(input: unknown) {
@@ -102,5 +111,15 @@ export async function deleteReminder(reminderId: string) {
     revalidatePath(`/vehicles/${reminder.vehicleId}`);
     revalidatePath("/");
     revalidatePath("/reminders");
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }] });
+    return { reminderId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }],
+    audit: ({ result }) => ({
+      action: "reminder.delete",
+      entity: "Reminder",
+      entityId: result.reminderId,
+      message: `Deleted reminder ${result.reminderId}`,
+      metadata: { reminderId: result.reminderId },
+    }),
+  });
 }

+ 42 - 5
src/features/vehicles/Actions/serviceActions.ts

@@ -339,7 +339,16 @@ export async function createServiceRecord(input: unknown) {
     revalidatePath(`/vehicles/${data.vehicleId}`);
     revalidatePath("/services");
     return record;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.SERVICES }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.SERVICES }],
+    audit: ({ result }) => ({
+      action: "service.create",
+      entity: "ServiceRecord",
+      entityId: result.id,
+      message: `Created service record ${result.invoiceNumber || result.id}`,
+      metadata: { serviceRecordId: result.id, vehicleId: result.vehicleId },
+    }),
+  });
 }
 
 export async function updateServiceRecord(input: unknown) {
@@ -476,7 +485,16 @@ export async function updateServiceRecord(input: unknown) {
     revalidatePath(`/vehicles/${existing.vehicleId}/service/${id}`);
     revalidatePath("/services");
     return record;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }],
+    audit: ({ result }) => ({
+      action: "service.update",
+      entity: "ServiceRecord",
+      entityId: result.id,
+      message: `Updated service record ${result.invoiceNumber || result.id}`,
+      metadata: { serviceRecordId: result.id },
+    }),
+  });
 }
 
 export async function updateServiceStatus(recordId: string, status: string) {
@@ -513,8 +531,17 @@ export async function updateServiceStatus(recordId: string, status: string) {
     revalidatePath("/work-orders");
     revalidatePath("/services");
     revalidatePath(`/vehicles/${record.vehicleId}`);
-    return { success: true };
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }] });
+    return { success: true, recordId, status };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.SERVICES }],
+    audit: ({ result }) => ({
+      action: "service.status",
+      entity: "ServiceRecord",
+      entityId: result.recordId,
+      message: `Changed service record status to ${result.status}`,
+      metadata: { serviceRecordId: result.recordId, status: result.status },
+    }),
+  });
 }
 
 export async function toggleManuallyPaid(recordId: string) {
@@ -645,7 +672,17 @@ export async function deleteServiceRecord(recordId: string) {
     revalidatePath("/");
     revalidatePath(`/vehicles/${record.vehicleId}`);
     revalidatePath("/services");
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.SERVICES }] });
+    return { recordId };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.SERVICES }],
+    audit: ({ result }) => ({
+      action: "service.delete",
+      entity: "ServiceRecord",
+      entityId: result.recordId,
+      message: `Deleted service record ${result.recordId}`,
+      metadata: { serviceRecordId: result.recordId },
+    }),
+  });
 }
 
 export async function deleteServiceAttachment(attachmentId: string) {

+ 8 - 1
src/features/vehicles/Actions/unarchiveVehicle.ts

@@ -17,12 +17,19 @@ export async function unarchiveVehicle(vehicleId: string) {
       revalidatePath("/vehicles");
       revalidatePath(`/vehicles/${vehicleId}`);
 
-      return { success: true as const };
+      return { success: true as const, vehicleId };
     },
     {
       requiredPermissions: [
         { action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES },
       ],
+      audit: ({ result }) => ({
+        action: "vehicle.unarchive",
+        entity: "Vehicle",
+        entityId: result.vehicleId,
+        message: `Unarchived vehicle ${result.vehicleId}`,
+        metadata: { vehicleId: result.vehicleId },
+      }),
     }
   );
 }

+ 50 - 8
src/features/vehicles/Actions/vehicleActions.ts

@@ -132,13 +132,29 @@ export async function createVehicle(input: unknown) {
     revalidatePath("/");
     revalidatePath("/vehicles");
     return vehicle;
-  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.VEHICLES }] });
+  }, {
+    requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.VEHICLES }],
+    audit: ({ result }) => ({
+      action: "vehicle.create",
+      entity: "Vehicle",
+      entityId: result.id,
+      message: `Created vehicle ${result.year} ${result.make} ${result.model}`,
+      metadata: { vehicleId: result.id },
+    }),
+  });
 }
 
 export async function updateVehicle(input: unknown) {
-  return withAuth(async ({ organizationId }) => {
+  return withAuth(async ({ organizationId, userId }) => {
     const { id, ...data } = updateVehicleSchema.parse(input);
 
+    // Fetch current record for display/diff
+    const before = await db.vehicle.findFirst({
+      where: { id, organizationId },
+      select: { year: true, make: true, model: true, licensePlate: true },
+    });
+    if (!before) throw new Error("Vehicle not found");
+
     // If image is being changed, delete the old file from disk
     if (data.imageUrl !== undefined) {
       const existing = await db.vehicle.findFirst({
@@ -154,7 +170,7 @@ export async function updateVehicle(input: unknown) {
       }
     }
 
-    const vehicle = await db.vehicle.updateMany({
+    const updateResult = await db.vehicle.updateMany({
       where: { id, organizationId },
       data: {
         ...data,
@@ -168,21 +184,36 @@ export async function updateVehicle(input: unknown) {
         customerId: data.customerId !== undefined ? (data.customerId || null) : undefined,
       },
     });
-    if (vehicle.count === 0) throw new Error("Vehicle not found");
+    if (updateResult.count === 0) throw new Error("Vehicle not found");
+    const vehicleDisplay = `${before.year} ${before.make} ${before.model}${before.licensePlate ? ` (${before.licensePlate})` : ""}`;
+    const changedKeys = Object.keys(data).filter((k) => (data as Record<string, unknown>)[k] !== undefined);
     revalidatePath("/");
     revalidatePath("/vehicles");
     revalidatePath(`/vehicles/${id}`);
-    return vehicle;
-  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }] });
+    return { id, count: updateResult.count, fields: changedKeys, vehicleDisplay };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }],
+    audit: ({ result }) => ({
+      action: "vehicle.update",
+      entity: "Vehicle",
+      entityId: result.id,
+      message: `Updated vehicle ${result.vehicleDisplay} — changed: ${result.fields.join(", ") || "(no changes)"}`,
+      metadata: { vehicleId: result.id, vehicleDisplay: result.vehicleDisplay, changed: result.fields },
+    }),
+  });
 }
 
 export async function deleteVehicle(vehicleId: string) {
-  return withAuth(async ({ organizationId }) => {
+  return withAuth(async ({ organizationId, userId }) => {
     // Fetch vehicle with its attachments so we can clean up files
     const vehicle = await db.vehicle.findFirst({
       where: { id: vehicleId, organizationId },
       select: {
         imageUrl: true,
+        year: true,
+        make: true,
+        model: true,
+        licensePlate: true,
         serviceRecords: {
           select: { attachments: { select: { fileUrl: true } } },
         },
@@ -210,7 +241,18 @@ export async function deleteVehicle(vehicleId: string) {
       }
     }
 
+    const vehicleDisplay = `${vehicle.year} ${vehicle.make} ${vehicle.model}${vehicle.licensePlate ? ` (${vehicle.licensePlate})` : ""}`;
     revalidatePath("/");
     revalidatePath("/vehicles");
-  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.VEHICLES }] });
+    return { vehicleId, vehicleDisplay };
+  }, {
+    requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.VEHICLES }],
+    audit: ({ result }) => ({
+      action: "vehicle.delete",
+      entity: "Vehicle",
+      entityId: result.vehicleId,
+      message: `Deleted vehicle ${result.vehicleDisplay}`,
+      metadata: { vehicleId: result.vehicleId, vehicleDisplay: result.vehicleDisplay },
+    }),
+  });
 }

+ 23 - 2
src/features/workboard/Actions/technicianActions.ts

@@ -60,6 +60,13 @@ export async function createTechnician(input: unknown) {
       requiredPermissions: [
         { action: PermissionAction.CREATE, subject: PermissionSubject.WORK_BOARD },
       ],
+      audit: ({ result }) => ({
+        action: "technician.create",
+        entity: "Technician",
+        entityId: result.id,
+        message: `Created technician "${result.name}"`,
+        metadata: { technicianId: result.id, technicianName: result.name },
+      }),
     },
   );
 }
@@ -82,12 +89,19 @@ export async function updateTechnician(input: unknown) {
       });
 
       revalidatePath("/work-board");
-      return technician;
+      return { ...technician, technicianId: id };
     },
     {
       requiredPermissions: [
         { action: PermissionAction.UPDATE, subject: PermissionSubject.WORK_BOARD },
       ],
+      audit: ({ result }) => ({
+        action: "technician.update",
+        entity: "Technician",
+        entityId: result.technicianId,
+        message: `Updated technician ${result.technicianId}`,
+        metadata: { technicianId: result.technicianId },
+      }),
     },
   );
 }
@@ -105,12 +119,19 @@ export async function deleteTechnician(id: string) {
       });
 
       revalidatePath("/work-board");
-      return { success: true };
+      return { success: true, technicianId: id };
     },
     {
       requiredPermissions: [
         { action: PermissionAction.DELETE, subject: PermissionSubject.WORK_BOARD },
       ],
+      audit: ({ result }) => ({
+        action: "technician.delete",
+        entity: "Technician",
+        entityId: result.technicianId,
+        message: `Deleted technician ${result.technicianId}`,
+        metadata: { technicianId: result.technicianId },
+      }),
     },
   );
 }

+ 2 - 0
src/i18n/request.ts

@@ -64,6 +64,7 @@ export default getRequestConfig(async () => {
   const quotes = (await import(`../../messages/${locale}/quotes.json`)).default
   const billing = (await import(`../../messages/${locale}/billing.json`)).default
   const reminders = (await import(`../../messages/${locale}/reminders.json`)).default
+  const audit = (await import(`../../messages/${locale}/audit.json`)).default
 
   return {
     locale,
@@ -90,6 +91,7 @@ export default getRequestConfig(async () => {
       quotes,
       billing,
       reminders,
+      audit,
     },
   }
 })

+ 2 - 1
src/instrumentation.ts

@@ -1,9 +1,10 @@
 export async function register() {
   if (process.env.NEXT_RUNTIME === 'nodejs') {
-    const { checkLicenses, checkSubscriptions, processRecurringInvoices, cleanupPortalSessions } = await import('./cronTasks')
+    const { checkLicenses, checkSubscriptions, processRecurringInvoices, cleanupPortalSessions, cleanupAuditLogs } = await import('./cronTasks')
     checkLicenses()
     checkSubscriptions()
     processRecurringInvoices()
     cleanupPortalSessions()
+    cleanupAuditLogs()
   }
 }

+ 37 - 0
src/lib/audit.ts

@@ -0,0 +1,37 @@
+import { db } from "@/lib/db";
+
+export type AuditEvent = {
+  action: string;
+  entity?: string;
+  entityId?: string;
+  message?: string;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  metadata?: Record<string, any> | null;
+  ip?: string | null;
+  userAgent?: string | null;
+};
+
+export async function logAudit(
+  ctx: { userId: string; organizationId: string },
+  event: AuditEvent,
+) {
+  try {
+    await db.auditLog.create({
+      data: {
+        userId: ctx.userId || null,
+        organizationId: ctx.organizationId || null,
+        action: event.action,
+        entity: event.entity ?? null,
+        entityId: event.entityId ?? null,
+        message: event.message ?? null,
+        metadata: event.metadata ?? undefined,
+        ip: event.ip ?? null,
+        userAgent: event.userAgent ?? null,
+      },
+    });
+  } catch (err) {
+    // Don't block core flows due to logging failure
+    console.error("[audit] failed to write log:", err);
+  }
+}
+

+ 17 - 0
src/lib/auth.ts

@@ -4,6 +4,7 @@ import { prismaAdapter } from 'better-auth/adapters/prisma'
 import { nextCookies } from 'better-auth/next-js'
 import { twoFactor } from 'better-auth/plugins/two-factor'
 import { db } from './db'
+import { logAudit } from './audit'
 
 const baseURL = process.env.NEXT_PUBLIC_APP_URL
 const isProduction = baseURL?.startsWith('https://')
@@ -135,6 +136,21 @@ export const auth = betterAuth({
             where: { id: session.userId },
             data: { lastLogin: new Date() },
           })
+
+          // Audit: log successful login
+          const membership = await db.organizationMember.findFirst({
+            where: { userId: session.userId },
+            select: { organizationId: true },
+          })
+          logAudit(
+            { userId: session.userId, organizationId: membership?.organizationId ?? '' },
+            {
+              action: 'auth.login',
+              message: 'User logged in',
+              ip: (session as Record<string, unknown>).ipAddress as string ?? null,
+              userAgent: (session as Record<string, unknown>).userAgent as string ?? null,
+            },
+          ).catch(() => { /* best-effort */ })
         },
       },
     },
@@ -174,6 +190,7 @@ export const auth = betterAuth({
               data: { termsAcceptedAt: new Date() },
             })
           }
+
         },
       },
     },

+ 32 - 0
src/lib/cron/cleanup-audit-logs.ts

@@ -0,0 +1,32 @@
+import { CronJob } from 'cron'
+import { db } from '@/lib/db'
+
+/**
+ * Audit log retention cleanup — runs daily at 03:00 UTC.
+ *
+ * ISO 27001/27002 (A.8.15) requires a defined retention policy for audit logs.
+ * Default retention: 365 days (1 year). Override with AUDIT_LOG_RETENTION_DAYS env var.
+ * ISO recommends minimum 1 year; many organizations retain for 2-3 years.
+ */
+export function cleanupAuditLogs() {
+  const job = new CronJob('0 3 * * *', async () => {
+    try {
+      const parsed = parseInt(process.env.AUDIT_LOG_RETENTION_DAYS || '365', 10)
+      const retentionDays = Number.isFinite(parsed) && parsed > 0 ? parsed : 365
+      const cutoff = new Date()
+      cutoff.setDate(cutoff.getDate() - retentionDays)
+
+      const result = await db.auditLog.deleteMany({
+        where: { timestamp: { lt: cutoff } },
+      })
+
+      if (result.count > 0) {
+        console.warn(`[cron] Audit log cleanup: deleted ${result.count} logs older than ${retentionDays} days`)
+      }
+    } catch (error) {
+      console.error('[cron] Audit log cleanup failed:', error)
+    }
+  })
+
+  job.start()
+}

+ 95 - 48
src/lib/with-auth.ts

@@ -1,88 +1,135 @@
-import { ZodError } from "zod";
-import { getCachedSession, getCachedMembership } from "./cached-session";
-import { db } from "./db";
-import type { PermissionInput } from "./permissions";
-import { hasAllPermissions } from "./permissions";
+import { ZodError } from 'zod'
+import { headers } from 'next/headers'
+import { getCachedSession, getCachedMembership } from './cached-session'
+import { db } from './db'
+import type { PermissionInput } from './permissions'
+import { hasAllPermissions } from './permissions'
+import { logAudit } from '@/lib/audit'
+import type { AuditEvent } from '@/lib/audit'
 
 export type ActionResult<T = unknown> = {
-  success: boolean;
-  data?: T;
-  error?: string;
-};
+  success: boolean
+  data?: T
+  error?: string
+}
 
 export type AuthContext = {
-  userId: string;
-  organizationId: string;
-  role: string;
-  isSuperAdmin: boolean;
-  isAdmin: boolean;
-};
+  userId: string
+  organizationId: string
+  role: string
+  isSuperAdmin: boolean
+  isAdmin: boolean
+}
+
+type AuditBuilder<T> = (args: { ctx: AuthContext; result: T }) => AuditEvent | null | undefined
 
-type WithAuthOptions = {
-  requiredPermissions?: PermissionInput[];
-};
+type WithAuthOptions<T = unknown> = {
+  requiredPermissions?: PermissionInput[]
+  // Optional audit config. If provided, runs after successful action.
+  audit?: AuditEvent | AuditBuilder<T>
+}
+
+async function getRequestMeta() {
+  try {
+    const h = await headers()
+    const forwarded = h.get('x-forwarded-for')
+    const ip = forwarded?.split(',')[0]?.trim() || h.get('x-real-ip') || null
+    const userAgent = h.get('user-agent') || null
+    return { ip, userAgent }
+  } catch {
+    return { ip: null, userAgent: null }
+  }
+}
 
 export async function withAuth<T>(
   action: (ctx: AuthContext) => Promise<T>,
-  options: WithAuthOptions = {},
+  options: WithAuthOptions<T> = {}
 ): Promise<ActionResult<T>> {
   try {
-    const session = await getCachedSession();
+    const session = await getCachedSession()
 
     if (!session?.user?.id) {
-      return { success: false, error: "Unauthorized" };
+      return { success: false, error: 'Unauthorized' }
     }
 
     const user = await db.user.findUnique({
       where: { id: session.user.id },
       select: { isSuperAdmin: true },
-    });
+    })
 
-    const isSuperAdmin = user?.isSuperAdmin ?? false;
+    const isSuperAdmin = user?.isSuperAdmin ?? false
 
-    const membership = await getCachedMembership(session.user.id);
+    const membership = await getCachedMembership(session.user.id)
 
     if (!membership?.organizationId) {
-      return { success: false, error: "No organization found" };
+      return { success: false, error: 'No organization found' }
     }
 
-    const isOwnerOrAdmin = membership?.role === "owner" || membership?.role === "admin";
-    const roleIsAdmin = membership?.customRole?.isAdmin === true;
+    const isOwnerOrAdmin = membership?.role === 'owner' || membership?.role === 'admin'
+    const roleIsAdmin = membership?.customRole?.isAdmin === true
+
+    const ctx: AuthContext = {
+      userId: session.user.id,
+      organizationId: membership.organizationId,
+      role: isSuperAdmin ? 'super_admin' : (membership?.role ?? 'member'),
+      isSuperAdmin,
+      isAdmin: isSuperAdmin || isOwnerOrAdmin || roleIsAdmin,
+    }
 
     // Check permissions if required (super admins bypass all permission checks)
     if (!isSuperAdmin && options.requiredPermissions && options.requiredPermissions.length > 0) {
       // Members without a custom role have full access (no restrictions)
-      const hasNoCustomRole = !membership?.roleId;
+      const hasNoCustomRole = !membership?.roleId
 
       if (!isOwnerOrAdmin && !roleIsAdmin && !hasNoCustomRole) {
-        const userPermissions = membership?.customRole?.permissions ?? [];
+        const userPermissions = membership?.customRole?.permissions ?? []
         if (!hasAllPermissions(userPermissions, options.requiredPermissions)) {
-          return { success: false, error: "Insufficient permissions" };
+          // Log failed permission attempt
+          const meta = await getRequestMeta()
+          logAudit(ctx, {
+            action: 'auth.permissionDenied',
+            message: `Permission denied: ${options.requiredPermissions.map((p) => `${p.action}:${p.subject}`).join(', ')}`,
+            metadata: { requiredPermissions: options.requiredPermissions },
+            ip: meta.ip,
+            userAgent: meta.userAgent,
+          }).catch(() => { /* best-effort */ })
+          return { success: false, error: 'Insufficient permissions' }
         }
       }
     }
 
-    const data = await action({
-      userId: session.user.id,
-      organizationId: membership.organizationId,
-      role: isSuperAdmin ? "super_admin" : (membership?.role ?? "member"),
-      isSuperAdmin,
-      isAdmin: isSuperAdmin || isOwnerOrAdmin || roleIsAdmin,
-    });
-    return { success: true, data };
+    const data = await action(ctx)
+
+    // Post-success audit logging (fire-and-forget, logAudit handles its own errors)
+    if (options.audit) {
+      getRequestMeta().then((meta) => {
+        const event =
+          typeof options.audit === 'function'
+            ? (options.audit as AuditBuilder<T>)({ ctx, result: data })
+            : options.audit
+        if (event && event.action) {
+          logAudit(ctx, {
+            ...event,
+            ip: event.ip ?? meta.ip,
+            userAgent: event.userAgent ?? meta.userAgent,
+          })
+        }
+      }).catch(() => { /* best-effort */ })
+    }
+
+    return { success: true, data }
   } catch (error) {
     if (error instanceof ZodError) {
       const messages = error.issues.map((e) => {
-        const field = e.path.join(".");
-        return field ? `${field}: ${e.message}` : e.message;
-      });
-      const message = messages.join(". ");
-      console.error("[withAuth] Validation error:", message);
-      return { success: false, error: message };
+        const field = e.path.join('.')
+        return field ? `${field}: ${e.message}` : e.message
+      })
+      const message = messages.join('. ')
+      console.error('[withAuth] Validation error:', message)
+      return { success: false, error: message }
     }
-    const message =
-      error instanceof Error ? error.message : "An unexpected error occurred";
-    console.error("[withAuth] Error:", message);
-    return { success: false, error: message };
+    const message = error instanceof Error ? error.message : 'An unexpected error occurred'
+    console.error('[withAuth] Error:', message)
+    return { success: false, error: message }
   }
 }