chore: add new access log module

This commit is contained in:
mahyargdz
2025-09-02 16:14:36 +03:30
parent 60deb8937d
commit 672fe9b783
36 changed files with 2066 additions and 41 deletions
+2
View File
@@ -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);
+9 -1
View File
@@ -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 = "لاگ دسترسی مورد نظر یافت نشد",
}
+1 -1
View File
@@ -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,
};
+7 -1
View File
@@ -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;
}
+2 -2
View File
@@ -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) " })
+7 -2
View File
@@ -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;
+49 -1
View File
@@ -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,
+6 -6
View File
@@ -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" })
+2
View File
@@ -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 {
+43 -3
View File
@@ -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();
+15 -2
View File
@@ -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,
};
+6 -6
View File
@@ -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" })
+2
View File
@@ -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,