Skip to content

Commit

Permalink
add admin guard, implemented ice cream creation with tests (#8)
Browse files Browse the repository at this point in the history
Co-authored-by: Dominik Maćkiewicz <[email protected]>
  • Loading branch information
mcdominik and Dominik Maćkiewicz authored Jan 25, 2024
1 parent 752b283 commit 12f5c9f
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 20 deletions.
2 changes: 1 addition & 1 deletion src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { compare } from 'bcryptjs';
import { firstValueFrom } from 'rxjs';
import { AccountType, User } from 'src/users/entities/user.entity';
import { AccountType, Role, User } from 'src/users/entities/user.entity';
import { UsersService } from 'src/users/users.service';
import { GoogleResponse } from './models/GoogleModels';

Expand Down
26 changes: 26 additions & 0 deletions src/ice-creams/guards/admin.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
Injectable,
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Role } from 'src/users/entities/user.entity';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class AdminGuard implements CanActivate {
constructor(private readonly usersService: UsersService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();

const userId = request.user.id;
const user = await this.usersService.getOneById(userId);

if (user.role === Role.ADMIN) {
return true;
} else {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
}
}
22 changes: 17 additions & 5 deletions src/ice-creams/ice-creams.controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { Controller, Get, Param, UseInterceptors, Query } from '@nestjs/common';
import {
Controller,
Get,
Param,
UseInterceptors,
Query,
Body,
Post,
UseGuards,
} from '@nestjs/common';
import { IceCreamsService } from './ice-creams.service';
import { CreateIceCreamDto } from './dto/create-ice-cream.dto';
import { SearchQueryDto } from './dto/search-query.dto';
import { CacheInterceptor } from '@nestjs/cache-manager';
import { AdminGuard } from './guards/admin.guard';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';

@UseInterceptors(CacheInterceptor)
@Controller('ice-creams')
export class IceCreamsController {
constructor(private readonly iceCreamsService: IceCreamsService) {}

// @Post('add')
// addNew(@Body() dto: CreateIceCreamDto) {
// return this.iceCreamsService.addNew(dto);
// }
@UseGuards(JwtAuthGuard, AdminGuard)
@Post('add')
addNew(@Body() dto: CreateIceCreamDto) {
return this.iceCreamsService.addNew(dto);
}

@Get()
findAndSortWithPagination(@Query() dto: SearchQueryDto) {
Expand Down
2 changes: 2 additions & 0 deletions src/ice-creams/ice-creams.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { IceCreamsService } from './ice-creams.service';
import { IceCreamsController } from './ice-creams.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { IceCreamSchema, IceCream } from './entities/ice-cream.entity';
import { UsersModule } from 'src/users/users.module';

@Module({
imports: [
MongooseModule.forFeature([
{ name: IceCream.name, schema: IceCreamSchema },
]),
UsersModule,
],
controllers: [IceCreamsController],
providers: [IceCreamsService],
Expand Down
2 changes: 1 addition & 1 deletion src/ice-creams/ice-creams.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class IceCreamsService {

async addNew(dto: CreateIceCreamDto) {
const newIceCream = new this.iceCreamModel(dto);
await newIceCream.save();
return await newIceCream.save();
}

async getOneById(iceCreamId: string) {
Expand Down
6 changes: 3 additions & 3 deletions src/users/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { AccountType } from '../entities/user.entity';
import { IsEmail, IsOptional, IsString } from 'class-validator';
import { AccountType, Role } from '../entities/user.entity';

export class CreateUserDtoFromFrontend {
@IsEmail()
Expand All @@ -12,7 +12,6 @@ export class CreateUserDtoFromFrontend {
username: string;

accountType: AccountType;

}

export class CreateUserDto {
Expand All @@ -23,4 +22,5 @@ export class CreateUserDto {
emailConfirmed: boolean;
accountType: AccountType;
emailConfirmationToken?: string;
role: Role;
}
15 changes: 11 additions & 4 deletions src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ export class User {

@Prop()
username: string;

@Prop()
passwordHash: string;

@Prop()
role: Role;

@Prop()
emailConfirmed: boolean;

@Prop()
accountType: AccountType;
accountType: AccountType;

@Prop()
avatarUrl: string;

Expand All @@ -30,7 +33,6 @@ export class User {

@Prop()
resetPasswordToken?: string;

}

export const UserSchema = SchemaFactory.createForClass(User);
Expand All @@ -39,3 +41,8 @@ export enum AccountType {
EMAIL = 'EMAIL',
GOOGLE = 'GOOGLE',
}

export enum Role {
ADMIN = 'ADMIN',
USER = 'USER',
}
4 changes: 3 additions & 1 deletion src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
CreateUserDto,
CreateUserDtoFromFrontend,
} from './dto/create-user.dto';
import { User, UserDocument, AccountType } from './entities/user.entity';
import { User, UserDocument, AccountType, Role } from './entities/user.entity';
import { v4 as uuidv4 } from 'uuid';
import { OurExceptionType } from 'src/common/errors/OurExceptionType';
import { OurHttpException } from 'src/common/errors/OurHttpException';
Expand All @@ -23,6 +23,7 @@ export class UsersService {
passwordHash: this.hashPassword(dto.password),
emailConfirmed: true,
accountType: dto.accountType,
role: Role.USER,
username: username,
avatarUrl:
'https://res.cloudinary.com/dfqe0wizz/image/upload/v1686562358/default-avatar_sueepb.png',
Expand All @@ -40,6 +41,7 @@ export class UsersService {
emailConfirmed: false,
emailConfirmationToken: token,
accountType: AccountType.EMAIL,
role: Role.USER,
username: dto.username,
avatarUrl:
'https://res.cloudinary.com/dfqe0wizz/image/upload/v1686562358/default-avatar_sueepb.png',
Expand Down
1 change: 1 addition & 0 deletions test/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AppModule } from '../src/app.module';
import { INestApplication } from '@nestjs/common';
import {
AccountType,
Role,
User,
UserDocument,
} from 'src/users/entities/user.entity';
Expand Down
56 changes: 51 additions & 5 deletions test/ice-cream.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
UserDocument,
User,
AccountType,
Role,
} from 'src/users/entities/user.entity';
import {
IceCream,
Expand Down Expand Up @@ -61,7 +62,11 @@ describe('reviews', () => {
await iceCreamModel.deleteMany();
});

const createAndLoginUser = async (email: string, password: string) => {
const createAndLoginUser = async (
email: string,
password: string,
role: Role = Role.USER,
) => {
await usersService.createUnverified({
email,
password,
Expand All @@ -82,6 +87,22 @@ describe('reviews', () => {
return response.body.token;
};

const createIceCreamDto: CreateIceCreamDto = {
brand_pl: 'brand_pl',
name_pl: 'name_pl',
description_pl: 'description_pl',
brand_en: 'brand_en',
name_en: 'name_en',
description_en: 'description_en',
rating: 5,
numberOfRatings: 0,
image: 'https://image',
vegan: false,
type: IceCreamType.BAR,
tags: [],
barcode: '123',
};

const createIceCream = async ({
brand_pl = 'brand_pl',
name_pl = 'name_pl',
Expand Down Expand Up @@ -139,10 +160,6 @@ describe('reviews', () => {
expect(response.body.iceCreams.length).toEqual(1);
});

afterAll(async () => {
await app.close();
});

it('should query only searchField matching ice cream', async () => {
// given
await createIceCream({ name_en: 'Ben Jerry' });
Expand Down Expand Up @@ -184,6 +201,35 @@ describe('reviews', () => {
expect(response.body.iceCreams[0].name_en).toEqual('most popular');
});

it('should add new ice cream by admin', async () => {
// given
const token = await createAndLoginUser('[email protected]', 'test');
const user = await usersService.getOneByEmail('[email protected]');
user.role = Role.ADMIN;
user.save();

// when
const response = await request(app.getHttpServer())
.post(`/ice-creams/add`)
.set('Authorization', `Bearer ${token}`)
.send(createIceCreamDto);
// then
expect(response.status).toBe(201);
});

it('should block add new ice cream by plain user', async () => {
// given
const token = await createAndLoginUser('[email protected]', 'test');

// when
const response = await request(app.getHttpServer())
.post(`/ice-creams/add`)
.set('Authorization', `Bearer ${token}`)
.send(createIceCreamDto);
// then
expect(response.status).toBe(403);
});

afterAll(async () => {
await app.close();
});
Expand Down
1 change: 1 addition & 0 deletions test/reviews.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UserDocument,
User,
AccountType,
Role,
} from 'src/users/entities/user.entity';
import {
IceCream,
Expand Down
1 change: 1 addition & 0 deletions test/user.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AppModule } from '../src/app.module';
import { INestApplication } from '@nestjs/common';
import {
AccountType,
Role,
User,
UserDocument,
} from 'src/users/entities/user.entity';
Expand Down

0 comments on commit 12f5c9f

Please sign in to comment.