MillerByte.Logging.Api

GDPR Compliance

Last updated: 1/22/2026

The logging package includes built-in support for GDPR compliance including data deletion (right to erasure) and data export (right to data portability).

Right to Erasure (Data Deletion)

Delete all logging data for a specific user:

[HttpDelete("users/{userId}/data")]
[Authorize]
public async Task<IActionResult> DeleteUserData(string userId)
{
    // Verify the requester has permission to delete this user's data
    var currentUserId = User.FindFirst("sub")?.Value;
    if (currentUserId != userId && !User.IsInRole("Admin"))
    {
        return Forbid();
    }
    
    var tenantId = User.FindFirst("tenantId")?.Value ?? "default";
    
    await _loggingService.DeleteUserDataAsync(userId, tenantId);
    
    _logger.LogInformation(
        "Deleted logging data for user {UserId} in tenant {TenantId}", 
        userId, 
        tenantId);
    
    return NoContent();
}

What Gets Deleted

  • All LoginSession records for the user
  • All ApiAction records for the user
  • Deletion is scoped by tenant ID for multi-tenant apps

Right to Data Portability (Export)

Export all user data in a streaming fashion for large datasets:

[HttpGet("users/{userId}/export")]
[Authorize]
public async Task<IActionResult> ExportUserData(
    string userId,
    CancellationToken cancellationToken)
{
    // Verify permission
    var currentUserId = User.FindFirst("sub")?.Value;
    if (currentUserId != userId && !User.IsInRole("Admin"))
    {
        return Forbid();
    }
    
    // Stream the response as JSON
    Response.ContentType = "application/json";
    Response.Headers.Append("Content-Disposition", 
        $"attachment; filename=user-data-{userId}.json");
    
    await using var writer = new Utf8JsonWriter(Response.Body);
    writer.WriteStartObject();
    writer.WritePropertyName("pages");
    writer.WriteStartArray();
    
    await foreach (var page in _loggingService.ExportUserDataStreamAsync(
        userId, 
        pageSize: 1000, 
        cancellationToken))
    {
        if (!page.Success)
        {
            _logger.LogError("Export error: {Error}", page.ErrorMessage);
            break;
        }
        
        JsonSerializer.Serialize(writer, page);
    }
    
    writer.WriteEndArray();
    writer.WriteEndObject();
    await writer.FlushAsync(cancellationToken);
    
    return new EmptyResult();
}

UserDataExportPage Structure

public class UserDataExportPage
{
    // Status
    public bool Success { get; set; }
    public string? ErrorMessage { get; set; }
    
    // Pagination
    public int PageNumber { get; set; }
    public bool HasMore { get; set; }
    
    // Data - Sessions only on first page
    public List<LoginSession>? Sessions { get; set; }
    
    // Data - Actions on every page
    public List<ApiAction> Actions { get; set; }
}

Configuration

builder.Services.AddApiLogging(options =>
{
    // Page size for exports (default: 1000)
    options.ExportPageSize = 1000;
});

Sensitive Data Filtering

Automatically sanitize sensitive data before logging:

builder.Services.AddApiLogging(options =>
{
    options.EnableDataSanitization = true;
    
    // Default sensitive fields
    options.SensitiveFieldNames = new List<string>
    {
        "password",
        "token",
        "authorization",
        "apiKey",
        "api_key",
        "secret",
        "creditCard",
        "credit_card",
        "ssn",
        "pin",
        "cvv",
        "bearer"
    };
    
    // Add your own
    options.SensitiveFieldNames.Add("socialSecurityNumber");
    options.SensitiveFieldNames.Add("bankAccountNumber");
});

Sanitization Behavior

Sensitive field values are replaced with [REDACTED] in both request and response bodies.

// Original request body:
{
  "username": "john@example.com",
  "password": "secret123",
  "creditCard": "4111111111111111"
}

// Logged as:
{
  "username": "john@example.com",
  "password": "[REDACTED]",
  "creditCard": "[REDACTED]"
}

Stack Trace Sanitization

Optionally redact stack traces in production to prevent information disclosure:

options.SanitizeStackTracesInProduction = true;
options.EnvironmentName = "Production";

// Stack traces logged as "[REDACTED]" in production

Data Retention

The package creates TTL indexes for automatic data cleanup:

// Default TTL (configured in MongoDB indexes):
// - Sessions: 30 days
// - Actions: 90 days

// These are created automatically by MongoDbIndexSetupService

Consent Management

Implement consent checking before logging. Use the [ApiLogging]attribute selectively or check consent in middleware:

public class ConsentMiddleware
{
    public async Task InvokeAsync(HttpContext context)
    {
        var hasConsent = await _consentService.HasLoggingConsent(
            context.User.FindFirst("sub")?.Value);
        
        // Skip logging for users without consent
        if (!hasConsent)
        {
            context.Items["ApiLogging_SkipLogging"] = true;
        }
        
        await _next(context);
    }
}

Audit Trail

Log GDPR operations for compliance audit trails:

[HttpDelete("users/{userId}/data")]
public async Task<IActionResult> DeleteUserData(string userId)
{
    var requesterId = User.FindFirst("sub")?.Value;
    var tenantId = User.FindFirst("tenantId")?.Value;
    
    // Log the GDPR request before deletion
    _logger.LogInformation(
        "GDPR deletion request: Requester={Requester}, Subject={Subject}, Tenant={Tenant}",
        requesterId,
        userId,
        tenantId);
    
    await _loggingService.DeleteUserDataAsync(userId, tenantId);
    
    // Log completion
    _logger.LogInformation(
        "GDPR deletion completed: Subject={Subject}, Tenant={Tenant}",
        userId,
        tenantId);
    
    return NoContent();
}

Complete GDPR Controller Example

[ApiController]
[Route("api/gdpr")]
[Authorize]
public class GdprController : ControllerBase
{
    private readonly IApiLoggingService _loggingService;
    private readonly ILogger<GdprController> _logger;
    
    public GdprController(
        IApiLoggingService loggingService,
        ILogger<GdprController> logger)
    {
        _loggingService = loggingService;
        _logger = logger;
    }
    
    [HttpGet("export")]
    public async IAsyncEnumerable<object> ExportMyData(
        [EnumeratorCancellation] CancellationToken ct)
    {
        var userId = User.FindFirst("sub")?.Value;
        
        await foreach (var page in _loggingService
            .ExportUserDataStreamAsync(userId, ct: ct))
        {
            if (!page.Success)
            {
                yield return new { error = page.ErrorMessage };
                yield break;
            }
            
            yield return page;
        }
    }
    
    [HttpDelete("delete")]
    public async Task<IActionResult> DeleteMyData()
    {
        var userId = User.FindFirst("sub")?.Value;
        var tenantId = User.FindFirst("tenantId")?.Value ?? "default";
        
        _logger.LogInformation(
            "User {UserId} requested data deletion", userId);
        
        await _loggingService.DeleteUserDataAsync(userId, tenantId);
        
        return NoContent();
    }
    
    [HttpDelete("users/{userId}")]
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> DeleteUserData(string userId)
    {
        var adminId = User.FindFirst("sub")?.Value;
        var tenantId = User.FindFirst("tenantId")?.Value ?? "default";
        
        _logger.LogInformation(
            "Admin {AdminId} requested deletion for user {UserId}",
            adminId, userId);
        
        await _loggingService.DeleteUserDataAsync(userId, tenantId);
        
        return NoContent();
    }
}