From 2f24abbe725f90c19167da98261966f3281de4e4 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Wed, 18 Jun 2025 11:10:11 -0300 Subject: [PATCH 01/35] =?UTF-8?q?feat:=20Criar=20service=20de=20autentica?= =?UTF-8?q?=C3=A7=C3=A3o=20e=20erro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit criar um service de autenticação e criar também o erro de credenciais --- .../errors/invalid-credentials-error.ts | 5 ++++ src/services/users/authenticate.ts | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/services/errors/invalid-credentials-error.ts create mode 100644 src/services/users/authenticate.ts diff --git a/src/services/errors/invalid-credentials-error.ts b/src/services/errors/invalid-credentials-error.ts new file mode 100644 index 0000000..8cff53d --- /dev/null +++ b/src/services/errors/invalid-credentials-error.ts @@ -0,0 +1,5 @@ +export class InvalidCredentialsError extends Error { + constructor() { + super('Invalid credentials.') + } +} diff --git a/src/services/users/authenticate.ts b/src/services/users/authenticate.ts new file mode 100644 index 0000000..f9e0ded --- /dev/null +++ b/src/services/users/authenticate.ts @@ -0,0 +1,28 @@ +import type { UsersRepository } from '@/repositories/users-repository' +import { compare } from 'bcryptjs' +import { InvalidCredentialsError } from '../errors/invalid-credentials-error' + +interface AuthenticateServiceRequest { + email: string + password: string +} + +export class AuthenticateService { + constructor(private usersRepository: UsersRepository) {} + + async execute({ email, password }: AuthenticateServiceRequest) { + const user = await this.usersRepository.findByEmail(email) + + if (!user) { + throw new InvalidCredentialsError() + } + + const doesPasswordMatches = await compare(password, user.password) + + if (!doesPasswordMatches) { + throw new InvalidCredentialsError() + } + + return { user } + } +} From a7eaef8378c2e4ab65c82032b30bd1a94ca99112 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 14:30:59 -0300 Subject: [PATCH 02/35] build: Adicionar @fastify/cookie e @fastify/jwt --- package-lock.json | 180 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f951ee..cb690ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@fastify/cookie": "^11.0.2", + "@fastify/jwt": "^9.1.0", "@prisma/client": "^6.4.1", "bcryptjs": "^3.0.2", "dotenv": "^16.5.0", @@ -829,6 +831,26 @@ "fast-uri": "^3.0.0" } }, + "node_modules/@fastify/cookie": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz", + "integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "cookie": "^1.0.0", + "fastify-plugin": "^5.0.0" + } + }, "node_modules/@fastify/error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", @@ -860,6 +882,29 @@ "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", "license": "MIT" }, + "node_modules/@fastify/jwt": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-9.1.0.tgz", + "integrity": "sha512-CiGHCnS5cPMdb004c70sUWhQTfzrJHAeTywt7nVw6dAiI0z1o4WRvU94xfijhkaId4bIxTCOjFgn4sU+Gvk43w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "@lukeed/ms": "^2.0.2", + "fast-jwt": "^5.0.0", + "fastify-plugin": "^5.0.0", + "steed": "^1.1.3" + } + }, "node_modules/@fastify/merge-json-schemas": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", @@ -970,6 +1015,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1459,6 +1513,18 @@ "dev": true, "license": "MIT" }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -1527,6 +1593,12 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1895,6 +1967,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2027,6 +2108,21 @@ "rfdc": "^1.2.0" } }, + "node_modules/fast-jwt": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-5.0.6.tgz", + "integrity": "sha512-LPE7OCGUl11q3ZgW681cEU2d0d2JZ37hhJAmetCgNyW8waVaJVZXhyFF6U2so1Iim58Yc7pfxJe2P7MNetQH2g==", + "license": "Apache-2.0", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "asn1.js": "^5.4.1", + "ecdsa-sig-formatter": "^1.0.11", + "mnemonist": "^0.40.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/fast-querystring": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", @@ -2061,6 +2157,18 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "license": "MIT", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fastify": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.3.2.tgz", @@ -2094,6 +2202,12 @@ "toad-cache": "^3.7.0" } }, + "node_modules/fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", + "license": "MIT" + }, "node_modules/fastify/node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -2110,6 +2224,16 @@ ], "license": "MIT" }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -2119,6 +2243,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fastseries": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-1.7.2.tgz", + "integrity": "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.0", + "xtend": "^4.0.0" + } + }, "node_modules/fdir": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", @@ -2316,7 +2450,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -2771,6 +2904,12 @@ "node": ">=6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2797,6 +2936,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mnemonist": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", + "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2836,6 +2984,12 @@ "node": ">=0.10.0" } }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -3364,7 +3518,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -3413,7 +3566,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/secure-json-parse": { @@ -3517,6 +3669,19 @@ "node": ">= 10.x" } }, + "node_modules/steed": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz", + "integrity": "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==", + "license": "MIT", + "dependencies": { + "fastfall": "^1.5.0", + "fastparallel": "^2.2.0", + "fastq": "^1.3.0", + "fastseries": "^1.7.0", + "reusify": "^1.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4035,6 +4200,15 @@ "node": ">=8" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", diff --git a/package.json b/package.json index bf261e4..6f8ab0d 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "typescript": "^5.8.2" }, "dependencies": { + "@fastify/cookie": "^11.0.2", + "@fastify/jwt": "^9.1.0", "@prisma/client": "^6.4.1", "fastify": "^5.3.2", "bcryptjs": "^3.0.2", From df746fb253447d7d2c7c2386d74a568760e2b9e7 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 14:42:46 -0300 Subject: [PATCH 03/35] =?UTF-8?q?chore(env):=20Adicionar=20verifica=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20secret=20JWT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/env/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/env/index.ts b/src/env/index.ts index 4af392b..3762a7a 100644 --- a/src/env/index.ts +++ b/src/env/index.ts @@ -4,6 +4,7 @@ import { z } from 'zod' const envSchema = z.object({ PORT: z.coerce.number().min(1000).max(9999).default(3333), NODE_ENV: z.enum(['dev', 'test', 'prod']).default('dev'), + JWT_SECRET: z.string(), }) const _env = envSchema.safeParse(process.env) From bae9298bc7b6228f97fa2488706e99a2ec7e539b Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 14:53:08 -0300 Subject: [PATCH 04/35] chore(types): Adicionar interface user para o @fastify/jwt --- src/@types/fastify-jwt.d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/@types/fastify-jwt.d.ts diff --git a/src/@types/fastify-jwt.d.ts b/src/@types/fastify-jwt.d.ts new file mode 100644 index 0000000..e2e946d --- /dev/null +++ b/src/@types/fastify-jwt.d.ts @@ -0,0 +1,10 @@ +import '@fastify/jwt' + +declare module '@fastify/jwt' { + export interface FastifyJWT { + user: { + sub: string + role: 'CUSTOMER' | 'EMPLOYEE' | 'ADMIN' + } + } +} From d28cc6b4724f6a1aca321ab4e2402b86dfa024ab Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 14:56:42 -0300 Subject: [PATCH 05/35] feat: Configurar fastifyJwt e cookie no fastify --- src/app.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app.ts b/src/app.ts index c5ac111..3479d02 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,3 +1,5 @@ +import fastifyCookie from '@fastify/cookie' +import fastifyJwt from '@fastify/jwt' import fastify, { type FastifyReply } from 'fastify' import { ZodError } from 'zod' import { env } from './env' @@ -12,6 +14,19 @@ app.get('/', (_, reply: FastifyReply) => { }) }) +app.register(fastifyJwt, { + secret: env.JWT_SECRET, + cookie: { + cookieName: 'refreshToken', + signed: false, + }, + sign: { + expiresIn: '10m', + }, +}) + +app.register(fastifyCookie) + app.register(appRoutes, { prefix: '/api' }) app.setErrorHandler((error, _, reply) => { From b9a2bf61a2c0ad6f61966920e7591038923a4d52 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 14:58:01 -0300 Subject: [PATCH 06/35] =?UTF-8?q?feat:=20Criar=20factory=20de=20autentica?= =?UTF-8?q?=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/factories/make-authenticate-service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/services/factories/make-authenticate-service.ts diff --git a/src/services/factories/make-authenticate-service.ts b/src/services/factories/make-authenticate-service.ts new file mode 100644 index 0000000..39e288e --- /dev/null +++ b/src/services/factories/make-authenticate-service.ts @@ -0,0 +1,9 @@ +import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' +import { AuthenticateService } from '../users/authenticate' + +export function makeAuthenticateService() { + const usersRepository = new PrismaUsersRepository() + const authenticateService = new AuthenticateService(usersRepository) + + return authenticateService +} From 5c3b31e3116325665ec5a8adbbdde9495eda8bcd Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 15:03:32 -0300 Subject: [PATCH 07/35] =?UTF-8?q?feat:=20Criar=20controller=20de=20autenti?= =?UTF-8?q?ca=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fazer a criação dos token JWT quando autenticado --- src/http/controllers/users/authenticate.ts | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/http/controllers/users/authenticate.ts diff --git a/src/http/controllers/users/authenticate.ts b/src/http/controllers/users/authenticate.ts new file mode 100644 index 0000000..38c57e6 --- /dev/null +++ b/src/http/controllers/users/authenticate.ts @@ -0,0 +1,62 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' +import { z } from 'zod' + +import { InvalidCredentialsError } from '@/services/errors/invalid-credentials-error' +import { makeAuthenticateService } from '@/services/factories/make-authenticate-service' + +export async function authenticate( + request: FastifyRequest, + reply: FastifyReply, +) { + const authenticateBodySchema = z.object({ + email: z.string().email(), + password: z.string().min(6), + }) + + const { email, password } = authenticateBodySchema.parse(request.body) + + try { + const authenticateService = makeAuthenticateService() + + const { user } = await authenticateService.execute({ email, password }) + + const token = await reply.jwtSign( + { + role: user.role, + }, + { + sign: { + sub: user.id, + }, + }, + ) + + const refreshToken = await reply.jwtSign( + { + role: user.role, + }, + { + sign: { + sub: user.id, + expiresIn: '7d', + }, + }, + ) + + return reply + .setCookie('refreshToken', refreshToken, { + path: '/', + secure: true, + sameSite: true, + httpOnly: true, + }) + .status(200) + .send({ token }) + } catch (err) { + if (err instanceof InvalidCredentialsError) { + return reply.status(400).send({ message: err.message }) + } + + throw err + } +} From 1e8052cbd4779c26560f154c6760a972c10508a3 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 15:09:52 -0300 Subject: [PATCH 08/35] feat: Criar controller de refresh token --- src/http/controllers/users/refresh.ts | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/http/controllers/users/refresh.ts diff --git a/src/http/controllers/users/refresh.ts b/src/http/controllers/users/refresh.ts new file mode 100644 index 0000000..4784efd --- /dev/null +++ b/src/http/controllers/users/refresh.ts @@ -0,0 +1,36 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +export async function refresh(request: FastifyRequest, reply: FastifyReply) { + await request.jwtVerify({ onlyCookie: true }) + + const { role } = request.user + + const token = await reply.jwtSign( + { role }, + { + sign: { + sub: request.user.sub, + }, + }, + ) + + const refreshToken = await reply.jwtSign( + { role }, + { + sign: { + sub: request.user.sub, + expiresIn: '7d', + }, + }, + ) + + return reply + .setCookie('refreshToken', refreshToken, { + path: '/', + secure: true, + sameSite: true, + httpOnly: true, + }) + .status(200) + .send({ token }) +} From f5f0b939babc658cdfe71d3dcef909efc0ce0117 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 15:11:37 -0300 Subject: [PATCH 09/35] feat: Criar controller de logout --- src/http/controllers/users/logout.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/http/controllers/users/logout.ts diff --git a/src/http/controllers/users/logout.ts b/src/http/controllers/users/logout.ts new file mode 100644 index 0000000..68073fd --- /dev/null +++ b/src/http/controllers/users/logout.ts @@ -0,0 +1,7 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +export async function logout(_: FastifyRequest, reply: FastifyReply) { + reply.clearCookie('refreshToken', { path: '/' }) + + return reply.status(200).send() +} From 3f053c890825b9ff5eaffb5b14a630f452652249 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 15:47:50 -0300 Subject: [PATCH 10/35] =?UTF-8?q?feat:=20Adicionar=20rotas=20para=20os=20c?= =?UTF-8?q?ontrollers=20de=20autentica=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/http/controllers/users/routes.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/http/controllers/users/routes.ts b/src/http/controllers/users/routes.ts index 6ecb433..326551d 100644 --- a/src/http/controllers/users/routes.ts +++ b/src/http/controllers/users/routes.ts @@ -1,7 +1,10 @@ import type { FastifyInstance } from 'fastify' +import { authenticate } from './authenticate' import { getUser } from './getUser' import { list } from './list' +import { logout } from './logout' +import { refresh } from './refresh' import { register } from './register' import { remove } from './remove' import { update } from './update' @@ -12,4 +15,8 @@ export async function usersRoutes(app: FastifyInstance) { app.post('/users', register) app.patch('/user/:id', update) app.delete('/user/:id', remove) + + app.post('/auth/login', authenticate) + app.patch('/auth/refresh', refresh) + app.delete('/auth/logout', logout) } From c43649e3cd1fb1d3d3054fe78d2dcf1db9c279d5 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 15:51:46 -0300 Subject: [PATCH 11/35] feat: Criar middleware para verificar jwt --- src/http/middlewares/verify-jwt.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/http/middlewares/verify-jwt.ts diff --git a/src/http/middlewares/verify-jwt.ts b/src/http/middlewares/verify-jwt.ts new file mode 100644 index 0000000..5a3df95 --- /dev/null +++ b/src/http/middlewares/verify-jwt.ts @@ -0,0 +1,9 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +export async function verifyJWT(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify() + } catch (err) { + return reply.status(401).send({ message: 'Unauthorized.' }) + } +} From 21f12ae02eb9747199740ba262f6e8cf5e526df6 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 15:54:55 -0300 Subject: [PATCH 12/35] feat: Criar middleware para verificar role do user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit criar um middleware que verificará o acesso do usuário a determinadas rotas com base na role (cargo) dele --- src/http/middlewares/verify-user-role.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/http/middlewares/verify-user-role.ts diff --git a/src/http/middlewares/verify-user-role.ts b/src/http/middlewares/verify-user-role.ts new file mode 100644 index 0000000..b7276d1 --- /dev/null +++ b/src/http/middlewares/verify-user-role.ts @@ -0,0 +1,22 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +type Role = 'CUSTOMER' | 'EMPLOYEE' | 'ADMIN' + +const roleHierarchy: Record = { + ADMIN: ['ADMIN', 'EMPLOYEE', 'CUSTOMER'], + EMPLOYEE: ['EMPLOYEE', 'CUSTOMER'], + CUSTOMER: ['CUSTOMER'], +} + +export function verifyUserRole(roleToVerify: Role) { + return async (request: FastifyRequest, reply: FastifyReply) => { + await request.jwtVerify() + const { role } = request.user + + const allowedRoles = roleHierarchy[role] + + if (!allowedRoles.includes(roleToVerify)) { + return reply.status(401).send({ message: 'Unauthorized.' }) + } + } +} From ad83d518807463f0a4ef983bb44c5762b9b93094 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 16:08:20 -0300 Subject: [PATCH 13/35] feat: Adicionar middlewares para algumas rotas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adicionar middlwares para algumas rotas que precisam verificar se o usuário está logado ou se pode acessar a rota com base na role (cargo) dele --- src/http/controllers/users/routes.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/http/controllers/users/routes.ts b/src/http/controllers/users/routes.ts index 326551d..afa2344 100644 --- a/src/http/controllers/users/routes.ts +++ b/src/http/controllers/users/routes.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' +import { verifyJWT } from '@/http/middlewares/verify-jwt' +import { verifyUserRole } from '@/http/middlewares/verify-user-role' + import { authenticate } from './authenticate' import { getUser } from './getUser' import { list } from './list' @@ -10,13 +13,15 @@ import { remove } from './remove' import { update } from './update' export async function usersRoutes(app: FastifyInstance) { - app.get('/user/:email', getUser) - app.get('/users', list) app.post('/users', register) - app.patch('/user/:id', update) - app.delete('/user/:id', remove) + app.get('/user/:email', { onRequest: [verifyJWT] }, getUser) + // TODO: separate the route of update into more + app.patch('/user/:id', { onRequest: [verifyJWT] }, update) + app.delete('/user/:id', { onRequest: [verifyJWT] }, remove) app.post('/auth/login', authenticate) app.patch('/auth/refresh', refresh) - app.delete('/auth/logout', logout) + app.delete('/auth/logout', { onRequest: [verifyJWT] }, logout) + + app.get('/users', { onRequest: [verifyUserRole('ADMIN')] }, list) } From a43c76c5a5189f6d74a7ad042b5e6a6e5411d68a Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 24 Jun 2025 18:45:17 -0300 Subject: [PATCH 14/35] refact: Remover a necessidade de passar o email para buscar o profile --- src/http/controllers/users/getUser.ts | 30 +++++++++------------------ src/http/controllers/users/routes.ts | 2 +- src/services/users/get-user.ts | 12 +++-------- 3 files changed, 14 insertions(+), 30 deletions(-) diff --git a/src/http/controllers/users/getUser.ts b/src/http/controllers/users/getUser.ts index 9c5f303..e8008ef 100644 --- a/src/http/controllers/users/getUser.ts +++ b/src/http/controllers/users/getUser.ts @@ -1,28 +1,18 @@ import type { FastifyReply, FastifyRequest } from 'fastify' -import { z } from 'zod' -import { UserNotExistsError } from '@/services/errors/user-not-exists-error' import { makeGetUserService } from '@/services/factories/make-get-user-service' export async function getUser(request: FastifyRequest, reply: FastifyReply) { - const getUserParamsSchema = z.object({ - email: z.string().email(), - }) - - const { email } = getUserParamsSchema.parse(request.params) - - let user = null - try { - const getUserService = makeGetUserService() + const getUser = makeGetUserService() - user = await getUserService.execute({ email }) - } catch (err) { - if (err instanceof UserNotExistsError) { - return reply.status(404).send({ message: err.message }) - } - - throw err - } + const { user } = await getUser.execute({ + userId: request.user.sub, + }) - return reply.status(200).send(user) + return reply.status(200).send({ + user: { + ...user, + password: undefined, + }, + }) } diff --git a/src/http/controllers/users/routes.ts b/src/http/controllers/users/routes.ts index afa2344..c127e88 100644 --- a/src/http/controllers/users/routes.ts +++ b/src/http/controllers/users/routes.ts @@ -14,7 +14,7 @@ import { update } from './update' export async function usersRoutes(app: FastifyInstance) { app.post('/users', register) - app.get('/user/:email', { onRequest: [verifyJWT] }, getUser) + app.get('/user', { onRequest: [verifyJWT] }, getUser) // TODO: separate the route of update into more app.patch('/user/:id', { onRequest: [verifyJWT] }, update) app.delete('/user/:id', { onRequest: [verifyJWT] }, remove) diff --git a/src/services/users/get-user.ts b/src/services/users/get-user.ts index 16fe8bb..7b790c2 100644 --- a/src/services/users/get-user.ts +++ b/src/services/users/get-user.ts @@ -1,19 +1,13 @@ import type { UsersRepository } from '@/repositories/users-repository' -import { UserNotExistsError } from '../errors/user-not-exists-error' - interface GetUserServiceRequest { - email: string + userId: string } export class GetUserService { constructor(private usersRepository: UsersRepository) {} - async execute({ email }: GetUserServiceRequest) { - const user = await this.usersRepository.findByEmail(email) - - if (!user) { - throw new UserNotExistsError() - } + async execute({ userId }: GetUserServiceRequest) { + const user = await this.usersRepository.findById(userId) return { user } } From 709b4fe6d2f35abb144be1854ea7501a231d5a7e Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Wed, 25 Jun 2025 14:39:38 -0300 Subject: [PATCH 15/35] =?UTF-8?q?refact:=20Passar=20o=20id=20do=20usu?= =?UTF-8?q?=C3=A1rio=20pelo=20token=20jwt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/http/controllers/users/routes.ts | 2 +- src/http/controllers/users/update.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/http/controllers/users/routes.ts b/src/http/controllers/users/routes.ts index c127e88..5737b03 100644 --- a/src/http/controllers/users/routes.ts +++ b/src/http/controllers/users/routes.ts @@ -16,7 +16,7 @@ export async function usersRoutes(app: FastifyInstance) { app.post('/users', register) app.get('/user', { onRequest: [verifyJWT] }, getUser) // TODO: separate the route of update into more - app.patch('/user/:id', { onRequest: [verifyJWT] }, update) + app.patch('/user', { onRequest: [verifyJWT] }, update) app.delete('/user/:id', { onRequest: [verifyJWT] }, remove) app.post('/auth/login', authenticate) diff --git a/src/http/controllers/users/update.ts b/src/http/controllers/users/update.ts index 155ebc9..1dd9e91 100644 --- a/src/http/controllers/users/update.ts +++ b/src/http/controllers/users/update.ts @@ -5,10 +5,6 @@ import { UserNotExistsError } from '@/services/errors/user-not-exists-error' import { makeUpdateService } from '@/services/factories/make-update-service' export async function update(request: FastifyRequest, reply: FastifyReply) { - const updateParamsSchema = z.object({ - id: z.string().cuid(), - }) - const updateBodySchema = z .object({ name: z.string().min(2), @@ -21,7 +17,7 @@ export async function update(request: FastifyRequest, reply: FastifyReply) { message: 'At least one field must be provided for update', }) - const { id } = updateParamsSchema.parse(request.params) + const id = request.user.sub const data = updateBodySchema.parse(request.body) let user = null @@ -37,5 +33,10 @@ export async function update(request: FastifyRequest, reply: FastifyReply) { throw err } - return reply.status(200).send(user) + return reply.status(200).send({ + user: { + ...user, + password: undefined, + }, + }) } From 8873b2b3f85082bc36ab2ac8cbb94770968a963f Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Wed, 25 Jun 2025 14:46:26 -0300 Subject: [PATCH 16/35] refact: Passar o id pelo jwt --- src/http/controllers/users/remove.ts | 7 +------ src/http/controllers/users/routes.ts | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/http/controllers/users/remove.ts b/src/http/controllers/users/remove.ts index 24bde42..093d291 100644 --- a/src/http/controllers/users/remove.ts +++ b/src/http/controllers/users/remove.ts @@ -1,15 +1,10 @@ import type { FastifyReply, FastifyRequest } from 'fastify' -import { z } from 'zod' import { UserNotExistsError } from '@/services/errors/user-not-exists-error' import { makeRemoveService } from '@/services/factories/make-remove-service' export async function remove(request: FastifyRequest, reply: FastifyReply) { - const removeParamsSchema = z.object({ - id: z.string().cuid(), - }) - - const { id } = removeParamsSchema.parse(request.params) + const id = request.user.sub try { const removeService = makeRemoveService() diff --git a/src/http/controllers/users/routes.ts b/src/http/controllers/users/routes.ts index 5737b03..c3a8892 100644 --- a/src/http/controllers/users/routes.ts +++ b/src/http/controllers/users/routes.ts @@ -17,7 +17,7 @@ export async function usersRoutes(app: FastifyInstance) { app.get('/user', { onRequest: [verifyJWT] }, getUser) // TODO: separate the route of update into more app.patch('/user', { onRequest: [verifyJWT] }, update) - app.delete('/user/:id', { onRequest: [verifyJWT] }, remove) + app.delete('/user', { onRequest: [verifyJWT] }, remove) app.post('/auth/login', authenticate) app.patch('/auth/refresh', refresh) From 94e366ac1c9b69285b29a9f10f60b44edfdb0d86 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Thu, 26 Jun 2025 15:25:10 -0300 Subject: [PATCH 17/35] refact: Adicionar logout ao deletar a conta --- src/http/controllers/users/remove.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/http/controllers/users/remove.ts b/src/http/controllers/users/remove.ts index 093d291..b774e3c 100644 --- a/src/http/controllers/users/remove.ts +++ b/src/http/controllers/users/remove.ts @@ -3,6 +3,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { UserNotExistsError } from '@/services/errors/user-not-exists-error' import { makeRemoveService } from '@/services/factories/make-remove-service' +import { logout } from './logout' + export async function remove(request: FastifyRequest, reply: FastifyReply) { const id = request.user.sub @@ -10,6 +12,8 @@ export async function remove(request: FastifyRequest, reply: FastifyReply) { const removeService = makeRemoveService() await removeService.execute({ id }) + + await logout(request, reply) } catch (err) { if (err instanceof UserNotExistsError) { return reply.status(404).send({ message: err.message }) From 387754737230772b48ab986ebb8aa266d9b0a7ac Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Thu, 26 Jun 2025 16:32:52 -0300 Subject: [PATCH 18/35] =?UTF-8?q?refact:=20Adicionar=20verifica=C3=A7?= =?UTF-8?q?=C3=A3o=20para=20o=20token=20existir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/http/controllers/users/refresh.ts | 6 +++++- src/http/middlewares/verify-user-role.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/http/controllers/users/refresh.ts b/src/http/controllers/users/refresh.ts index 4784efd..b3bbcbb 100644 --- a/src/http/controllers/users/refresh.ts +++ b/src/http/controllers/users/refresh.ts @@ -1,7 +1,11 @@ import type { FastifyReply, FastifyRequest } from 'fastify' export async function refresh(request: FastifyRequest, reply: FastifyReply) { - await request.jwtVerify({ onlyCookie: true }) + try { + await request.jwtVerify({ onlyCookie: true }) + } catch (err) { + return reply.status(401).send({ message: 'Unauthorized.' }) + } const { role } = request.user diff --git a/src/http/middlewares/verify-user-role.ts b/src/http/middlewares/verify-user-role.ts index b7276d1..d57d605 100644 --- a/src/http/middlewares/verify-user-role.ts +++ b/src/http/middlewares/verify-user-role.ts @@ -10,7 +10,11 @@ const roleHierarchy: Record = { export function verifyUserRole(roleToVerify: Role) { return async (request: FastifyRequest, reply: FastifyReply) => { - await request.jwtVerify() + try { + await request.jwtVerify() + } catch (err) { + return reply.status(401).send({ message: 'Unauthorized.' }) + } const { role } = request.user const allowedRoles = roleHierarchy[role] From 5096e2b73305954b2aae00f1005cedf4260d75a5 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Sat, 28 Jun 2025 15:25:07 -0300 Subject: [PATCH 19/35] feat: Habilitar CORS --- package-lock.json | 21 +++++++++++++++++++++ package.json | 3 ++- src/app.ts | 21 +++++++++++++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb690ce..00f0733 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.0.1", "@fastify/jwt": "^9.1.0", "@prisma/client": "^6.4.1", "bcryptjs": "^3.0.2", @@ -851,6 +852,26 @@ "fastify-plugin": "^5.0.0" } }, + "node_modules/@fastify/cors": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.0.1.tgz", + "integrity": "sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, "node_modules/@fastify/error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", diff --git a/package.json b/package.json index 6f8ab0d..bd8d37e 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,12 @@ }, "dependencies": { "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.0.1", "@fastify/jwt": "^9.1.0", "@prisma/client": "^6.4.1", - "fastify": "^5.3.2", "bcryptjs": "^3.0.2", "dotenv": "^16.5.0", + "fastify": "^5.3.2", "zod": "^3.24.2" } } diff --git a/src/app.ts b/src/app.ts index 3479d02..48cbf3d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,6 @@ -import fastifyCookie from '@fastify/cookie' -import fastifyJwt from '@fastify/jwt' +import { fastifyCookie } from '@fastify/cookie' +import { fastifyCors } from '@fastify/cors' +import { fastifyJwt } from '@fastify/jwt' import fastify, { type FastifyReply } from 'fastify' import { ZodError } from 'zod' import { env } from './env' @@ -8,6 +9,22 @@ import { appRoutes } from './http/controllers/routes' export const app = fastify() +app.register(fastifyCors, { + origin: (origin, cb) => { + const allowedOrigins = [ + 'http://localhost:5500', + 'https://mlkp1.github.io/Front2/', + ] + if (!origin || allowedOrigins.includes(origin)) { + cb(null, true) + return + } + cb(new Error('Not allowed'), false) + }, + credentials: true, + methods: '*', +}) + app.get('/', (_, reply: FastifyReply) => { return reply.status(200).send({ message: 'Hello, World!', From a2bf7cc9c310ef513ea29e6e26c5f150c43cad9b Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Mon, 30 Jun 2025 00:01:23 -0300 Subject: [PATCH 20/35] =?UTF-8?q?fix:=20Melhorar=20mensagem=20de=20erro=20?= =?UTF-8?q?para=20origem=20n=C3=A3o=20permitida=20no=20CORS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 48cbf3d..69d6e12 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,7 +19,7 @@ app.register(fastifyCors, { cb(null, true) return } - cb(new Error('Not allowed'), false) + cb(new Error(`Not allowed from origin ${origin}`), false) }, credentials: true, methods: '*', From 711569fdd6c7e0c95605ef2935d3e5de962fa178 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Mon, 30 Jun 2025 00:10:46 -0300 Subject: [PATCH 21/35] fix: Remover barra extra de origem permitida no CORS --- src/app.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index 69d6e12..1da3e56 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,10 +11,7 @@ export const app = fastify() app.register(fastifyCors, { origin: (origin, cb) => { - const allowedOrigins = [ - 'http://localhost:5500', - 'https://mlkp1.github.io/Front2/', - ] + const allowedOrigins = ['http://localhost:5500', 'https://mlkp1.github.io'] if (!origin || allowedOrigins.includes(origin)) { cb(null, true) return From d2779a36c64c95c59ab03fb5223fdac929503691 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Tue, 1 Jul 2025 00:20:09 -0300 Subject: [PATCH 22/35] =?UTF-8?q?docs:=20Adicionar=20se=C3=A7=C3=B5es=20ao?= =?UTF-8?q?=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 274 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b455145..8254000 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,275 @@ -# TCC - Pizzaria +# 🍕 Pizza Stars - API Backend -Esse é o back-end de um projeto de tcc, na qual estamos desenvolvendo para uma pizzaria. +> Sistema de gerenciamento para pizzaria desenvolvido como Trabalho de Conclusão de Curso (TCC) + +Este é o backend de um sistema completo para pizzaria, oferecendo funcionalidades de autenticação, gerenciamento de usuários, produtos e pedidos. + +## 📋 Sumário + +- [Funcionalidades](#-funcionalidades) +- [Tecnologias](#-tecnologias) +- [Pré-requisitos](#-pré-requisitos) +- [Instalação](#-instalação) +- [Configuração](#-configuração) +- [Uso](#-uso) +- [API Endpoints](#-api-endpoints) +- [Banco de Dados](#-banco-de-dados) +- [Scripts Disponíveis](#-scripts-disponíveis) +- [Estrutura do Projeto](#-estrutura-do-projeto) +- [Contribuição](#-contribuição) + +## 🚀 Funcionalidades + +### Autenticação e Autorização +- ✅ Registro de usuários +- ✅ Login com JWT +- ✅ Refresh token +- ✅ Logout +- ✅ Controle de acesso por roles (CUSTOMER, EMPLOYEE, ADMIN) + +### Gerenciamento de Usuários +- ✅ Perfil do usuário +- ✅ Atualização de dados +- ✅ Remoção de conta +- ✅ Listagem de usuários (Admin) + +### Sistema de Produtos +- 🍕 **Pizzas**: Diferentes tamanhos (Média, Grande, Família) e tipos (Doce, Salgada) +- 🥤 **Bebidas**: Categorias (Refrigerante, Suco, Alcoólica) com volume +- 🧁 **Sobremesas**: Tipos variados (Doce, Salgada) + +### Sistema de Pedidos +- 🛒 Carrinho de compras +- 📦 Gerenciamento de pedidos +- 📍 Sistema de endereços +- 🔄 Status de pedidos (Pendente, Processando, Entregando, Entregue, Cancelado) + +## 🛠 Tecnologias + +- **Runtime**: Node.js 20 +- **Framework**: Fastify +- **Database**: PostgreSQL +- **ORM**: Prisma +- **Autenticação**: JWT (@fastify/jwt) +- **Validação**: Zod +- **Criptografia**: bcryptjs +- **Linguagem principal**: TypeScript +- **Build**: tsup +- **Dev Tools**: tsx, biome (linting) + +## 📋 Pré-requisitos + +- Node.js 20 ou superior +- Docker e Docker Compose +- PostgreSQL (via Docker) + +## 🔧 Instalação + +1. **Clone o repositório** + ```bash + git clone https://github.com/MLKP1/Back.git + cd Back + ``` + +2. **Instale as dependências** + ```bash + npm install + ``` + +3. **Configuração do banco de dados** + ```bash + # Suba o container PostgreSQL + docker-compose up -d + ``` + +## ⚙️ Configuração + +### Variáveis de Ambiente + +Crie um arquivo `.env` na raiz do projeto: + +```env +# Database +DATABASE_URL="postgresql://lucas:lucas123@localhost:5432/pizza-stars" + +# Server +PORT=3333 +NODE_ENV="dev" + +# JWT +JWT_SECRET="sua-chave-secreta-super-segura" + +# Token de exemplo (opcional) +TOKEN="seu-token-de-teste" +``` + +### Configuração do Banco + +```bash +# Execute as migrações +npx prisma migrate dev + +# Opcional: Popular com dados de exemplo +npx prisma db seed +``` + +## 🚀 Uso + +### Desenvolvimento +```bash +npm run dev +``` +O servidor será iniciado em `http://localhost:3333` + +### Produção +```bash +# Build +npm run build + +# Start +npm start +``` + +### Outros comandos úteis +```bash +# Linting +npm run lint + +# Commit padronizado +npm run commit + +# Visualizar banco de dados +npx prisma studio +``` + +## 📚 API Endpoints + +### Autenticação +| Método | Endpoint | Descrição | Auth | +|--------|----------|-----------|------| +| `POST` | `/auth/login` | Login | ❌ | +| `PATCH` | `/auth/refresh` | Renovar token | ✅ | +| `DELETE` | `/auth/logout` | Logout | ✅ | + +### Usuários +| Método | Endpoint | Descrição | Auth | +|--------|----------|-----------|------| +| `POST` | `/users` | Registrar usuário | ❌ | +| `GET` | `/user` | Perfil do usuário | ✅ | +| `PATCH` | `/user` | Atualizar perfil | ✅ | +| `DELETE` | `/user` | Remover conta | ✅ | +| `GET` | `/users` | Listar usuários | 👑 Admin | + +### Headers de Autenticação +```http +Authorization: Bearer +``` + +### Exemplo de Requisição + +```javascript +// Registro de usuário +POST /users +Content-Type: application/json + +{ + "name": "Lucas", + "email": "lucas@gmail.com", + "password": "senha123" +} +``` + +## 💾 Banco de Dados + +### Modelos Principais + +- **User**: Usuários do sistema com roles +- **Address**: Endereços dos usuários +- **Pizza**: Produtos pizza com tamanhos e tipos +- **Drink**: Bebidas com categorias e volumes +- **Dessert**: Sobremesas variadas +- **Order**: Pedidos dos clientes +- **Cart**: Carrinho de compras + +### Diagrama ER + +#### Entidades e Relacionamentos Principais + +**Usuário (User)** +- → Tem um Endereço (Address) [1:1] +- → Tem um Carrinho (Cart) [1:1] +- → Realiza vários Pedidos (Order) [1:N] + +**Pedido (Order)** +- ← Pertence a um Usuário (User) [N:1] +- → Contém várias Pizzas (Pizza) [N:N] +- → Contém várias Bebidas (Drink) [N:N] +- → Contém várias Sobremesas (Dessert) [N:N] + +**Carrinho (Cart)** +- ← Pertence a um Usuário (User) [1:1] +- → Contém várias Pizzas (Pizza) [N:N] +- → Contém várias Bebidas (Drink) [N:N] +- → Contém várias Sobremesas (Dessert) [N:N] + +## 📜 Scripts Disponíveis + +| Script | Descrição | +|--------|-----------| +| `npm run dev` | Inicia servidor de desenvolvimento | +| `npm run build` | Build para produção | +| `npm start` | Inicia servidor de produção | +| `npm run lint` | Executa linting | +| `npm run commit` | Commit padronizado | + +## 📁 Estrutura do Projeto + +``` +prisma/ +├── schema.prisma # Schema do banco +├── seed.ts # Dados iniciais +└── migrations/ # Histórico de migrações + +src/ +└── @types/ # Definições de tipos TypeScript +├── env/ # Configuração de variáveis +├── http/ +│ ├── controllers/ # Controladores das rotas +│ │ └── users/ # Rotas de usuários +│ └── middlewares/ # Middlewares de autenticação +├── lib/ # Bibliotecas e configurações +├── repositories/ # Camada de dados +├── services/ # Lógica de negócio +│ ├── errors/ # Classes de erro customizadas +│ ├── factories/ # Factory pattern para services +│ └── users/ # Serviços de usuário +├── app.ts # Configuração do Fastify +├── server.ts # Servidor principal +``` + +## 🎯 Roadmap + +- [ ] API de produtos (CRUD pizzas, bebidas, sobremesas) +- [ ] Sistema completo de pedidos +- [ ] Integração com pagamentos +- [ ] Sistema de delivery +- [ ] Dashboard administrativo +- [ ] Notificações em tempo real +- [ ] Relatórios e analytics + +## 🤝 Contribuição + +1. Fork o projeto +2. Crie uma branch para sua feature (`git checkout -b feat/funcionalidade-legal`) +3. Faça linting e formatação (`npx biome check --write`) +4. Commit suas mudanças (`git commit -m 'feat: Adicionar uma funcionalidade legal'`) +5. Push para a branch (`git push origin feat/funcionalidade-legal`) +6. Abra um Pull Request + +## 📝 Licença + +Este projeto é desenvolvido como Trabalho de Conclusão de Curso (TCC). + +--- + +⭐ **Pizza Stars** - Transformando a experiência da sua pizzaria favorita! From 104dc5e7f242d873891d11f0d6cac025e4f4a667 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Mon, 14 Jul 2025 16:32:32 -0300 Subject: [PATCH 23/35] refact: Colocar factories de users dentro de pasta users --- src/http/controllers/users/authenticate.ts | 2 +- src/http/controllers/users/getUser.ts | 2 +- src/http/controllers/users/list.ts | 2 +- src/http/controllers/users/register.ts | 2 +- src/http/controllers/users/remove.ts | 2 +- src/http/controllers/users/update.ts | 2 +- src/services/factories/{ => users}/make-authenticate-service.ts | 2 +- src/services/factories/{ => users}/make-get-user-service.ts | 0 src/services/factories/{ => users}/make-list-service.ts | 0 src/services/factories/{ => users}/make-register-service.ts | 2 +- src/services/factories/{ => users}/make-remove-service.ts | 2 +- src/services/factories/{ => users}/make-update-service.ts | 0 12 files changed, 9 insertions(+), 9 deletions(-) rename src/services/factories/{ => users}/make-authenticate-service.ts (82%) rename src/services/factories/{ => users}/make-get-user-service.ts (100%) rename src/services/factories/{ => users}/make-list-service.ts (100%) rename src/services/factories/{ => users}/make-register-service.ts (83%) rename src/services/factories/{ => users}/make-remove-service.ts (83%) rename src/services/factories/{ => users}/make-update-service.ts (100%) diff --git a/src/http/controllers/users/authenticate.ts b/src/http/controllers/users/authenticate.ts index 38c57e6..8f19a38 100644 --- a/src/http/controllers/users/authenticate.ts +++ b/src/http/controllers/users/authenticate.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { z } from 'zod' import { InvalidCredentialsError } from '@/services/errors/invalid-credentials-error' -import { makeAuthenticateService } from '@/services/factories/make-authenticate-service' +import { makeAuthenticateService } from '@/services/factories/users/make-authenticate-service' export async function authenticate( request: FastifyRequest, diff --git a/src/http/controllers/users/getUser.ts b/src/http/controllers/users/getUser.ts index e8008ef..56d55ae 100644 --- a/src/http/controllers/users/getUser.ts +++ b/src/http/controllers/users/getUser.ts @@ -1,6 +1,6 @@ import type { FastifyReply, FastifyRequest } from 'fastify' -import { makeGetUserService } from '@/services/factories/make-get-user-service' +import { makeGetUserService } from '@/services/factories/users/make-get-user-service' export async function getUser(request: FastifyRequest, reply: FastifyReply) { const getUser = makeGetUserService() diff --git a/src/http/controllers/users/list.ts b/src/http/controllers/users/list.ts index 123f860..64935d6 100644 --- a/src/http/controllers/users/list.ts +++ b/src/http/controllers/users/list.ts @@ -1,7 +1,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { UserNotExistsError } from '@/services/errors/user-not-exists-error' -import { makeListService } from '@/services/factories/make-list-service' +import { makeListService } from '@/services/factories/users/make-list-service' export async function list(_: FastifyRequest, reply: FastifyReply) { let users = null diff --git a/src/http/controllers/users/register.ts b/src/http/controllers/users/register.ts index 5d78ecc..ed6aa9f 100644 --- a/src/http/controllers/users/register.ts +++ b/src/http/controllers/users/register.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { z } from 'zod' import { UserAlreadyExistsError } from '@/services/errors/user-already-exists-error' -import { makeRegisterService } from '@/services/factories/make-register-service' +import { makeRegisterService } from '@/services/factories/users/make-register-service' export async function register(request: FastifyRequest, reply: FastifyReply) { const registerBodySchema = z.object({ diff --git a/src/http/controllers/users/remove.ts b/src/http/controllers/users/remove.ts index b774e3c..b171749 100644 --- a/src/http/controllers/users/remove.ts +++ b/src/http/controllers/users/remove.ts @@ -1,7 +1,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { UserNotExistsError } from '@/services/errors/user-not-exists-error' -import { makeRemoveService } from '@/services/factories/make-remove-service' +import { makeRemoveService } from '@/services/factories/users/make-remove-service' import { logout } from './logout' diff --git a/src/http/controllers/users/update.ts b/src/http/controllers/users/update.ts index 1dd9e91..28f44fb 100644 --- a/src/http/controllers/users/update.ts +++ b/src/http/controllers/users/update.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { z } from 'zod' import { UserNotExistsError } from '@/services/errors/user-not-exists-error' -import { makeUpdateService } from '@/services/factories/make-update-service' +import { makeUpdateService } from '@/services/factories/users/make-update-service' export async function update(request: FastifyRequest, reply: FastifyReply) { const updateBodySchema = z diff --git a/src/services/factories/make-authenticate-service.ts b/src/services/factories/users/make-authenticate-service.ts similarity index 82% rename from src/services/factories/make-authenticate-service.ts rename to src/services/factories/users/make-authenticate-service.ts index 39e288e..9fe52f1 100644 --- a/src/services/factories/make-authenticate-service.ts +++ b/src/services/factories/users/make-authenticate-service.ts @@ -1,5 +1,5 @@ import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' -import { AuthenticateService } from '../users/authenticate' +import { AuthenticateService } from '../../users/authenticate' export function makeAuthenticateService() { const usersRepository = new PrismaUsersRepository() diff --git a/src/services/factories/make-get-user-service.ts b/src/services/factories/users/make-get-user-service.ts similarity index 100% rename from src/services/factories/make-get-user-service.ts rename to src/services/factories/users/make-get-user-service.ts diff --git a/src/services/factories/make-list-service.ts b/src/services/factories/users/make-list-service.ts similarity index 100% rename from src/services/factories/make-list-service.ts rename to src/services/factories/users/make-list-service.ts diff --git a/src/services/factories/make-register-service.ts b/src/services/factories/users/make-register-service.ts similarity index 83% rename from src/services/factories/make-register-service.ts rename to src/services/factories/users/make-register-service.ts index 7505741..a3b1395 100644 --- a/src/services/factories/make-register-service.ts +++ b/src/services/factories/users/make-register-service.ts @@ -1,5 +1,5 @@ import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' -import { RegisterService } from '../users/register' +import { RegisterService } from '../../users/register' export function makeRegisterService() { const usersRepository = new PrismaUsersRepository() diff --git a/src/services/factories/make-remove-service.ts b/src/services/factories/users/make-remove-service.ts similarity index 83% rename from src/services/factories/make-remove-service.ts rename to src/services/factories/users/make-remove-service.ts index 921b66e..d5b33ae 100644 --- a/src/services/factories/make-remove-service.ts +++ b/src/services/factories/users/make-remove-service.ts @@ -1,5 +1,5 @@ import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' -import { RemoveService } from '../users/remove' +import { RemoveService } from '../../users/remove' export function makeRemoveService() { const usersRepository = new PrismaUsersRepository() diff --git a/src/services/factories/make-update-service.ts b/src/services/factories/users/make-update-service.ts similarity index 100% rename from src/services/factories/make-update-service.ts rename to src/services/factories/users/make-update-service.ts From 072915e23cc94cc150079d8c42cb7ff463ce0e28 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Thu, 24 Jul 2025 18:20:30 -0300 Subject: [PATCH 24/35] chore: Tornar campo zipCode Int e complement opcional --- .../migration.sql | 10 ++++++++++ prisma/schema.prisma | 10 +++++----- prisma/seed.ts | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 prisma/migrations/20250724210200_mudar_dois_campos_da_tabela_address/migration.sql diff --git a/prisma/migrations/20250724210200_mudar_dois_campos_da_tabela_address/migration.sql b/prisma/migrations/20250724210200_mudar_dois_campos_da_tabela_address/migration.sql new file mode 100644 index 0000000..a57fa57 --- /dev/null +++ b/prisma/migrations/20250724210200_mudar_dois_campos_da_tabela_address/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - Changed the type of `zip_code` on the `addresses` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- AlterTable +ALTER TABLE "addresses" ALTER COLUMN "complement" DROP NOT NULL, +DROP COLUMN "zip_code", +ADD COLUMN "zip_code" INTEGER NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7b3b1e6..6849d5f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,15 +8,15 @@ datasource db { } model Address { - id String @id @default(cuid()) + id String @id @default(cuid()) number Int street String neighborhood String - complement String + complement String? city String - state String @default("SP") - country String @default("BR") - zipCode String @map("zip_code") + state String @default("SP") + country String @default("BR") + zipCode Int @map("zip_code") latitude Float longitude Float diff --git a/prisma/seed.ts b/prisma/seed.ts index 71da612..41e2d31 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -56,7 +56,7 @@ async function seed() { street: faker.location.street(), neighborhood: faker.location.secondaryAddress(), number: Number.parseInt(faker.location.buildingNumber()), - zipCode: faker.location.zipCode(), + zipCode: Number.parseInt(faker.location.zipCode()), latitude: faker.location.latitude(), longitude: faker.location.longitude(), userId: randomUser.id, From 5a07e5f26028e2c3ba698a885b9e1822b101c971 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Fri, 25 Jul 2025 18:48:48 -0300 Subject: [PATCH 25/35] =?UTF-8?q?feat:=20Implementar=20reposit=C3=B3rios?= =?UTF-8?q?=20de=20address?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/address-repository.ts | 12 ++++ .../prisma/prisma-address-repository.ts | 58 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/repositories/address-repository.ts create mode 100644 src/repositories/prisma/prisma-address-repository.ts diff --git a/src/repositories/address-repository.ts b/src/repositories/address-repository.ts new file mode 100644 index 0000000..e212698 --- /dev/null +++ b/src/repositories/address-repository.ts @@ -0,0 +1,12 @@ +import type { Address, Prisma } from '@prisma/client' + +export interface AddressRepository { + create(userId: string, data: Prisma.AddressCreateInput): Promise
+ findByUserId(userId: string): Promise
+ updateByUserId( + userId: string, + data: Prisma.AddressUpdateInput, + ): Promise
+ removeByUserId(userId: string): Promise
+ list(): Promise +} diff --git a/src/repositories/prisma/prisma-address-repository.ts b/src/repositories/prisma/prisma-address-repository.ts new file mode 100644 index 0000000..5a5a2b7 --- /dev/null +++ b/src/repositories/prisma/prisma-address-repository.ts @@ -0,0 +1,58 @@ +import { prisma } from '@/lib/prisma' +import type { Prisma } from '@prisma/client' + +export class PrismaAddressRepository { + async create(userId: string, data: Prisma.AddressCreateInput) { + const address = await prisma.address.create({ + data: { + ...data, + user: { + connect: { id: userId }, + }, + }, + }) + + return address + } + + async findByUserId(userId: string) { + const address = await prisma.address.findFirst({ + where: { + userId, + }, + }) + + return address + } + + async updateByUserId(userId: string, data: Prisma.AddressUpdateInput) { + const address = await prisma.address.update({ + where: { + userId, + }, + data, + }) + + return address + } + + async list() { + const address = await prisma.address.findMany({ + orderBy: { + updatedAt: 'desc', + }, + }) + + return address + } + + async removeByUserId(userId: string) { + const address = await prisma.address.delete({ + where: { + userId, + }, + }) + + return address + } +} From 8388f483cfe7c00a27feeaae4ca1daa13b8ace3b Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Fri, 25 Jul 2025 18:50:26 -0300 Subject: [PATCH 26/35] feat: Criar classes de erros para address --- src/services/errors/address-already-exists-error.ts | 5 +++++ src/services/errors/address-not-exists-error.ts | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 src/services/errors/address-already-exists-error.ts create mode 100644 src/services/errors/address-not-exists-error.ts diff --git a/src/services/errors/address-already-exists-error.ts b/src/services/errors/address-already-exists-error.ts new file mode 100644 index 0000000..26dab77 --- /dev/null +++ b/src/services/errors/address-already-exists-error.ts @@ -0,0 +1,5 @@ +export class AddressAlreadyExistsError extends Error { + constructor() { + super('Address already exists.') + } +} diff --git a/src/services/errors/address-not-exists-error.ts b/src/services/errors/address-not-exists-error.ts new file mode 100644 index 0000000..03d9a82 --- /dev/null +++ b/src/services/errors/address-not-exists-error.ts @@ -0,0 +1,5 @@ +export class AddressNotExistsError extends Error { + constructor() { + super('Address not exists.') + } +} From 666accf437006886659e56862fc27484fd2e6fef Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Fri, 25 Jul 2025 19:40:18 -0300 Subject: [PATCH 27/35] feat: Criar services de address --- src/services/addresses/get-address.ts | 20 ++++++++ src/services/addresses/list.ts | 11 ++++ src/services/addresses/register.ts | 73 +++++++++++++++++++++++++++ src/services/addresses/remove.ts | 22 ++++++++ src/services/addresses/update.ts | 27 ++++++++++ 5 files changed, 153 insertions(+) create mode 100644 src/services/addresses/get-address.ts create mode 100644 src/services/addresses/list.ts create mode 100644 src/services/addresses/register.ts create mode 100644 src/services/addresses/remove.ts create mode 100644 src/services/addresses/update.ts diff --git a/src/services/addresses/get-address.ts b/src/services/addresses/get-address.ts new file mode 100644 index 0000000..4ed0d34 --- /dev/null +++ b/src/services/addresses/get-address.ts @@ -0,0 +1,20 @@ +import type { AddressRepository } from '@/repositories/address-repository' +import { AddressNotExistsError } from '../errors/address-not-exists-error' + +interface GetAddressServiceRequest { + userId: string +} + +export class GetAddressService { + constructor(private addressRepository: AddressRepository) {} + + async execute({ userId }: GetAddressServiceRequest) { + const address = await this.addressRepository.findByUserId(userId) + + if (!address) { + throw new AddressNotExistsError() + } + + return { address } + } +} diff --git a/src/services/addresses/list.ts b/src/services/addresses/list.ts new file mode 100644 index 0000000..38bf61d --- /dev/null +++ b/src/services/addresses/list.ts @@ -0,0 +1,11 @@ +import type { AddressRepository } from '@/repositories/address-repository' + +export class ListService { + constructor(private addressRepository: AddressRepository) {} + + async execute() { + const address = await this.addressRepository.list() + + return { address } + } +} diff --git a/src/services/addresses/register.ts b/src/services/addresses/register.ts new file mode 100644 index 0000000..ad44642 --- /dev/null +++ b/src/services/addresses/register.ts @@ -0,0 +1,73 @@ +import type { AddressRepository } from '@/repositories/address-repository' +import type { UsersRepository } from '@/repositories/users-repository' +import { AddressAlreadyExistsError } from '../errors/address-already-exists-error' +import { UserNotExistsError } from '../errors/user-not-exists-error' + +interface RegisterServiceRequest { + number: number + street: string + neighborhood: string + complement?: string + city: string + state?: string + country?: string + zipCode: number + latitude: number + longitude: number + + userId: string +} + +export class RegisterService { + constructor( + private addressRepository: AddressRepository, + private userRepository: UsersRepository, + ) {} + + async execute({ + number, + street, + neighborhood, + complement, + city, + state, + country, + zipCode, + latitude, + longitude, + userId, + }: RegisterServiceRequest) { + const doesUserExists = await this.userRepository.findById(userId) + + if (!doesUserExists) { + throw new UserNotExistsError() + } + + const doesAddressAlreadyExists = + await this.addressRepository.findByUserId(userId) + + if (doesAddressAlreadyExists) { + throw new AddressAlreadyExistsError() + } + + const address = await this.addressRepository.create(userId, { + number, + street, + neighborhood, + complement, + city, + state, + country, + zipCode, + latitude, + longitude, + user: { + connect: { + id: userId, + }, + }, + }) + + return { address } + } +} diff --git a/src/services/addresses/remove.ts b/src/services/addresses/remove.ts new file mode 100644 index 0000000..2c7a354 --- /dev/null +++ b/src/services/addresses/remove.ts @@ -0,0 +1,22 @@ +import type { AddressRepository } from '@/repositories/address-repository' +import { AddressNotExistsError } from '../errors/address-not-exists-error' + +interface RemoveServiceRequest { + userId: string +} + +export class RemoveService { + constructor(private addressRepository: AddressRepository) {} + + async execute({ userId }: RemoveServiceRequest) { + const doesAddressExists = await this.addressRepository.findByUserId(userId) + + if (!doesAddressExists) { + throw new AddressNotExistsError() + } + + const address = await this.addressRepository.removeByUserId(userId) + + return { address } + } +} diff --git a/src/services/addresses/update.ts b/src/services/addresses/update.ts new file mode 100644 index 0000000..034303f --- /dev/null +++ b/src/services/addresses/update.ts @@ -0,0 +1,27 @@ +import type { AddressRepository } from '@/repositories/address-repository' +import { AddressNotExistsError } from '../errors/address-not-exists-error' + +interface UpdateServiceRequest { + userId: string + data: { + zipCode?: number + number?: number + complement?: string + } +} + +export class UpdateService { + constructor(private addressRepository: AddressRepository) {} + + async execute({ userId, data }: UpdateServiceRequest) { + const doesAddressExists = await this.addressRepository.findByUserId(userId) + + if (!doesAddressExists) { + throw new AddressNotExistsError() + } + + const address = await this.addressRepository.updateByUserId(userId, data) + + return { address } + } +} From 3422254b26af88a5a42de95c3642876ed5bb0d49 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Fri, 25 Jul 2025 19:46:04 -0300 Subject: [PATCH 28/35] feat: Implementar factories de address --- .../addresses/make-get-address-service.ts | 9 +++++++++ .../factories/addresses/make-list-service.ts | 9 +++++++++ .../factories/addresses/make-register-service.ts | 15 +++++++++++++++ .../factories/addresses/make-remove-service.ts | 9 +++++++++ .../factories/addresses/make-update-service.ts | 9 +++++++++ 5 files changed, 51 insertions(+) create mode 100644 src/services/factories/addresses/make-get-address-service.ts create mode 100644 src/services/factories/addresses/make-list-service.ts create mode 100644 src/services/factories/addresses/make-register-service.ts create mode 100644 src/services/factories/addresses/make-remove-service.ts create mode 100644 src/services/factories/addresses/make-update-service.ts diff --git a/src/services/factories/addresses/make-get-address-service.ts b/src/services/factories/addresses/make-get-address-service.ts new file mode 100644 index 0000000..9b09a10 --- /dev/null +++ b/src/services/factories/addresses/make-get-address-service.ts @@ -0,0 +1,9 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { GetAddressService } from '@/services/addresses/get-address' + +export function makeGetAddressService() { + const addressRepository = new PrismaAddressRepository() + const getAddressService = new GetAddressService(addressRepository) + + return getAddressService +} diff --git a/src/services/factories/addresses/make-list-service.ts b/src/services/factories/addresses/make-list-service.ts new file mode 100644 index 0000000..9677010 --- /dev/null +++ b/src/services/factories/addresses/make-list-service.ts @@ -0,0 +1,9 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { ListService } from '@/services/addresses/list' + +export function makeListService() { + const addressRepository = new PrismaAddressRepository() + const listService = new ListService(addressRepository) + + return listService +} diff --git a/src/services/factories/addresses/make-register-service.ts b/src/services/factories/addresses/make-register-service.ts new file mode 100644 index 0000000..edbae20 --- /dev/null +++ b/src/services/factories/addresses/make-register-service.ts @@ -0,0 +1,15 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' +import { RegisterService } from '@/services/addresses/register' + +export function makeRegisterService() { + const addressRepository = new PrismaAddressRepository() + const usersRepository = new PrismaUsersRepository() + + const registerService = new RegisterService( + addressRepository, + usersRepository, + ) + + return registerService +} diff --git a/src/services/factories/addresses/make-remove-service.ts b/src/services/factories/addresses/make-remove-service.ts new file mode 100644 index 0000000..36888c7 --- /dev/null +++ b/src/services/factories/addresses/make-remove-service.ts @@ -0,0 +1,9 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { RemoveService } from '@/services/addresses/remove' + +export function makeRemoveService() { + const addressRepository = new PrismaAddressRepository() + const removeService = new RemoveService(addressRepository) + + return removeService +} diff --git a/src/services/factories/addresses/make-update-service.ts b/src/services/factories/addresses/make-update-service.ts new file mode 100644 index 0000000..0d4c8b0 --- /dev/null +++ b/src/services/factories/addresses/make-update-service.ts @@ -0,0 +1,9 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { UpdateService } from '@/services/addresses/update' + +export function makeUpdateService() { + const addressRepository = new PrismaAddressRepository() + const updateService = new UpdateService(addressRepository) + + return updateService +} From bf61d345fa9540bebaa000e87f9d8d68af318f01 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Sat, 26 Jul 2025 12:02:22 -0300 Subject: [PATCH 29/35] feat: Criar utils de obter address pelo zipCode --- src/utils/get-address-by-zipcode.ts | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/utils/get-address-by-zipcode.ts diff --git a/src/utils/get-address-by-zipcode.ts b/src/utils/get-address-by-zipcode.ts new file mode 100644 index 0000000..fd5d65c --- /dev/null +++ b/src/utils/get-address-by-zipcode.ts @@ -0,0 +1,70 @@ +interface BrazilApiResponse { + cep: number + state: string + city: string + neighborhood: string + street: string + service: string + location: { + type: string + coordinates: { + latitude: number + longitude: number + } + } +} + +export interface AddressApiResponse { + latitudeApi: number + longitudeApi: number + streetApi: string + neighborhoodApi: string + cityApi: string + zipCodeApi: number +} + +export async function getAddressByZipCode( + zipCode: number, +): Promise { + let data: BrazilApiResponse + try { + const response = await fetch( + `https://brasilapi.com.br/api/cep/v2/${zipCode}`, + { + signal: AbortSignal.timeout(5000), + }, + ) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + data = await response.json() + } catch (err) { + throw new Error( + `Erro ao buscar dados do endereço: ${(err as Error).message}`, + ) + } + + const zipCodeApi = Number(data.cep) + const streetApi = data.street + const neighborhoodApi = data.neighborhood + const cityApi = data.city + const latitudeApi = Number(data.location.coordinates.latitude) + const longitudeApi = Number(data.location.coordinates.longitude) + + if (!latitudeApi || !longitudeApi) { + throw new Error( + 'Não foi possível obter as coordenadas para o CEP informado.', + ) + } + + return { + zipCodeApi, + streetApi, + neighborhoodApi, + cityApi, + latitudeApi, + longitudeApi, + } +} From bba90f44ecd4a88d125df110e4f314f3acec27a9 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Sat, 26 Jul 2025 12:05:19 -0300 Subject: [PATCH 30/35] fix: Implementar tratativa correta para alterar o address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit antes não seria possível alterar o address, porque seria alterado somente o zipCode --- src/services/addresses/update.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/services/addresses/update.ts b/src/services/addresses/update.ts index 034303f..5f9b678 100644 --- a/src/services/addresses/update.ts +++ b/src/services/addresses/update.ts @@ -1,4 +1,5 @@ import type { AddressRepository } from '@/repositories/address-repository' +import type { AddressApiResponse } from '@/utils/get-address-by-zipcode' import { AddressNotExistsError } from '../errors/address-not-exists-error' interface UpdateServiceRequest { @@ -7,6 +8,7 @@ interface UpdateServiceRequest { zipCode?: number number?: number complement?: string + addressApi: AddressApiResponse | null } } @@ -20,7 +22,26 @@ export class UpdateService { throw new AddressNotExistsError() } - const address = await this.addressRepository.updateByUserId(userId, data) + let address = null + if (data.zipCode !== undefined && data.addressApi !== null) { + address = await this.addressRepository.updateByUserId(userId, { + zipCode: data.zipCode, + number: data.number, + complement: data.complement, + neighborhood: data.addressApi.neighborhoodApi, + street: data.addressApi.streetApi, + city: data.addressApi.cityApi, + latitude: data.addressApi.latitudeApi, + longitude: data.addressApi.longitudeApi, + }) + } + + if (data.zipCode === undefined) { + address = await this.addressRepository.updateByUserId(userId, { + number: data.number, + complement: data.complement, + }) + } return { address } } From 8d0f8eae4d1a7c0a18d737489232facef4248e51 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Sat, 26 Jul 2025 15:17:05 -0300 Subject: [PATCH 31/35] feat: Criar utils de validar address --- src/utils/validate-address.ts | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/utils/validate-address.ts diff --git a/src/utils/validate-address.ts b/src/utils/validate-address.ts new file mode 100644 index 0000000..d4b2443 --- /dev/null +++ b/src/utils/validate-address.ts @@ -0,0 +1,41 @@ +import type { AddressApiResponse } from './get-address-by-zipcode' + +interface AddressUser { + street: string + neighborhood: string + city: string + zipCode: number + latitude: number + longitude: number +} + +const errorMessage = + 'Os dados fornecidos não correspondem ao endereço retornado pela API.' + +export function validateAddressFromApi( + { latitude, longitude, street, neighborhood, city, zipCode }: AddressUser, + { + zipCodeApi, + streetApi, + neighborhoodApi, + cityApi, + latitudeApi, + longitudeApi, + }: AddressApiResponse, +) { + if ( + zipCodeApi !== zipCode || + latitudeApi !== latitude || + longitudeApi !== longitude + ) { + throw new Error(errorMessage) + } + + if ( + streetApi.toLowerCase() !== street.toLowerCase() || + neighborhoodApi.toLowerCase() !== neighborhood.toLowerCase() || + cityApi.toLowerCase() !== city.toLowerCase() + ) { + throw new Error(errorMessage) + } +} From 9b876b6ab8840d8b0ed6de02c5dcf2d7c9b20135 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Sat, 26 Jul 2025 15:23:28 -0300 Subject: [PATCH 32/35] feat: Criar controllers de address --- src/http/controllers/address/getAddress.ts | 23 ++++++ src/http/controllers/address/list.ts | 17 +++++ src/http/controllers/address/register.ts | 86 ++++++++++++++++++++++ src/http/controllers/address/remove.ts | 21 ++++++ src/http/controllers/address/update.ts | 79 ++++++++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 src/http/controllers/address/getAddress.ts create mode 100644 src/http/controllers/address/list.ts create mode 100644 src/http/controllers/address/register.ts create mode 100644 src/http/controllers/address/remove.ts create mode 100644 src/http/controllers/address/update.ts diff --git a/src/http/controllers/address/getAddress.ts b/src/http/controllers/address/getAddress.ts new file mode 100644 index 0000000..d42c727 --- /dev/null +++ b/src/http/controllers/address/getAddress.ts @@ -0,0 +1,23 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +import { AddressNotExistsError } from '@/services/errors/address-not-exists-error' +import { makeGetAddressService } from '@/services/factories/addresses/make-get-address-service' + +export async function getAddress(request: FastifyRequest, reply: FastifyReply) { + const userId = request.user.sub + + let address = null + try { + const getAddressService = makeGetAddressService() + + address = await getAddressService.execute({ userId }) + } catch (err) { + if (err instanceof AddressNotExistsError) { + return reply.status(404).send({ message: err.message }) + } + + throw err + } + + return reply.status(200).send(address) +} diff --git a/src/http/controllers/address/list.ts b/src/http/controllers/address/list.ts new file mode 100644 index 0000000..704c16d --- /dev/null +++ b/src/http/controllers/address/list.ts @@ -0,0 +1,17 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +import { makeListService } from '@/services/factories/addresses/make-list-service' + +export async function list(_: FastifyRequest, reply: FastifyReply) { + let addresses = null + try { + const listService = makeListService() + + addresses = await listService.execute() + } catch (err) { + // biome-ignore lint/complexity/noUselessCatch: + throw err + } + + return reply.status(200).send({ addresses: addresses.address }) +} diff --git a/src/http/controllers/address/register.ts b/src/http/controllers/address/register.ts new file mode 100644 index 0000000..f3d952a --- /dev/null +++ b/src/http/controllers/address/register.ts @@ -0,0 +1,86 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' +import { z } from 'zod' + +import { AddressAlreadyExistsError } from '@/services/errors/address-already-exists-error' +import { UserNotExistsError } from '@/services/errors/user-not-exists-error' +import { makeRegisterService } from '@/services/factories/addresses/make-register-service' +import { + type AddressApiResponse, + getAddressByZipCode, +} from '@/utils/get-address-by-zipcode' +import { validateAddressFromApi } from '@/utils/validate-address' + +export async function register(request: FastifyRequest, reply: FastifyReply) { + const registerBodySchema = z.object({ + number: z.number().int().nonnegative().min(100).max(99999), + street: z.string().nonempty(), + neighborhood: z.string().nonempty(), + complement: z.string().optional(), + city: z.string().nonempty(), + zipCode: z + .number() + .int() + .min(10000000) + .max(99999999) + .refine( + zip => { + const prefix = Number.parseInt(zip.toString().substring(0, 2)) + return prefix >= 1 && prefix <= 19 + }, + { + message: 'CEP não pertence ao estado de São Paulo', + }, + ), + latitude: z.number().min(-90).max(90).optional(), + longitude: z.number().min(-180).max(180).optional(), + }) + + const parsedBody = registerBodySchema.parse(request.body) + let { zipCode, latitude, longitude, street, neighborhood, city } = parsedBody + + let addressData: AddressApiResponse + try { + addressData = await getAddressByZipCode(zipCode) + } catch (err) { + return reply.status(400).send({ + message: (err as Error).message, + }) + } + + if (!latitude || !longitude) { + latitude = addressData.latitudeApi + longitude = addressData.longitudeApi + } + + try { + validateAddressFromApi( + { zipCode, street, neighborhood, city, latitude, longitude }, + addressData, + ) + } catch (err) { + return reply.status(400).send({ message: (err as Error).message }) + } + + try { + const registerService = makeRegisterService() + + await registerService.execute({ + ...parsedBody, + userId: request.user.sub, + latitude, + longitude, + }) + } catch (err) { + if (err instanceof UserNotExistsError) { + return reply.status(404).send({ message: err.message }) + } + + if (err instanceof AddressAlreadyExistsError) { + return reply.status(409).send({ message: err.message }) + } + + throw err + } + + return reply.status(201).send() +} diff --git a/src/http/controllers/address/remove.ts b/src/http/controllers/address/remove.ts new file mode 100644 index 0000000..3413b10 --- /dev/null +++ b/src/http/controllers/address/remove.ts @@ -0,0 +1,21 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +import { AddressNotExistsError } from '@/services/errors/address-not-exists-error' +import { makeRemoveService } from '@/services/factories/addresses/make-remove-service' + +export async function remove(request: FastifyRequest, reply: FastifyReply) { + const userId = request.user.sub + try { + const removeService = makeRemoveService() + + await removeService.execute({ userId }) + } catch (err) { + if (err instanceof AddressNotExistsError) { + return reply.status(400).send({ message: err.message }) + } + + throw err + } + + return reply.status(204).send() +} diff --git a/src/http/controllers/address/update.ts b/src/http/controllers/address/update.ts new file mode 100644 index 0000000..b1e5bfd --- /dev/null +++ b/src/http/controllers/address/update.ts @@ -0,0 +1,79 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' +import { z } from 'zod' + +import { AddressNotExistsError } from '@/services/errors/address-not-exists-error' +import { makeUpdateService } from '@/services/factories/addresses/make-update-service' +import { getAddressByZipCode } from '@/utils/get-address-by-zipcode' + +export async function update(request: FastifyRequest, reply: FastifyReply) { + const updateBodySchema = z + .object({ + zipCode: z + .number() + .int() + .min(10000000) + .max(99999999) + .refine( + zip => { + const prefix = Number.parseInt(zip.toString().substring(0, 2)) + return prefix >= 1 && prefix <= 19 + }, + { + message: 'CEP não pertence ao estado de São Paulo', + }, + ) + .optional(), + number: z.number().int().nonnegative().min(100).max(99999).optional(), + complement: z.string().optional(), + }) + .refine( + data => { + if (data.zipCode !== undefined && data.number === undefined) { + return false + } + + return ( + data.zipCode !== undefined || + data.number !== undefined || + data.complement !== undefined + ) + }, + { + message: + 'Se zipCode for fornecido, number também deve ser fornecido. Pelo menos um campo deve ser fornecido.', + }, + ) + + const userId = request.user.sub + const parsedBody = updateBodySchema.parse(request.body) + const { zipCode } = parsedBody + + let addressApi = null + try { + if (zipCode) { + addressApi = await getAddressByZipCode(zipCode) + } + } catch (err) { + return reply.status(400).send({ + message: (err as Error).message, + }) + } + + let address = null + try { + const updateService = makeUpdateService() + + address = await updateService.execute({ + userId, + data: { ...parsedBody, addressApi }, + }) + } catch (err) { + if (err instanceof AddressNotExistsError) { + return reply.status(404).send({ message: err.message }) + } + + throw err + } + + return reply.status(200).send(address) +} From 1bf6f77fdc80c2aa8b9c206634609ba61f898df7 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Sat, 26 Jul 2025 15:24:51 -0300 Subject: [PATCH 33/35] feat: Implementar rotas de address --- src/http/controllers/address/routes.ts | 19 +++++++++++++++++++ src/http/controllers/routes.ts | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 src/http/controllers/address/routes.ts diff --git a/src/http/controllers/address/routes.ts b/src/http/controllers/address/routes.ts new file mode 100644 index 0000000..f1fc52c --- /dev/null +++ b/src/http/controllers/address/routes.ts @@ -0,0 +1,19 @@ +import type { FastifyInstance } from 'fastify' + +import { verifyJWT } from '@/http/middlewares/verify-jwt' +import { verifyUserRole } from '@/http/middlewares/verify-user-role' + +import { getAddress } from './getAddress' +import { list } from './list' +import { register } from './register' +import { remove } from './remove' +import { update } from './update' + +export async function addressRoutes(app: FastifyInstance) { + app.post('/address', { onRequest: [verifyJWT] }, register) + app.get('/address', { onRequest: [verifyJWT] }, getAddress) + app.patch('/address', { onRequest: [verifyJWT] }, update) + app.delete('/address', { onRequest: [verifyJWT] }, remove) + + app.get('/addresses', { onRequest: [verifyUserRole('ADMIN')] }, list) +} diff --git a/src/http/controllers/routes.ts b/src/http/controllers/routes.ts index 99fc169..c942564 100644 --- a/src/http/controllers/routes.ts +++ b/src/http/controllers/routes.ts @@ -1,7 +1,9 @@ import type { FastifyInstance } from 'fastify' +import { addressRoutes } from './address/routes' import { usersRoutes } from './users/routes' export async function appRoutes(app: FastifyInstance) { app.register(usersRoutes) + app.register(addressRoutes) } From 2c977fbd7a9eae41f2eef967bf9ad9ce3c237903 Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Sat, 26 Jul 2025 15:48:08 -0300 Subject: [PATCH 34/35] feat: Implementar tratativa para o banco implementar tratativa para quando o banco estiver fora do ar --- src/app.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app.ts b/src/app.ts index 48cbf3d..25a2768 100644 --- a/src/app.ts +++ b/src/app.ts @@ -53,6 +53,10 @@ app.setErrorHandler((error, _, reply) => { .send({ message: 'Validation error.', issues: error.format() }) } + if (error.message.includes("Can't reach database server")) { + return reply.status(503).send({ message: 'Database error.' }) + } + if (env.NODE_ENV !== 'prod') { console.error(error) } else { From 8312e31a0f6004a9bb0084c21d10bf0871bdaaff Mon Sep 17 00:00:00 2001 From: lucaslinyker Date: Sat, 26 Jul 2025 15:48:46 -0300 Subject: [PATCH 35/35] =?UTF-8?q?chore:=20Adicionar=20configura=C3=A7?= =?UTF-8?q?=C3=B5es=20ao=20.vscode/settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b6fc9e8..9623b34 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + "files.eol": "\r\n", "editor.defaultFormatter": "biomejs.biome", "editor.formatOnSave": true, "files.autoSave": "off", @@ -8,5 +9,6 @@ "[prisma]": { "editor.defaultFormatter": "Prisma.prisma", "editor.formatOnSave": true - } + }, + "cSpell.words": ["bitnami", "zipcode"] }