From a00533172c46c4b0255bf8ecb76b8d459fc72bab Mon Sep 17 00:00:00 2001 From: Victor Gomes Date: Wed, 25 Sep 2024 08:14:40 -0400 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20auth=20service=20=F0=9F=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/auth.service.spec.ts | 73 +++++++++++++++++++++++++++++++ src/services/auth.service.ts | 37 ++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/services/auth.service.spec.ts create mode 100644 src/services/auth.service.ts diff --git a/src/services/auth.service.spec.ts b/src/services/auth.service.spec.ts new file mode 100644 index 0000000..a8873c6 --- /dev/null +++ b/src/services/auth.service.spec.ts @@ -0,0 +1,73 @@ +import { User } from "../models"; +import { Auth } from "./auth.service"; + +const userData = { + id: 1, + name: 'foo', + username: 'bar', + password: '$----secret----$' +} as unknown as User + +describe('should success in all flow methods', () => { + const appSecret = '--key--' + + const service = new Auth(userData, appSecret) + + test('should get the sign payload', () => { + const payloadExpected = { + id: userData.id, + username: userData.username + } + + expect(payloadExpected).toEqual(service.tokenPayload) + }) + + test('should get the token options', () => { + const optionsExpected = { + expiresIn: '20min' + } + + expect(optionsExpected).toEqual(service.tokenOptions) + }) + + test('should sign a token', () => { + const tokenSigned = service.signToken() + + const tokenParts = tokenSigned.split('.').length + + expect(typeof tokenSigned).toEqual('string') + expect(tokenParts).toBe(3) + }) +}) + +describe('should test all validation methods', () => { + const generateService = (appSecret: undefined | number | object | null | string) => new Auth(userData, appSecret as unknown as string) + + const validationErrorMessage = 'The application secret must be provided.' + + test('should not sign a token when havent app secret', () => { + const service = generateService(undefined) + + expect(() => service.signToken()).toThrow(validationErrorMessage) + + const serviceWithNullSecret = generateService(null) + + expect(() => serviceWithNullSecret.signToken()).toThrow(validationErrorMessage) + + const serviceWithEmptyString = generateService('') + + expect(() => serviceWithEmptyString.signToken()).toThrow(validationErrorMessage) + }) + + test('shoult not sign a token when the app secret is a number', () => { + const service = generateService(9) + + expect(() => service.signToken()).toThrow(validationErrorMessage) + }) + + test('should not sign a token when the app secret is an object', () => { + const service = generateService({}) + + expect(() => service.signToken()).toThrow(validationErrorMessage) + }) +}) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..ca18dc4 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,37 @@ +import jwt from 'jsonwebtoken' +import type { User } from '../models' +import { InternalValidation } from '../web/errors' + +class Auth { + constructor (private readonly userData: User, private readonly applicationSecret: string) {} + + get tokenPayload () { + return { + id: this.userData.id, + username: this.userData.username + } + } + + get tokenOptions () { + return { + expiresIn: '20min' + } + } + + private validate(): void { + const haventApplicationSecret = !this.applicationSecret + const applicationSecretIsString = typeof this.applicationSecret === 'string' + + if (haventApplicationSecret || !applicationSecretIsString) { + throw new InternalValidation('The application secret must be provided.') + } + } + + public signToken () { + this.validate() + + return jwt.sign(this.tokenPayload, this.applicationSecret, this.tokenOptions) + } +} + +export { Auth } From 113cf722b36f6cfa24612f95cebb038eb186a7e0 Mon Sep 17 00:00:00 2001 From: Victor Gomes Date: Wed, 25 Sep 2024 08:15:06 -0400 Subject: [PATCH 2/4] =?UTF-8?q?Patch:=20internal=20validation=20error=20ha?= =?UTF-8?q?s=20created=20=F0=9F=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/web/errors.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/web/errors.ts b/src/web/errors.ts index 1e69738..56ec8c6 100644 --- a/src/web/errors.ts +++ b/src/web/errors.ts @@ -4,4 +4,10 @@ class LegendHttpError extends Error { } } -export { LegendHttpError } \ No newline at end of file +class InternalValidation extends Error { + constructor(public message: string) { + super(message) + } +} + +export { LegendHttpError, InternalValidation } From 0ab6071c9c30bed69d3be72de548bff715523592 Mon Sep 17 00:00:00 2001 From: Victor Gomes Date: Wed, 25 Sep 2024 08:17:46 -0400 Subject: [PATCH 3/4] =?UTF-8?q?Chore:=20correct=20environment=20file=20nam?= =?UTF-8?q?e=20and=20getter=20to=20app=20secret=20added=20=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{evironment.service.ts => environment.service.ts} | 4 ++++ 1 file changed, 4 insertions(+) rename src/services/{evironment.service.ts => environment.service.ts} (87%) diff --git a/src/services/evironment.service.ts b/src/services/environment.service.ts similarity index 87% rename from src/services/evironment.service.ts rename to src/services/environment.service.ts index 17c7f95..25aee2a 100644 --- a/src/services/evironment.service.ts +++ b/src/services/environment.service.ts @@ -22,6 +22,10 @@ class Environment implements EnvironmentService { getPort(): number { return Number(this.environmentVars.PORT) } + + getApplicationSecret(): string { + return String(this.environmentVars.JWT_SECRET) + } } export { From 7d587cc5a9fc070dc7888c2776a210541e851a6a Mon Sep 17 00:00:00 2001 From: Victor Gomes Date: Wed, 25 Sep 2024 08:18:38 -0400 Subject: [PATCH 4/4] =?UTF-8?q?Chore:=20auth=20service=20implemented=20at?= =?UTF-8?q?=20user=20service=20=F0=9F=AA=9A=F0=9F=9B=A1=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/user.controller.ts | 5 +++-- src/services/index.ts | 2 +- src/services/user.service.spec.ts | 2 +- src/services/user.service.ts | 21 ++++++++++----------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 1855b92..7c3eb71 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -11,8 +11,6 @@ import { LogService } from "../services/log.service"; class UserController extends BaseController { - private Service = new UserService(UserModel) - private LogService = new LogService() public readonly router = Router() constructor() { @@ -24,6 +22,9 @@ class UserController extends BaseController { } } + private Service = new UserService(UserModel, this.getApplicationSecret()) + private LogService = new LogService() + private loadRoutes() { this.router.get('/', Guard, this.getAll) this.router.get('/:id', Guard, this.getById) diff --git a/src/services/index.ts b/src/services/index.ts index ae31f2f..02be2f8 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1 @@ -export { Environment as EnvironmentService } from "./evironment.service"; +export { Environment as EnvironmentService } from "./environment.service"; diff --git a/src/services/user.service.spec.ts b/src/services/user.service.spec.ts index 172a78e..dc98e5e 100644 --- a/src/services/user.service.spec.ts +++ b/src/services/user.service.spec.ts @@ -11,7 +11,7 @@ let UserModel = { findOne: jest.fn() } as unknown as ModelCtor> -const service = new UserService(UserModel) +const service = new UserService(UserModel, 'key') const hash = '$2b$10$1Q6Zz1' diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 11d8847..f72521a 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -5,28 +5,27 @@ import { User, UserModel } from "../models"; import { searchEntity } from "../utils/searchEntity"; import * as bcrypt from 'bcrypt' import { LegendHttpError } from "../web/errors"; -import jwt from 'jsonwebtoken' +import { Auth as AuthService } from "./auth.service"; class UserService { - constructor(private readonly userModel: typeof UserModel) {} + constructor(private readonly userModel: typeof UserModel, private readonly applicationSecret: string = '') {} async signIn(signInDto: SignInDto): Promise { const user = await searchEntity(this.userModel, { username: signInDto.username }, false, false) - if (user === null) { - throw new LegendHttpError(401, 'User or password invalid.') - } - - const isPasswordMatch = await bcrypt.compare(signInDto.password, user.password) + const isPasswordMatch = await bcrypt.compare( + signInDto.password, + user?.password || '' + ) - if (!isPasswordMatch) { + if ((user === null) || !isPasswordMatch) { throw new LegendHttpError(401, 'User or password invalid.') } - const token = await jwt.sign({ id: user.id, username: user.username }, process.env.JWT_SECRET, { - expiresIn: '20min' - }) + const auth = new AuthService(user, this.applicationSecret) + + const token = auth.signToken() return token }