Skip to content

Access Logs System

The Access Logging System provides centralized tracking of user actions performed within the application when accessing sensitive resources. Its main goal is to ensure accountability, traceability, and security compliance by recording all key events — such as viewing, modifying, or exporting client data.

Each action is logged with its timestamp, actor, action type, target entity, and result status (success or failure).

Overview

Key Features

  • Records every access to sensitive resources (e.g., client documents, credit card photos)
  • Stores both successful and failed access attempts
  • Provides structured logs suitable for querying and analysis
  • Supports filtering by user, action type, and time period
  • Automatic data validation before writing logs
  • Integration with Sentry for error monitoring
  • Pagination support for large datasets
  • Multi-tenant architecture support

Architecture

System Layers

  1. Resolver Layer - GraphQL resolvers that initiate log creation
  2. Service Layer - AccessLogsService for working with logs
  3. Validation Layer - data validation via Zod schemas
  4. Data Layer - Prisma ORM for database operations

Access Logs Service

Main Class

File: services/global/AccessLogsService/index.ts

export class AccessLogsService extends BaseService<
access_logs,
Prisma.access_logsWhereInput,
Prisma.access_logsCreateInput,
Prisma.access_logsOrderByWithRelationInput
>

Methods

createLog(props: AccessLogData): Promise<void>

Creates a new entry in access logs.

Parameters:

Process:

  1. Validates data via AccessLogsValidator
  2. Writes to DB via Prisma
  3. On error - logs to Sentry

getLogsById(id: string | string[], params: AccessLogsPaginationParams): Promise<PaginationResult<access_logs>>

Retrieves logs by user ID with pagination and filtering.

Parameters:

  • id - user ID or array of IDs
  • params - pagination and filtering parameters
    • take - number of records
    • after - cursor for next page
    • before - cursor for previous page
    • event - filter by event type
    • role - filter by role

Returns:

{
data: access_logs[],
startCursor: string | null,
endCursor: string | null,
hasNextPage: boolean,
hasPreviousPage: boolean
}

Event Types

AccessEventType Enum

Currently supported event types:

enum AccessEventType {
CREDIT_CARD_PHOTO_VIEWED // View credit card photo
CREDIT_CARD_PHOTO_DENIED // Denied access to credit card photo
}

Data Structures by Event Type

CreditCardPhotoData

File: services/global/AccessLogsService/types/CreditCardData.ts

interface CreditCardPhotoData extends AccessLogDataBase {
requestedByMetadata: {
userAgent: string; // Browser user agent string
platform: string; // Platform (web, mobile, etc.)
clientIp: string; // Client's IP address
clientCountry: string; // Country where request originated
clientCity: string; // City where request originated
};
resourceMetadata: {
id: string; // Unique identifier of the accessed resource
type: string; // Type of the accessed resource
owner: string; // ID of the resource owner
};
}

AccessLogDataBase (base fields)

File: services/global/AccessLogsService/types/AccessLogDataBase.ts

interface AccessLogDataBase {
event: AccessEventType; // Event type
tenantId: string; // Tenant ID
requestedById: string; // Requested by ID
role: Role; // User role
}

Data Validation

Validation Architecture

The system uses Zod for data validation at two levels:

  1. General validation - checking required fields
  2. Specific validation - checking event-type-specific fields

AccessLogsValidator

File: services/global/AccessLogsService/validation/AccessLogsValidator.ts

Method validateAccessLogData(props: AccessLogData): ValidationResult

Validation Process:

1. validateRequiredFields(props)
├─ check event, tenantId, requestedById, role
└─ return error on failure
2. validateSpecificFields(props)
├─ select schema from registry by event type
├─ validate specific fields
└─ return error on failure
3. return { success: true, error: null }

Validation Result:

type ValidationResult =
| { success: true; error: null }
| { success: false; error: GraphQLError };

Validation Schemas

Required Fields Schema

File: services/global/AccessLogsService/validation/schema/requiredFields.schema.ts

const requiredFieldsSchema = z.object({
event: z.enum(AccessEventType).describe('Event type'),
tenantId: z.string().describe('Tenant ID'),
requestedById: z.string().describe('Requested by ID'),
role: z.enum(Role).describe('Role'),
});

Credit Card Photo Data Schema

File: services/global/AccessLogsService/validation/schema/CreditCardData.schema.ts

const CreditCardPhotoDataSchema = z.object({
requestedByMetadata: z.object({
userAgent: z.string().describe('Browser user agent string'),
platform: z.string().describe('Platform information'),
clientIp: z.string().describe("Client's IP address"),
clientCountry: z.string().describe('Country name'),
clientCity: z.string().describe('City name'),
}),
resourceMetadata: z.object({
id: z.string().describe('Resource ID'),
type: z.string().min(1).describe('Resource type'),
owner: z.string().describe('Resource owner ID'),
}),
});

Validation Registry

File: services/global/AccessLogsService/validation/schema/registry.ts

Maps event types to their validation schemas:

export const validationSchemas = {
[AccessEventType.CREDIT_CARD_PHOTO_VIEWED]: CreditCardPhotoDataSchema,
[AccessEventType.CREDIT_CARD_PHOTO_DENIED]: CreditCardPhotoDataSchema,
// Add more schemas as needed
} as const;

Extending the System

Adding a New Event Type

Follow these steps to add a new event type to the logging system:

Step 1: Add Event Type to Enum

Update the Prisma schema:

File: packages/prisma-client/prisma/schema.prisma

enum AccessEventType {
CREDIT_CARD_PHOTO_VIEWED
CREDIT_CARD_PHOTO_DENIED
DOCUMENT_DOWNLOAD // New event type
}

Run migration:

Terminal window
npm run prisma:migrate:dev

Step 2: Create TypeScript Interface

File: services/global/AccessLogsService/types/DocumentDownloadData.ts

import AccessLogDataBase from './AccessLogDataBase';
interface DocumentDownloadData extends AccessLogDataBase {
requestedByMetadata: {
userAgent: string;
platform: string;
clientIp: string;
clientCountry: string;
clientCity: string;
};
resourceMetadata: {
documentId: string;
documentType: string;
documentName: string;
ownerId: string;
};
}
export default DocumentDownloadData;

Step 3: Update Union Type

File: services/global/AccessLogsService/types/index.ts

import CreditCardPhotoData from './CreditCardData';
import DocumentDownloadData from './DocumentDownloadData';
type AccessLogData =
| CreditCardPhotoData
| DocumentDownloadData; // Add new type
export default AccessLogData;

Step 4: Create Validation Schema

File: services/global/AccessLogsService/validation/schema/DocumentDownloadData.schema.ts

import { z } from 'zod';
const DocumentDownloadDataSchema = z.object({
requestedByMetadata: z.object({
userAgent: z.string().describe('Browser user agent string'),
platform: z.string().describe('Platform information'),
clientIp: z.string().describe("Client's IP address"),
clientCountry: z.string().describe('Country name'),
clientCity: z.string().describe('City name'),
}),
resourceMetadata: z.object({
documentId: z.string().describe('Document ID'),
documentType: z.string().describe('Document type'),
documentName: z.string().describe('Document name'),
ownerId: z.string().describe('Document owner ID'),
}),
});
export default DocumentDownloadDataSchema;

Step 5: Register Schema

File: services/global/AccessLogsService/validation/schema/registry.ts

import { AccessEventType } from '@power-rent/prisma-client';
import CreditCardPhotoDataSchema from './CreditCardData.schema';
import DocumentDownloadDataSchema from './DocumentDownloadData.schema';
export const validationSchemas = {
[AccessEventType.CREDIT_CARD_PHOTO_VIEWED]: CreditCardPhotoDataSchema,
[AccessEventType.CREDIT_CARD_PHOTO_DENIED]: CreditCardPhotoDataSchema,
[AccessEventType.DOCUMENT_DOWNLOAD]: DocumentDownloadDataSchema, // Add new schema
} as const;

Step 6: Use in Resolver

await accessLogsService.createLog({
event: AccessEventType.DOCUMENT_DOWNLOAD,
tenantId: companyId,
role: userRole,
requestedById: userId,
requestedByMetadata: {
userAgent: req.headers['user-agent'],
platform: 'web',
clientIp: clientIp,
clientCountry: 'Italy',
clientCity: 'Rome',
},
resourceMetadata: {
documentId: document.id,
documentType: document.type,
documentName: document.name,
ownerId: document.ownerId,
},
});