chore: add uploader and user profile picture

This commit is contained in:
mahyargdz
2025-07-24 12:37:08 +03:30
parent f42280846c
commit 5f4b679467
23 changed files with 1639 additions and 232 deletions
+2
View File
@@ -25,6 +25,7 @@ import { QuotaSyncModule } from "./modules/quota-sync/quota-sync.module";
import { SettingModule } from "./modules/settings/settings.module";
import { EmailSignaturesModule } from "./modules/signatures/email-signatures.module";
import { TemplatesModule } from "./modules/templates/templates.module";
import { UploaderModule } from "./modules/uploader/uploader.module";
import { UsersModule } from "./modules/users/users.module";
import { UtilsModule } from "./modules/utils/utils.module";
@@ -50,6 +51,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
TemplatesModule,
SettingModule,
NotificationModule,
UploaderModule,
],
})
export class AppModule implements NestModule {
@@ -1,9 +1,13 @@
import { UseGuards, applyDecorators } from "@nestjs/common";
import { ApiBearerAuth } from "@nestjs/swagger";
import { ApiBearerAuth, ApiHeader } from "@nestjs/swagger";
import { AdminRouteGuard } from "../../modules/auth/guards/admin.guard";
import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard";
export function AuthGuards() {
return applyDecorators(UseGuards(JwtAuthGuard, AdminRouteGuard), ApiBearerAuth("authorization"));
return applyDecorators(
UseGuards(JwtAuthGuard, AdminRouteGuard),
ApiBearerAuth("authorization"),
ApiHeader({ name: "x-business-id", description: "Business ID" }),
);
}
+3
View File
@@ -107,6 +107,9 @@ export const enum UserMessage {
FORWARDERS_EMAIL_VALID = "آدرس ایمیل فوروارد باید معتبر باشد",
FORWARDERS_ARRAY = "آدرس های فوروارد باید یک آرایه باشد",
FORWARDERS_STRING = "آدرس های فوروارد باید یک رشته باشد",
PROFILE_PICTURE_URL = "آدرس تصویر پروفایل باید یک آدرس اینترنتی معتبر باشد",
USER_UPDATED_SUCCESSFULLY = "کاربر با موفقیت به روز رسانی شد",
PROFILE_PICTURE_NOT_EMPTY = "تصویر پروفایل نمیتواند خالی باشد",
}
export const enum CommonMessage {
@@ -1,105 +1,15 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from "class-validator";
import { EmailLength, EmailPurpose, WritingTone } from "../enums/ai.enum";
export class EmailContextDto {
@IsOptional()
@IsString({ message: "Original email content must be a string" })
@ApiPropertyOptional({
description: "Original email content (for replies/forwards)",
example: "Hi, I need to discuss the project timeline...",
})
originalContent?: string;
@IsOptional()
@IsString({ message: "Original sender must be a string" })
@ApiPropertyOptional({
description: "Original sender (for replies/forwards)",
example: "john.doe@company.com",
})
originalSender?: string;
@IsOptional()
@IsString({ message: "Original subject must be a string" })
@ApiPropertyOptional({
description: "Original subject (for replies/forwards)",
example: "Project Timeline Discussion",
})
originalSubject?: string;
}
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
export class WriteEmailAssistanceDto {
@IsNotEmpty({ message: "Email purpose is required" })
@IsEnum(EmailPurpose, { message: "Email purpose must be one of: reply, forward, new, follow_up" })
@IsNotEmpty({ message: "Prompt is required" })
@IsString({ message: "Prompt must be a string" })
@ApiProperty({
description: "Purpose of the email",
enum: EmailPurpose,
example: EmailPurpose.REPLY,
description: "User prompt describing what kind of email they want to write",
example:
"Write a professional email to schedule a meeting with my team to discuss the Q4 project timeline. I want to address concerns about resource allocation and set a deadline for next Friday.",
})
purpose: EmailPurpose;
@IsNotEmpty({ message: "Main message or intent is required" })
@IsString({ message: "Main message must be a string" })
@ApiProperty({
description: "Main message or intent of the email",
example: "I want to schedule a meeting to discuss the project timeline and address any concerns.",
})
mainMessage: string;
@IsOptional()
@IsString({ message: "Recipient information must be a string" })
@ApiPropertyOptional({
description: "Recipient information (optional)",
example: "john.doe@company.com",
})
recipient?: string;
@IsOptional()
@IsEnum(WritingTone, { message: "Writing tone must be one of: formal, casual, professional, friendly, urgent" })
@ApiPropertyOptional({
description: "Desired tone of the email",
enum: WritingTone,
default: WritingTone.PROFESSIONAL,
})
tone?: WritingTone;
@IsOptional()
@IsEnum(EmailLength, { message: "Email length must be one of: short, medium, long" })
@ApiPropertyOptional({
description: "Desired length of the email",
enum: EmailLength,
default: EmailLength.MEDIUM,
})
length?: EmailLength;
@IsOptional()
@IsArray({ message: "Key points must be an array" })
@IsString({ each: true, message: "Each key point must be a string" })
@ApiPropertyOptional({
description: "Key points to include in the email",
type: [String],
example: ["Discuss project timeline", "Address resource allocation", "Set next meeting date"],
})
keyPoints?: string[];
@IsOptional()
@ValidateNested()
@Type(() => EmailContextDto)
@ApiPropertyOptional({
description: "Context from original email (for replies/forwards)",
type: EmailContextDto,
})
context?: EmailContextDto;
@IsOptional()
@IsString({ message: "Additional instructions must be a string" })
@ApiPropertyOptional({
description: "Additional instructions or preferences",
example: "Please keep it concise and mention the deadline",
})
additionalInstructions?: string;
prompt: string;
}
export class EmailSuggestionDto {
@@ -122,25 +32,3 @@ export class EmailSuggestionDto {
})
alternatives: string[];
}
export class EmailWritingResponseDto {
@ApiProperty({
description: "Primary email suggestion",
type: EmailSuggestionDto,
})
suggestion: EmailSuggestionDto;
@ApiProperty({
description: "Writing tips and recommendations",
type: [String],
example: ["Consider adding a specific deadline", "You might want to include meeting agenda", "Don't forget to CC relevant stakeholders"],
})
tips: string[];
@ApiProperty({
description: "Suggested improvements for the original request",
type: [String],
example: ["Consider being more specific about the timeline", "Add context about project urgency"],
})
improvements: string[];
}
@@ -8,8 +8,6 @@ import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { UserDec } from "../../common/decorators/user.decorator";
@Controller("ai-assistant")
// @ApiHeader({ name: "x-business-id", description: "Business ID" })
// @UseInterceptors(BusinessInterceptor)
@AuthGuards()
export class AiAssistantController {
constructor(private readonly aiAssistantService: AiAssistantService) {}
@@ -14,20 +14,6 @@ export interface SummarizationOptions {
language?: "Persian" | "English";
}
export interface WritingAssistanceOptions {
purpose: "reply" | "forward" | "new" | "follow_up";
tone: "formal" | "casual" | "professional" | "friendly" | "urgent";
length: "short" | "medium" | "long";
keyPoints?: string[];
context?: {
originalContent?: string;
originalSender?: string;
originalSubject?: string;
};
additionalInstructions?: string;
language?: "Persian" | "English";
}
export interface AISummaryResponse {
summary: string;
keyPoints: string[];
@@ -46,5 +32,5 @@ export interface AIWritingResponse {
export interface AIServiceInterface {
summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise<AIWritingResponse>;
assistEmailWriting(prompt: string): Promise<AIWritingResponse>;
}
@@ -8,7 +8,7 @@ import { WildDuckMessagesService } from "../../mail-server/services/wildduck-mes
import { SummarizeEmailDto } from "../DTO/summarize-email.dto";
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
import { SummaryLength, SummaryTone } from "../enums/ai.enum";
import { EmailData, SummarizationOptions, WritingAssistanceOptions } from "../interfaces/ai-service.interface";
import { EmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
@Injectable()
export class AiAssistantService {
@@ -132,22 +132,7 @@ export class AiAssistantService {
//************************************************* */
private async generateEmailWritingAssistance(writeAssistanceDto: WriteEmailAssistanceDto, _userId: string) {
const writingOptions: WritingAssistanceOptions = {
purpose: writeAssistanceDto.purpose.toLowerCase() as "reply" | "forward" | "new" | "follow_up",
tone: (writeAssistanceDto.tone?.toLowerCase() as "formal" | "casual" | "professional" | "friendly" | "urgent") || "professional",
length: (writeAssistanceDto.length?.toLowerCase() as "short" | "medium" | "long") || "medium",
keyPoints: writeAssistanceDto.keyPoints,
context: writeAssistanceDto.context
? {
originalContent: writeAssistanceDto.context.originalContent,
originalSender: writeAssistanceDto.context.originalSender,
originalSubject: writeAssistanceDto.context.originalSubject,
}
: undefined,
additionalInstructions: writeAssistanceDto.additionalInstructions,
};
const aiResponse = await this.aiService.assistEmailWriting(writeAssistanceDto.mainMessage, writingOptions);
const aiResponse = await this.aiService.assistEmailWriting(writeAssistanceDto.prompt);
return {
suggestion: {
+15 -60
View File
@@ -4,18 +4,15 @@ import OpenAI from "openai";
import { AI_CONFIG } from "../../../common/constants";
import { AiMessage } from "../../../common/enums/message.enum";
import { AIConfig } from "../../../configs/ai.config";
import {
AIServiceInterface,
AISummaryResponse,
AIWritingResponse,
EmailData,
SummarizationOptions,
WritingAssistanceOptions,
} from "../interfaces/ai-service.interface";
import { AIServiceInterface, AISummaryResponse, AIWritingResponse, EmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
@Injectable()
export class AiService implements AIServiceInterface {
private readonly openai: OpenAI;
private readonly language: "Persian" | "English" = "Persian";
private readonly defaultTone: "formal" | "casual" | "professional" = "professional";
private readonly defaultLength: "short" | "medium" | "detailed" = "medium";
private readonly defaultPurpose: "general" | "reply" | "forward" | "new" | "follow_up" = "general";
constructor(@Inject(AI_CONFIG) private readonly aiConfig: AIConfig) {
this.openai = new OpenAI({
@@ -25,6 +22,7 @@ export class AiService implements AIServiceInterface {
});
}
//************************************************* */
async summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise<AISummaryResponse> {
const prompt = this.buildSummarizationPrompt(emailData, options);
@@ -52,16 +50,15 @@ export class AiService implements AIServiceInterface {
return parsedResponse;
}
//************************************************* */
async assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise<AIWritingResponse> {
const prompt = this.buildWritingAssistancePrompt(mainMessage, options);
//************************************************* */
async assistEmailWriting(prompt: string): Promise<AIWritingResponse> {
const completion = await this.openai.chat.completions.create({
model: this.aiConfig.defaultModel,
messages: [
{
role: "system",
content: this.getWritingAssistanceSystemPrompt(options),
content: this.getWritingAssistanceSystemPrompt(),
},
{
role: "user",
@@ -80,24 +77,23 @@ export class AiService implements AIServiceInterface {
return parsedResponse;
}
//************************************************* */
private getSummarizationSystemPrompt(options: SummarizationOptions): string {
const language = options.language || "Persian";
const language = options.language || this.language;
const template = this.aiConfig.farsiPrompts.summarization.systemPrompt;
return template.replace("{tone}", options.tone).replace("{length}", options.length).replace("{language}", language);
}
//************************************************* */
private getWritingAssistanceSystemPrompt(options: WritingAssistanceOptions): string {
const language = options.language || "Persian";
private getWritingAssistanceSystemPrompt(): string {
const language = this.language;
const template = this.aiConfig.farsiPrompts.writingAssistance.systemPrompt;
return template
.replace("{tone}", options.tone)
.replace("{length}", options.length)
.replace("{purpose}", options.purpose)
.replace("{tone}", this.defaultTone)
.replace("{length}", this.defaultLength)
.replace("{purpose}", this.defaultPurpose)
.replace("{language}", language);
}
@@ -105,7 +101,6 @@ export class AiService implements AIServiceInterface {
private buildSummarizationPrompt(emailData: EmailData, options: SummarizationOptions): string {
const template = this.aiConfig.farsiPrompts.summarization.promptTemplate;
let prompt = `${template.intro}\n\n`;
console.log(emailData);
prompt += `${template.subjectLabel} ${emailData.subject}\n`;
prompt += `${template.fromLabel} ${emailData.from}\n`;
@@ -123,44 +118,4 @@ export class AiService implements AIServiceInterface {
return prompt;
}
//************************************************* */
private buildWritingAssistancePrompt(mainMessage: string, options: WritingAssistanceOptions): string {
const template = this.aiConfig.farsiPrompts.writingAssistance.promptTemplate;
let prompt = `${template.intro}\n\n`;
prompt += `${template.mainMessageLabel} ${mainMessage}\n\n`;
prompt += `${template.emailDetailsLabel}\n`;
prompt += `${template.purposeLabel} ${options.purpose}\n`;
prompt += `${template.toneLabel} ${options.tone}\n`;
prompt += `${template.lengthLabel} ${options.length}\n\n`;
if (options.keyPoints && options.keyPoints.length > 0) {
prompt += `${template.keyPointsLabel}\n`;
options.keyPoints.forEach((point, index) => {
prompt += `${index + 1}. ${point}\n`;
});
prompt += `\n`;
}
if (options.context) {
prompt += `${template.contextLabel}\n`;
if (options.context.originalSubject) {
prompt += `${template.originalSubjectLabel} ${options.context.originalSubject}\n`;
}
if (options.context.originalSender) {
prompt += `${template.originalSenderLabel} ${options.context.originalSender}\n`;
}
if (options.context.originalContent) {
prompt += `${template.originalContentLabel} ${options.context.originalContent}\n`;
}
prompt += `\n`;
}
if (options.additionalInstructions) {
prompt += `${template.additionalInstructionsLabel} ${options.additionalInstructions}\n\n`;
}
return prompt;
}
}
@@ -1,5 +1,5 @@
import { Body, Controller, Get, Param, Patch, Post, UseInterceptors } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { BusinessQuotaResponseDto } from "./DTO/business-quota-response.dto";
import { BusinessSlugParamDto } from "./DTO/business-slug-param.dto";
@@ -12,7 +12,6 @@ import { BusinessDec } from "../../common/decorators/business.decorator";
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
@Controller("business")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
export class BusinessesController {
constructor(private readonly businessesService: BusinessesService) {}
+1 -2
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateDomainDto } from "./DTO/create-domain.dto";
import { UpdateDomainDto } from "./DTO/update-domain.dto";
@@ -12,7 +12,6 @@ import { ParamDto } from "../../common/DTO/param.dto";
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
@Controller("domains")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
@UseInterceptors(BusinessInterceptor)
@AuthGuards()
export class DomainsController {
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateTemplateDto } from "./DTO/create-template.dto";
import { TemplateParamDto } from "./DTO/template-param.dto";
@@ -12,7 +12,6 @@ import { BusinessDec } from "../../common/decorators/business.decorator";
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
@Controller("templates")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
@UseInterceptors(BusinessInterceptor)
@AuthGuards()
export class TemplatesController {
+19
View File
@@ -0,0 +1,19 @@
import { File } from "@nest-lab/fastify-multer";
import { ApiProperty } from "@nestjs/swagger";
export class UploadSingleFileDto {
@ApiProperty({ type: "string", format: "binary", nullable: false })
file: File;
}
export class UploadMultipleFileDto {
@ApiProperty({
required: true,
type: "array",
items: {
type: "string",
format: "binary",
},
})
files: File[];
}
@@ -0,0 +1,37 @@
export interface S3UploadResult {
key: string;
url: string;
bucket: string;
}
export interface S3UploadOptions {
contentType: string;
metadata?: Record<string, string>;
acl?: "private" | "public-read" | "public-read-write";
}
export interface S3DownloadOptions {
expiresIn?: number;
responseContentType?: string;
responseContentDisposition?: string;
}
export interface S3FileInfo {
key: string;
bucket: string;
size: number;
lastModified: Date;
contentType: string;
etag: string;
}
export interface S3DeleteResult {
deleted: boolean;
key: string;
}
export interface S3ListResult {
files: S3FileInfo[];
continuationToken?: string;
isTruncated: boolean;
}
@@ -0,0 +1,129 @@
import { Readable } from "stream";
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { S3UploadResult } from "../interfaces/s3.interface";
@Injectable()
export class S3Service {
private readonly logger = new Logger(S3Service.name);
private readonly s3Client: S3Client;
private readonly bucketName: string;
private readonly region: string;
private readonly url: string;
constructor(private readonly configService: ConfigService) {
this.bucketName = this.configService.getOrThrow<string>("BUCKET_NAME");
this.region = this.configService.getOrThrow<string>("BUCKET_REGION");
this.url = this.configService.getOrThrow<string>("BUCKET_URL");
this.s3Client = new S3Client({
region: this.region,
endpoint: this.url,
credentials: {
accessKeyId: this.configService.getOrThrow<string>("BUCKET_ACCESS_KEY"),
secretAccessKey: this.configService.getOrThrow<string>("BUCKET_SECRET_KEY"),
},
});
}
/**
* Upload file to S3
*/
async uploadFile(buffer: Buffer, key: string, contentType: string, metadata?: Record<string, string>): Promise<S3UploadResult> {
try {
const command = new PutObjectCommand({
Bucket: this.bucketName,
Key: key,
Body: buffer,
ACL: "public-read",
ContentType: contentType,
Metadata: metadata,
});
await this.s3Client.send(command);
// const url = `https://${this.bucketName}.s3.${this.region}.amazonaws.com/${key}`;
const url = `https://${this.bucketName}.s3.ir-thr-at1.arvanstorage.ir/${key}`;
this.logger.log(`File uploaded to S3: ${key}`);
return {
key,
url,
bucket: this.bucketName,
};
} catch (error: unknown) {
this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Get file stream from S3
*/
async getFileStream(key: string): Promise<Readable> {
try {
const command = new GetObjectCommand({
Bucket: this.bucketName,
Key: key,
});
const response = await this.s3Client.send(command);
return response.Body as Readable;
} catch (error: unknown) {
this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Generate presigned URL for file download
*/
async getPresignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
try {
const command = new GetObjectCommand({
Bucket: this.bucketName,
Key: key,
});
return await getSignedUrl(this.s3Client, command, { expiresIn });
} catch (error: unknown) {
this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Delete file from S3
*/
async deleteFile(key: string): Promise<void> {
try {
const command = new DeleteObjectCommand({
Bucket: this.bucketName,
Key: key,
});
await this.s3Client.send(command);
this.logger.log(`File deleted from S3: ${key}`);
} catch (error: unknown) {
this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Generate S3 key for file
*/
generateFileKey(originalName: string): string {
const timestamp = Date.now();
const randomSuffix = Math.round(Math.random() * 1e9);
const extension = originalName.split(".").pop();
const basePath = `images`;
return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
}
}
@@ -0,0 +1,109 @@
import { File } from "@nest-lab/fastify-multer";
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { S3Service } from "./s3.service";
import { UploaderMessage } from "../../../common/enums/message.enum";
@Injectable()
export class UploaderService {
private readonly logger = new Logger(UploaderService.name);
private readonly maxFileSize: number;
private readonly allowedMimeTypes: string[];
constructor(
private readonly configService: ConfigService,
private readonly s3Service: S3Service,
) {
this.maxFileSize = this.configService.get("MAX_FILE_SIZE", 1024 * 1024 * 100); // 100MB
this.allowedMimeTypes = [
"video/mp4",
"video/webm",
"video/ogg",
"video/avi",
"video/mov",
"video/mkv",
"audio/mp3",
"audio/wav",
"audio/ogg",
"audio/webm",
"audio/m4a",
"audio/aac",
"audio/flac",
"audio/m4a",
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/svg+xml",
"image/tiff",
"image/bmp",
];
}
async uploadMultiple(files: File[]) {
return await Promise.all(files.map((file) => this.uploadFile(file)));
}
/**
* Handle file upload
*/
async uploadFile(file: File) {
if (!file || !file.buffer || !file.size) {
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
}
this.logger.log(`Uploading file: ${file.originalname}`);
// Validate file
this.validateFile(file);
// Determine file type
this.determineFileType(file.mimetype);
// Generate S3 key
const s3Key = this.s3Service.generateFileKey(file.originalname);
// Upload to S3
const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, {
originalName: file.originalname,
uploadedBy: "admin",
});
this.logger.log(`File uploaded successfully: `);
return {
file: s3Result,
url: s3Result.url,
};
}
/**
* Validate uploaded file
*/
private validateFile(file: File): void {
if (!file) throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
if (file.size && file.size > this.maxFileSize) throw new BadRequestException(`File too large. Maximum size is ${this.maxFileSize} bytes`);
if (!this.allowedMimeTypes.includes(file.mimetype)) throw new BadRequestException(`File type not supported: ${file.mimetype}`);
}
/**
* Determine file type based on mimetype
*/
private determineFileType(mimetype: string) {
if (mimetype.startsWith("video/")) {
return "video";
} else if (mimetype.startsWith("audio/")) {
return "audio";
} else if (mimetype.startsWith("image/")) {
return "image";
} else if (mimetype.startsWith("application/")) {
return "application";
} else if (mimetype.startsWith("text/")) {
return "text";
}
// Default to video for unknown types
return "video";
}
}
@@ -0,0 +1,31 @@
import { File, FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer";
import { Controller, Post, UploadedFile, UploadedFiles, UseInterceptors } from "@nestjs/common";
import { ApiBody, ApiConsumes, ApiOperation } from "@nestjs/swagger";
import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto";
import { UploaderService } from "./providers/uploader.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
@Controller("uploader")
@AuthGuards()
export class UploaderController {
constructor(private readonly uploaderService: UploaderService) {}
@ApiOperation({ summary: "Upload a file (admin)" })
@ApiConsumes("multipart/form-data")
@UseInterceptors(FileInterceptor("file"))
@ApiBody({ type: UploadSingleFileDto })
@Post("single-file")
async uploadFile(@UploadedFile() file: File) {
return this.uploaderService.uploadFile(file);
}
@ApiOperation({ summary: "Uploads multiple files" })
@ApiConsumes("multipart/form-data")
@UseInterceptors(FilesInterceptor("files"))
@ApiBody({ type: UploadMultipleFileDto })
@Post("multi-file")
uploadFiles(@UploadedFiles() files: File[]) {
return this.uploaderService.uploadMultiple(files);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { S3Service } from "./providers/s3.service";
import { UploaderService } from "./providers/uploader.service";
import { UploaderController } from "./uploader.controller";
@Module({
controllers: [UploaderController],
providers: [UploaderService, S3Service],
exports: [UploaderService],
})
export class UploaderModule {}
@@ -0,0 +1,12 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsUrl } from "class-validator";
import { UserMessage } from "../../../common/enums/message.enum";
export class UpdateUserProfileDto {
@IsOptional()
@IsNotEmpty({ message: UserMessage.PROFILE_PICTURE_NOT_EMPTY })
@IsUrl({ protocols: ["https"] }, { message: UserMessage.PROFILE_PICTURE_URL })
@ApiPropertyOptional({ description: "Profile picture URL", example: "https://example.com/profile.jpg" })
profilePic?: string;
}
@@ -20,6 +20,9 @@ export class User extends BaseEntity {
@Property({ type: "varchar", length: 150, nullable: false })
password!: string;
@Property({ type: "varchar", length: 255, nullable: true })
profilePic?: string;
@Property({ type: "varchar", length: 255, nullable: false })
emailAddress!: string;
@@ -16,6 +16,7 @@ import { Template } from "../../templates/entities/template.entity";
import { PasswordService } from "../../utils/services/password.service";
import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
import { UpdateEmailUserDto } from "../DTO/update-email-user.dto";
import { UpdateUserProfileDto } from "../DTO/update-user-profile.dto";
import { UserListQueryDto } from "../DTO/user-list-query.dto";
import { Role } from "../entities/role.entity";
import { User } from "../entities/user.entity";
@@ -80,6 +81,16 @@ export class UsersService {
};
}
/*******************************/
async updateProfile(userId: string, updateMeDto: UpdateUserProfileDto) {
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
this.userRepository.assign(user, updateMeDto);
await this.em.flush();
return { message: UserMessage.USER_UPDATED_SUCCESSFULLY, user };
}
/*******************************/
async findOneByIdWithEntityManager(id: string, em: EntityManager) {
const user = await em.findOne(User, { id }, { populate: ["role"] });
+17 -10
View File
@@ -1,8 +1,9 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateEmailUserDto } from "./DTO/create-email-user.dto";
import { UpdateEmailUserDto } from "./DTO/update-email-user.dto";
import { UpdateUserProfileDto } from "./DTO/update-user-profile.dto";
import { UserListQueryDto } from "./DTO/user-list-query.dto";
import { UsersService } from "./services/users.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
@@ -12,7 +13,6 @@ import { ParamDto } from "../../common/DTO/param.dto";
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
@Controller("users")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
@AuthGuards()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@@ -24,6 +24,21 @@ export class UsersController {
return this.usersService.getMe(userId);
}
@Patch(":id")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user" })
@ApiResponse({ status: 200, description: "Email user updated successfully" })
updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) {
return this.usersService.updateEmailUser(paramDto.id, businessId, body);
}
@Patch("profile")
@ApiOperation({ summary: "Update current user" })
@ApiResponse({ status: 200, description: "Current user updated successfully" })
updateProfile(@UserDec("id") userId: string, @Body() updateMeDto: UpdateUserProfileDto) {
return this.usersService.updateProfile(userId, updateMeDto);
}
@Post()
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Create a new email user" })
@@ -48,14 +63,6 @@ export class UsersController {
return this.usersService.deleteEmailUser(paramDto.id, businessId);
}
@Patch(":id")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user" })
@ApiResponse({ status: 200, description: "Email user updated successfully" })
updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) {
return this.usersService.updateEmailUser(paramDto.id, businessId, body);
}
@Patch(":id/quota")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user quota" })