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
- Resolver Layer - GraphQL resolvers that initiate log creation
- Service Layer -
AccessLogsServicefor working with logs - Validation Layer - data validation via Zod schemas
- 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:
props- log data (see Data Types)
Process:
- Validates data via
AccessLogsValidator - Writes to DB via Prisma
- 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 IDsparams- pagination and filtering parameterstake- number of recordsafter- cursor for next pagebefore- cursor for previous pageevent- filter by event typerole- 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:
- General validation - checking required fields
- 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:
npm run prisma:migrate:devStep 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, },});