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

S1 tnr 38 backend autenticacion endpoint #13

Merged
merged 12 commits into from
Sep 3, 2024
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.0",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"mongoose": "^8.5.4",
Expand All @@ -41,6 +42,7 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
Expand Down
17 changes: 17 additions & 0 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginCredentialsDto } from './dto/login-credentials.dto';
import { ApiTags } from '@nestjs/swagger';

@ApiTags('Auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) { }

@Post('credentials')
async loginWithCredentials(
@Body() loginWithCredentials: LoginCredentialsDto,
) {
return await this.authService.login(loginWithCredentials);
}
}
13 changes: 8 additions & 5 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Module } from '@nestjs/common';
import { JwtStrategy } from './strategies/jwt.strategy';

import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from 'src/user/user.module';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';

import { UserModule } from '../user/user.module';

@Module({
providers: [JwtStrategy],
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),

Expand All @@ -23,9 +25,10 @@ import { UserModule } from 'src/user/user.module';
};
},
}),

UserModule,
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [PassportModule],
})
export class AuthModule {}
44 changes: 44 additions & 0 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { LoginCredentialsDto } from './dto/login-credentials.dto';
import { HashAdapter } from '../common/adapters/hash.adapter';
import { UserService } from '../user/user.service';
import { JwtPayload } from './interfaces/jwt-payload.interface';

@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) { }

async login({ email, password }: LoginCredentialsDto) {
const userFound = await this.userService.findOneByEmail(email);

const hashAdapter = new HashAdapter();

const isPasswordMatch = hashAdapter.verifyHash(
password,
userFound.password,
);

if (!isPasswordMatch) throw new UnauthorizedException('Incorrect password');

const token = this.getJwt({ id: userFound.id });

return {
id: userFound._id,
name: `${userFound.firstname} ${userFound.lastname}`,
username: userFound.username,
isAdmin: userFound.isAdmin,
email: userFound.email,
token,
};
}

private getJwt(payload: JwtPayload) {
const token = this.jwtService.sign(payload);
return token;
}
}
37 changes: 37 additions & 0 deletions backend/src/auth/dto/login-credentials.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { IsEmail, IsString, Length, Matches } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class LoginCredentialsDto {
@ApiProperty({
description: "The user's email address",
example: 'user@example.com',
})
@IsEmail()
readonly email: string;

@ApiProperty({
description:
"The user's password. It must have between 8 and 20 characters, including at least one uppercase letter, one lowercase letter, one number, and one special character.",
example: 'P@ssw0rd!',
minLength: 8,
maxLength: 20,
})
@IsString()
@Length(8, 20, {
message: 'The password must have between 8 and 20 characters',
})
@Matches(/(?=.*[A-Z])/, {
message: 'The password must have at least one uppercase letter',
})
@Matches(/(?=.*[a-z])/, {
message: 'The password must have at least one lowercase letter',
})
@Matches(/(?=.*\d)/, {
message: 'The password must have at least one number',
})
@Matches(/(?=.*[@$!%*?&#])/, {
message:
'The password must have at least one special character (for example, @, $, !, %, *, ?, &, #)',
})
readonly password: string;
}
13 changes: 13 additions & 0 deletions backend/src/common/adapters/hash.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as bcrypt from 'bcrypt';

export class HashAdapter {
private readonly hashDependency = bcrypt;

createHash(data: string | Buffer, saltOrRounds: string | number): string {
return this.hashDependency.hashSync(data, saltOrRounds);
}

verifyHash(data: string | Buffer, encrypted: string): boolean {
return this.hashDependency.compareSync(data, encrypted);
}
}
37 changes: 35 additions & 2 deletions backend/src/user/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,69 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsBoolean,
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
Length,
Matches,
} from 'class-validator';
import { ApiProperty, ApiBody } from '@nestjs/swagger';

export class CreateUserDto {
@IsOptional()
@IsString()
firstname: string;

@IsOptional()
@IsString()
lastname: string;

@IsNotEmpty()
@IsString()
@IsEmail()
email: string;
@IsNotEmpty()

@ApiProperty({
description:
"The user's password. It must have between 8 and 20 characters, including at least one uppercase letter, one lowercase letter, one number, and one special character.",
example: 'P@ssw0rd!',
minLength: 8,
maxLength: 20,
})
@IsString()
@Length(8, 20, {
message: 'The password must have between 8 and 20 characters',
})
@Matches(/(?=.*[A-Z])/, {
message: 'The password must have at least one uppercase letter',
})
@Matches(/(?=.*[a-z])/, {
message: 'The password must have at least one lowercase letter',
})
@Matches(/(?=.*\d)/, {
message: 'The password must have at least one number',
})
@Matches(/(?=.*[@$!%*?&#])/, {
message:
'The password must have at least one special character (for example, @, $, !, %, *, ?, &, #)',
})
password: string;

@IsOptional()
@IsString()
country: string;

@IsOptional()
tags: [string];

@IsNotEmpty()
@IsBoolean()
isAdmin: boolean;

@IsOptional()
@IsString()
description: string;

@IsOptional()
@IsString()
imageUrl: string;
Expand Down
6 changes: 4 additions & 2 deletions backend/src/user/user.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { UserService } from './user.service';
import { UserController } from './user.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from './entities/user.entity';

@Module({
imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])],
controllers: [UserController],
providers: [UserService],
exports: [MongooseModule],
exports: [UserService, MongooseModule],
})
export class UserModule {}
9 changes: 9 additions & 0 deletions backend/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,13 @@ export class UserService {
const res = await this.userModel.findByIdAndDelete(id);
return res;
}

async findOneByEmail(email: string) {
const userFound = await this.userModel.findOne({ email });

if (!userFound)
throw new NotFoundException(`User with email ${email} not exists`);

return userFound;
}
}
Loading