From 76eb2e1b626b5d2ebb21d3badc2f1bd4bbd1a927 Mon Sep 17 00:00:00 2001 From: pedrodecf Date: Thu, 25 Jan 2024 20:18:56 -0300 Subject: [PATCH 1/2] User authentication, controller, routes and unit testing --- src/app.ts | 4 +- src/controller/session/authUser.ts | 24 ++++++++ src/controller/session/routes.ts | 7 +++ src/use-cases/authUserUseCase.spec.ts | 85 +++++++++++++++++++++++++++ src/use-cases/authUserUseCase.ts | 40 +++++++++++++ 5 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 src/controller/session/authUser.ts create mode 100644 src/controller/session/routes.ts create mode 100644 src/use-cases/authUserUseCase.spec.ts create mode 100644 src/use-cases/authUserUseCase.ts diff --git a/src/app.ts b/src/app.ts index 73ab25d..4250f3f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,12 +2,12 @@ import fastify from 'fastify' import { userRoutes } from './controller/user/routes' import { env } from './env' import { ZodError } from 'zod' +import { authRoutes } from './controller/session/routes' export const app = fastify() app.register(userRoutes) - - +app.register(authRoutes) app.setErrorHandler((error, _, response) => { if (env.NODE_ENV !== 'production') { diff --git a/src/controller/session/authUser.ts b/src/controller/session/authUser.ts new file mode 100644 index 0000000..5b0d32f --- /dev/null +++ b/src/controller/session/authUser.ts @@ -0,0 +1,24 @@ +import { FastifyReply, FastifyRequest } from 'fastify' +import { InMemoryUserRepository } from '../../repositories/in-memory-db/inMemoryUserRepository' +import { AuthUserUseCase } from '../../use-cases/authUserUseCase' +import { z } from 'zod' + +export async function authUser( + request: FastifyRequest, + response: FastifyReply, +) { + + const userRepository = new InMemoryUserRepository() + const authUserUseCase = new AuthUserUseCase(userRepository) + + const AuthUserUseCaseSchema = z.object({ + email: z.string().email(), + password: z.string() + }) + + const { email, password } = AuthUserUseCaseSchema.parse(request.body) + + const { user } = await authUserUseCase.execute({ email, password }) + + return response.status(200).send({ user }) +} \ No newline at end of file diff --git a/src/controller/session/routes.ts b/src/controller/session/routes.ts new file mode 100644 index 0000000..218c9c3 --- /dev/null +++ b/src/controller/session/routes.ts @@ -0,0 +1,7 @@ +import { FastifyInstance } from 'fastify' +import { authUser } from './authUser' + +export async function authRoutes(app: FastifyInstance) { + + app.post('/login', authUser) +} \ No newline at end of file diff --git a/src/use-cases/authUserUseCase.spec.ts b/src/use-cases/authUserUseCase.spec.ts new file mode 100644 index 0000000..893ab74 --- /dev/null +++ b/src/use-cases/authUserUseCase.spec.ts @@ -0,0 +1,85 @@ +import { describe, expect, beforeEach, it } from 'vitest' +import { InMemoryUserRepository } from '../repositories/in-memory-db/inMemoryUserRepository' +import { AuthUserUseCase } from './authUserUseCase' +import { compare, hash } from 'bcryptjs' +import { ResourceNotFoundError } from './errors/ResourceNotFoundError' + +let userRepository: InMemoryUserRepository +let authUserUseCase: AuthUserUseCase + +describe('Get user by email and validate password', () => { + beforeEach(() => { + userRepository = new InMemoryUserRepository() + authUserUseCase = new AuthUserUseCase(userRepository) + }) + + it('should be able to validate the user login by comparing the email and password', async () => { + const email = 'johndoe@email.com' + const password = '12345' + const name = 'John' + const surname = 'Doe' + + const newUser = await userRepository.create({ + email, + name, + surname, + password_hash: await hash(password, 6), + }) + + const { user } = await authUserUseCase.execute({ email, password }) + + const passwordMatched = await compare(password, newUser.password_hash) + + expect(user.id).toEqual(newUser.id) + expect(user.name).toEqual(name) + expect(user.surname).toEqual(surname) + expect(user.email).toEqual(email) + expect(passwordMatched).toEqual(true) + }) + + it('return an error message if the password entered is not correct', async () => { + const email = 'johndoe@email.com' + const password = '12345' + const name = 'John' + const surname = 'Doe' + + await userRepository.create({ + email, + name, + surname, + password_hash: await hash(password, 6), + }) + + const passwordIncorrect = 'password-incorrect' + + await expect(() => + authUserUseCase.execute({ + email, + password: passwordIncorrect, + }), + ).rejects.toBeInstanceOf(ResourceNotFoundError) // need another error instance + }) + + it('return an error message if the email entered is not correct', async () => { + const email = 'johndoe@email.com' + const password = '12345' + const name = 'John' + const surname = 'Doe' + + await userRepository.create({ + email, + name, + surname, + password_hash: await hash(password, 6), + }) + + const emailIncorrect = 'email-incorrect@email.com' + + await expect(() => + authUserUseCase.execute({ + email: emailIncorrect, + password, + }), + ).rejects.toBeInstanceOf(ResourceNotFoundError) // need another error instance + }) +}) diff --git a/src/use-cases/authUserUseCase.ts b/src/use-cases/authUserUseCase.ts new file mode 100644 index 0000000..afe5b16 --- /dev/null +++ b/src/use-cases/authUserUseCase.ts @@ -0,0 +1,40 @@ +import { User } from '@prisma/client' +import { UserRepository } from '../repositories/user-repository' +import { ResourceNotFoundError } from './errors/ResourceNotFoundError' +import { compare } from 'bcryptjs' + +interface AuthUserUseCaseRequest { + email: string + password: string +} + +interface AuthUserProfileUseCaseResponse { + user: User +} + +export class AuthUserUseCase { + constructor(private userRepository: UserRepository) {} + + async execute({ + email, + password, + }: AuthUserUseCaseRequest): Promise { + const user = await this.userRepository.findByEmail(email) + + if (!user) { + throw new ResourceNotFoundError() + } + + const passwordMatched = await compare(password, user.password_hash) + + if (!passwordMatched) { + throw new ResourceNotFoundError() + } + + // if (password !== user.password_hash) { + // throw new ResourceNotFoundError() + // } + + return { user } + } +} From 4ce418d5ed566996aa3f1a302894d05ce9698e88 Mon Sep 17 00:00:00 2001 From: pedrodecf Date: Fri, 26 Jan 2024 16:25:45 -0300 Subject: [PATCH 2/2] Authentication with JWT Token and middleware --- package.json | 2 + pnpm-lock.yaml | 117 +++++++++++++++++++++++- src/app.ts | 13 +++ src/configs/auth.ts | 6 ++ src/controller/middlewares/verifyJwt.ts | 12 +++ src/controller/session/authUser.ts | 13 ++- src/controller/user/routes.ts | 4 +- src/use-cases/authUserUseCase.ts | 10 +- 8 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 src/configs/auth.ts create mode 100644 src/controller/middlewares/verifyJwt.ts diff --git a/package.json b/package.json index 7bccfbd..cf88518 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "vitest": "^1.2.1" }, "dependencies": { + "@fastify/cors": "^9.0.0", + "@fastify/jwt": "^8.0.0", "@prisma/client": "5.8.1", "@types/bcryptjs": "^2.4.6", "@types/express": "^4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5bb9fe..44be470 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,12 @@ settings: excludeLinksFromLockfile: false dependencies: + '@fastify/cors': + specifier: ^9.0.0 + version: 9.0.0 + '@fastify/jwt': + specifier: ^8.0.0 + version: 8.0.0 '@prisma/client': specifier: 5.8.1 version: 5.8.1(prisma@5.8.1) @@ -367,6 +373,13 @@ packages: fast-uri: 2.3.0 dev: false + /@fastify/cors@9.0.0: + resolution: {integrity: sha512-KVsBFs2jZHbtN4vI/jJFaeRHXr3htB3koquGD5TwQQt/lmspyrS1a2UOBTlMOC/5hawC81vdoCzmiR03HbjdXg==} + dependencies: + fastify-plugin: 4.5.1 + mnemonist: 0.39.6 + dev: false + /@fastify/deepmerge@1.3.0: resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} dev: false @@ -381,6 +394,16 @@ packages: fast-json-stringify: 5.10.0 dev: false + /@fastify/jwt@8.0.0: + resolution: {integrity: sha512-pJHjmZaokteZFMbsVVt7pbyJpbDogTnpl/aD7eR3vLOPgfktp4k4gUM6cd7RtjL/Ol1qDwL5mup+vdNlZI3K0Q==} + dependencies: + '@fastify/error': 3.4.1 + '@lukeed/ms': 2.0.2 + fast-jwt: 3.3.2 + fastify-plugin: 4.5.1 + steed: 1.1.3 + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -455,6 +478,11 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@lukeed/ms@2.0.2: + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1146,6 +1174,15 @@ packages: is-shared-array-buffer: 1.0.2 dev: true + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: false + /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true @@ -1208,6 +1245,10 @@ packages: engines: {node: '>=8'} dev: true + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1449,6 +1490,12 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true @@ -2017,6 +2064,16 @@ packages: rfdc: 1.3.1 dev: false + /fast-jwt@3.3.2: + resolution: {integrity: sha512-H+JYxaFy2LepiC1AQWM/2hzKlQOWaWUkEnu/yebhYu4+ameb3qG77WiRZ1Ct6YBk6d/ESsNguBfTT5+q0XMtKg==} + engines: {node: '>=16 <22'} + dependencies: + '@lukeed/ms': 2.0.2 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.39.7 + dev: false + /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true @@ -2036,6 +2093,17 @@ packages: resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==} dev: false + /fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + dependencies: + reusify: 1.0.4 + dev: false + + /fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + dev: false + /fastify@4.25.2: resolution: {integrity: sha512-SywRouGleDHvRh054onj+lEZnbC1sBCLkR0UY3oyJwjD4BdZJUrxBqfkfCaqn74pVCwBaRHGuL3nEWeHbHzAfw==} dependencies: @@ -2059,11 +2127,25 @@ packages: - supports-color dev: false + /fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} + dependencies: + reusify: 1.0.4 + xtend: 4.0.2 + dev: false + /fastq@1.16.0: resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} dependencies: reusify: 1.0.4 + /fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + dependencies: + reusify: 1.0.4 + xtend: 4.0.2 + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2351,7 +2433,6 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} @@ -2805,6 +2886,10 @@ packages: engines: {node: '>=12'} dev: true + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -2836,6 +2921,18 @@ packages: ufo: 1.3.2 dev: true + /mnemonist@0.39.6: + resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} + dependencies: + obliterator: 2.0.4 + dev: false + + /mnemonist@0.39.7: + resolution: {integrity: sha512-ix3FwHWZgdXUt0dHM8bCrI4r1KMeYx8bCunPCYmvKXq4tn6gbNsqrsb4q0kDbDqbpIOvEaW5Sn+dmDwGydfrwA==} + dependencies: + obliterator: 2.0.4 + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -2951,6 +3048,10 @@ packages: es-abstract: 1.22.3 dev: true + /obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + dev: false + /on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -3490,6 +3591,10 @@ packages: engines: {node: '>=10'} dev: false + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + /secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} dev: false @@ -3604,6 +3709,16 @@ packages: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true + /steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.16.0 + fastseries: 1.7.2 + reusify: 1.0.4 + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} diff --git a/src/app.ts b/src/app.ts index 4250f3f..97c1a55 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,12 +3,25 @@ import { userRoutes } from './controller/user/routes' import { env } from './env' import { ZodError } from 'zod' import { authRoutes } from './controller/session/routes' +import fastifyJwt from '@fastify/jwt' +import cors from '@fastify/cors' export const app = fastify() +app.register(cors, { + origin: ['http://127.0.0.1:5173', 'http://localhost:5173'], +}) app.register(userRoutes) app.register(authRoutes) + +app.register(fastifyJwt, { + secret: 'squad40', + // sign: { + // expiresIn: '10s', + // }, +}) + app.setErrorHandler((error, _, response) => { if (env.NODE_ENV !== 'production') { console.error(error) diff --git a/src/configs/auth.ts b/src/configs/auth.ts new file mode 100644 index 0000000..63d4007 --- /dev/null +++ b/src/configs/auth.ts @@ -0,0 +1,6 @@ +module.exports = { + jwt: { + secret: "squad40", + expiresIn: "7d" + } +} \ No newline at end of file diff --git a/src/controller/middlewares/verifyJwt.ts b/src/controller/middlewares/verifyJwt.ts new file mode 100644 index 0000000..c709985 --- /dev/null +++ b/src/controller/middlewares/verifyJwt.ts @@ -0,0 +1,12 @@ +import { FastifyReply, FastifyRequest } from 'fastify' + +export async function verifyJWT( + request: FastifyRequest, + response: FastifyReply, +) { + try { + await request.jwtVerify() + } catch (e) { + return response.status(401).send({ message: 'Unauthorized' }) + } +} \ No newline at end of file diff --git a/src/controller/session/authUser.ts b/src/controller/session/authUser.ts index 5b0d32f..de428c4 100644 --- a/src/controller/session/authUser.ts +++ b/src/controller/session/authUser.ts @@ -2,6 +2,8 @@ import { FastifyReply, FastifyRequest } from 'fastify' import { InMemoryUserRepository } from '../../repositories/in-memory-db/inMemoryUserRepository' import { AuthUserUseCase } from '../../use-cases/authUserUseCase' import { z } from 'zod' +// const { sign } = require('jsonwebtoken') +// const authConfig = require('../../configs/auth') export async function authUser( request: FastifyRequest, @@ -20,5 +22,14 @@ export async function authUser( const { user } = await authUserUseCase.execute({ email, password }) - return response.status(200).send({ user }) + const token = await response.jwtSign( + {}, + { + sign: { + sub: user.id, + }, + }, + ) + + return response.status(200).send({ user, token }) } \ No newline at end of file diff --git a/src/controller/user/routes.ts b/src/controller/user/routes.ts index f69b683..00313b8 100644 --- a/src/controller/user/routes.ts +++ b/src/controller/user/routes.ts @@ -1,9 +1,11 @@ import { FastifyInstance } from 'fastify' import { getUserById } from './getUserById' import { getUserByEmail } from './getUserByEmail' +import { verifyJWT } from '../middlewares/verifyJwt' export async function userRoutes(app: FastifyInstance) { app.get('/user/:id', getUserById) - app.get('/user', getUserByEmail) + // app.get('/user', getUserByEmail) + app.get('/user', { onRequest: [verifyJWT] }, getUserByEmail) } diff --git a/src/use-cases/authUserUseCase.ts b/src/use-cases/authUserUseCase.ts index afe5b16..08b7b37 100644 --- a/src/use-cases/authUserUseCase.ts +++ b/src/use-cases/authUserUseCase.ts @@ -27,14 +27,14 @@ export class AuthUserUseCase { const passwordMatched = await compare(password, user.password_hash) - if (!passwordMatched) { - throw new ResourceNotFoundError() - } - - // if (password !== user.password_hash) { + // if (!passwordMatched) { // throw new ResourceNotFoundError() // } + if (password !== user.password_hash) { + throw new ResourceNotFoundError() + } + return { user } } }