forked from Chatbot/v3-api
first commit v3 initialiazed a temporary repository for darmanet client
This commit is contained in:
20
src/common/decorators/Identity.decorator.ts
Normal file
20
src/common/decorators/Identity.decorator.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentIdentity = createParamDecorator(
|
||||
(data: keyof any, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
// If a specific property of the user is requested (e.g., user ID or role)
|
||||
// Return just that property; otherwise, return the full user object
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
export const AdminIdentity = createParamDecorator(
|
||||
(data: keyof any, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request['admin'];
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
8
src/common/decorators/permission.decorator.ts
Normal file
8
src/common/decorators/permission.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Permission } from 'src/common/types/permissions.catalog';
|
||||
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
|
||||
/** Require ANY of the listed permissions (OR). Owner always passes. */
|
||||
export const Permissions = (...permissions: Permission[]) =>
|
||||
SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
4
src/common/decorators/role.decorator.ts
Normal file
4
src/common/decorators/role.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
4
src/common/decorators/skip-admin-rate-limit.decorator.ts
Normal file
4
src/common/decorators/skip-admin-rate-limit.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const SKIP_ADMIN_RATE_LIMIT_KEY = 'skipAdminRateLimit';
|
||||
export const SkipAdminRateLimit = () => SetMetadata(SKIP_ADMIN_RATE_LIMIT_KEY, true);
|
||||
77
src/common/dto/base-response.dto.ts
Normal file
77
src/common/dto/base-response.dto.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsOptional, Min } from 'class-validator';
|
||||
|
||||
export class PageOptionsDto {
|
||||
@ApiPropertyOptional({
|
||||
minimum: 1,
|
||||
default: 1,
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
readonly page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
minimum: 1,
|
||||
// maximum: 50,
|
||||
default: 10,
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
// @Max(50)
|
||||
@IsOptional()
|
||||
readonly take?: number = 10;
|
||||
|
||||
get skip(): number {
|
||||
return (this.page - 1) * this.take;
|
||||
}
|
||||
}
|
||||
export interface PageMetaDtoParameters {
|
||||
pageOptionsDto: PageOptionsDto;
|
||||
itemCount: number;
|
||||
}
|
||||
export class PageMetaDto {
|
||||
@ApiProperty()
|
||||
readonly page: number;
|
||||
|
||||
@ApiProperty()
|
||||
readonly take: number;
|
||||
|
||||
@ApiProperty()
|
||||
readonly itemCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
readonly pageCount: number;
|
||||
|
||||
constructor({ pageOptionsDto, itemCount }: PageMetaDtoParameters) {
|
||||
this.page = pageOptionsDto.page;
|
||||
this.take = pageOptionsDto.take;
|
||||
this.itemCount = itemCount;
|
||||
this.pageCount = Math.ceil(this.itemCount / this.take);
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseResponseDTO {
|
||||
statusCode: HttpStatus;
|
||||
message: string;
|
||||
data: any;
|
||||
@ApiProperty({ type: () => PageMetaDto })
|
||||
readonly meta: PageMetaDto;
|
||||
|
||||
constructor(statusCode: HttpStatus, message, data: any, meta?: PageMetaDto) {
|
||||
this.statusCode = statusCode;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
this.meta = meta;
|
||||
}
|
||||
mapDataIfArray<T>(mapper: (item: any) => T): T[] {
|
||||
if (Array.isArray(this.data)) {
|
||||
return this.data.map(mapper);
|
||||
}
|
||||
return { ...this.data };
|
||||
}
|
||||
}
|
||||
8
src/common/dto/forgetPassword.dto.ts
Normal file
8
src/common/dto/forgetPassword.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail } from 'class-validator';
|
||||
|
||||
export class ForgetPasswordDTO {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
username: string;
|
||||
}
|
||||
70
src/common/dto/login.dto.ts
Normal file
70
src/common/dto/login.dto.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, ValidateIf } from 'class-validator';
|
||||
|
||||
export class UserLoginDTO {
|
||||
@ApiProperty({
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'mobile of user',
|
||||
example: '09226187419',
|
||||
})
|
||||
mobile: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
type: 'string',
|
||||
description: 'nationalCode of user',
|
||||
example: '4311402422',
|
||||
})
|
||||
@ValidateIf((o) => o.twoFactor === true)
|
||||
nationalCode: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
required: true,
|
||||
type: 'string',
|
||||
default: false,
|
||||
description:
|
||||
'if true , both nationalCode and mobile and if false , just mobile should be sent.',
|
||||
example: false,
|
||||
})
|
||||
twoFactor: boolean;
|
||||
}
|
||||
|
||||
export class UserVerifyOtpDTO {
|
||||
@ApiProperty({
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'mobile of user',
|
||||
example: '09226187419',
|
||||
})
|
||||
mobile: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'otp send to the mobile of user',
|
||||
example: '27246',
|
||||
})
|
||||
otp: string;
|
||||
}
|
||||
|
||||
export class AdminLoginDTO {
|
||||
@ApiProperty({
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'username of admin',
|
||||
example: 'admin@chatbot.com',
|
||||
})
|
||||
@IsEmail()
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'password of admin',
|
||||
example: 'p@$$word123321',
|
||||
})
|
||||
password: string;
|
||||
// @ApiProperty({ examples: Role, required: true, enum: Role })
|
||||
// role: Role;
|
||||
}
|
||||
12
src/common/dto/resetPassword.dto.ts
Normal file
12
src/common/dto/resetPassword.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsStrongPassword } from 'class-validator';
|
||||
|
||||
export class ResetPasswordDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
resetToken: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsStrongPassword()
|
||||
newPassword: string;
|
||||
}
|
||||
30
src/common/filters/all-exceptions.filter.ts
Normal file
30
src/common/filters/all-exceptions.filter.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { BaseExceptionFilter, HttpAdapterHost } from '@nestjs/core';
|
||||
import { Request, Response } from 'express';
|
||||
import { INestApplication } from '@nestjs/common'; // Keep this import for app.close()
|
||||
|
||||
@Catch()
|
||||
export class AllExceptionsFilter<T> extends BaseExceptionFilter {
|
||||
private readonly nestApplication: INestApplication; // Rename to avoid conflict with super constructor
|
||||
|
||||
constructor(httpAdapterHost: HttpAdapterHost, nestApplication: INestApplication) {
|
||||
super(httpAdapterHost.httpAdapter);
|
||||
this.nestApplication = nestApplication;
|
||||
}
|
||||
|
||||
async catch(exception: T, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
if (exception instanceof HttpException || (exception && typeof exception === 'object' && 'statusCode' in exception && 'message' in exception)) {
|
||||
super.catch(exception, host);
|
||||
} else {
|
||||
const status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
console.error(`CRITICAL ERROR - HTTP Status: ${status} - Path: ${request.url}`);
|
||||
console.error('Caught by AllExceptionsFilter (forcing exit):', exception);
|
||||
|
||||
await this.nestApplication.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/common/filters/ws-all-exceptions.filter.ts
Normal file
54
src/common/filters/ws-all-exceptions.filter.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ArgumentsHost, Catch, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
|
||||
|
||||
@Catch()
|
||||
export class WsAllExceptionsFilter extends BaseWsExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const client: any = host.switchToWs().getClient();
|
||||
|
||||
let message = 'Unknown error';
|
||||
let status = HttpStatus.BAD_REQUEST;
|
||||
let details: any = undefined;
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus?.() ?? HttpStatus.BAD_REQUEST;
|
||||
const res: any = exception.getResponse?.();
|
||||
if (typeof res === 'string') message = res;
|
||||
else if (res && typeof res === 'object') {
|
||||
message = res.message || exception.message;
|
||||
details = res;
|
||||
} else {
|
||||
message = exception.message;
|
||||
}
|
||||
} else if (exception instanceof WsException) {
|
||||
const err = exception.getError();
|
||||
if (typeof err === 'string') message = err;
|
||||
else if (err && typeof err === 'object') {
|
||||
message = (err as any).message || 'Websocket error';
|
||||
details = err;
|
||||
} else {
|
||||
message = exception.message;
|
||||
}
|
||||
} else if (exception && typeof exception === 'object') {
|
||||
message = (exception as any).message || message;
|
||||
}
|
||||
|
||||
try {
|
||||
client.emit('response', {
|
||||
event: 'error',
|
||||
statusCode: status,
|
||||
message,
|
||||
data: {},
|
||||
meta: details,
|
||||
});
|
||||
} catch {
|
||||
// ignore emit errors
|
||||
}
|
||||
|
||||
super.catch(exception, host);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
66
src/common/helpers/redis.service.ts
Normal file
66
src/common/helpers/redis.service.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// redis.service.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRedis } from '@nestjs-modules/ioredis';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService {
|
||||
constructor(@InjectRedis() private readonly redis: Redis) {}
|
||||
|
||||
//* Returns the raw ioredis client
|
||||
getClient(): Redis {
|
||||
return this.redis;
|
||||
}
|
||||
|
||||
//* Sets a key-value pair with optional expiration (in seconds)
|
||||
async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
|
||||
const stringValue = JSON.stringify(value);
|
||||
if (ttlSeconds) {
|
||||
await this.redis.set(key, stringValue, 'EX', ttlSeconds);
|
||||
} else {
|
||||
await this.redis.set(key, stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
//* Gets a parsed JSON value from Redis
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
const result = await this.redis.get(key);
|
||||
return result ? JSON.parse(result) : null;
|
||||
}
|
||||
|
||||
//* Deletes a key
|
||||
async del(key: string): Promise<void> {
|
||||
await this.redis.del(key);
|
||||
}
|
||||
|
||||
//* Push a value to a list (for queue operations)
|
||||
async enqueue(queueName: string, value: any): Promise<void> {
|
||||
await this.redis.rpush(queueName, JSON.stringify(value));
|
||||
}
|
||||
|
||||
//* Pops a value from the left of a list (FIFO queue)
|
||||
async dequeue<T = any>(queueName: string): Promise<T | null> {
|
||||
const result = await this.redis.lpop(queueName);
|
||||
return result ? JSON.parse(result) : null;
|
||||
}
|
||||
|
||||
//* Returns the queue length
|
||||
async queueLength(queueName: string): Promise<number> {
|
||||
return await this.redis.llen(queueName);
|
||||
}
|
||||
|
||||
// * Gets all values from a queue
|
||||
async getAllFromQueue<T = any>(queueName: string): Promise<T[]> {
|
||||
const items = await this.redis.lrange(queueName, 0, -1);
|
||||
return items.map((x) => JSON.parse(x));
|
||||
}
|
||||
async blacklistToken(token: string, ttlInSeconds: number): Promise<void> {
|
||||
const ttlInMs = ttlInSeconds * 1000;
|
||||
await this.redis.set(`blacklist:${token}`, 'revoked', 'PX', ttlInMs);
|
||||
}
|
||||
|
||||
async isTokenBlacklisted(token: string): Promise<boolean> {
|
||||
const result = await this.redis.get(`blacklist:${token}`);
|
||||
return result === 'revoked';
|
||||
}
|
||||
}
|
||||
204
src/common/helpers/structured-logger.service.ts
Normal file
204
src/common/helpers/structured-logger.service.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { Logger, LoggerService } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Log categories for better organization
|
||||
*/
|
||||
export enum LogCategory {
|
||||
// Connection lifecycle
|
||||
CONNECTION = 'CONNECTION',
|
||||
DISCONNECT = 'DISCONNECT',
|
||||
|
||||
// Room management
|
||||
ROOM = 'ROOM',
|
||||
JOIN = 'JOIN',
|
||||
LEAVE = 'LEAVE',
|
||||
|
||||
// Expert management
|
||||
EXPERT = 'EXPERT',
|
||||
EXPERT_ONLINE = 'EXPERT_ONLINE',
|
||||
EXPERT_OFFLINE = 'EXPERT_OFFLINE',
|
||||
EXPERT_REASSIGN = 'EXPERT_REASSIGN',
|
||||
|
||||
// User management
|
||||
USER = 'USER',
|
||||
USER_QUEUE = 'USER_QUEUE',
|
||||
|
||||
// Messaging
|
||||
MESSAGE = 'MESSAGE',
|
||||
MESSAGE_SEND = 'MESSAGE_SEND',
|
||||
MESSAGE_EDIT = 'MESSAGE_EDIT',
|
||||
MESSAGE_SEEN = 'MESSAGE_SEEN',
|
||||
|
||||
// Chat lifecycle
|
||||
CHAT = 'CHAT',
|
||||
CHAT_START = 'CHAT_START',
|
||||
CHAT_END = 'CHAT_END',
|
||||
CHAT_CLOSE = 'CHAT_CLOSE',
|
||||
CHAT_AUTO_CLOSE = 'CHAT_AUTO_CLOSE',
|
||||
|
||||
// Redis operations
|
||||
REDIS = 'REDIS',
|
||||
REDIS_SET = 'REDIS_SET',
|
||||
REDIS_HASH = 'REDIS_HASH',
|
||||
REDIS_QUEUE = 'REDIS_QUEUE',
|
||||
REDIS_REPAIR = 'REDIS_REPAIR',
|
||||
|
||||
// Validation
|
||||
VALIDATION = 'VALIDATION',
|
||||
|
||||
// Background tasks
|
||||
BACKGROUND = 'BACKGROUND',
|
||||
WATCHER = 'WATCHER',
|
||||
SCHEDULER = 'SCHEDULER',
|
||||
|
||||
// Errors
|
||||
ERROR = 'ERROR',
|
||||
|
||||
// General
|
||||
INFO = 'INFO',
|
||||
DEBUG = 'DEBUG',
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured Logger Service
|
||||
* Provides organized logging with categories and context
|
||||
*/
|
||||
export class StructuredLogger implements LoggerService {
|
||||
private readonly logger: Logger;
|
||||
private readonly context: string;
|
||||
|
||||
constructor(context: string) {
|
||||
this.context = context;
|
||||
this.logger = new Logger(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format log message with category and context
|
||||
*/
|
||||
private formatMessage(category: LogCategory, message: string, context?: Record<string, any>): string {
|
||||
const categoryTag = `[${category}]`;
|
||||
const contextStr = context ? ` ${JSON.stringify(context)}` : '';
|
||||
return `${categoryTag} ${message}${contextStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info message
|
||||
*/
|
||||
log(category: LogCategory, message: string, context?: Record<string, any>): void;
|
||||
log(message: string, context?: Record<string, any>): void;
|
||||
log(categoryOrMessage: LogCategory | string, messageOrContext?: string | Record<string, any>, context?: Record<string, any>): void {
|
||||
if (typeof categoryOrMessage === 'string') {
|
||||
// Legacy format: just message
|
||||
this.logger.log(messageOrContext ? `${categoryOrMessage} ${JSON.stringify(messageOrContext)}` : categoryOrMessage);
|
||||
} else {
|
||||
// Structured format: category + message + context
|
||||
const category = categoryOrMessage as LogCategory;
|
||||
const message = messageOrContext as string;
|
||||
const ctx = context || {};
|
||||
this.logger.log(this.formatMessage(category, message, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
*/
|
||||
warn(category: LogCategory, message: string, context?: Record<string, any>, error?: any): void;
|
||||
warn(message: string, context?: Record<string, any>): void;
|
||||
warn(categoryOrMessage: LogCategory | string, messageOrContext?: string | Record<string, any>, contextOrError?: Record<string, any> | any, error?: any): void {
|
||||
if (typeof categoryOrMessage === 'string') {
|
||||
// Legacy format
|
||||
this.logger.warn(messageOrContext ? `${categoryOrMessage} ${JSON.stringify(messageOrContext)}` : categoryOrMessage);
|
||||
} else {
|
||||
// Structured format
|
||||
const category = categoryOrMessage as LogCategory;
|
||||
const message = messageOrContext as string;
|
||||
// Check if contextOrError is an error object or context
|
||||
let ctx: Record<string, any> = {};
|
||||
let err: any = undefined;
|
||||
if (contextOrError) {
|
||||
if (contextOrError instanceof Error || (typeof contextOrError === 'object' && contextOrError !== null && 'stack' in contextOrError)) {
|
||||
err = contextOrError;
|
||||
} else {
|
||||
ctx = contextOrError as Record<string, any>;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
err = error;
|
||||
}
|
||||
const formattedMessage = this.formatMessage(category, message, ctx);
|
||||
if (err) {
|
||||
this.logger.warn(formattedMessage, err);
|
||||
} else {
|
||||
this.logger.warn(formattedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error message
|
||||
*/
|
||||
error(category: LogCategory, message: string, error?: any, context?: Record<string, any>): void;
|
||||
error(message: string, error?: any, context?: Record<string, any>): void;
|
||||
error(categoryOrMessage: LogCategory | string, messageOrError?: string | any, errorOrContext?: any, context?: Record<string, any>): void {
|
||||
if (typeof categoryOrMessage === 'string') {
|
||||
// Legacy format
|
||||
const message = categoryOrMessage;
|
||||
const error = messageOrError;
|
||||
const ctx = errorOrContext;
|
||||
if (error) {
|
||||
this.logger.error(`${message} ${ctx ? JSON.stringify(ctx) : ''}`, error);
|
||||
} else {
|
||||
this.logger.error(message, ctx);
|
||||
}
|
||||
} else {
|
||||
// Structured format
|
||||
const category = categoryOrMessage as LogCategory;
|
||||
const message = messageOrError as string;
|
||||
const error = errorOrContext;
|
||||
const ctx = context || {};
|
||||
const formattedMessage = this.formatMessage(category, message, ctx);
|
||||
if (error) {
|
||||
this.logger.error(formattedMessage, error);
|
||||
} else {
|
||||
this.logger.error(formattedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug message
|
||||
*/
|
||||
debug(category: LogCategory, message: string, context?: Record<string, any>): void;
|
||||
debug(message: string, context?: Record<string, any>): void;
|
||||
debug(categoryOrMessage: LogCategory | string, messageOrContext?: string | Record<string, any>, context?: Record<string, any>): void {
|
||||
if (typeof categoryOrMessage === 'string') {
|
||||
// Legacy format
|
||||
this.logger.debug(messageOrContext ? `${categoryOrMessage} ${JSON.stringify(messageOrContext)}` : categoryOrMessage);
|
||||
} else {
|
||||
// Structured format
|
||||
const category = categoryOrMessage as LogCategory;
|
||||
const message = messageOrContext as string;
|
||||
const ctx = context || {};
|
||||
this.logger.debug(this.formatMessage(category, message, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log verbose message
|
||||
*/
|
||||
verbose(category: LogCategory, message: string, context?: Record<string, any>): void;
|
||||
verbose(message: string, context?: Record<string, any>): void;
|
||||
verbose(categoryOrMessage: LogCategory | string, messageOrContext?: string | Record<string, any>, context?: Record<string, any>): void {
|
||||
if (typeof categoryOrMessage === 'string') {
|
||||
// Legacy format
|
||||
this.logger.verbose(messageOrContext ? `${categoryOrMessage} ${JSON.stringify(messageOrContext)}` : categoryOrMessage);
|
||||
} else {
|
||||
// Structured format
|
||||
const category = categoryOrMessage as LogCategory;
|
||||
const message = messageOrContext as string;
|
||||
const ctx = context || {};
|
||||
this.logger.verbose(this.formatMessage(category, message, ctx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
src/common/helpers/versioning.helper.ts
Normal file
46
src/common/helpers/versioning.helper.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { AiServiceModule } from 'src/ai-service/ai-service.module';
|
||||
import { AiV2Module } from 'src/ai-v2/ai-v2.module';
|
||||
import { AdminModule } from 'src/api/admin/admin.module';
|
||||
import { UserModule } from 'src/api/user/user.module';
|
||||
import { AuthModule } from 'src/auth/auth.module';
|
||||
import { AclModule } from 'src/acl/acl.module';
|
||||
import { CategoriesModule } from 'src/categories/categories.module';
|
||||
import { ClientManagementModule } from 'src/client-management/client-management.module';
|
||||
import { ConversationModule } from 'src/conversation/conversation.module';
|
||||
import { DatabaseModule } from 'src/database/database.module';
|
||||
import { StorageModule } from 'src/storage/storage.module';
|
||||
import { DictionariesModule } from 'src/dictionaries/dictionaries.module';
|
||||
import { InquiriesModule } from 'src/externals/inquiries/inquiries.module';
|
||||
import { SmsModule } from 'src/externals/sms/sms.module';
|
||||
import { SsoModule } from 'src/externals/sso/sso.module';
|
||||
import { ChatAttachmentsModule } from 'src/chat-attachments/chat-attachments.module';
|
||||
import { SupportManagementModule } from 'src/socket/support-management/support-management.module';
|
||||
import { UploadModule } from 'src/upload/upload.module';
|
||||
|
||||
export function getAppImports(): any[] {
|
||||
const modules = [
|
||||
DatabaseModule,
|
||||
StorageModule,
|
||||
AuthModule,
|
||||
AclModule,
|
||||
AiServiceModule,
|
||||
AiV2Module,
|
||||
SmsModule,
|
||||
SsoModule,
|
||||
InquiriesModule,
|
||||
UserModule,
|
||||
AdminModule,
|
||||
ClientManagementModule,
|
||||
UploadModule,
|
||||
DictionariesModule,
|
||||
CategoriesModule,
|
||||
];
|
||||
|
||||
if (process.env.SUPPORT_MANAGEMENT === 'true') {
|
||||
modules.push(SupportManagementModule);
|
||||
modules.push(ConversationModule);
|
||||
modules.push(ChatAttachmentsModule);
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
18
src/common/middlewares/security-headers.middleware.ts
Normal file
18
src/common/middlewares/security-headers.middleware.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class SecurityHeadersMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
// Clickjacking protection
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
|
||||
|
||||
// Other recommended security headers
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
53
src/common/middlewares/swagger-auth.middleware.ts
Normal file
53
src/common/middlewares/swagger-auth.middleware.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class SwaggerAuthMiddleware implements NestMiddleware {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
// Only protect Swagger endpoints
|
||||
if (req.path.startsWith('/docs')) {
|
||||
const swaggerUsername = this.configService.get<string>('SWAGGER_USERNAME');
|
||||
const swaggerPassword = this.configService.get<string>('SWAGGER_PASSWORD');
|
||||
|
||||
// If no credentials are set, allow access (optional protection)
|
||||
if (!swaggerUsername && !swaggerPassword) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// If only one is set, require both
|
||||
if (!swaggerUsername || !swaggerPassword) {
|
||||
return this.sendAuthRequired(res);
|
||||
}
|
||||
|
||||
// Extract Basic Auth credentials
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
return this.sendAuthRequired(res);
|
||||
}
|
||||
|
||||
// Decode Basic Auth
|
||||
const base64Credentials = authHeader.split(' ')[1];
|
||||
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
||||
const [username, password] = credentials.split(':');
|
||||
|
||||
// Validate both username and password
|
||||
if (username === swaggerUsername && password === swaggerPassword) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return this.sendAuthRequired(res);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
private sendAuthRequired(res: Response) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Swagger API Documentation"');
|
||||
res.status(401).send('Authentication required to access Swagger documentation');
|
||||
}
|
||||
}
|
||||
|
||||
201
src/common/services/audit-log.service.ts
Normal file
201
src/common/services/audit-log.service.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { AuditLogModel } from 'src/database/model/audit-log.model';
|
||||
import { Socket } from 'socket.io';
|
||||
|
||||
export interface AuditLogData {
|
||||
userId?: string;
|
||||
action: string;
|
||||
resource?: string;
|
||||
resourceId?: string;
|
||||
oldValues?: Record<string, any>;
|
||||
newValues?: Record<string, any>;
|
||||
changes?: Record<string, { from: any; to: any }>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
method?: string;
|
||||
endpoint?: string;
|
||||
statusCode?: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogService {
|
||||
constructor(
|
||||
@InjectModel(AuditLogModel.name)
|
||||
private readonly auditLogModel: Model<AuditLogModel>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Extract IP address and user agent from Socket.io client
|
||||
*/
|
||||
private extractClientInfo(client: Socket): { ipAddress?: string; userAgent?: string } {
|
||||
const ipAddress =
|
||||
(client.handshake.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
|
||||
(client.handshake.headers['x-real-ip'] as string) ||
|
||||
client.handshake.address ||
|
||||
client.conn?.remoteAddress ||
|
||||
undefined;
|
||||
|
||||
const userAgent =
|
||||
(client.handshake.headers['user-agent'] as string) || undefined;
|
||||
|
||||
return { ipAddress, userAgent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate changes between old and new values
|
||||
*/
|
||||
private calculateChanges(
|
||||
oldValues?: Record<string, any>,
|
||||
newValues?: Record<string, any>,
|
||||
): Record<string, { from: any; to: any }> | undefined {
|
||||
if (!oldValues || !newValues) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const changes: Record<string, { from: any; to: any }> = {};
|
||||
const allKeys = new Set([...Object.keys(oldValues), ...Object.keys(newValues)]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
const oldVal = oldValues[key];
|
||||
const newVal = newValues[key];
|
||||
|
||||
// Only include if values actually changed
|
||||
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
||||
changes[key] = { from: oldVal, to: newVal };
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(changes).length > 0 ? changes : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an audit event
|
||||
*/
|
||||
async log(data: AuditLogData, client?: Socket): Promise<void> {
|
||||
try {
|
||||
// Extract client info if socket is provided
|
||||
const clientInfo = client ? this.extractClientInfo(client) : {};
|
||||
|
||||
// Calculate changes if both old and new values are provided
|
||||
const changes = data.changes || this.calculateChanges(data.oldValues, data.newValues);
|
||||
|
||||
const auditLog = new this.auditLogModel({
|
||||
userId: data.userId ? (data.userId as any) : undefined,
|
||||
action: data.action,
|
||||
resource: data.resource,
|
||||
resourceId: data.resourceId,
|
||||
oldValues: data.oldValues,
|
||||
newValues: data.newValues,
|
||||
changes,
|
||||
ipAddress: data.ipAddress || clientInfo.ipAddress,
|
||||
userAgent: data.userAgent || clientInfo.userAgent,
|
||||
method: data.method,
|
||||
endpoint: data.endpoint,
|
||||
statusCode: data.statusCode,
|
||||
metadata: data.metadata,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
await auditLog.save();
|
||||
} catch (error) {
|
||||
// Don't throw errors - audit logging should not break the main flow
|
||||
console.error('Failed to save audit log:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a WebSocket event
|
||||
*/
|
||||
async logWebSocketEvent(
|
||||
action: string,
|
||||
client: Socket,
|
||||
options: {
|
||||
userId?: string;
|
||||
resource?: string;
|
||||
resourceId?: string;
|
||||
oldValues?: Record<string, any>;
|
||||
newValues?: Record<string, any>;
|
||||
statusCode?: number;
|
||||
metadata?: Record<string, any>;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const clientInfo = this.extractClientInfo(client);
|
||||
|
||||
await this.log(
|
||||
{
|
||||
action,
|
||||
resource: options.resource,
|
||||
resourceId: options.resourceId,
|
||||
oldValues: options.oldValues,
|
||||
newValues: options.newValues,
|
||||
ipAddress: clientInfo.ipAddress,
|
||||
userAgent: clientInfo.userAgent,
|
||||
method: action, // For WebSocket, method is the event name
|
||||
endpoint: options.resourceId ? `room:${options.resourceId}` : undefined,
|
||||
statusCode: options.statusCode || 200,
|
||||
metadata: options.metadata,
|
||||
userId: options.userId,
|
||||
},
|
||||
client,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an HTTP request (for future use)
|
||||
*/
|
||||
async logHttpRequest(
|
||||
action: string,
|
||||
req: any,
|
||||
options: {
|
||||
userId?: string;
|
||||
resource?: string;
|
||||
resourceId?: string;
|
||||
oldValues?: Record<string, any>;
|
||||
newValues?: Record<string, any>;
|
||||
statusCode?: number;
|
||||
metadata?: Record<string, any>;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const ipAddress =
|
||||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.ip ||
|
||||
req.connection?.remoteAddress ||
|
||||
undefined;
|
||||
|
||||
const userAgent = req.headers['user-agent'] || undefined;
|
||||
|
||||
await this.log({
|
||||
action,
|
||||
resource: options.resource,
|
||||
resourceId: options.resourceId,
|
||||
oldValues: options.oldValues,
|
||||
newValues: options.newValues,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
method: req.method,
|
||||
endpoint: req.path || req.url,
|
||||
statusCode: options.statusCode || 200,
|
||||
metadata: options.metadata,
|
||||
userId: options.userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
39
src/common/tools/encryption-helper.ts
Normal file
39
src/common/tools/encryption-helper.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
|
||||
export class EncryptionHelper {
|
||||
private static enc_algorithm = 'aes-256-cbc';
|
||||
private static enc_key = Buffer.from(
|
||||
'8l4VcgZ7b2pysZznW025123kjhUIOHGF',
|
||||
'utf8',
|
||||
);
|
||||
private static enc_iv = Buffer.from('-y/B?D(R+R;lQeTh', 'utf8');
|
||||
|
||||
public static initialize(algorithm: string, key: string, iv: string): void {
|
||||
EncryptionHelper.enc_algorithm = algorithm;
|
||||
EncryptionHelper.enc_key = Buffer.from(key, 'utf8');
|
||||
EncryptionHelper.enc_iv = Buffer.from(iv, 'utf8');
|
||||
}
|
||||
|
||||
public static encrypt(text): string {
|
||||
const cipher = crypto.createCipheriv(
|
||||
this.enc_algorithm,
|
||||
Buffer.from(this.enc_key),
|
||||
this.enc_iv,
|
||||
);
|
||||
let encrypted = cipher.update(text);
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
return encrypted.toString('hex');
|
||||
}
|
||||
|
||||
public static decrypt(text): string {
|
||||
const encryptedText = Buffer.from(text, 'hex');
|
||||
const decipher = crypto.createDecipheriv(
|
||||
this.enc_algorithm,
|
||||
Buffer.from(this.enc_key),
|
||||
this.enc_iv,
|
||||
);
|
||||
let decrypted = decipher.update(encryptedText);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
return decrypted.toString();
|
||||
}
|
||||
}
|
||||
95
src/common/tools/time-helper.ts
Normal file
95
src/common/tools/time-helper.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as jalaliMoment from 'moment-jalaali';
|
||||
|
||||
export class TimeHelper {
|
||||
public static unix2Date(unix, time = false) {
|
||||
let date = new Date(unix * 1000);
|
||||
if (time)
|
||||
return `${(date.getHours() + '').padStart(2, '0')}:${(date.getMinutes() + '').padStart(2, '0')}`;
|
||||
return date;
|
||||
}
|
||||
public static isValid(date, type = 'miladi') {
|
||||
if (type === 'miladi') {
|
||||
const valid = Date.parse(date);
|
||||
return !isNaN(valid) && valid > 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public static miladi2jalali(date, format = 'jYYYY-jMM-jDD') {
|
||||
if (!this.isValid(date)) return date;
|
||||
|
||||
date = jalaliMoment(date);
|
||||
return date.format(format);
|
||||
}
|
||||
|
||||
public static unix2PersianTimeAndDate(date) {
|
||||
date = this.unix2Date(date);
|
||||
|
||||
// Get time in HH:mm format
|
||||
let time = `${(date.getHours() + '').padStart(2, '0')}:${(date.getMinutes() + '').padStart(2, '0')}`;
|
||||
|
||||
// Convert to Jalali date with double-digit months and days
|
||||
let jalaliDate = this.miladi2jalali(date, 'jYYYY-jMM-jDD');
|
||||
|
||||
return [time, jalaliDate];
|
||||
}
|
||||
|
||||
public static iso2PersianTimeAndDate(isoDate: string | Date): [string, string] {
|
||||
const date = typeof isoDate === 'string' ? new Date(isoDate) : isoDate;
|
||||
|
||||
// Get time in HH:mm format
|
||||
let time = `${(date.getHours() + '').padStart(2, '0')}:${(date.getMinutes() + '').padStart(2, '0')}`;
|
||||
|
||||
// Convert to Jalali date with double-digit months and days
|
||||
let jalaliDate = this.miladi2jalali(date, 'jYYYY-jMM-jDD');
|
||||
|
||||
return [time, jalaliDate];
|
||||
}
|
||||
|
||||
public static readonly normalizeDate = (date: string) => {
|
||||
const [year, month, day] = date.split('/');
|
||||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
public static jalaliToISO(jalaliDate: string): string {
|
||||
// Convert Jalali date to Gregorian
|
||||
const [year, month, day] = jalaliDate.split('/');
|
||||
const gregorianDate = jalaliMoment(
|
||||
`${year}/${month}/${day}`,
|
||||
'jYYYY/jMM/jDD',
|
||||
).format('YYYY-MM-DD');
|
||||
|
||||
// Create a new date object with the converted date
|
||||
const date = new Date(gregorianDate);
|
||||
|
||||
// Format to ISO string
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
public static divideDateRange(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
): { start: string; end: string }[] {
|
||||
// Convert dates to moment objects
|
||||
const start = jalaliMoment(startDate, 'jYYYY/jMM/jDD');
|
||||
const end = jalaliMoment(endDate, 'jYYYY/jMM/jDD');
|
||||
|
||||
// Calculate total days
|
||||
const totalDays = end.diff(start, 'days');
|
||||
const segmentDays = Math.floor(totalDays / 5);
|
||||
|
||||
const segments: { start: string; end: string }[] = [];
|
||||
let currentStart = start.clone();
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const segmentEnd =
|
||||
i === 4 ? end : currentStart.clone().add(segmentDays, 'days');
|
||||
segments.push({
|
||||
start: currentStart.format('jYYYY/jMM/jDD'),
|
||||
end: segmentEnd.format('jYYYY/jMM/jDD'),
|
||||
});
|
||||
currentStart = segmentEnd.clone().add(1, 'days');
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
12
src/common/types/categories.type.ts
Normal file
12
src/common/types/categories.type.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export enum CategoriesEnum {
|
||||
carInsurance = 'carInsurance',
|
||||
liabilityInsurance = 'liabilityInsurance',
|
||||
healthInsurance = 'healthInsurance',
|
||||
lifeInsurance = 'lifeInsurance',
|
||||
motorcycleInsurance = 'motorcycleInsurance',
|
||||
fireInsurance = 'fireInsurance',
|
||||
travelInsurance = 'travelInsurance',
|
||||
transportInsurance = 'transportInsurance',
|
||||
incidentsInsurance = 'incidentsInsurance',
|
||||
general = 'general',
|
||||
}
|
||||
24
src/common/types/expert-prepared-message-category.type.ts
Normal file
24
src/common/types/expert-prepared-message-category.type.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export enum ExpertPreparedMessageCategory {
|
||||
Greetings = 'greetings',
|
||||
QuickAnswers = 'quick_answers',
|
||||
Closing = 'closing',
|
||||
FollowUp = 'follow_up',
|
||||
Apology = 'apology',
|
||||
Other = 'other',
|
||||
}
|
||||
|
||||
export const EXPERT_PREPARED_MESSAGE_CATEGORIES = Object.values(
|
||||
ExpertPreparedMessageCategory,
|
||||
);
|
||||
|
||||
export const EXPERT_PREPARED_MESSAGE_CATEGORY_LABELS: Record<
|
||||
ExpertPreparedMessageCategory,
|
||||
string
|
||||
> = {
|
||||
[ExpertPreparedMessageCategory.Greetings]: 'Greetings',
|
||||
[ExpertPreparedMessageCategory.QuickAnswers]: 'Quick answers',
|
||||
[ExpertPreparedMessageCategory.Closing]: 'Closing',
|
||||
[ExpertPreparedMessageCategory.FollowUp]: 'Follow-up',
|
||||
[ExpertPreparedMessageCategory.Apology]: 'Apology',
|
||||
[ExpertPreparedMessageCategory.Other]: 'Other',
|
||||
};
|
||||
4
src/common/types/expert.type.ts
Normal file
4
src/common/types/expert.type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface ExpertData {
|
||||
activeSessions: string;
|
||||
maxSessions: string;
|
||||
}
|
||||
6
src/common/types/func.type.ts
Normal file
6
src/common/types/func.type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum funcsEnum {
|
||||
create = 'create',
|
||||
update = 'update',
|
||||
replace = 'replace',
|
||||
}
|
||||
|
||||
153
src/common/types/permissions.catalog.ts
Normal file
153
src/common/types/permissions.catalog.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Role } from './role.type';
|
||||
|
||||
/**
|
||||
* Code-defined permission catalog (ADR 0001).
|
||||
* Hybrid: coarse modules + fine keys where defaults differ.
|
||||
*/
|
||||
export enum Permission {
|
||||
ProfileRead = 'profile.read',
|
||||
ProfileWrite = 'profile.write',
|
||||
|
||||
StaffCreateExpert = 'staff.create_expert',
|
||||
StaffCreateSupervisor = 'staff.create_supervisor',
|
||||
StaffCreateAdmin = 'staff.create_admin',
|
||||
|
||||
DashboardView = 'dashboard.view',
|
||||
|
||||
ConversationsOwn = 'conversations.own',
|
||||
ConversationsAll = 'conversations.all',
|
||||
ConversationsExport = 'conversations.export',
|
||||
ConversationsRate = 'conversations.rate',
|
||||
|
||||
UsersList = 'users.list',
|
||||
UsersExport = 'users.export',
|
||||
ExpertsList = 'experts.list',
|
||||
ExpertsReport = 'experts.report',
|
||||
|
||||
ReportsView = 'reports.view',
|
||||
ReportsExpertTiming = 'reports.expert_timing',
|
||||
|
||||
BusinessHoursManage = 'business_hours.manage',
|
||||
ClientsManage = 'clients.manage',
|
||||
CategoriesManage = 'categories.manage',
|
||||
|
||||
PreparedMessagesManage = 'prepared_messages.manage',
|
||||
PreparedMessagesRead = 'prepared_messages.read',
|
||||
|
||||
DictionariesRead = 'dictionaries.read',
|
||||
DictionariesWrite = 'dictionaries.write',
|
||||
DictionariesCreate = 'dictionaries.create',
|
||||
DictionariesApprove = 'dictionaries.approve',
|
||||
DictionariesAssets = 'dictionaries.assets',
|
||||
/** Admin-style direct question edit without modification request. */
|
||||
DictionariesDirectEdit = 'dictionaries.direct_edit',
|
||||
|
||||
AttachmentsExpert = 'attachments.expert',
|
||||
}
|
||||
|
||||
export const ALL_ASSIGNABLE_PERMISSIONS: readonly Permission[] =
|
||||
Object.values(Permission);
|
||||
|
||||
/** Permissions that may never appear on role templates or overrides. */
|
||||
export const OWNER_ONLY_META = {
|
||||
RolesManage: 'acl.roles.manage',
|
||||
OverridesManage: 'acl.overrides.manage',
|
||||
} as const;
|
||||
|
||||
export function isAssignablePermission(key: string): key is Permission {
|
||||
return (ALL_ASSIGNABLE_PERMISSIONS as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
export function assertAssignablePermissions(keys: string[]): Permission[] {
|
||||
const invalid = keys.filter((k) => !isAssignablePermission(k));
|
||||
if (invalid.length) {
|
||||
throw new BadRequestException(
|
||||
`Unknown or non-assignable permissions: ${invalid.join(', ')}`,
|
||||
);
|
||||
}
|
||||
return keys as Permission[];
|
||||
}
|
||||
|
||||
const ADMIN_DEFAULT: Permission[] = [
|
||||
Permission.ProfileRead,
|
||||
Permission.ProfileWrite,
|
||||
Permission.StaffCreateExpert,
|
||||
Permission.DashboardView,
|
||||
Permission.ConversationsAll,
|
||||
Permission.ConversationsExport,
|
||||
Permission.ConversationsRate,
|
||||
Permission.UsersList,
|
||||
Permission.UsersExport,
|
||||
Permission.ExpertsList,
|
||||
Permission.ExpertsReport,
|
||||
Permission.ReportsView,
|
||||
Permission.ReportsExpertTiming,
|
||||
Permission.BusinessHoursManage,
|
||||
Permission.ClientsManage,
|
||||
Permission.CategoriesManage,
|
||||
Permission.PreparedMessagesManage,
|
||||
Permission.DictionariesRead,
|
||||
Permission.DictionariesWrite,
|
||||
Permission.DictionariesCreate,
|
||||
Permission.DictionariesApprove,
|
||||
Permission.DictionariesAssets,
|
||||
Permission.DictionariesDirectEdit,
|
||||
];
|
||||
|
||||
const SUPERVISOR_DEFAULT: Permission[] = [
|
||||
Permission.ProfileRead,
|
||||
Permission.ProfileWrite,
|
||||
Permission.StaffCreateExpert,
|
||||
Permission.DashboardView,
|
||||
Permission.ConversationsAll,
|
||||
Permission.ConversationsExport,
|
||||
Permission.ConversationsRate,
|
||||
Permission.UsersList,
|
||||
Permission.UsersExport,
|
||||
Permission.ExpertsList,
|
||||
Permission.ExpertsReport,
|
||||
Permission.ReportsView,
|
||||
Permission.ReportsExpertTiming,
|
||||
Permission.BusinessHoursManage,
|
||||
Permission.ClientsManage,
|
||||
Permission.CategoriesManage,
|
||||
Permission.PreparedMessagesManage,
|
||||
];
|
||||
|
||||
const EXPERT_DEFAULT: Permission[] = [
|
||||
Permission.ProfileRead,
|
||||
Permission.ProfileWrite,
|
||||
Permission.ConversationsOwn,
|
||||
Permission.UsersList,
|
||||
Permission.UsersExport,
|
||||
Permission.ReportsExpertTiming,
|
||||
Permission.PreparedMessagesRead,
|
||||
Permission.DictionariesRead,
|
||||
Permission.DictionariesWrite,
|
||||
Permission.AttachmentsExpert,
|
||||
];
|
||||
|
||||
export const DEFAULT_ROLE_PERMISSIONS: Record<
|
||||
Role.Admin | Role.Supervisor | Role.Expert,
|
||||
Permission[]
|
||||
> = {
|
||||
[Role.Admin]: ADMIN_DEFAULT,
|
||||
[Role.Supervisor]: SUPERVISOR_DEFAULT,
|
||||
[Role.Expert]: EXPERT_DEFAULT,
|
||||
};
|
||||
|
||||
export function createPermissionForRole(
|
||||
role: string,
|
||||
): Permission | null {
|
||||
switch (role) {
|
||||
case Role.Expert:
|
||||
return Permission.StaffCreateExpert;
|
||||
case Role.Supervisor:
|
||||
return Permission.StaffCreateSupervisor;
|
||||
case Role.Admin:
|
||||
return Permission.StaffCreateAdmin;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
5
src/common/types/react.type.ts
Normal file
5
src/common/types/react.type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum ReactEnum {
|
||||
like = 'Like',
|
||||
dislike = 'Dislike',
|
||||
nothing = 'Nothing',
|
||||
}
|
||||
22
src/common/types/role.type.ts
Normal file
22
src/common/types/role.type.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export enum Role {
|
||||
User = 'user',
|
||||
Admin = 'admin',
|
||||
Expert = 'expert',
|
||||
Supervisor = 'supervisor',
|
||||
Owner = 'owner',
|
||||
}
|
||||
|
||||
/** System staff roles that cannot be deleted/renamed. */
|
||||
export const SYSTEM_STAFF_ROLES: readonly Role[] = [
|
||||
Role.Admin,
|
||||
Role.Expert,
|
||||
Role.Supervisor,
|
||||
] as const;
|
||||
|
||||
export function isOwnerRole(role: string | Role | undefined | null): boolean {
|
||||
return role === Role.Owner;
|
||||
}
|
||||
|
||||
export function isSystemStaffRole(role: string): boolean {
|
||||
return (SYSTEM_STAFF_ROLES as readonly string[]).includes(role);
|
||||
}
|
||||
5
src/common/types/sender.type.ts
Normal file
5
src/common/types/sender.type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum Sender {
|
||||
User = 'User',
|
||||
Bot = 'Bot',
|
||||
Expert = 'Expert',
|
||||
}
|
||||
Reference in New Issue
Block a user