Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backend: handle upload image #63

Merged
merged 16 commits into from
Dec 29, 2024
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
Loading