Skip to content

Commit

Permalink
Merge pull request #62 from bakaqc/feature/handle-upload-image_chuong
Browse files Browse the repository at this point in the history
Feature/handle upload image
  • Loading branch information
bakaqc authored Dec 29, 2024
2 parents 05f8c2a + 6dfa0e7 commit e3b22cb
Show file tree
Hide file tree
Showing 13 changed files with 223 additions and 7 deletions.
6 changes: 5 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "evora-backend",
"version": "1.3.2",
"version": "1.4.3",
"description": "Evora connects customers with event organizers quickly through detailed online booking.",
"repository": "https://github.com/bakaqc/evora-17c",
"author": "Quốc Chương",
Expand Down Expand Up @@ -29,10 +29,13 @@
"axios": "1.7.9",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"cloudinary": "2.5.1",
"date-fns": "4.1.0",
"module-alias": "2.2.3",
"mongoose": "8.9.1",
"morgan": "1.10.0",
"multer": "1.4.5-lts.1",
"multer-storage-cloudinary": "4.0.0",
"nodemailer": "6.9.16",
"passport": "0.7.0",
"passport-jwt": "4.0.1",
Expand All @@ -46,6 +49,7 @@
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/express": "4.17.21",
"@types/morgan": "1.9.9",
"@types/multer": "1.4.12",
"@types/node": "20.16.7",
"@types/passport-jwt": "4.0.1",
"@types/passport-local": "1.0.38",
Expand Down
61 changes: 61 additions & 0 deletions backend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MongooseModule } from '@nestjs/mongoose';
import corsConfig from '@/config/cors.config';
import jwtConfig from '@/domains/auth/config/jwt.config';
import { DomainsModule } from '@/domains/domains.module';
import { configureCloudinary } from '@/domains/upload/config/cloudinary.config';
import { MorganMiddleware } from '@/middlewares/morgan.middleware';

@Module({
Expand All @@ -24,4 +25,8 @@ export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(MorganMiddleware).forRoutes('*');
}

constructor(private readonly configService: ConfigService) {
configureCloudinary(this.configService);
}
}
9 changes: 9 additions & 0 deletions backend/src/domains/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export class AuthService {

async login(user: any) {
const payload = { email: user.email, role: user.role, sub: user._id };

const findUser = await this.userModel.findOne({ email: user.email });

if (!findUser.isVerified) {
throw new UnauthorizedException(
'Your account is not verified. Please verify your email.',
);
}

return {
access_token: this.jwtService.sign(payload),
};
Expand Down
3 changes: 3 additions & 0 deletions backend/src/domains/auth/otp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export class OtpService {

user.verificationCode = null;
user.verificationCodeExpires = null;
if (!user.isVerified) {
user.isVerified = true;
}

await user.save();

Expand Down
2 changes: 2 additions & 0 deletions backend/src/domains/domains.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { NotifiesModule } from '@/domains/notifies/notifies.module';
import { PartiesModule } from '@/domains/parties/parties.module';
import { PaymentsModule } from '@/domains/payments/payments.module';
import { ReviewsModule } from '@/domains/reviews/reviews.module';
import { UploadModule } from '@/domains/upload/upload.module';
import { UsersModule } from '@/domains/users/users.module';
import { VouchersModule } from '@/domains/vouchers/vouchers.module';

Expand All @@ -21,6 +22,7 @@ import { VouchersModule } from '@/domains/vouchers/vouchers.module';
PaymentsModule,
ReviewsModule,
UsersModule,
UploadModule,
VouchersModule,
],
providers: [
Expand Down
12 changes: 12 additions & 0 deletions backend/src/domains/upload/config/cloudinary.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ConfigService } from '@nestjs/config';
import { v2 as cloudinary } from 'cloudinary';

export const configureCloudinary = (configService: ConfigService) => {
cloudinary.config({
cloud_name: configService.get<string>('CLOUDINARY_CLOUD_NAME'),
api_key: configService.get<string>('CLOUDINARY_API_KEY'),
api_secret: configService.get<string>('CLOUDINARY_API_SECRET'),
});
};

export { cloudinary };
15 changes: 15 additions & 0 deletions backend/src/domains/upload/config/multer.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as multer from 'multer';

export const multerOptions = {
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024,
},
fileFilter: (req, file, cb) => {
if (!file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) {
cb(new Error('Only image files are allowed!'), false);
} else {
cb(null, true);
}
},
};
61 changes: 61 additions & 0 deletions backend/src/domains/upload/upload.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
BadRequestException,
Controller,
Post,
UploadedFiles,
UseInterceptors,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';

import { multerOptions } from '@/domains/upload/config/multer.config';
import { UploadService } from '@/domains/upload/upload.service';

@ApiTags('Upload')
@ApiBearerAuth()
@Controller('upload')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}

@Post()
@ApiConsumes('multipart/form-data')
@ApiBody({
description: 'File upload (only image files are allowed)',
schema: {
type: 'object',
properties: {
files: {
type: 'array',
items: {
type: 'string',
format: 'binary',
},
},
},
},
})
@UseInterceptors(FilesInterceptor('files', 20, multerOptions))
async uploadFiles(@UploadedFiles() files: Express.Multer.File[]) {
if (!files || files.length === 0) {
throw new BadRequestException('At least one file is required');
}

try {
const uploadResults = await Promise.all(
files.map((file) => this.uploadService.uploadImage(file)),
);

return {
message: 'Files uploaded successfully',
uploads: uploadResults.map((result) => ({
url: result.secure_url,
public_id: result.public_id,
})),
};
} catch (error) {
throw new BadRequestException(
`Failed to upload files: ${error.message || 'Unknown error'}`,
);
}
}
}
11 changes: 11 additions & 0 deletions backend/src/domains/upload/upload.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';

import { UploadController } from '@/domains/upload/upload.controller';
import { UploadService } from '@/domains/upload/upload.service';

@Module({
controllers: [UploadController],
providers: [UploadService],
exports: [UploadService],
})
export class UploadModule {}
38 changes: 38 additions & 0 deletions backend/src/domains/upload/upload.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Injectable, Logger } from '@nestjs/common';

import { cloudinary } from '@/domains/upload/config/cloudinary.config';

@Injectable()
export class UploadService {
private readonly logger = new Logger(UploadService.name);

async uploadImage(file: Express.Multer.File): Promise<any> {
try {
const result = await new Promise((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream(
{ folder: process.env.CLOUDINARY_FOLDER_STORAGE || 'default_folder' },
(error, result) => {
if (error) {
const err = new Error(
`Error uploading image to Cloudinary: ${error.message}`,
);
this.logger.error(err.message, error.stack);
reject(err);
} else {
this.logger.log('Image uploaded to Cloudinary successfully');
resolve(result);
}
},
);
uploadStream.end(file.buffer);
});
return result;
} catch (error) {
this.logger.error(
`Error uploading image to Cloudinary: ${error.message}`,
error.stack,
);
throw error;
}
}
}
5 changes: 0 additions & 5 deletions backend/src/domains/users/dto/createUser.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,4 @@ export class CreateUserDto {
@IsIn(['male', 'female'])
@IsNotEmpty()
gender: string;

@ApiProperty({ description: 'User avatar' })
@IsString()
@IsNotEmpty()
avatar: string;
}
2 changes: 1 addition & 1 deletion backend/src/schemas/user.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class User {
@Prop({ required: true })
gender: 'male' | 'female';

@Prop({ required: true })
@Prop({ required: false, default: () => process.env.AVATAR_DEFAULT })
avatar: string;

@Prop({ required: true, default: 'user' })
Expand Down

0 comments on commit e3b22cb

Please sign in to comment.