Skip to content

Commit

Permalink
implement user routes and user validation
Browse files Browse the repository at this point in the history
  • Loading branch information
amrtgaber committed Jun 16, 2024
1 parent 0cbce31 commit 8f1a571
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
Expand Down
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80
}
}
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ services:
networks:
- appnetwork
networks:
appnetwork:
appnetwork:
13 changes: 12 additions & 1 deletion src/auth/dto/auth.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import {
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
MinLength,
} from 'class-validator';
import { MIN_PASSWORD_LENGTH, MIN_USERNAME_LENGTH } from '../../constants';
import { IsUsername } from '../validators/is-username';

export class AuthDto {
@ApiProperty()
Expand All @@ -10,10 +18,13 @@ export class AuthDto {
@ApiProperty()
@IsString()
@IsOptional()
@MinLength(MIN_USERNAME_LENGTH)
@IsUsername()
username: string | null;

@ApiProperty()
@IsString()
@IsNotEmpty()
@MinLength(MIN_PASSWORD_LENGTH)
password: string;
}
18 changes: 18 additions & 0 deletions src/auth/validators/is-username.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ValidationOptions, registerDecorator } from 'class-validator';

export function IsUsername(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isUsername',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: string) {
// alphanumeric + dashes, cannot start with a dash
return !!value.match(/^[\w][\w-]+$/);
},
},
});
};
}
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MIN_USERNAME_LENGTH = 3;
export const MIN_PASSWORD_LENGTH = 8;
4 changes: 2 additions & 2 deletions src/draft/draft.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class DraftController {

@ApiNoContentResponse()
@Delete(':id')
remove(@Param('id') id: string) {
return this.draftService.remove(+id);
async remove(@Param('id') id: string) {
return await this.draftService.remove(+id);
}
}
23 changes: 22 additions & 1 deletion src/user/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
export class CreateUserDto {}
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsOptional, IsString, MinLength } from 'class-validator';
import { IsUsername } from '../../auth/validators/is-username';
import { MIN_PASSWORD_LENGTH, MIN_USERNAME_LENGTH } from '../../constants';

export class CreateUserDto {
@ApiProperty()
@IsEmail()
email: string;

@ApiProperty()
@IsString()
@MinLength(MIN_USERNAME_LENGTH)
@IsUsername()
@IsOptional()
username?: string;

@ApiProperty()
@IsString()
@MinLength(MIN_PASSWORD_LENGTH)
password: string;
}
33 changes: 32 additions & 1 deletion src/user/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
export class User {}
import { ApiProperty } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { Exclude } from 'class-transformer';
import { IsOptional } from 'class-validator';

export class UserEntity implements User {
@ApiProperty()
id: number;

@ApiProperty()
createdAt: Date;

@ApiProperty()
updatedAt: Date;

@ApiProperty()
email: string;

@ApiProperty()
@IsOptional()
username: string;

@Exclude()
hash: string;

@ApiProperty()
drafts: [];

constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
13 changes: 10 additions & 3 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import {
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Patch,
UseGuards,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiNoContentResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { GetUser } from '../auth/decorator/get-user.decorator';
import { JwtGuard } from '../auth/guard/jwt.guard';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserEntity } from './entities/user.entity';
import { UserService } from './user.service';

@ApiTags('Users')
Expand All @@ -19,16 +22,20 @@ import { UserService } from './user.service';
export class UserController {
constructor(private readonly userService: UserService) {}

@ApiOkResponse({ type: UserEntity })
@Get()
getSelf(@GetUser() user: User) {
return user;
}

@ApiOkResponse({ type: UserEntity })
@Patch()
update(@Body() updateUserDto: UpdateUserDto, @GetUser('id') id: number) {
return this.userService.update(id, updateUserDto);
async update(@Body() dto: UpdateUserDto, @GetUser() user: User) {
return await this.userService.update(user, dto);
}

@ApiNoContentResponse()
@HttpCode(HttpStatus.NO_CONTENT)
@Delete()
remove(@GetUser('id') id: number) {
return this.userService.remove(id);
Expand Down
43 changes: 29 additions & 14 deletions src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from '@prisma/client';
import * as argon from 'argon2';
import { PrismaService } from '../prisma/prisma.service';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
create(createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
constructor(private prisma: PrismaService) {}

findAll() {
return `This action returns all user`;
}
async update(user: User, dto: UpdateUserDto) {
const data = {
...user,
...dto,
};

findOne(id: number) {
return `This action returns a #${id} user`;
}
delete data.password;

let hash: string;
if (dto.password) {
hash = await argon.hash(dto.password);
data.hash = hash;
console.log({ hash });
}

update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
return await this.prisma.user.update({
where: {
id: user.id,
},
data,
});
}

remove(id: number) {
return `This action removes a #${id} user`;
async remove(id: number) {
return await this.prisma.user.delete({
where: {
id,
},
});
}
}
127 changes: 123 additions & 4 deletions test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ describe('App e2e', () => {

const dto: AuthDto = {
email: 'test@test.com',
username: 'test username',
password: '123',
username: 'testUsername',
password: '12345678',
};

describe('Auth', () => {
Expand All @@ -57,6 +57,30 @@ describe('App e2e', () => {
.expectStatus(HttpStatus.CREATED);
});

it('should throw if password is too short', async () => {
await pactum
.spec()
.post(`/auth/signup`)
.withBody({ ...dto, password: '123' })
.expectStatus(HttpStatus.BAD_REQUEST);
});

it('should throw if username is too short', async () => {
await pactum
.spec()
.post(`/auth/signup`)
.withBody({ ...dto, username: 'a' })
.expectStatus(HttpStatus.BAD_REQUEST);
});

it('should throw if username is not valid', async () => {
await pactum
.spec()
.post(`/auth/signup`)
.withBody({ ...dto, username: 'abcdefg!%$' })
.expectStatus(HttpStatus.BAD_REQUEST);
});

it('should login', async () => {
await pactum
.spec()
Expand Down Expand Up @@ -100,11 +124,106 @@ describe('App e2e', () => {
.spec()
.get(`/users`)
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.OK)
.inspect();
});

it('edits user email', async () => {
await pactum
.spec()
.patch(`/users`)
.withBody({
email: 'test2@test.com',
})
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.OK);
});

it('edits user password', async () => {
await pactum
.spec()
.patch(`/users`)
.withBody({
password: 'a different password',
})
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.OK);
});

it('logs in after email and password changed', async () => {
await pactum
.spec()
.patch(`/users`)
.withBody({
email: 'test2@test.com',
password: 'a different password',
})
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.OK);
});

it('throws if password is too short', async () => {
await pactum
.spec()
.patch(`/users`)
.withBody({
password: '123',
})
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.BAD_REQUEST);
});

it('throws if username is too short', async () => {
await pactum
.spec()
.patch(`/users`)
.withBody({
username: 'a',
})
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.BAD_REQUEST);
});

it('throws if username is not valid', async () => {
await pactum
.spec()
.patch(`/users`)
.withBody({
username: 'abcdefg!%$',
})
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.BAD_REQUEST);
});

it('edits user username', async () => {
await pactum
.spec()
.patch(`/users`)
.withBody({
username: 'test2Username',
})
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.OK);
});

it.todo('edits user');
it.todo('deletes user');
it('throws if username is too short', async () => {
await pactum
.spec()
.patch(`/users`)
.withBody({
username: 'a',
})
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.BAD_REQUEST);
});

it('deletes user', async () => {
await pactum
.spec()
.delete(`/users`)
.withHeaders({ Authorization: 'Bearer $S{accessToken}' })
.expectStatus(HttpStatus.NO_CONTENT);
});
});

describe('Draft', () => {
Expand Down

0 comments on commit 8f1a571

Please sign in to comment.