Skip to main content

Overview

The AuditService provides a centralized audit logging system for tracking all user actions and system events in the P.FLEX application. It maintains an in-memory log using Angular signals for reactive UI updates. Location: src/services/audit.service.ts Provider: Root (singleton)

Properties

logs
Signal<AuditLog[]>
Signal containing all audit log entries, sorted with newest first. Initialized with a system startup entry.

Types

AuditLog Interface

interface AuditLog {
  id: string;          // Unique identifier (9-character random string)
  timestamp: Date;     // Date/time of the event
  user: string;        // User who performed the action
  role: string;        // User's role at time of action
  module: string;      // Module/section (uppercase)
  action: string;      // Action description
  details: string;     // Additional details about the action
  ip: string;          // Simulated IP address
}
Common Modules:
  • SISTEMA - System events
  • ACCESO - Authentication events
  • OPERACIONES - Operational changes
  • INVENTARIO - Inventory operations
  • STOCK PT - Finished goods stock
  • ADMIN - Administrative actions
  • ORDENES - Work order operations

Methods

log

Creates and stores a new audit log entry.
log(user: string, role: string, module: string, action: string, details: string = ''): void
user
string
required
Name of the user performing the action. Uses ‘Desconocido’ if empty/null.
role
string
required
Role of the user at the time of action. Uses ’---’ if empty/null.
module
string
required
Module or section where action occurred. Automatically converted to uppercase.
action
string
required
Brief description of the action (e.g., ‘Inicio de Sesión’, ‘Crear Usuario’).
details
string
Optional detailed description of the action. Defaults to empty string.
Behavior:
  • Generates unique 9-character ID using base36 encoding
  • Sets timestamp to current date/time
  • Converts module to uppercase
  • Generates simulated IP address (192.168.1.1-254)
  • Prepends new entry to logs array (newest first)
  • Logs to browser console with ‘[AUDIT]’ prefix
import { inject } from '@angular/core';
import { AuditService } from './services/audit.service';

const audit = inject(AuditService);

// Log a user login
audit.log(
  'Juan Perez',
  'Supervisor',
  'ACCESO',
  'Inicio de Sesión',
  'Usuario jperez inició sesión en Turno Día.'
);

// Log a system configuration change
audit.log(
  'Carlos Admin',
  'Sistemas',
  'ADMIN',
  'Configuración',
  'Se actualizaron los parámetros globales del sistema.'
);

// Log inventory operation
audit.log(
  'Maria Garcia',
  'Jefatura',
  'INVENTARIO',
  'Alta Clisés',
  'Se agregaron 15 clichés al inventario.'
);
import { Injectable, inject } from '@angular/core';
import { AuditService } from './audit.service';
import { StateService } from './state.service';

@Injectable({ providedIn: 'root' })
export class OrderService {
  private audit = inject(AuditService);
  private state = inject(StateService);
  
  createOrder(orderData: any) {
    // ... create order logic
    
    // Log the action
    this.audit.log(
      this.state.userName(),
      this.state.userRole(),
      'ORDENES',
      'Crear OT',
      `Se creó la orden de trabajo ${orderData.ot} para ${orderData.client}`
    );
  }
  
  updateOrderStatus(ot: string, newStatus: string) {
    // ... update logic
    
    this.audit.log(
      this.state.userName(),
      this.state.userRole(),
      'ORDENES',
      'Cambio de Estado',
      `OT ${ot} cambió a estado: ${newStatus}`
    );
  }
}

Usage Examples

Display Audit Log in Component

import { Component, inject } from '@angular/core';
import { AuditService } from './services/audit.service';
import { DatePipe } from '@angular/common';

@Component({
  selector: 'app-audit-log',
  standalone: true,
  imports: [DatePipe],
  template: `
    <div class="audit-log">
      <h2>Registro de Auditoría</h2>
      
      <table>
        <thead>
          <tr>
            <th>Fecha/Hora</th>
            <th>Usuario</th>
            <th>Rol</th>
            <th>Módulo</th>
            <th>Acción</th>
            <th>Detalles</th>
            <th>IP</th>
          </tr>
        </thead>
        <tbody>
          @for (log of auditService.logs(); track log.id) {
            <tr>
              <td>{{ log.timestamp | date:'short' }}</td>
              <td>{{ log.user }}</td>
              <td>{{ log.role }}</td>
              <td><span class="module-badge">{{ log.module }}</span></td>
              <td>{{ log.action }}</td>
              <td>{{ log.details }}</td>
              <td>{{ log.ip }}</td>
            </tr>
          }
        </tbody>
      </table>
      
      @if (auditService.logs().length === 0) {
        <p>No hay registros de auditoría.</p>
      }
    </div>
  `,
  styles: [`
    .audit-log {
      padding: 20px;
    }
    
    table {
      width: 100%;
      border-collapse: collapse;
    }
    
    th, td {
      padding: 8px;
      text-align: left;
      border-bottom: 1px solid #ddd;
    }
    
    th {
      background-color: #f5f5f5;
      font-weight: bold;
    }
    
    .module-badge {
      padding: 4px 8px;
      border-radius: 4px;
      background-color: #e3f2fd;
      font-size: 0.85em;
      font-weight: 500;
    }
  `]
})
export class AuditLogComponent {
  auditService = inject(AuditService);
}

Filter and Search Audit Logs

import { Component, inject, signal, computed } from '@angular/core';
import { AuditService, AuditLog } from './services/audit.service';

@Component({
  selector: 'app-audit-search',
  template: `
    <div class="audit-search">
      <h2>Búsqueda de Auditoría</h2>
      
      <div class="filters">
        <input 
          type="text" 
          [(ngModel)]="searchTerm"
          (input)="updateSearch($event)"
          placeholder="Buscar usuario, acción, detalles..."
        >
        
        <select [(ngModel)]="selectedModule" (change)="updateModule($event)">
          <option value="">Todos los módulos</option>
          <option value="ACCESO">Acceso</option>
          <option value="ADMIN">Admin</option>
          <option value="INVENTARIO">Inventario</option>
          <option value="OPERACIONES">Operaciones</option>
          <option value="ORDENES">Órdenes</option>
          <option value="STOCK PT">Stock PT</option>
        </select>
        
        <input 
          type="date" 
          [(ngModel)]="filterDate"
          (change)="updateDate($event)"
        >
      </div>
      
      <p>Mostrando {{ filteredLogs().length }} de {{ auditService.logs().length }} registros</p>
      
      <div class="log-list">
        @for (log of filteredLogs(); track log.id) {
          <div class="log-entry">
            <div class="log-header">
              <span class="timestamp">{{ log.timestamp | date:'medium' }}</span>
              <span class="module">{{ log.module }}</span>
            </div>
            <div class="log-body">
              <strong>{{ log.action }}</strong>
              <p>{{ log.details }}</p>
              <small>{{ log.user }} ({{ log.role }}) - {{ log.ip }}</small>
            </div>
          </div>
        }
      </div>
    </div>
  `
})
export class AuditSearchComponent {
  auditService = inject(AuditService);
  
  searchTerm = signal<string>('');
  selectedModule = signal<string>('');
  filterDate = signal<string>('');
  
  filteredLogs = computed(() => {
    let logs = this.auditService.logs();
    
    // Filter by search term
    const term = this.searchTerm().toLowerCase();
    if (term) {
      logs = logs.filter(log => 
        log.user.toLowerCase().includes(term) ||
        log.action.toLowerCase().includes(term) ||
        log.details.toLowerCase().includes(term)
      );
    }
    
    // Filter by module
    const module = this.selectedModule();
    if (module) {
      logs = logs.filter(log => log.module === module);
    }
    
    // Filter by date
    const date = this.filterDate();
    if (date) {
      const filterDate = new Date(date);
      logs = logs.filter(log => {
        const logDate = new Date(log.timestamp);
        return logDate.toDateString() === filterDate.toDateString();
      });
    }
    
    return logs;
  });
  
  updateSearch(event: any) {
    this.searchTerm.set(event.target.value);
  }
  
  updateModule(event: any) {
    this.selectedModule.set(event.target.value);
  }
  
  updateDate(event: any) {
    this.filterDate.set(event.target.value);
  }
}

Export Audit Logs

import { inject } from '@angular/core';
import { AuditService } from './services/audit.service';

class AuditExportService {
  auditService = inject(AuditService);
  
  exportToCSV() {
    const logs = this.auditService.logs();
    
    // CSV header
    const headers = ['ID', 'Timestamp', 'User', 'Role', 'Module', 'Action', 'Details', 'IP'];
    
    // CSV rows
    const rows = logs.map(log => [
      log.id,
      log.timestamp.toISOString(),
      log.user,
      log.role,
      log.module,
      log.action,
      log.details,
      log.ip
    ]);
    
    // Build CSV content
    const csvContent = [
      headers.join(','),
      ...rows.map(row => row.map(cell => `"${cell}"`).join(','))
    ].join('\n');
    
    // Download file
    const blob = new Blob([csvContent], { type: 'text/csv' });
    const url = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `audit-log-${new Date().toISOString()}.csv`;
    link.click();
    window.URL.revokeObjectURL(url);
  }
  
  exportToJSON() {
    const logs = this.auditService.logs();
    const json = JSON.stringify(logs, null, 2);
    
    const blob = new Blob([json], { type: 'application/json' });
    const url = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `audit-log-${new Date().toISOString()}.json`;
    link.click();
    window.URL.revokeObjectURL(url);
  }
}

Audit Statistics Component

import { Component, inject, computed } from '@angular/core';
import { AuditService } from './services/audit.service';

@Component({
  selector: 'app-audit-stats',
  template: `
    <div class="audit-stats">
      <h2>Estadísticas de Auditoría</h2>
      
      <div class="stats-grid">
        <div class="stat-card">
          <h3>Total de Eventos</h3>
          <p class="stat-number">{{ totalLogs() }}</p>
        </div>
        
        <div class="stat-card">
          <h3>Usuarios Activos</h3>
          <p class="stat-number">{{ uniqueUsers() }}</p>
        </div>
        
        <div class="stat-card">
          <h3>Eventos Hoy</h3>
          <p class="stat-number">{{ logsToday() }}</p>
        </div>
      </div>
      
      <div class="module-breakdown">
        <h3>Eventos por Módulo</h3>
        @for (stat of moduleStats(); track stat.module) {
          <div class="module-stat">
            <span>{{ stat.module }}</span>
            <span>{{ stat.count }} eventos</span>
          </div>
        }
      </div>
      
      <div class="recent-activity">
        <h3>Actividad Reciente</h3>
        @for (log of recentLogs(); track log.id) {
          <div class="activity-item">
            <strong>{{ log.user }}</strong> - {{ log.action }}
            <small>{{ log.timestamp | date:'short' }}</small>
          </div>
        }
      </div>
    </div>
  `
})
export class AuditStatsComponent {
  auditService = inject(AuditService);
  
  totalLogs = computed(() => this.auditService.logs().length);
  
  uniqueUsers = computed(() => {
    const users = new Set(this.auditService.logs().map(log => log.user));
    return users.size;
  });
  
  logsToday = computed(() => {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    
    return this.auditService.logs().filter(log => {
      const logDate = new Date(log.timestamp);
      logDate.setHours(0, 0, 0, 0);
      return logDate.getTime() === today.getTime();
    }).length;
  });
  
  moduleStats = computed(() => {
    const counts: Record<string, number> = {};
    
    this.auditService.logs().forEach(log => {
      counts[log.module] = (counts[log.module] || 0) + 1;
    });
    
    return Object.entries(counts)
      .map(([module, count]) => ({ module, count }))
      .sort((a, b) => b.count - a.count);
  });
  
  recentLogs = computed(() => {
    return this.auditService.logs().slice(0, 10);
  });
}

Implementation Notes

Data Persistence

The current implementation stores logs in memory only. They will be lost on page refresh. For production:
import { Injectable, signal, effect } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuditService {
  readonly logs = signal<AuditLog[]>(this.loadLogsFromStorage());
  
  constructor() {
    // Auto-save to localStorage on changes
    effect(() => {
      localStorage.setItem('auditLogs', JSON.stringify(this.logs()));
    });
  }
  
  private loadLogsFromStorage(): AuditLog[] {
    const stored = localStorage.getItem('auditLogs');
    if (stored) {
      const parsed = JSON.parse(stored);
      // Convert timestamp strings back to Date objects
      return parsed.map((log: any) => ({
        ...log,
        timestamp: new Date(log.timestamp)
      }));
    }
    return [this.getInitialLog()];
  }
  
  private getInitialLog(): AuditLog {
    return {
      id: 'log-init',
      timestamp: new Date(Date.now() - 3600000),
      user: 'Sistema',
      role: 'System',
      module: 'SISTEMA',
      action: 'Inicio de Servicios',
      details: 'El sistema se ha iniciado correctamente.',
      ip: 'localhost'
    };
  }
}

Backend Integration

For production, send logs to backend:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class AuditService {
  private http = inject(HttpClient);
  
  log(user: string, role: string, module: string, action: string, details: string = '') {
    const newEntry: AuditLog = {
      id: Math.random().toString(36).substr(2, 9),
      timestamp: new Date(),
      user: user || 'Desconocido',
      role: role || '---',
      module: module.toUpperCase(),
      action: action,
      details: details,
      ip: this.getClientIP()
    };
    
    // Add to local state
    this.logs.update(currentLogs => [newEntry, ...currentLogs]);
    
    // Send to backend (fire and forget)
    this.http.post('/api/audit/log', newEntry).subscribe({
      error: (err) => console.error('Failed to send audit log:', err)
    });
  }
  
  private getClientIP(): string {
    // Get real IP from backend or use placeholder
    return 'pending';
  }
}

Notes

  • Logs are stored with newest first for efficient recent activity display
  • IP addresses are simulated (192.168.1.1-254) since browsers cannot access real client IP
  • Module names are automatically uppercased for consistency
  • Empty/null user defaults to ‘Desconocido’, empty/null role defaults to ’---’
  • All logs are also output to browser console with ‘[AUDIT]’ prefix
  • ID generation uses base36 encoding for short, URL-safe identifiers
  • Initial log entry demonstrates system startup tracking

Build docs developers (and LLMs) love