chore: add new access log module
This commit is contained in:
@@ -16,6 +16,7 @@ import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||
// import { telegrafConfig } from "./configs/telegraf.config";
|
||||
import { databaseConfigs } from "./configs/typeorm.config";
|
||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||
import { AccessLogsModule } from "./modules/access-logs/access-logs.module";
|
||||
import { AddressModule } from "./modules/address/address.module";
|
||||
import { AdsModule } from "./modules/ads/ads.module";
|
||||
import { AnnouncementsModule } from "./modules/announcements/announcement.module";
|
||||
@@ -83,6 +84,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module";
|
||||
ReferralsModule,
|
||||
DiscountsModule,
|
||||
SupportPlansModule,
|
||||
AccessLogsModule,
|
||||
],
|
||||
controllers: [],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
import { OperationType } from "../../modules/access-logs/enums/operation-type.enum";
|
||||
|
||||
export interface AccessLogOptions {
|
||||
operationType: OperationType;
|
||||
entityName: string;
|
||||
description?: string;
|
||||
logOldValues?: boolean;
|
||||
logNewValues?: boolean;
|
||||
logRequest?: boolean;
|
||||
}
|
||||
|
||||
export const ACCESS_LOG_KEY = "access_log";
|
||||
|
||||
export const AccessLog = (options: AccessLogOptions) => SetMetadata(ACCESS_LOG_KEY, options);
|
||||
@@ -123,7 +123,9 @@ export const enum CommonMessage {
|
||||
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
|
||||
UPDATED = "با موفقیت آپدیت شد",
|
||||
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت باید یکی از مقادیر ۰ و ۱ باشد",
|
||||
PAGINATE_SHOULD_BE_1_0 = "PAGINATE_SHOULD_BE_1_0",
|
||||
PAGINATE_SHOULD_BE_1_0 = "صفحه بندی باید یکی از مقادیر ۰ و ۱ باشد",
|
||||
ENTITY_NAME_REQUIRED = "نام موجودیت مورد نیاز است",
|
||||
ENTITY_NAME_STRING = "نام موجودیت باید یک رشته باشد",
|
||||
}
|
||||
|
||||
export const enum CategoryMessage {
|
||||
@@ -817,6 +819,8 @@ export const enum BlogMessage {
|
||||
PARENT_REPLY_ID_SHOULD_BE_A_UUID = "شناسه نظر والد باید یک UUID باشد",
|
||||
PARENT_REPLY_ID_REQUIRED = "شناسه نظر والد مورد نیاز است",
|
||||
REPLY_ADDED = "نظر با موفقیت ثبت شد",
|
||||
AUDIO_URL_STRING = "AUDIO_URL_STRING",
|
||||
AUDIO_URL_REQUIRED = "AUDIO_URL_REQUIRED",
|
||||
}
|
||||
|
||||
export const enum SliderMessage {
|
||||
@@ -890,3 +894,7 @@ export const enum UploaderMessage {
|
||||
UPLOAD_FILE_TOO_LARGE = "[MAX_FILE_SIZE] فایل بزرگتر از حد مجاز است",
|
||||
UPLOAD_FILE_TYPE_NOT_SUPPORTED = "[MIME_TYPES] نوع فایل مجاز نیست",
|
||||
}
|
||||
|
||||
export const enum AccessLogMessage {
|
||||
ACCESS_LOG_NOT_FOUND = "لاگ دسترسی مورد نظر یافت نشد",
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions {
|
||||
username: configService.getOrThrow<string>("DB_USER"),
|
||||
password: configService.getOrThrow<string>("DB_PASS"),
|
||||
autoLoadEntities: true,
|
||||
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
|
||||
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : false,
|
||||
logging: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
|
||||
migrationsTableName: "typeorm_migrations",
|
||||
migrationsRun: false,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ApiPropertyOptional, PickType } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsDateString, IsEnum, IsNumber, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
import { OperationType } from "../enums/operation-type.enum";
|
||||
import { UserType } from "../enums/user-type.enum";
|
||||
|
||||
export class AccessLogQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@IsEnum(OperationType)
|
||||
@ApiPropertyOptional({ enum: OperationType, description: "operation type" })
|
||||
operationType?: OperationType;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(UserType)
|
||||
@ApiPropertyOptional({ enum: UserType, description: "user type" })
|
||||
userType?: UserType;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ description: "entity name" })
|
||||
entityName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ description: "entity id" })
|
||||
entityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@ApiPropertyOptional({ description: "user id" })
|
||||
userId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ description: "endpoint" })
|
||||
endpoint?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ description: "status code" })
|
||||
statusCode?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString({}, { message: "startDate must be a valid date" })
|
||||
@ApiPropertyOptional({ description: "start date" })
|
||||
startDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString({}, { message: "endDate must be a valid date" })
|
||||
@ApiPropertyOptional({ description: "end date" })
|
||||
endDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ description: "search" })
|
||||
search?: string; // For searching in actionDescription, entityName, etc.
|
||||
}
|
||||
|
||||
export class AccessLogStatsQueryDto extends PickType(AccessLogQueryDto, ["page", "limit", "startDate", "endDate"]) {}
|
||||
|
||||
export class EntityIdQueryDto extends PickType(AccessLogQueryDto, ["entityId", "page", "limit"]) {}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { IsEnum, IsNumber, IsObject, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { OperationType } from "../enums/operation-type.enum";
|
||||
import { UserType } from "../enums/user-type.enum";
|
||||
|
||||
export class CreateAccessLogDto {
|
||||
@IsEnum(OperationType)
|
||||
operationType: OperationType;
|
||||
|
||||
@IsEnum(UserType)
|
||||
userType: UserType;
|
||||
|
||||
@IsString()
|
||||
entityName: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
entityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
actionDescription?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
oldValues?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
newValues?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ipAddress?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userAgent?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
endpoint?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
statusCode?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
user?: Pick<User, "id">;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
requestId?: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IsOptional, IsString } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
|
||||
export class EntityIdQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
entityId?: string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
import { CommonMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class EntityNameParamDto {
|
||||
@IsNotEmpty({ message: CommonMessage.ENTITY_NAME_REQUIRED })
|
||||
@IsString({ message: CommonMessage.ENTITY_NAME_STRING })
|
||||
@ApiProperty({ description: "Entity name", example: "user" })
|
||||
entityName: string;
|
||||
}
|
||||
@@ -0,0 +1,861 @@
|
||||
# Access Log Module - Frontend Implementation Guide
|
||||
|
||||
This guide provides everything a frontend engineer needs to implement the Access Log module interface.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [API Endpoints](#api-endpoints)
|
||||
2. [Data Models](#data-models)
|
||||
3. [Enums & Constants](#enums--constants)
|
||||
4. [Request/Response Examples](#requestresponse-examples)
|
||||
5. [UI Components](#ui-components)
|
||||
6. [State Management](#state-management)
|
||||
7. [Implementation Examples](#implementation-examples)
|
||||
|
||||
## 🔗 API Endpoints
|
||||
|
||||
### Base URL
|
||||
|
||||
```
|
||||
/api/access-logs
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
All endpoints require:
|
||||
|
||||
- **Authorization**: Bearer token in header
|
||||
- **Permissions**: `LOGS` permission
|
||||
- **Role**: Admin access only
|
||||
|
||||
### Endpoints Overview
|
||||
|
||||
| Method | Endpoint | Description | Query Parameters |
|
||||
| ------ | --------------------------------- | ---------------------------------- | ----------------------------------------- |
|
||||
| `GET` | `/access-logs` | Get all access logs with filtering | See [Query Parameters](#query-parameters) |
|
||||
| `GET` | `/access-logs/stats` | Get operation statistics | `startDate`, `endDate`, `page`, `limit` |
|
||||
| `GET` | `/access-logs/top-users` | Get top users by activity | `startDate`, `endDate`, `limit` |
|
||||
| `GET` | `/access-logs/errors` | Get error logs only | See [Query Parameters](#query-parameters) |
|
||||
| `GET` | `/access-logs/user/:id` | Get logs for specific user | See [Query Parameters](#query-parameters) |
|
||||
| `GET` | `/access-logs/entity/:entityName` | Get logs for specific entity | `entityId`, `page`, `limit` |
|
||||
| `GET` | `/access-logs/:id` | Get specific log by ID | None |
|
||||
|
||||
## 📊 Data Models
|
||||
|
||||
### AccessLog Entity
|
||||
|
||||
```typescript
|
||||
interface AccessLog {
|
||||
id: string;
|
||||
operationType: OperationType;
|
||||
userType: UserType;
|
||||
entityName: string;
|
||||
entityId?: string;
|
||||
actionDescription?: string;
|
||||
oldValues?: string; // JSON string
|
||||
newValues?: string; // JSON string
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
endpoint?: string;
|
||||
httpMethod?: string;
|
||||
statusCode?: number;
|
||||
errorMessage?: string;
|
||||
metadata?: Record<string, any>; // JSON object
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
requestId?: string;
|
||||
executionTime?: number; // in milliseconds
|
||||
createdAt: string; // ISO date string
|
||||
updatedAt: string; // ISO date string
|
||||
user?: {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### API Response Models
|
||||
|
||||
```typescript
|
||||
// Standard paginated response
|
||||
interface AccessLogsResponse {
|
||||
accessLogs: AccessLog[];
|
||||
count: number;
|
||||
paginate: boolean;
|
||||
}
|
||||
|
||||
// Statistics response
|
||||
interface OperationStatsResponse {
|
||||
operationStats: Array<{
|
||||
operationType: OperationType;
|
||||
userType: UserType;
|
||||
entityName: string;
|
||||
count: number;
|
||||
}>;
|
||||
count: number;
|
||||
paginate: boolean;
|
||||
}
|
||||
|
||||
// Top users response
|
||||
interface TopUsersResponse {
|
||||
topUsers: Array<{
|
||||
userId: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
activityCount: number;
|
||||
}>;
|
||||
count: number;
|
||||
paginate: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## 🏷️ Enums & Constants
|
||||
|
||||
### Operation Types
|
||||
|
||||
```typescript
|
||||
enum OperationType {
|
||||
CREATE = "CREATE",
|
||||
UPDATE = "UPDATE",
|
||||
DELETE = "DELETE",
|
||||
READ = "READ",
|
||||
LOGIN = "LOGIN",
|
||||
LOGOUT = "LOGOUT",
|
||||
APPROVE = "APPROVE",
|
||||
REJECT = "REJECT",
|
||||
ACTIVATE = "ACTIVATE",
|
||||
DEACTIVATE = "DEACTIVATE",
|
||||
EXPORT = "EXPORT",
|
||||
IMPORT = "IMPORT",
|
||||
BULK_UPDATE = "BULK_UPDATE",
|
||||
BULK_DELETE = "BULK_DELETE",
|
||||
}
|
||||
```
|
||||
|
||||
### User Types
|
||||
|
||||
```typescript
|
||||
enum UserType {
|
||||
SYSTEM = "SYSTEM",
|
||||
ADMIN = "ADMIN",
|
||||
USER = "USER",
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Methods
|
||||
|
||||
```typescript
|
||||
enum HttpMethod {
|
||||
GET = "GET",
|
||||
POST = "POST",
|
||||
PUT = "PUT",
|
||||
PATCH = "PATCH",
|
||||
DELETE = "DELETE",
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 Query Parameters
|
||||
|
||||
### AccessLogQueryDto
|
||||
|
||||
```typescript
|
||||
interface AccessLogQuery {
|
||||
// Pagination
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
|
||||
// Filters
|
||||
operationType?: OperationType;
|
||||
userType?: UserType;
|
||||
entityName?: string;
|
||||
entityId?: string;
|
||||
userId?: string;
|
||||
ipAddress?: string;
|
||||
endpoint?: string;
|
||||
httpMethod?: string;
|
||||
statusCode?: number;
|
||||
|
||||
// Date range
|
||||
startDate?: string; // ISO date string
|
||||
endDate?: string; // ISO date string
|
||||
|
||||
// Search
|
||||
search?: string; // Searches in actionDescription and entityName
|
||||
|
||||
// Performance
|
||||
minExecutionTime?: number; // milliseconds
|
||||
maxExecutionTime?: number; // milliseconds
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 Request/Response Examples
|
||||
|
||||
### 1. Get All Access Logs
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
GET /api/access-logs?page=1&limit=20&operationType=CREATE&userType=ADMIN&startDate=2024-01-01&endDate=2024-12-31
|
||||
|
||||
// Response
|
||||
{
|
||||
"accessLogs": [
|
||||
{
|
||||
"id": "uuid-123",
|
||||
"operationType": "CREATE",
|
||||
"userType": "ADMIN",
|
||||
"entityName": "User",
|
||||
"entityId": "user-uuid-456",
|
||||
"actionDescription": "Admin created user: john@example.com",
|
||||
"ipAddress": "192.168.1.1",
|
||||
"userAgent": "Mozilla/5.0...",
|
||||
"endpoint": "/api/users",
|
||||
"httpMethod": "POST",
|
||||
"statusCode": 201,
|
||||
"executionTime": 150,
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"user": {
|
||||
"id": "admin-uuid-789",
|
||||
"firstName": "Admin",
|
||||
"lastName": "User",
|
||||
"email": "admin@example.com"
|
||||
},
|
||||
"metadata": {
|
||||
"createdBy": "admin",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 150,
|
||||
"paginate": true
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Get Operation Statistics
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
GET /api/access-logs/stats?startDate=2024-01-01&endDate=2024-01-31&limit=10
|
||||
|
||||
// Response
|
||||
{
|
||||
"operationStats": [
|
||||
{
|
||||
"operationType": "CREATE",
|
||||
"userType": "ADMIN",
|
||||
"entityName": "User",
|
||||
"count": 45
|
||||
},
|
||||
{
|
||||
"operationType": "UPDATE",
|
||||
"userType": "ADMIN",
|
||||
"entityName": "Blog",
|
||||
"count": 32
|
||||
}
|
||||
],
|
||||
"count": 8,
|
||||
"paginate": true
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Get Top Users by Activity
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
GET /api/access-logs/top-users?limit=5&startDate=2024-01-01&endDate=2024-01-31
|
||||
|
||||
// Response
|
||||
{
|
||||
"topUsers": [
|
||||
{
|
||||
"userId": "admin-uuid-123",
|
||||
"firstName": "Admin",
|
||||
"lastName": "User",
|
||||
"email": "admin@example.com",
|
||||
"activityCount": 156
|
||||
}
|
||||
],
|
||||
"count": 5,
|
||||
"paginate": true
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Get Error Logs
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
GET /api/access-logs/errors?page=1&limit=10&startDate=2024-01-01
|
||||
|
||||
// Response
|
||||
{
|
||||
"errorLogs": [
|
||||
{
|
||||
"id": "error-uuid-123",
|
||||
"operationType": "CREATE",
|
||||
"entityName": "User",
|
||||
"errorMessage": "Email already exists",
|
||||
"statusCode": 400,
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"user": {
|
||||
"id": "admin-uuid-789",
|
||||
"firstName": "Admin",
|
||||
"lastName": "User"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 25,
|
||||
"paginate": true
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 UI Components
|
||||
|
||||
### 1. Access Logs Table Component
|
||||
|
||||
```typescript
|
||||
interface AccessLogsTableProps {
|
||||
logs: AccessLog[];
|
||||
loading: boolean;
|
||||
onFilterChange: (filters: AccessLogQuery) => void;
|
||||
onPageChange: (page: number) => void;
|
||||
onSort: (field: string, order: "ASC" | "DESC") => void;
|
||||
}
|
||||
|
||||
// Key columns to display:
|
||||
// - Date/Time
|
||||
// - Operation Type (with color coding)
|
||||
// - User Type (with badges)
|
||||
// - Entity Name
|
||||
// - Action Description
|
||||
// - User (who performed action)
|
||||
// - IP Address
|
||||
// - Status Code
|
||||
// - Execution Time
|
||||
// - Actions (View Details)
|
||||
```
|
||||
|
||||
### 2. Filter Panel Component
|
||||
|
||||
```typescript
|
||||
interface FilterPanelProps {
|
||||
filters: AccessLogQuery;
|
||||
onFiltersChange: (filters: AccessLogQuery) => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
// Filter options:
|
||||
// - Date Range Picker
|
||||
// - Operation Type Dropdown
|
||||
// - User Type Dropdown
|
||||
// - Entity Name Input
|
||||
// - User ID Input
|
||||
// - IP Address Input
|
||||
// - HTTP Method Dropdown
|
||||
// - Status Code Input
|
||||
// - Search Input
|
||||
// - Execution Time Range
|
||||
```
|
||||
|
||||
### 3. Statistics Dashboard Component
|
||||
|
||||
```typescript
|
||||
interface StatsDashboardProps {
|
||||
stats: OperationStatsResponse;
|
||||
topUsers: TopUsersResponse;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
// Display:
|
||||
// - Operation Type Chart (Pie/Bar)
|
||||
// - User Type Distribution
|
||||
// - Entity Activity Chart
|
||||
// - Top Users Table
|
||||
// - Time-based Activity Chart
|
||||
```
|
||||
|
||||
### 4. Log Details Modal Component
|
||||
|
||||
```typescript
|
||||
interface LogDetailsModalProps {
|
||||
log: AccessLog | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Display:
|
||||
// - Basic Information
|
||||
// - Request Details
|
||||
// - Old Values (if available)
|
||||
// - New Values (if available)
|
||||
// - Metadata
|
||||
// - Error Details (if error)
|
||||
```
|
||||
|
||||
## 🔄 State Management
|
||||
|
||||
### Redux/Zustand Store Structure
|
||||
|
||||
```typescript
|
||||
interface AccessLogsState {
|
||||
// Data
|
||||
logs: AccessLog[];
|
||||
stats: OperationStatsResponse | null;
|
||||
topUsers: TopUsersResponse | null;
|
||||
errorLogs: AccessLog[];
|
||||
currentLog: AccessLog | null;
|
||||
|
||||
// UI State
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
filters: AccessLogQuery;
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
// Actions
|
||||
fetchLogs: (filters?: AccessLogQuery) => Promise<void>;
|
||||
fetchStats: (filters?: AccessLogStatsQueryDto) => Promise<void>;
|
||||
fetchTopUsers: (filters?: AccessLogStatsQueryDto) => Promise<void>;
|
||||
fetchErrorLogs: (filters?: AccessLogQuery) => Promise<void>;
|
||||
fetchLogById: (id: string) => Promise<void>;
|
||||
setFilters: (filters: AccessLogQuery) => void;
|
||||
setPage: (page: number) => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
## 💻 Implementation Examples
|
||||
|
||||
### 1. React Hook for Access Logs
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect } from "react";
|
||||
import { AccessLog, AccessLogQuery } from "./types";
|
||||
|
||||
export const useAccessLogs = () => {
|
||||
const [logs, setLogs] = useState<AccessLog[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filters, setFilters] = useState<AccessLogQuery>({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
const fetchLogs = async (newFilters?: AccessLogQuery) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
const currentFilters = newFilters || filters;
|
||||
|
||||
Object.entries(currentFilters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
queryParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/access-logs?${queryParams}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch access logs");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setLogs(data.accessLogs);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [filters]);
|
||||
|
||||
return {
|
||||
logs,
|
||||
loading,
|
||||
error,
|
||||
filters,
|
||||
setFilters,
|
||||
fetchLogs,
|
||||
refetch: () => fetchLogs(),
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 2. API Service Class
|
||||
|
||||
```typescript
|
||||
class AccessLogsService {
|
||||
private baseUrl = "/api/access-logs";
|
||||
private token = localStorage.getItem("token");
|
||||
|
||||
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getLogs(filters: AccessLogQuery): Promise<AccessLogsResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return this.request<AccessLogsResponse>(`?${params}`);
|
||||
}
|
||||
|
||||
async getStats(filters: AccessLogStatsQueryDto): Promise<OperationStatsResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return this.request<OperationStatsResponse>(`/stats?${params}`);
|
||||
}
|
||||
|
||||
async getTopUsers(filters: AccessLogStatsQueryDto): Promise<TopUsersResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return this.request<TopUsersResponse>(`/top-users?${params}`);
|
||||
}
|
||||
|
||||
async getErrorLogs(filters: AccessLogQuery): Promise<AccessLogsResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return this.request<AccessLogsResponse>(`/errors?${params}`);
|
||||
}
|
||||
|
||||
async getLogById(id: string): Promise<{ accessLog: AccessLog }> {
|
||||
return this.request<{ accessLog: AccessLog }>(`/${id}`);
|
||||
}
|
||||
|
||||
async getUserLogs(userId: string, filters: AccessLogQuery): Promise<AccessLogsResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return this.request<AccessLogsResponse>(`/user/${userId}?${params}`);
|
||||
}
|
||||
|
||||
async getEntityLogs(entityName: string, filters: EntityIdQueryDto): Promise<AccessLogsResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return this.request<AccessLogsResponse>(`/entity/${entityName}?${params}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const accessLogsService = new AccessLogsService();
|
||||
```
|
||||
|
||||
### 3. React Component Example
|
||||
|
||||
```typescript
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AccessLog, OperationType, UserType } from './types';
|
||||
import { useAccessLogs } from './hooks/useAccessLogs';
|
||||
|
||||
const AccessLogsPage: React.FC = () => {
|
||||
const { logs, loading, error, filters, setFilters, fetchLogs } = useAccessLogs();
|
||||
const [selectedLog, setSelectedLog] = useState<AccessLog | null>(null);
|
||||
|
||||
const handleFilterChange = (newFilters: Partial<AccessLogQuery>) => {
|
||||
setFilters({ ...filters, ...newFilters, page: 1 });
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setFilters({ ...filters, page });
|
||||
};
|
||||
|
||||
const getOperationTypeColor = (type: OperationType): string => {
|
||||
const colors = {
|
||||
CREATE: 'green',
|
||||
UPDATE: 'blue',
|
||||
DELETE: 'red',
|
||||
READ: 'gray',
|
||||
LOGIN: 'purple',
|
||||
LOGOUT: 'orange',
|
||||
APPROVE: 'green',
|
||||
REJECT: 'red',
|
||||
ACTIVATE: 'green',
|
||||
DEACTIVATE: 'red',
|
||||
EXPORT: 'blue',
|
||||
IMPORT: 'blue',
|
||||
BULK_UPDATE: 'blue',
|
||||
BULK_DELETE: 'red'
|
||||
};
|
||||
return colors[type] || 'gray';
|
||||
};
|
||||
|
||||
const getUserTypeBadge = (type: UserType): string => {
|
||||
const badges = {
|
||||
SYSTEM: 'bg-gray-100 text-gray-800',
|
||||
ADMIN: 'bg-red-100 text-red-800',
|
||||
USER: 'bg-blue-100 text-blue-800'
|
||||
};
|
||||
return badges[type] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error}</div>;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">Access Logs</h1>
|
||||
|
||||
{/* Filter Panel */}
|
||||
<div className="bg-white p-4 rounded-lg shadow mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Operation Type</label>
|
||||
<select
|
||||
value={filters.operationType || ''}
|
||||
onChange={(e) => handleFilterChange({ operationType: e.target.value as OperationType })}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="">All</option>
|
||||
{Object.values(OperationType).map(type => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">User Type</label>
|
||||
<select
|
||||
value={filters.userType || ''}
|
||||
onChange={(e) => handleFilterChange({ userType: e.target.value as UserType })}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="">All</option>
|
||||
{Object.values(UserType).map(type => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Entity Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={filters.entityName || ''}
|
||||
onChange={(e) => handleFilterChange({ entityName: e.target.value })}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="e.g., User, Blog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Search</label>
|
||||
<input
|
||||
type="text"
|
||||
value={filters.search || ''}
|
||||
onChange={(e) => handleFilterChange({ search: e.target.value })}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Search in descriptions..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logs Table */}
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date/Time
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Operation
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
User Type
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Entity
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
User
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{logs.map((log) => (
|
||||
<tr key={log.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(log.createdAt).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full text-white bg-${getOperationTypeColor(log.operationType)}-500`}
|
||||
>
|
||||
{log.operationType}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getUserTypeBadge(log.userType)}`}>
|
||||
{log.userType}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{log.entityName}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-900 max-w-xs truncate">
|
||||
{log.actionDescription}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{log.user ? `${log.user.firstName} ${log.user.lastName}` : 'System'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
log.statusCode && log.statusCode >= 200 && log.statusCode < 300
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{log.statusCode || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => setSelectedLog(log)}
|
||||
className="text-indigo-600 hover:text-indigo-900"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Log Details Modal */}
|
||||
{selectedLog && (
|
||||
<LogDetailsModal
|
||||
log={selectedLog}
|
||||
isOpen={!!selectedLog}
|
||||
onClose={() => setSelectedLog(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessLogsPage;
|
||||
```
|
||||
|
||||
## 🎯 Key Features to Implement
|
||||
|
||||
### 1. **Advanced Filtering**
|
||||
|
||||
- Date range picker
|
||||
- Multi-select dropdowns
|
||||
- Real-time search
|
||||
- Filter presets
|
||||
- Export filtered results
|
||||
|
||||
### 2. **Data Visualization**
|
||||
|
||||
- Operation type distribution chart
|
||||
- User activity timeline
|
||||
- Error rate trends
|
||||
- Performance metrics
|
||||
|
||||
### 3. **Real-time Updates**
|
||||
|
||||
- WebSocket connection for live updates
|
||||
- Auto-refresh functionality
|
||||
- Real-time notifications for critical operations
|
||||
|
||||
### 4. **Export Functionality**
|
||||
|
||||
- CSV export
|
||||
- PDF reports
|
||||
- Scheduled reports
|
||||
- Custom date ranges
|
||||
|
||||
### 5. **Security Features**
|
||||
|
||||
- IP address masking for privacy
|
||||
- Sensitive data filtering
|
||||
- Access control based on user roles
|
||||
- Audit trail for log access
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
1. **Set up API client** with authentication
|
||||
2. **Create TypeScript interfaces** for all data models
|
||||
3. **Implement basic table component** with pagination
|
||||
4. **Add filtering capabilities**
|
||||
5. **Create detailed view modal**
|
||||
6. **Add statistics dashboard**
|
||||
7. **Implement real-time updates**
|
||||
8. **Add export functionality**
|
||||
|
||||
## 📱 Mobile Considerations
|
||||
|
||||
- Responsive table design
|
||||
- Collapsible filter panel
|
||||
- Touch-friendly interactions
|
||||
- Optimized for smaller screens
|
||||
- Swipe gestures for navigation
|
||||
|
||||
This guide provides everything needed to implement a comprehensive access logs interface. The modular approach allows for incremental implementation and easy maintenance.
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Controller, Get, Param, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||
|
||||
import { AccessLogQueryDto, AccessLogStatsQueryDto, EntityIdQueryDto } from "./DTO/access-log-query.dto";
|
||||
import { EntityNameParamDto } from "./DTO/entity-name-param.dto";
|
||||
import { AccessLogService } from "./providers/access-log.service";
|
||||
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { PermissionsDec } from "../../common/decorators/permission.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { PermissionEnum } from "../users/enums/permission.enum";
|
||||
|
||||
@AuthGuards()
|
||||
@Controller("access-logs")
|
||||
export class AccessLogsController {
|
||||
constructor(private readonly accessLogService: AccessLogService) {}
|
||||
|
||||
@Get()
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.LOGS)
|
||||
@ApiOperation({ summary: "Get access logs with filtering and pagination" })
|
||||
@ApiResponse({ status: 200, description: "Access logs retrieved successfully" })
|
||||
getLogs(@Query() queryDto: AccessLogQueryDto) {
|
||||
return this.accessLogService.getLogs(queryDto);
|
||||
}
|
||||
|
||||
@Get("stats")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.LOGS)
|
||||
@ApiOperation({ summary: "Get operation statistics" })
|
||||
@ApiResponse({ status: 200, description: "Operation statistics retrieved successfully" })
|
||||
getOperationStats(@Query() queryDto: AccessLogStatsQueryDto) {
|
||||
return this.accessLogService.getOperationStats(queryDto);
|
||||
}
|
||||
|
||||
@Get("top-users")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.LOGS)
|
||||
@ApiOperation({ summary: "Get top users by activity" })
|
||||
@ApiResponse({ status: 200, description: "Top users retrieved successfully" })
|
||||
getTopUsersByActivity(@Query() queryDto: AccessLogStatsQueryDto) {
|
||||
return this.accessLogService.getTopUsersByActivity(queryDto);
|
||||
}
|
||||
|
||||
@Get("errors")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.LOGS)
|
||||
@ApiOperation({ summary: "Get error logs" })
|
||||
@ApiResponse({ status: 200, description: "Error logs retrieved successfully" })
|
||||
getErrorLogs(@Query() queryDto: AccessLogQueryDto) {
|
||||
return this.accessLogService.getErrorLogs(queryDto);
|
||||
}
|
||||
|
||||
@Get("user/:id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.LOGS)
|
||||
@ApiOperation({ summary: "Get logs for a specific user" })
|
||||
@ApiResponse({ status: 200, description: "User logs retrieved successfully" })
|
||||
getUserLogs(@Param() paramDto: ParamDto, @Query() queryDto: AccessLogQueryDto) {
|
||||
return this.accessLogService.getLogsByUser(paramDto.id, queryDto);
|
||||
}
|
||||
|
||||
@Get("entity/:entityName")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.LOGS)
|
||||
@ApiOperation({ summary: "Get logs for a specific entity" })
|
||||
@ApiResponse({ status: 200, description: "Entity logs retrieved successfully" })
|
||||
getEntityLogs(@Param() paramDto: EntityNameParamDto, @Query() queryDto: EntityIdQueryDto) {
|
||||
return this.accessLogService.getLogsByEntity(paramDto.entityName, queryDto);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.LOGS)
|
||||
@ApiOperation({ summary: "Get a specific access log by ID" })
|
||||
@ApiResponse({ status: 200, description: "Access log retrieved successfully" })
|
||||
@ApiResponse({ status: 404, description: "Access log not found" })
|
||||
getLogById(@Param() paramDto: ParamDto) {
|
||||
return this.accessLogService.getLogById(paramDto.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { AccessLogsController } from "./access-logs.controller";
|
||||
import { AccessLog } from "./entities/access-log.entity";
|
||||
import { AccessLogService } from "./providers/access-log.service";
|
||||
import { AccessLogRepository } from "./repositories/access-log.repository";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AccessLog])],
|
||||
controllers: [AccessLogsController],
|
||||
providers: [AccessLogService, AccessLogRepository],
|
||||
exports: [AccessLogService, AccessLogRepository],
|
||||
})
|
||||
export class AccessLogsModule {}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Column, Entity, ManyToOne } from "typeorm";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { OperationType } from "../enums/operation-type.enum";
|
||||
import { UserType } from "../enums/user-type.enum";
|
||||
|
||||
@Entity("access_logs")
|
||||
export class AccessLog extends BaseEntity {
|
||||
@Column({ type: "enum", enum: OperationType })
|
||||
operationType: OperationType;
|
||||
|
||||
@Column({ type: "enum", enum: UserType })
|
||||
userType: UserType;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
entityName: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: true })
|
||||
entityId: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: true })
|
||||
actionDescription: string;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
oldValues: string;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
newValues: string;
|
||||
|
||||
@Column({ type: "varchar", length: 45, nullable: true })
|
||||
ipAddress: string;
|
||||
|
||||
@Column({ type: "varchar", length: 500, nullable: true })
|
||||
userAgent: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: true })
|
||||
endpoint: string;
|
||||
|
||||
@Column({ type: "integer", nullable: true })
|
||||
statusCode: number;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
errorMessage: string;
|
||||
|
||||
@Column({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
@ManyToOne(() => User, { nullable: true })
|
||||
user: User;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: true })
|
||||
requestId: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export enum OperationType {
|
||||
CREATE = "CREATE",
|
||||
UPDATE = "UPDATE",
|
||||
DELETE = "DELETE",
|
||||
READ = "READ",
|
||||
LOGIN = "LOGIN",
|
||||
LOGOUT = "LOGOUT",
|
||||
APPROVE = "APPROVE",
|
||||
REJECT = "REJECT",
|
||||
ACTIVATE = "ACTIVATE",
|
||||
DEACTIVATE = "DEACTIVATE",
|
||||
EXPORT = "EXPORT",
|
||||
IMPORT = "IMPORT",
|
||||
BULK_UPDATE = "BULK_UPDATE",
|
||||
BULK_DELETE = "BULK_DELETE",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum UserType {
|
||||
SYSTEM = "SYSTEM",
|
||||
ADMIN = "ADMIN",
|
||||
USER = "USER",
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { OperationType } from "../enums/operation-type.enum";
|
||||
import { UserType } from "../enums/user-type.enum";
|
||||
|
||||
export interface ILogContext {
|
||||
operationType: OperationType;
|
||||
entityName: string;
|
||||
entityId?: string;
|
||||
actionDescription?: string;
|
||||
oldValues?: any;
|
||||
newValues?: any;
|
||||
user?: Pick<User, "id">;
|
||||
userType?: UserType;
|
||||
metadata?: Record<string, any>;
|
||||
request?: FastifyRequest;
|
||||
statusCode?: number;
|
||||
errorMessage?: string;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// import { Injectable } from "@nestjs/common";
|
||||
|
||||
// import { AccessLogService } from "./access-log.service";
|
||||
// import { OperationType } from "../enums/operation-type.enum";
|
||||
// import { UserType } from "../enums/user-type.enum";
|
||||
|
||||
// /**
|
||||
// * Example service demonstrating how to use the AccessLogService
|
||||
// * This service shows various ways to log operations in your application
|
||||
// */
|
||||
// @Injectable()
|
||||
// export class AccessLogExampleService {
|
||||
// constructor(private readonly accessLogService: AccessLogService) {}
|
||||
|
||||
// /**
|
||||
// * Example: Log a user creation
|
||||
// */
|
||||
// async logUserCreation(userId: string, userData: any, adminId: string) {
|
||||
// return await this.accessLogService.logCreate("User", userId, userData, {
|
||||
// userId: adminId,
|
||||
// userType: UserType.ADMIN,
|
||||
// actionDescription: `Admin created new user: ${userData.email}`,
|
||||
// metadata: { createdBy: adminId, userEmail: userData.email },
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Example: Log a user update
|
||||
// */
|
||||
// async logUserUpdate(userId: string, oldData: any, newData: any, adminId: string) {
|
||||
// return await this.accessLogService.logUpdate("User", userId, oldData, newData, {
|
||||
// userId: adminId,
|
||||
// userType: UserType.ADMIN,
|
||||
// actionDescription: `Admin updated user: ${newData.email}`,
|
||||
// metadata: { updatedBy: adminId, userEmail: newData.email },
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Example: Log a user deletion
|
||||
// */
|
||||
// async logUserDeletion(userId: string, userData: any, adminId: string) {
|
||||
// return await this.accessLogService.logDelete("User", userId, userData, {
|
||||
// userId: adminId,
|
||||
// userType: UserType.ADMIN,
|
||||
// actionDescription: `Admin deleted user: ${userData.email}`,
|
||||
// metadata: { deletedBy: adminId, userEmail: userData.email },
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Example: Log a blog post creation
|
||||
// */
|
||||
// async logBlogCreation(blogId: string, blogData: any, authorId: string) {
|
||||
// return await this.accessLogService.logCreate("Blog", blogId, blogData, {
|
||||
// userId: authorId,
|
||||
// userType: UserType.ADMIN,
|
||||
// actionDescription: `Created blog post: ${blogData.title}`,
|
||||
// metadata: { authorId, blogTitle: blogData.title },
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Example: Log a payment processing
|
||||
// */
|
||||
// async logPaymentProcessing(paymentId: string, paymentData: any, systemContext: any) {
|
||||
// return await this.accessLogService.logSystemAction(
|
||||
// OperationType.UPDATE,
|
||||
// "Payment",
|
||||
// paymentId,
|
||||
// `Payment processed: ${paymentData.amount} ${paymentData.currency}`,
|
||||
// {
|
||||
// userType: UserType.SYSTEM,
|
||||
// metadata: {
|
||||
// amount: paymentData.amount,
|
||||
// currency: paymentData.currency,
|
||||
// gateway: paymentData.gateway,
|
||||
// status: paymentData.status,
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Example: Log a user login
|
||||
// */
|
||||
// async logUserLogin(userId: string, loginData: any, request: any) {
|
||||
// return await this.accessLogService.logLogin(userId, {
|
||||
// request,
|
||||
// metadata: {
|
||||
// loginMethod: loginData.method,
|
||||
// userAgent: request?.get?.("User-Agent"),
|
||||
// ipAddress: request?.ip,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Example: Log a bulk operation
|
||||
// */
|
||||
// async logBulkUserUpdate(userIds: string[], updateData: any, adminId: string) {
|
||||
// return await this.accessLogService.logAdminAction(
|
||||
// OperationType.BULK_UPDATE,
|
||||
// "User",
|
||||
// userIds.join(","),
|
||||
// `Bulk updated ${userIds.length} users`,
|
||||
// {
|
||||
// userId: adminId,
|
||||
// userType: UserType.ADMIN,
|
||||
// metadata: {
|
||||
// updatedBy: adminId,
|
||||
// affectedUsers: userIds.length,
|
||||
// updateFields: Object.keys(updateData),
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Example: Log an error
|
||||
// */
|
||||
// async logError(error: Error, context: any, userId?: string) {
|
||||
// return await this.accessLogService.logError(OperationType.UPDATE, context.entityName || "Unknown", error.message, {
|
||||
// userId,
|
||||
// userType: userId ? UserType.ADMIN : UserType.SYSTEM,
|
||||
// metadata: {
|
||||
// errorStack: error.stack,
|
||||
// context,
|
||||
// timestamp: new Date().toISOString(),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,240 @@
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { AccessLogMessage } from "../../../common/enums/message.enum";
|
||||
import { AccessLogQueryDto, AccessLogStatsQueryDto, EntityIdQueryDto } from "../DTO/access-log-query.dto";
|
||||
import { CreateAccessLogDto } from "../DTO/create-access-log.dto";
|
||||
import { OperationType } from "../enums/operation-type.enum";
|
||||
import { UserType } from "../enums/user-type.enum";
|
||||
import { ILogContext } from "../interfaces/log-context.interface";
|
||||
import { AccessLogRepository } from "../repositories/access-log.repository";
|
||||
|
||||
@Injectable()
|
||||
export class AccessLogService {
|
||||
constructor(private readonly accessLogRepository: AccessLogRepository) {}
|
||||
|
||||
async logOperation(context: ILogContext) {
|
||||
const logData: CreateAccessLogDto = {
|
||||
operationType: context.operationType,
|
||||
userType: context.userType || UserType.ADMIN,
|
||||
entityName: context.entityName,
|
||||
entityId: context.entityId,
|
||||
actionDescription: context.actionDescription,
|
||||
oldValues: context.oldValues ? JSON.stringify(context.oldValues) : undefined,
|
||||
newValues: context.newValues ? JSON.stringify(context.newValues) : undefined,
|
||||
user: context.user,
|
||||
metadata: context.metadata,
|
||||
statusCode: context.statusCode,
|
||||
errorMessage: context.errorMessage,
|
||||
};
|
||||
|
||||
// Extract request information if available
|
||||
if (context.request) {
|
||||
logData.ipAddress = this.getClientIp(context.request);
|
||||
logData.userAgent = context.request.headers["user-agent"];
|
||||
logData.endpoint = context.request.url;
|
||||
logData.requestId = context.request.headers["x-request-id"] as string;
|
||||
}
|
||||
|
||||
const accessLog = this.accessLogRepository.create({ ...logData });
|
||||
await this.accessLogRepository.save(accessLog);
|
||||
|
||||
return { accessLog };
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async logCreate(entityName: string, entityId: string, newValues: Record<string, any>, context: Partial<ILogContext> = {}) {
|
||||
return this.logOperation({
|
||||
operationType: OperationType.CREATE,
|
||||
entityName,
|
||||
entityId,
|
||||
newValues,
|
||||
actionDescription: `Created ${entityName}`,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async logUpdate(
|
||||
entityName: string,
|
||||
entityId: string,
|
||||
oldValues: Record<string, any>,
|
||||
newValues: Record<string, any>,
|
||||
context: Partial<ILogContext> = {},
|
||||
) {
|
||||
return this.logOperation({
|
||||
operationType: OperationType.UPDATE,
|
||||
entityName,
|
||||
entityId,
|
||||
oldValues,
|
||||
newValues,
|
||||
actionDescription: `Updated ${entityName}`,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
//########################################################################################
|
||||
async logDelete(entityName: string, entityId: string, oldValues: Record<string, any>, context: Partial<ILogContext> = {}) {
|
||||
return this.logOperation({
|
||||
operationType: OperationType.DELETE,
|
||||
entityName,
|
||||
entityId,
|
||||
oldValues,
|
||||
actionDescription: `Deleted ${entityName}`,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async logRead(entityName: string, entityId?: string, context: Partial<ILogContext> = {}) {
|
||||
return this.logOperation({
|
||||
operationType: OperationType.READ,
|
||||
entityName,
|
||||
entityId,
|
||||
actionDescription: `Read ${entityName}`,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async logLogin(userId: string, context: Partial<ILogContext> = {}) {
|
||||
return await this.logOperation({
|
||||
operationType: OperationType.LOGIN,
|
||||
entityName: "User",
|
||||
entityId: userId,
|
||||
user: { id: userId },
|
||||
userType: UserType.USER,
|
||||
actionDescription: "User login",
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async logLogout(userId: string, context: Partial<ILogContext> = {}) {
|
||||
return await this.logOperation({
|
||||
operationType: OperationType.LOGOUT,
|
||||
entityName: "User",
|
||||
entityId: userId,
|
||||
user: { id: userId },
|
||||
userType: UserType.USER,
|
||||
actionDescription: "User logout",
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async logAdminAction(
|
||||
operationType: OperationType,
|
||||
entityName: string,
|
||||
entityId: string,
|
||||
actionDescription: string,
|
||||
context: Partial<ILogContext> = {},
|
||||
) {
|
||||
return await this.logOperation({
|
||||
operationType,
|
||||
entityName,
|
||||
entityId,
|
||||
actionDescription,
|
||||
userType: UserType.ADMIN,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
|
||||
async logUserAction(
|
||||
operationType: OperationType,
|
||||
entityName: string,
|
||||
entityId: string,
|
||||
actionDescription: string,
|
||||
context: Partial<ILogContext> = {},
|
||||
) {
|
||||
return this.logOperation({
|
||||
operationType,
|
||||
entityName,
|
||||
entityId,
|
||||
actionDescription,
|
||||
userType: UserType.USER,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async logSystemAction(
|
||||
operationType: OperationType,
|
||||
entityName: string,
|
||||
entityId: string,
|
||||
actionDescription: string,
|
||||
context: Partial<ILogContext> = {},
|
||||
) {
|
||||
return this.logOperation({
|
||||
operationType,
|
||||
entityName,
|
||||
entityId,
|
||||
actionDescription,
|
||||
userType: UserType.SYSTEM,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async logError(operationType: OperationType, entityName: string, errorMessage: string, context: Partial<ILogContext> = {}) {
|
||||
return this.logOperation({
|
||||
operationType,
|
||||
entityName,
|
||||
errorMessage,
|
||||
actionDescription: `Error in ${operationType} operation on ${entityName}`,
|
||||
statusCode: 500,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
//########################################################################################
|
||||
async getLogs(queryDto: AccessLogQueryDto) {
|
||||
const [accessLogs, count] = await this.accessLogRepository.findAll(queryDto);
|
||||
return { accessLogs, count, paginate: true };
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async getLogsByUser(userId: string, queryDto: AccessLogQueryDto) {
|
||||
const [accessLogs, count] = await this.accessLogRepository.findByUser(userId, queryDto);
|
||||
return { accessLogs, count, paginate: true };
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async getLogsByEntity(entityName: string, queryDto: EntityIdQueryDto) {
|
||||
const [accessLogs, count] = await this.accessLogRepository.findByEntity(entityName, queryDto);
|
||||
return { accessLogs, count, paginate: true };
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async getOperationStats(queryDto: AccessLogStatsQueryDto) {
|
||||
const [operationStats, count] = await this.accessLogRepository.getOperationStats(queryDto);
|
||||
return { operationStats, count, paginate: true };
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async getTopUsersByActivity(queryDto: AccessLogStatsQueryDto) {
|
||||
const [topUsers, count] = await this.accessLogRepository.getTopUsersByActivity(queryDto);
|
||||
return { topUsers, count, paginate: true };
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async getErrorLogs(queryDto: AccessLogQueryDto) {
|
||||
const [errorLogs, count] = await this.accessLogRepository.getErrorLogs(queryDto);
|
||||
|
||||
return { errorLogs, count, paginate: true };
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async getLogById(id: string) {
|
||||
const accessLog = await this.accessLogRepository.findById(id);
|
||||
if (!accessLog) throw new BadRequestException(AccessLogMessage.ACCESS_LOG_NOT_FOUND);
|
||||
return { accessLog };
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
private getClientIp(request: FastifyRequest): string {
|
||||
return (
|
||||
(request.headers["x-forwarded-for"] as string)?.split(",")[0] || (request.headers["x-real-ip"] as string) || request.ip || "unknown"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository, SelectQueryBuilder } from "typeorm";
|
||||
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { AccessLogQueryDto, AccessLogStatsQueryDto, EntityIdQueryDto } from "../DTO/access-log-query.dto";
|
||||
import { AccessLog } from "../entities/access-log.entity";
|
||||
|
||||
@Injectable()
|
||||
export class AccessLogRepository extends Repository<AccessLog> {
|
||||
//
|
||||
constructor(@InjectRepository(AccessLog) repository: Repository<AccessLog>) {
|
||||
super(repository.target, repository.manager, repository.queryRunner);
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async findById(id: string): Promise<AccessLog | null> {
|
||||
return await this.findOne({ where: { id }, relations: ["user"] });
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async findAll(queryDto: AccessLogQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.createQueryBuilder("accessLog");
|
||||
queryBuilder.leftJoinAndSelect("accessLog.user", "user").addSelect(["user.id", "user.firstName", "user.lastName", "user.email"]);
|
||||
|
||||
this.applyFilters(queryBuilder, queryDto);
|
||||
|
||||
queryBuilder.take(limit).skip(skip).orderBy("accessLog.createdAt", "DESC");
|
||||
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async findByUser(userId: string, queryDto: AccessLogQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.createQueryBuilder("accessLog");
|
||||
queryBuilder.where("accessLog.userId = :userId", { userId });
|
||||
|
||||
this.applyFilters(queryBuilder, queryDto);
|
||||
queryBuilder.take(limit).skip(skip).orderBy("accessLog.createdAt", "DESC");
|
||||
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async findByEntity(entityName: string, queryDto: EntityIdQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.createQueryBuilder("accessLog");
|
||||
queryBuilder.where("accessLog.entityName = :entityName", { entityName });
|
||||
|
||||
if (queryDto.entityId) {
|
||||
queryBuilder.andWhere("accessLog.entityId = :entityId", { entityId: queryDto.entityId });
|
||||
}
|
||||
|
||||
queryBuilder.orderBy("accessLog.createdAt", "DESC").take(limit).skip(skip);
|
||||
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
//########################################################################################
|
||||
async getOperationStats(queryDto: AccessLogStatsQueryDto) {
|
||||
const startDate = queryDto.startDate ? new Date(queryDto.startDate) : undefined;
|
||||
const endDate = queryDto.endDate ? new Date(queryDto.endDate) : undefined;
|
||||
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.createQueryBuilder("accessLog");
|
||||
queryBuilder
|
||||
.select(["accessLog.operationType", "accessLog.userType", "accessLog.entityName", "COUNT(*) as count"])
|
||||
.groupBy("accessLog.operationType, accessLog.userType, accessLog.entityName");
|
||||
|
||||
if (startDate) {
|
||||
queryBuilder.andWhere("accessLog.createdAt >= :startDate", { startDate });
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
queryBuilder.andWhere("accessLog.createdAt <= :endDate", { endDate });
|
||||
}
|
||||
|
||||
queryBuilder.orderBy("count", "DESC");
|
||||
queryBuilder.take(limit).skip(skip);
|
||||
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
//########################################################################################
|
||||
async getTopUsersByActivity(queryDto: AccessLogStatsQueryDto) {
|
||||
const startDate = queryDto.startDate ? new Date(queryDto.startDate) : undefined;
|
||||
const endDate = queryDto.endDate ? new Date(queryDto.endDate) : undefined;
|
||||
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.createQueryBuilder("accessLog");
|
||||
queryBuilder
|
||||
.select(["accessLog.userId", "user.firstName", "user.lastName", "user.email", "COUNT(*) as activityCount"])
|
||||
.leftJoin("accessLog.user", "user")
|
||||
.where("accessLog.userId IS NOT NULL")
|
||||
.groupBy("accessLog.userId, user.firstName, user.lastName, user.email");
|
||||
|
||||
if (startDate) {
|
||||
queryBuilder.andWhere("accessLog.createdAt >= :startDate", { startDate });
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
queryBuilder.andWhere("accessLog.createdAt <= :endDate", { endDate });
|
||||
}
|
||||
|
||||
queryBuilder.orderBy("activityCount", "DESC").take(limit).skip(skip);
|
||||
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async getErrorLogs(queryDto: AccessLogQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.createQueryBuilder("accessLog");
|
||||
queryBuilder.where("accessLog.errorMessage IS NOT NULL");
|
||||
|
||||
this.applyFilters(queryBuilder, queryDto);
|
||||
|
||||
queryBuilder.orderBy("accessLog.createdAt", "DESC").take(limit).skip(skip);
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
async deleteOldLogs(olderThanDays: number): Promise<number> {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
|
||||
|
||||
const result = await this.createQueryBuilder().delete().where("createdAt < :cutoffDate", { cutoffDate }).execute();
|
||||
|
||||
return result.affected || 0;
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
private applyFilters(queryBuilder: SelectQueryBuilder<AccessLog>, queryDto: AccessLogQueryDto): void {
|
||||
if (queryDto.operationType) {
|
||||
queryBuilder.andWhere("accessLog.operationType = :operationType", {
|
||||
operationType: queryDto.operationType,
|
||||
});
|
||||
}
|
||||
|
||||
if (queryDto.userType) {
|
||||
queryBuilder.andWhere("accessLog.userType = :userType", { userType: queryDto.userType });
|
||||
}
|
||||
|
||||
if (queryDto.entityName) {
|
||||
queryBuilder.andWhere("accessLog.entityName ILIKE :entityName", {
|
||||
entityName: `%${queryDto.entityName}%`,
|
||||
});
|
||||
}
|
||||
|
||||
if (queryDto.entityId) {
|
||||
queryBuilder.andWhere("accessLog.entityId = :entityId", { entityId: queryDto.entityId });
|
||||
}
|
||||
|
||||
if (queryDto.userId) {
|
||||
queryBuilder.andWhere("accessLog.userId = :userId", { userId: queryDto.userId });
|
||||
}
|
||||
|
||||
if (queryDto.endpoint) {
|
||||
queryBuilder.andWhere("accessLog.endpoint ILIKE :endpoint", {
|
||||
endpoint: `%${queryDto.endpoint}%`,
|
||||
});
|
||||
}
|
||||
|
||||
if (queryDto.statusCode) {
|
||||
queryBuilder.andWhere("accessLog.statusCode = :statusCode", { statusCode: queryDto.statusCode });
|
||||
}
|
||||
|
||||
if (queryDto.startDate) {
|
||||
queryBuilder.andWhere("accessLog.createdAt >= :startDate", { startDate: queryDto.startDate });
|
||||
}
|
||||
|
||||
if (queryDto.endDate) {
|
||||
queryBuilder.andWhere("accessLog.createdAt <= :endDate", { endDate: queryDto.endDate });
|
||||
}
|
||||
|
||||
if (queryDto.search) {
|
||||
queryBuilder.andWhere("(accessLog.actionDescription ILIKE :search OR accessLog.entityName ILIKE :search)", {
|
||||
search: `%${queryDto.search}%`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//########################################################################################
|
||||
}
|
||||
@@ -19,22 +19,22 @@ export class AnnouncementController {
|
||||
@PermissionsDec(PermissionEnum.ANNOUNCEMENTS)
|
||||
@ApiProperty({ description: "Create a new announcement (admin route)" })
|
||||
@Post()
|
||||
create(@Body() createAnnouncementDto: CreateAnnouncementDto) {
|
||||
return this.announcementService.createAnnouncement(createAnnouncementDto);
|
||||
create(@Body() createAnnouncementDto: CreateAnnouncementDto, @UserDec("id") userId: string) {
|
||||
return this.announcementService.createAnnouncement(createAnnouncementDto, userId);
|
||||
}
|
||||
|
||||
@PermissionsDec(PermissionEnum.ANNOUNCEMENTS)
|
||||
@ApiProperty({ description: "Update an announcement (admin route)" })
|
||||
@Patch(":id")
|
||||
update(@Param() paramDto: ParamDto, @Body() updateAnnouncementDto: UpdateAnnouncementDto) {
|
||||
return this.announcementService.updateAnnouncement(paramDto.id, updateAnnouncementDto);
|
||||
update(@Param() paramDto: ParamDto, @Body() updateAnnouncementDto: UpdateAnnouncementDto, @UserDec("id") userId: string) {
|
||||
return this.announcementService.updateAnnouncement(paramDto.id, updateAnnouncementDto, userId);
|
||||
}
|
||||
|
||||
@PermissionsDec(PermissionEnum.ANNOUNCEMENTS)
|
||||
@ApiProperty({ description: "Delete an announcement (admin route)" })
|
||||
@Delete(":id")
|
||||
delete(@Param() paramDto: ParamDto) {
|
||||
return this.announcementService.deleteAnnouncement(paramDto.id);
|
||||
delete(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.announcementService.deleteAnnouncement(paramDto.id, userId);
|
||||
}
|
||||
|
||||
@ApiProperty({ description: "Get all announcements (admin route)" })
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DanakServicesModule } from "../danak-services/danak-services.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { UserAnnouncement } from "./entities/user-announcement.entity";
|
||||
import { UserAnnouncementRepository } from "./repositories/user-announcement.repository";
|
||||
import { AccessLogsModule } from "../access-logs/access-logs.module";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -20,6 +21,7 @@ import { NotificationModule } from "../notifications/notifications.module";
|
||||
DanakServicesModule,
|
||||
UsersModule,
|
||||
NotificationModule,
|
||||
AccessLogsModule,
|
||||
],
|
||||
providers: [AnnouncementService, AnnouncementRepository, UserAnnouncementRepository, AnnouncementProcessor],
|
||||
controllers: [AnnouncementController],
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Queue } from "bullmq";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { AnnouncementMessage } from "../../../common/enums/message.enum";
|
||||
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { ANNOUNCEMENT } from "../constants";
|
||||
@@ -20,11 +22,12 @@ export class AnnouncementService {
|
||||
private readonly announcementRepository: AnnouncementRepository,
|
||||
private readonly userAnnouncementRepository: UserAnnouncementRepository,
|
||||
private readonly danakServicesService: DanakServicesService,
|
||||
private readonly accessLogService: AccessLogService,
|
||||
) {}
|
||||
|
||||
/***************************************** */
|
||||
|
||||
async createAnnouncement(createAnnouncementDto: CreateAnnouncementDto) {
|
||||
async createAnnouncement(createAnnouncementDto: CreateAnnouncementDto, userId: string) {
|
||||
const { serviceId, userIds } = createAnnouncementDto;
|
||||
|
||||
let isPublic = true;
|
||||
@@ -62,6 +65,15 @@ export class AnnouncementService {
|
||||
},
|
||||
);
|
||||
|
||||
await this.accessLogService.logCreate("Announcement", announcement.id, announcement, {
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin created announcement: ${announcement.title}`,
|
||||
metadata: {
|
||||
serviceId: serviceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: AnnouncementMessage.CREATED_AND_SENT_TO_USERS_AFTER_PUBLISH,
|
||||
announcement,
|
||||
@@ -198,7 +210,7 @@ export class AnnouncementService {
|
||||
}
|
||||
//************************************ */
|
||||
|
||||
async updateAnnouncement(id: string, updateAnnouncementDto: UpdateAnnouncementDto) {
|
||||
async updateAnnouncement(id: string, updateAnnouncementDto: UpdateAnnouncementDto, userId: string) {
|
||||
const announcement = await this.announcementRepository.findOneBy({ id });
|
||||
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||
|
||||
@@ -243,6 +255,15 @@ export class AnnouncementService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.accessLogService.logUpdate("Announcement", announcement.id, announcement, {
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin updated announcement: ${announcement.title}`,
|
||||
metadata: {
|
||||
serviceId: serviceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: AnnouncementMessage.UPDATED_SUCCESSFULLY,
|
||||
announcement,
|
||||
@@ -251,7 +272,7 @@ export class AnnouncementService {
|
||||
|
||||
//************************************ */
|
||||
|
||||
async deleteAnnouncement(id: string) {
|
||||
async deleteAnnouncement(id: string, userId: string) {
|
||||
const announcement = await this.announcementRepository.findOneBy({ id });
|
||||
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||
|
||||
@@ -263,6 +284,15 @@ export class AnnouncementService {
|
||||
|
||||
await this.announcementRepository.delete(id);
|
||||
|
||||
await this.accessLogService.logDelete("Announcement", announcement.id, announcement, {
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin deleted announcement: ${announcement.title}`,
|
||||
metadata: {
|
||||
serviceId: announcement.targetService.id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: AnnouncementMessage.DELETED_SUCCESSFULLY,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { ArrayMinSize, IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
|
||||
import { ArrayMinSize, IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, MaxLength } from "class-validator";
|
||||
|
||||
import { BlogMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@@ -69,4 +69,10 @@ export class CreateBlogDto {
|
||||
@IsOptional()
|
||||
@IsBoolean({ message: BlogMessage.IS_PINNED_SHOULD_BE_A_BOOLEAN })
|
||||
isPinned?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: BlogMessage.AUDIO_URL_REQUIRED })
|
||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: BlogMessage.AUDIO_URL_STRING })
|
||||
@ApiPropertyOptional({ description: "Audio URL of the blog", example: "https://example.com/audio.mp3" })
|
||||
audioUrl?: string;
|
||||
}
|
||||
|
||||
@@ -108,8 +108,8 @@ export class BlogsController {
|
||||
@Patch(":id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
updateBlog(@Param() paramDto: ParamDto, @Body() updateBlogDto: UpdateBlogDto) {
|
||||
return this.blogsService.updateBlog(paramDto.id, updateBlogDto);
|
||||
updateBlog(@Param() paramDto: ParamDto, @Body() updateBlogDto: UpdateBlogDto, @UserDec("id") userId: string) {
|
||||
return this.blogsService.updateBlog(paramDto.id, updateBlogDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Delete a blog (admin route) " })
|
||||
|
||||
@@ -12,11 +12,16 @@ import { BlogCategoriesRepository } from "./repositories/blog-categories.reposit
|
||||
import { BlogCommentReplyRepository } from "./repositories/blog-comment-reply.repository";
|
||||
import { BlogCommentsRepository } from "./repositories/blog-comments.repository";
|
||||
import { BlogsRepository } from "./repositories/blogs.repository";
|
||||
import { AccessLogsModule } from "../access-logs/access-logs.module";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Blog, BlogComment, BlogCommentReply, BlogCategory]), UsersModule, NotificationModule],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Blog, BlogComment, BlogCommentReply, BlogCategory]),
|
||||
UsersModule,
|
||||
NotificationModule,
|
||||
AccessLogsModule,
|
||||
],
|
||||
controllers: [BlogsController],
|
||||
providers: [
|
||||
BlogsService,
|
||||
|
||||
@@ -45,6 +45,9 @@ export class Blog extends BaseEntity {
|
||||
@Index()
|
||||
isPinned: boolean;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: true })
|
||||
audioUrl: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.blogs, { onDelete: "CASCADE", nullable: false })
|
||||
@Index()
|
||||
author: User;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { Not } from "typeorm";
|
||||
|
||||
import { AdminMessage, BlogMessage, CommonMessage } from "../../../common/enums/message.enum";
|
||||
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||
import { CategoryListSearchQueryDto } from "../../danak-services/DTO/category-search-query.dto";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { CreateBlogDto } from "../DTO/create-blog.dto";
|
||||
@@ -19,6 +21,7 @@ export class BlogsService {
|
||||
private readonly blogCommentsRepository: BlogCommentsRepository,
|
||||
private readonly blogCategoriesRepository: BlogCategoriesRepository,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly accessLogService: AccessLogService,
|
||||
) {}
|
||||
|
||||
//*********************************** */
|
||||
@@ -121,6 +124,22 @@ export class BlogsService {
|
||||
|
||||
await this.blogsRepository.save(blog);
|
||||
|
||||
await this.accessLogService.logCreate(
|
||||
"Blog",
|
||||
blog.id,
|
||||
{ blog: createBlogDto.title },
|
||||
{
|
||||
user: { id: authorId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin created blog: ${createBlogDto.title}`,
|
||||
metadata: {
|
||||
categoryId: createBlogDto.categoryId,
|
||||
slug: slug,
|
||||
authorId: authorId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
blog,
|
||||
@@ -129,7 +148,7 @@ export class BlogsService {
|
||||
|
||||
//*********************************** */
|
||||
|
||||
async updateBlog(id: string, updateBlogDto: UpdateBlogDto) {
|
||||
async updateBlog(id: string, updateBlogDto: UpdateBlogDto, userId: string) {
|
||||
const blog = await this.findBlogById(id);
|
||||
|
||||
if (updateBlogDto.title) {
|
||||
@@ -153,6 +172,21 @@ export class BlogsService {
|
||||
slug: this.generateSlug(updateBlogDto.slug ?? blog.slug),
|
||||
});
|
||||
|
||||
await this.accessLogService.logUpdate(
|
||||
"Blog",
|
||||
blog.id,
|
||||
{ blog: blog.title },
|
||||
{ blog: updateBlogDto.title },
|
||||
{
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin updated blog: ${blog.title}`,
|
||||
metadata: {
|
||||
categoryId: updateBlogDto.categoryId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
blog,
|
||||
@@ -168,6 +202,20 @@ export class BlogsService {
|
||||
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
|
||||
|
||||
await this.blogsRepository.softDelete({ id });
|
||||
|
||||
await this.accessLogService.logDelete(
|
||||
"Blog",
|
||||
blog.id,
|
||||
{ blog: blog.title },
|
||||
{
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin deleted blog: ${blog.title}`,
|
||||
metadata: {
|
||||
categoryId: blog.category.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
return {
|
||||
message: CommonMessage.DELETED,
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { DanakServiceVideo } from "./entities/danak-service-video.entity";
|
||||
import { DanakServicesVideoRepository } from "./repositories/danak-services-video.repository";
|
||||
import { AccessLogsModule } from "../access-logs/access-logs.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -40,6 +41,7 @@ import { DanakServicesVideoRepository } from "./repositories/danak-services-vide
|
||||
]),
|
||||
UsersModule,
|
||||
NotificationModule,
|
||||
AccessLogsModule,
|
||||
],
|
||||
providers: [
|
||||
DanakServicesService,
|
||||
|
||||
@@ -3,6 +3,8 @@ import { DataSource, FindOptionsWhere, In, IsNull, LessThan, MoreThan, Not, Quer
|
||||
|
||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||
import { CategoryMessage, CommonMessage, ServiceMessage } from "../../../common/enums/message.enum";
|
||||
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||
import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
@@ -42,6 +44,7 @@ export class DanakServicesService {
|
||||
private readonly danakServicesAudioRepository: DanakServicesAudioRepository,
|
||||
private readonly danakServicesVideoRepository: DanakServicesVideoRepository,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly accessLogService: AccessLogService,
|
||||
) {}
|
||||
/******************************************** */
|
||||
|
||||
@@ -306,6 +309,21 @@ export class DanakServicesService {
|
||||
|
||||
const service = this.danakServicesRepository.create({ ...createDto, category, images, audios, videos, slug });
|
||||
await this.danakServicesRepository.save(service);
|
||||
|
||||
// Log the service creation
|
||||
await this.accessLogService.logCreate("DanakService", service.id, createDto, {
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin created service: ${createDto.name}`,
|
||||
metadata: {
|
||||
categoryId: createDto.categoryId,
|
||||
slug: slug,
|
||||
hasImages: createDto.images && createDto.images.length > 0,
|
||||
hasAudios: createDto.audios && createDto.audios.length > 0,
|
||||
hasVideos: createDto.videos && createDto.videos.length > 0,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
service,
|
||||
|
||||
@@ -23,22 +23,22 @@ export class InvoicesController {
|
||||
@ApiOperation({ summary: "create an invoice ==> admin route" })
|
||||
@PermissionsDec(PermissionEnum.INVOICES)
|
||||
@Post()
|
||||
createInvoice(@Body() createDto: CreateInvoiceDto) {
|
||||
return this.invoiceService.createInvoiceAdmin(createDto);
|
||||
createInvoice(@Body() createDto: CreateInvoiceDto, @UserDec("id") userId: string) {
|
||||
return this.invoiceService.createInvoiceAdmin(createDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "update an invoice ==> admin route" })
|
||||
@PermissionsDec(PermissionEnum.INVOICES)
|
||||
@Patch(":id")
|
||||
updateInvoice(@Param() paramDto: ParamDto, @Body() updateDto: UpdateInvoiceDto) {
|
||||
return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto);
|
||||
updateInvoice(@Param() paramDto: ParamDto, @Body() updateDto: UpdateInvoiceDto, @UserDec("id") userId: string) {
|
||||
return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "delete an invoice ==> admin route" })
|
||||
@PermissionsDec(PermissionEnum.INVOICES)
|
||||
@Delete(":id")
|
||||
deleteInvoice(@Param() paramDto: ParamDto) {
|
||||
return this.invoiceService.deleteInvoiceAdmin(paramDto.id);
|
||||
deleteInvoice(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.invoiceService.deleteInvoiceAdmin(paramDto.id, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "get all invoices ==> admin route" })
|
||||
|
||||
@@ -10,6 +10,7 @@ import { InvoicesService } from "./providers/invoices.service";
|
||||
import { InvoiceProcessor } from "./queue/invoice.processor";
|
||||
import { InvoiceItemsRepository } from "./repositories/invoice-items.repository";
|
||||
import { InvoicesRepository } from "./repositories/invoices.repository";
|
||||
import { AccessLogsModule } from "../access-logs/access-logs.module";
|
||||
import { Discount } from "../discounts/entities/discount.entity";
|
||||
import { UsageDiscount } from "../discounts/entities/usage-discount.entity";
|
||||
import { DiscountRepository } from "../discounts/repositories/discounts.repository";
|
||||
@@ -44,6 +45,7 @@ import { WalletsModule } from "../wallets/wallets.module";
|
||||
UtilsModule,
|
||||
NotificationModule,
|
||||
PaymentsModule,
|
||||
AccessLogsModule,
|
||||
],
|
||||
providers: [InvoicesService, InvoicesRepository, InvoiceItemsRepository, DiscountRepository, UsageDiscountRepository, InvoiceProcessor],
|
||||
controllers: [InvoicesController],
|
||||
|
||||
@@ -6,6 +6,9 @@ import Decimal from "decimal.js";
|
||||
import { Between, DataSource, In, QueryRunner } from "typeorm";
|
||||
|
||||
import { AuthMessage, InvoiceMessage, WalletMessage } from "../../../common/enums/message.enum";
|
||||
import { OperationType } from "../../access-logs/enums/operation-type.enum";
|
||||
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||
import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto";
|
||||
import { Discount } from "../../discounts/entities/discount.entity";
|
||||
import { DiscountType } from "../../discounts/enums/discount-type.enum";
|
||||
@@ -48,11 +51,12 @@ export class InvoicesService {
|
||||
private readonly otpService: OTPService,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly accessLogService: AccessLogService,
|
||||
) {}
|
||||
|
||||
///********************************** */
|
||||
|
||||
async createInvoiceAdmin(createDto: CreateInvoiceDto) {
|
||||
async createInvoiceAdmin(createDto: CreateInvoiceDto, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
@@ -79,7 +83,7 @@ export class InvoicesService {
|
||||
const dueDate = dayjs().add(INVOICE.DUEDATE, "day").toDate();
|
||||
|
||||
const invoice = queryRunner.manager.create(this.invoiceRepository.target, {
|
||||
user: { id: createDto.userId },
|
||||
user: { id: userId },
|
||||
totalPrice: totalPrice.add(tax).toNumber(),
|
||||
originalPrice: totalPrice.add(tax).toNumber(),
|
||||
items: invoiceItems,
|
||||
@@ -108,6 +112,15 @@ export class InvoicesService {
|
||||
});
|
||||
|
||||
await this.scheduleInvoiceJobs(invoice, createDto);
|
||||
|
||||
await this.accessLogService.logCreate("Invoice", invoice.id, createDto, {
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin created invoice: ${invoice.numericId}`,
|
||||
metadata: {
|
||||
items: invoiceItems.map((item) => item.name).join(", "),
|
||||
},
|
||||
});
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
@@ -128,7 +141,7 @@ export class InvoicesService {
|
||||
|
||||
//********************************** */
|
||||
|
||||
async updateInvoiceAdmin(invoiceId: string, updateDto: UpdateInvoiceDto) {
|
||||
async updateInvoiceAdmin(invoiceId: string, updateDto: UpdateInvoiceDto, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
@@ -191,6 +204,12 @@ export class InvoicesService {
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
await this.accessLogService.logUpdate("Invoice", invoice.id, updateDto, {
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin updated invoice: ${invoice.numericId}`,
|
||||
});
|
||||
|
||||
return {
|
||||
message: InvoiceMessage.INVOICE_UPDATED,
|
||||
invoice,
|
||||
@@ -205,7 +224,7 @@ export class InvoicesService {
|
||||
|
||||
//********************************** */
|
||||
|
||||
async deleteInvoiceAdmin(invoiceId: string) {
|
||||
async deleteInvoiceAdmin(invoiceId: string, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
@@ -226,6 +245,12 @@ export class InvoicesService {
|
||||
|
||||
await queryRunner.manager.remove(this.invoiceRepository.target, invoice);
|
||||
|
||||
await this.accessLogService.logDelete("Invoice", invoice.id, invoice, {
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin deleted invoice: ${invoice.numericId}`,
|
||||
});
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
@@ -239,9 +264,7 @@ export class InvoicesService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all queue jobs related to a specific invoice
|
||||
*/
|
||||
//################################################
|
||||
private async cleanupInvoiceQueueJobs(invoiceId: string): Promise<void> {
|
||||
try {
|
||||
this.logger.log(`Starting cleanup of queue jobs for invoice: ${invoiceId}`);
|
||||
@@ -334,6 +357,17 @@ export class InvoicesService {
|
||||
items,
|
||||
);
|
||||
|
||||
await this.accessLogService.logUserAction(
|
||||
OperationType.APPROVE,
|
||||
"Invoice",
|
||||
invoiceId,
|
||||
`user approved invoice request: ${invoice.numericId}`,
|
||||
{
|
||||
user: { id: userId },
|
||||
userType: UserType.USER,
|
||||
},
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
@@ -380,6 +414,17 @@ export class InvoicesService {
|
||||
|
||||
await this.otpService.delOtpFormCache(user.phone, "INVOICE_VERIFY");
|
||||
|
||||
await this.accessLogService.logUserAction(
|
||||
OperationType.APPROVE,
|
||||
"Invoice",
|
||||
invoiceId,
|
||||
`user approved invoice by user: ${invoice.numericId}`,
|
||||
{
|
||||
user: { id: userId },
|
||||
userType: UserType.USER,
|
||||
},
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
@@ -739,6 +784,11 @@ export class InvoicesService {
|
||||
paidAt: invoice.paidAt,
|
||||
});
|
||||
|
||||
await this.accessLogService.logUserAction(OperationType.UPDATE, "Invoice", invoiceId, `user paid invoice: ${invoice.numericId}`, {
|
||||
user: { id: userId },
|
||||
userType: UserType.USER,
|
||||
});
|
||||
|
||||
if (transactionStarted) await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,8 @@ import slugify from "slugify";
|
||||
import { DataSource, In, Not, QueryRunner } from "typeorm";
|
||||
|
||||
import { AdminMessage, AdsMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { PasswordService } from "../../utils/providers/password.service";
|
||||
import { CreateAdminDto } from "../DTO/create-admin.dto";
|
||||
@@ -23,9 +25,10 @@ export class AdminsService {
|
||||
private readonly permissionsRepository: PermissionsRepository,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly accessLogService: AccessLogService,
|
||||
) {}
|
||||
|
||||
async createAdmin(createDto: CreateAdminDto) {
|
||||
async createAdmin(createDto: CreateAdminDto, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
@@ -61,6 +64,19 @@ export class AdminsService {
|
||||
await queryRunner.manager.save(this.userRepository.target, adminUser);
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// Log the admin creation
|
||||
await this.accessLogService.logCreate("User", adminUser.id, createDto, {
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin created new admin: ${createDto.email}`,
|
||||
metadata: {
|
||||
roleId: createDto.roleId,
|
||||
createdBy: "admin",
|
||||
email: createDto.email,
|
||||
phone: createDto.phone,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: AdminMessage.ADMIN_CREATED,
|
||||
adminUser,
|
||||
@@ -74,7 +90,7 @@ export class AdminsService {
|
||||
}
|
||||
/************************************************************ */
|
||||
|
||||
async updateAdmin(id: string, updateDto: UpdateAdminDto) {
|
||||
async updateAdmin(id: string, updateDto: UpdateAdminDto, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
@@ -121,6 +137,18 @@ export class AdminsService {
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
await this.accessLogService.logUpdate(
|
||||
"User",
|
||||
admin.id,
|
||||
{ ...admin },
|
||||
{ ...admin },
|
||||
{
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin updated admin: ${admin.email}`,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
message: AdminMessage.ADMIN_UPDATED,
|
||||
};
|
||||
@@ -133,11 +161,23 @@ export class AdminsService {
|
||||
}
|
||||
/************************************************************ */
|
||||
|
||||
async deleteAdmin(id: string) {
|
||||
async deleteAdmin(id: string, userId: string) {
|
||||
const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } } });
|
||||
if (!admin) throw new BadRequestException(AdminMessage.ADMIN_NOT_FOUND);
|
||||
|
||||
await this.userRepository.softDelete(id);
|
||||
|
||||
await this.accessLogService.logDelete(
|
||||
"User",
|
||||
admin.id,
|
||||
{ ...admin },
|
||||
{
|
||||
user: { id: userId },
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin deleted admin: ${admin.email}`,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
message: AdminMessage.ADMIN_DELETED,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { DataSource, Not } from "typeorm";
|
||||
|
||||
import { CommonMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||
import { Address } from "../../address/entities/address.entity";
|
||||
import { AddressService } from "../../address/providers/address.service";
|
||||
import { UserSettingsService } from "../../settings/providers/user-settings.service";
|
||||
@@ -32,6 +34,7 @@ export class CustomersService {
|
||||
private readonly userSettingsService: UserSettingsService,
|
||||
private readonly rolesRepository: RoleRepository,
|
||||
private readonly walletsService: WalletsService,
|
||||
private readonly accessLogService: AccessLogService,
|
||||
) {}
|
||||
|
||||
async createCustomer(createDto: CreateCustomerDto) {
|
||||
@@ -68,6 +71,18 @@ export class CustomersService {
|
||||
await this.createLegalUserData(createDto, user.id, queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// Log the customer creation
|
||||
await this.accessLogService.logCreate("User", user.id, createDto, {
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin created customer: ${createDto.email}`,
|
||||
metadata: {
|
||||
createdBy: "admin",
|
||||
email: createDto.email,
|
||||
phone: createDto.phone,
|
||||
},
|
||||
});
|
||||
|
||||
return { message: CommonMessage.CREATED, user };
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
|
||||
@@ -7,6 +7,8 @@ import slugify from "slugify";
|
||||
import { In, Not, QueryRunner } from "typeorm";
|
||||
|
||||
import { AuthMessage, CommonMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||
import { AddressService } from "../../address/providers/address.service";
|
||||
import { RequestOtpDto } from "../../auth/DTO/request-otp.dto";
|
||||
import { VerifyOtpDto } from "../../auth/DTO/verify-otp.dto";
|
||||
@@ -47,6 +49,7 @@ export class UsersService {
|
||||
private readonly smsService: SmsService,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly referralsService: ReferralsService,
|
||||
private readonly accessLogService: AccessLogService,
|
||||
) {}
|
||||
|
||||
/************************************************************ */
|
||||
@@ -114,6 +117,8 @@ export class UsersService {
|
||||
const user = await this.userRepository.findOneBy({ id: userId });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
const oldUserData = { ...user };
|
||||
|
||||
if (updateProfileDto.cityId) {
|
||||
const { city: existCity } = await this.addressService.getCity(+updateProfileDto.cityId);
|
||||
if (!existCity) throw new BadRequestException(UserMessage.CITY_NOT_FOUND);
|
||||
@@ -127,8 +132,16 @@ export class UsersService {
|
||||
if (existUserName) throw new BadRequestException(UserMessage.USERNAME_EXIST);
|
||||
}
|
||||
|
||||
await this.userRepository.save({ ...user, ...updateProfileDto, city });
|
||||
//
|
||||
const updatedUser = await this.userRepository.save({ ...user, ...updateProfileDto, city });
|
||||
|
||||
// Log the profile update
|
||||
await this.accessLogService.logUpdate("User", userId, oldUserData, updatedUser, {
|
||||
user: { id: userId },
|
||||
userType: UserType.USER,
|
||||
actionDescription: `User updated profile: ${user.email}`,
|
||||
metadata: { updatedFields: Object.keys(updateProfileDto) },
|
||||
});
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
};
|
||||
|
||||
@@ -180,22 +180,22 @@ export class UsersController {
|
||||
@ApiOperation({ summary: "update admin ==> admin route" })
|
||||
@PermissionsDec(PermissionEnum.ADMINS, PermissionEnum.CUSTOMERS)
|
||||
@Patch("admins/:id")
|
||||
updateAdmin(@Param() paramDto: ParamDto, @Body() updateDto: UpdateAdminDto) {
|
||||
return this.adminsService.updateAdmin(paramDto.id, updateDto);
|
||||
updateAdmin(@Param() paramDto: ParamDto, @Body() updateDto: UpdateAdminDto, @UserDec("id") userId: string) {
|
||||
return this.adminsService.updateAdmin(paramDto.id, updateDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "delete admin ==> admin route" })
|
||||
@PermissionsDec(PermissionEnum.ADMINS, PermissionEnum.CUSTOMERS)
|
||||
@Delete("admins/:id")
|
||||
deleteAdmin(@Param() paramDto: ParamDto) {
|
||||
return this.adminsService.deleteAdmin(paramDto.id);
|
||||
deleteAdmin(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.adminsService.deleteAdmin(paramDto.id, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "create admin ==> admin route" })
|
||||
@PermissionsDec(PermissionEnum.ADMINS, PermissionEnum.CUSTOMERS)
|
||||
@Post("admins")
|
||||
createAdmin(@Body() createDto: CreateAdminDto) {
|
||||
return this.adminsService.createAdmin(createDto);
|
||||
createAdmin(@Body() createDto: CreateAdminDto, @UserDec("id") userId: string) {
|
||||
return this.adminsService.createAdmin(createDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "get admin permissions" })
|
||||
|
||||
@@ -13,6 +13,7 @@ import { PermissionsRepository } from "./repositories/permissions.repository";
|
||||
import { RoleRepository } from "./repositories/roles.repository";
|
||||
import { UserGroupRepository } from "./repositories/user-group.repository";
|
||||
import { UserRepository } from "./repositories/users.repository";
|
||||
import { AccessLogsModule } from "../access-logs/access-logs.module";
|
||||
import { AddressModule } from "../address/address.module";
|
||||
import { AddressService } from "../address/providers/address.service";
|
||||
import { UserSetting } from "../settings/entities/user-setting.entity";
|
||||
@@ -35,6 +36,7 @@ import { ReferralsModule } from "../referrals/referrals.module";
|
||||
UtilsModule,
|
||||
AddressModule,
|
||||
ReferralsModule,
|
||||
AccessLogsModule,
|
||||
],
|
||||
providers: [
|
||||
UsersService,
|
||||
|
||||
Reference in New Issue
Block a user