Skip to content

Commit

Permalink
update: object storage added to movie api with payment gateway.
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilipOyelegbin committed Dec 11, 2024
1 parent e8ba757 commit 8aaa170
Show file tree
Hide file tree
Showing 16 changed files with 3,431 additions and 1,367 deletions.
4,509 changes: 3,214 additions & 1,295 deletions movie_api/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions movie_api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"seed": "ts-node prisma/seed.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.705.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
Expand All @@ -32,6 +33,7 @@
"@nestjs/swagger": "^8.0.7",
"@prisma/client": "^5.22.0",
"argon2": "^0.41.1",
"aws-sdk": "^2.1692.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"passport": "^0.7.0",
Expand All @@ -46,6 +48,7 @@
"@nestjs/testing": "^10.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0",
Expand Down
31 changes: 31 additions & 0 deletions movie_api/src/auth/middleware/auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,34 @@ export class ReserviationRoleAuth implements NestMiddleware {
next();
}
}

// middleware for reservation role based authorization
export class PaymentRoleAuth implements NestMiddleware {
constructor(
private jwt: JwtService,
private prisma: PrismaService,
) {}

async use(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
const bearerToken = authHeader?.split(' ')[1];
if (!bearerToken) {
throw new BadRequestException('Token must be provided');
}

const decoded = await this.jwt.verify(bearerToken);
const user = await this.prisma.user.findUnique({
where: { id: decoded.sub },
});
delete user.password;

if (user.role === 'Customer') {
if (req.method !== 'GET') {
throw new ForbiddenException('Customers can only perform GET requests');
}
}

req.user = user;
next();
}
}
2 changes: 1 addition & 1 deletion movie_api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ async function bootstrap() {
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('/', app, documentFactory);

await app.listen(process.env.PORT ?? 3030);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
11 changes: 5 additions & 6 deletions movie_api/src/movie/dto/create-movie.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ export class CreateMovieDto {
@IsOptional()
description: string;

@ApiProperty({
description: 'The cover image of the movie',
example:
'https://image.tmdb.org/t/p/w500/q6725a94750c802b1ca5c3a576016e37.jpg',
@ApiPropertyOptional({
description: 'The cover image of the movie must be a file upload',
example: '/path_to_file',
})
@IsNotEmpty({ message: 'The cover image can not be blank' })
@IsString({ message: 'The cover image must be a string' })
@IsOptional()
@IsString({ message: 'The cover image must be a string of file path' })
cover_image: string;

@ApiProperty({
Expand Down
32 changes: 28 additions & 4 deletions movie_api/src/movie/movie.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import {
Patch,
Param,
Delete,
UseInterceptors,
UploadedFile,
ParseFilePipe,
MaxFileSizeValidator,
FileTypeValidator,
} from '@nestjs/common';
import { MovieService } from './movie.service';
import { CreateMovieDto, UpdateMovieDto } from './dto';
Expand All @@ -15,6 +20,7 @@ import {
ApiParam,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';

@ApiBearerAuth()
@ApiUnauthorizedResponse({
Expand All @@ -26,8 +32,20 @@ export class MovieController {
constructor(private readonly movieService: MovieService) {}

@Post()
create(@Body() dto: CreateMovieDto) {
return this.movieService.create(dto);
@UseInterceptors(FileInterceptor('cover_image'))
create(
@Body() dto: CreateMovieDto,
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 10000000 }),
new FileTypeValidator({ fileType: 'image/*' }),
],
}),
)
file: Express.Multer.File,
) {
return this.movieService.create(dto, file.originalname, file.buffer);
}

@Get()
Expand All @@ -43,8 +61,14 @@ export class MovieController {

@ApiParam({ name: 'id' })
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateMovieDto) {
return this.movieService.update(id, dto);
@UseInterceptors(FileInterceptor('cover_image'))
update(
@Param('id') id: string,
@Body() dto: UpdateMovieDto,
@UploadedFile()
file: Express.Multer.File,
) {
return this.movieService.update(id, dto, file?.originalname, file?.buffer);
}

@ApiParam({ name: 'id' })
Expand Down
4 changes: 2 additions & 2 deletions movie_api/src/movie/movie.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
} from '@nestjs/common';
import { MovieService } from './movie.service';
import { MovieController } from './movie.controller';
import { MovieRoleAuth } from 'src/auth/middleware';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from 'src/auth/strategy';
import { JwtStrategy } from '../auth/strategy/jwt.strategy';
import { MovieRoleAuth } from '../auth/middleware/auth.middleware';

@Module({
imports: [
Expand Down
118 changes: 106 additions & 12 deletions movie_api/src/movie/movie.service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateMovieDto, UpdateMovieDto } from './dto';
import { PrismaService } from 'src/prisma/prisma.service';
import {
DeleteObjectCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class MovieService {
constructor(private prisma: PrismaService) {}
private readonly s3Client = new S3Client({
region: 'auto',
endpoint: this.configService.getOrThrow('S3_API'),
credentials: {
accessKeyId: this.configService.getOrThrow('ACCESS_KEY_ID'),
secretAccessKey: this.configService.getOrThrow('SECRET_ACCESS_KEY'),
},
});

constructor(
private prisma: PrismaService,
private configService: ConfigService,
) {}

// function to add a movie to the database
async create(dto: CreateMovieDto) {
async create(dto: CreateMovieDto, fileName: string, file: Buffer) {
try {
const resp = await this.s3Client.send(

Check failure on line 30 in movie_api/src/movie/movie.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test (18.x)

'resp' is assigned a value but never used

Check failure on line 30 in movie_api/src/movie/movie.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test (20.x)

'resp' is assigned a value but never used
new PutObjectCommand({
Bucket: this.configService.getOrThrow('BUCKET_NAME'),
Key: fileName,
Body: file,
}),
);

const movie = await this.prisma.movie.create({
data: {
title: dto.title,
description: dto.description,
cover_image: dto.cover_image,
cover_image: fileName,
show_time: dto.show_time,
},
});
Expand All @@ -28,7 +54,16 @@ export class MovieService {
async findAll() {
try {
const movie = await this.prisma.movie.findMany();
return movie;

const moviesWithImage = movie.map((list) => {
const cover_image = `${process.env.S3_API}/${list.cover_image}`;
return {
...list,
cover_image,
};
});

return moviesWithImage;
} catch (error) {
throw error;
}
Expand All @@ -43,25 +78,77 @@ export class MovieService {
'Movie with the provdied ID does not exist.',
);

return movie;
const cover_image = `${process.env.S3_API}/${movie.cover_image}`;

return { ...movie, cover_image };
} catch (error) {
throw error;
}
}

// function to update a movie in the database
async update(id: string, dto: UpdateMovieDto) {
async update(
id: string,
dto: UpdateMovieDto,
fileName?: string,
file?: Buffer,
) {
try {
const movie = await this.prisma.movie.update({
const existingMovie = await this.prisma.movie.findUnique({
where: { id },
data: <any>{ ...dto },
});
if (!movie)

if (!existingMovie) {
throw new NotFoundException(
'Movie with the provdied ID does not exist.',
'Movie with the provided ID does not exist.',
);
}

return movie;
let movieImage = existingMovie.cover_image || null;

// If a new file is provided, handle the image upload and deletion
if (fileName && file) {
// Upload the new image
await this.s3Client.send(
new PutObjectCommand({
Bucket: this.configService.getOrThrow('BUCKET_NAME'),
Key: fileName,
Body: file,
}),
);

// Delete the old image from Cloudflare R2 if it exists
if (movieImage) {
await this.s3Client.send(
new DeleteObjectCommand({
Bucket: this.configService.getOrThrow('BUCKET_NAME'),
Key: movieImage,
}),
);
}

// Update the movie image filename
movieImage = fileName;
}

// Update the movie record in the database
const updatedMovie = await this.prisma.movie.update({
where: { id },
data: <any>{
...dto,
cover_image: movieImage,
},
});

// Construct the full movie image URL
const fullMovieImage = updatedMovie.cover_image
? `${process.env.S3_API}/${updatedMovie.cover_image}`
: null;

return {
...updatedMovie,
cover_image: fullMovieImage,
};
} catch (error) {
throw error;
}
Expand All @@ -76,6 +163,13 @@ export class MovieService {
'Movie with the provdied ID does not exist.',
);

await this.s3Client.send(
new DeleteObjectCommand({
Bucket: this.configService.getOrThrow('BUCKET_NAME'),
Key: movie.cover_image,
}),
);

return { message: 'Movie deleted.' };
} catch (error) {
throw error;
Expand Down
22 changes: 22 additions & 0 deletions movie_api/src/payment/dto/create-payment.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';

export class CreatePaymentDto {
@ApiProperty({ description: 'The amount of the order', example: 500 })
@IsNumber({}, { message: 'The amount must be a number' })
@IsNotEmpty({ message: 'The amount must not be empty' })
amount: number;

@ApiProperty({
description: 'The name of the order',
example: 'The Shawshank Redemption',
})
@IsString({ message: 'The name must be a string' })
@IsNotEmpty({ message: 'The name must not be empty' })
name: string;

@ApiProperty({
description: 'The description of the order',
example:
'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.',
})
@IsString({ message: 'The description must be a string' })
@IsNotEmpty({ message: 'The description must not be empty' })
description: string;

@ApiProperty({ description: 'The quantity of the order', example: 2 })
@IsNumber({}, { message: 'The quantity must be a number' })
@IsNotEmpty({ message: 'The quantity must not be empty' })
quantity: number;
}
1 change: 1 addition & 0 deletions movie_api/src/payment/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './create-payment.dto';
4 changes: 0 additions & 4 deletions movie_api/src/payment/dto/update-payment.dto.ts

This file was deleted.

1 change: 0 additions & 1 deletion movie_api/src/payment/entities/payment.entity.ts

This file was deleted.

Loading

0 comments on commit 8aaa170

Please sign in to comment.