Markdown Converter
Agent skill for markdown-converter
Système d'audit et de journalisation des événements utilisateur et système pour traçabilité complète des actions.
Sign in to like and favorite skills
Système d'audit et de journalisation des événements utilisateur et système pour traçabilité complète des actions.
events/ ├── AGENTS.md # Ce fichier ├── constants.ts # Types d'événements et constantes ├── client/ │ └── AdminEventsPage.tsx # Interface admin de consultation des événements └── server/ ├── api.ts # API REST pour récupération des événements └── service.ts # Service de gestion des événements
Le module events permet de :
// Authentification 'user_login' // Connexion utilisateur 'user_activated' // Activation de compte 'user_created' // Création de compte 'user_updated' // Modification de compte 'user_deleted' // Suppression de compte
// Demandes d'éligibilité 'demand_created' // Nouvelle demande 'demand_assigned' // Assignation à un gestionnaire 'demand_updated' // Modification de demande 'demand_deleted' // Suppression de demande // Tests d'éligibilité professionnel 'pro_eligibility_test_created' // Nouveau test 'pro_eligibility_test_renamed' // Renommage 'pro_eligibility_test_updated' // Modification 'pro_eligibility_test_deleted' // Suppression
// Tâches techniques 'build_tiles' // Génération de tuiles cartographiques 'sync_metadata_from_airtable' // Synchronisation métadonnées depuis Airtable 'sync_geometries_to_airtable' // Synchronisation géométries vers Airtable
server/service.ts)listEvents(options)Récupère la liste des événements avec filtres et jointures utilisateur.
import { listEvents, type ListEventsOptions } from '@/modules/events/server/service'; const events = await listEvents({ authorId: 'user-uuid', // Filtrer par auteur type: 'user_login', // Filtrer par type d'événement context: { type: 'demand', id: '123' } // Filtrer par contexte });
Options de filtrage :
authorId?: string - UUID de l'utilisateur auteurcontext?: { type: string; id: string } - Contexte métier (demande, test, etc.)type?: EventType - Type d'événement spécifiqueRetourne :
type AdminEvent = { id: number; author_id: string | null; type: EventType; context_type: string; context_id: string; data: unknown; created_at: string; author: { id: string; email: string; role: string; } | null; };
Caractéristiques :
data JSON flexible par type d'événementcreateEvent(event)Crée un événement système (sans auteur identifié).
import { createEvent } from '@/modules/events/server/service'; await createEvent({ type: 'build_tiles', context_type: 'tiles', context_id: 'reseaux-de-chaleur', data: { name: 'Réseaux de chaleur', tilesGenerated: 1250 } });
createUserEvent(event)Crée un événement avec auteur utilisateur identifié.
import { createUserEvent } from '@/modules/events/server/service'; await createUserEvent({ author_id: userId, type: 'demand_updated', context_type: 'demand', context_id: demandId, data: { fields: ['status', 'gestionnaire'], previous: {...}, current: {...} } });
server/api.ts)Endpoint :
/api/admin/events
Méthode : GET
Authentification : Rôle admin requis
// Query parameters { authorId?: string; // UUID utilisateur type?: EventType; // Type d'événement contextType?: string; // Type de contexte contextId?: string; // ID de contexte }
Validation Zod :
const querySchema = { authorId: z.string().uuid().optional(), type: z.enum(eventTypes).optional(), contextType: z.string().optional(), contextId: z.string().optional(), };
client/AdminEventsPage.tsx)Interface complète de consultation et filtrage des événements avec virtualisation pour les performances.
import AdminEventsPage from '@/modules/events/client/AdminEventsPage'; export default AdminEventsPage;
Fonctionnalités principales :
nuqs// Exemple de filtrages interactifs <FilterButton onClick={() => updateFilters({ authorId: event.author_id })}> {event.author.email} </FilterButton> <FilterButton onClick={() => updateFilters({ contextType: 'demand', contextId: event.context_id })}> demande </FilterButton>
Chaque type d'événement a un rendu personnalisé pour une lisibilité optimale :
const eventLabelRenderers: Record<EventType, (event, updateFilters) => ReactNode> = { user_login: () => "s'est connecté", demand_created: (event, updateFilters) => ( <> <span>Une </span> <FilterButton onClick={() => updateFilters({ contextType: 'demand', contextId: event.context_id })}> demande </FilterButton> <span> a été créée</span> </> ), build_tiles: (event) => ( <span>a reconstruit les tuiles <strong>{event.data?.name}</strong></span> ), // ... autres types };
-- Table des événements CREATE TABLE events ( id SERIAL PRIMARY KEY, author_id UUID REFERENCES users(id), -- Auteur (optionnel) type VARCHAR NOT NULL, -- Type d'événement context_type VARCHAR NOT NULL, -- Type de contexte métier context_id VARCHAR NOT NULL, -- ID du contexte data JSONB, -- Données spécifiques à l'événement created_at TIMESTAMP DEFAULT NOW() ); -- Index pour optimiser les requêtes fréquentes CREATE INDEX idx_events_author_id ON events(author_id); CREATE INDEX idx_events_type ON events(type); CREATE INDEX idx_events_context ON events(context_type, context_id); CREATE INDEX idx_events_created_at ON events(created_at DESC); -- Index composite pour filtres combinés CREATE INDEX idx_events_author_type ON events(author_id, type); CREATE INDEX idx_events_context_type ON events(context_type, context_id, type);
// Dans un service métier import { createUserEvent } from '@/modules/events/server/service'; export async function updateDemand(demandId: string, updates: any, userId: string) { const previous = await getDemand(demandId); // Logique métier de mise à jour await updateDemandInDatabase(demandId, updates); // Événement d'audit await createUserEvent({ author_id: userId, type: 'demand_updated', context_type: 'demand', context_id: demandId, data: { fields: Object.keys(updates), previous: { status: previous.status, gestionnaire: previous.gestionnaire }, current: { status: updates.status, gestionnaire: updates.gestionnaire } } }); }
// Dans un job de synchronisation import { createEvent } from '@/modules/events/server/service'; export async function syncReseauxMetadata() { const startTime = Date.now(); try { const syncResults = await performSync(); await createEvent({ type: 'sync_metadata_from_airtable', context_type: 'sync', context_id: 'reseaux-metadata', data: { duration: Date.now() - startTime, recordsUpdated: syncResults.updated, recordsCreated: syncResults.created, success: true } }); } catch (error) { await createEvent({ type: 'sync_metadata_from_airtable', context_type: 'sync', context_id: 'reseaux-metadata', data: { duration: Date.now() - startTime, error: error.message, success: false } }); throw error; } }
// Interface de recherche personnalisée import { listEvents } from '@/modules/events/server/service'; // Toutes les actions d'un utilisateur suspect const userActivity = await listEvents({ authorId: suspiciousUserId }); // Toutes les modifications d'une demande spécifique const demandHistory = await listEvents({ context: { type: 'demand', id: demandId } }); // Tous les échecs de synchronisation const syncFailures = await listEvents({ type: 'sync_metadata_from_airtable' }); const failures = syncFailures.filter(e => e.data?.success === false);
// Dashboard de statistiques d'activité import { listEvents } from '@/modules/events/server/service'; export async function getActivityStats(dateRange: { from: Date, to: Date }) { const events = await listEvents({}); const eventsByType = events.reduce((acc, event) => { acc[event.type] = (acc[event.type] || 0) + 1; return acc; }, {} as Record<string, number>); const eventsByUser = events.reduce((acc, event) => { if (event.author) { acc[event.author.email] = (acc[event.author.email] || 0) + 1; } return acc; }, {} as Record<string, number>); return { totalEvents: events.length, eventsByType, eventsByUser, mostActiveUsers: Object.entries(eventsByUser) .sort(([,a], [,b]) => b - a) .slice(0, 10) }; }
// Middleware TRPC pour audit automatique import { createUserEvent } from '@/modules/events/server/service'; export const auditMiddleware = t.middleware(async ({ ctx, type, path, next }) => { const result = await next(); // Audit des mutations uniquement if (type === 'mutation' && ctx.userId) { await createUserEvent({ author_id: ctx.userId, type: determineEventType(path), context_type: extractContextType(path), context_id: extractContextId(result), data: { path, input: sanitizeInput(ctx.input) } }); } return result; });
// Hook personnalisé pour actions côté client import { useUserEvent } from '@/modules/events/client/hooks'; export function useAuditedAction() { const logEvent = useUserEvent(); const executeWithAudit = async (action: () => Promise<any>, eventType: EventType) => { try { const result = await action(); await logEvent({ type: eventType, context_type: 'client_action', context_id: 'success', data: { timestamp: Date.now() } }); return result; } catch (error) { await logEvent({ type: eventType, context_type: 'client_action', context_id: 'error', data: { error: error.message, timestamp: Date.now() } }); throw error; } }; return executeWithAudit; }
dataLe module events fournit une infrastructure complète d'audit et de monitoring pour la transparence et la conformité de l'application.