From dc4bdbd792c6f8c0b0ea11e7cce1db4f39af166d Mon Sep 17 00:00:00 2001 From: Rafael do Amaral Costa Date: Sun, 20 Sep 2026 21:18:52 -0300 Subject: [PATCH 1/3] feat: login --- back/package-lock.json | 243 +++++++++++++++++- back/package.json | 7 +- back/src/controllers/authcontroller.ts | 27 ++ .../database/connection/DatabaseConnection.ts | 0 back/src/interfaces/usuarioInterface.ts | 7 + back/src/middleware/authMiddleware.ts | 30 +++ back/src/routes/authRoutes.ts | 8 + back/src/services/authService.ts | 38 +++ back/src/yup/authYup.ts | 24 ++ 9 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 back/src/controllers/authcontroller.ts create mode 100644 back/src/database/connection/DatabaseConnection.ts create mode 100644 back/src/interfaces/usuarioInterface.ts create mode 100644 back/src/middleware/authMiddleware.ts create mode 100644 back/src/routes/authRoutes.ts create mode 100644 back/src/services/authService.ts create mode 100644 back/src/yup/authYup.ts diff --git a/back/package-lock.json b/back/package-lock.json index 881d631..b388724 100644 --- a/back/package-lock.json +++ b/back/package-lock.json @@ -9,15 +9,20 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", "knex": "^3.3.0", - "pg": "^8.23.0" + "pg": "^8.23.0", + "yup": "^1.7.1" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^26.4.0", "@types/pg": "^8.23.1", "tsx": "^4.23.13", @@ -466,6 +471,16 @@ "node": ">=18" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -529,6 +544,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.4.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", @@ -939,6 +972,20 @@ "node": ">= 0.6" } }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -976,6 +1023,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1138,6 +1191,15 @@ "node": ">= 0.4" } }, + "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/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1533,6 +1595,49 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/knex": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/knex/-/knex-3.3.0.tgz", @@ -1622,6 +1727,48 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1716,6 +1863,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1917,6 +2084,12 @@ "node": ">=0.10.0" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2032,12 +2205,44 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -2209,6 +2414,12 @@ "node": ">=8" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2218,6 +2429,12 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, "node_modules/tsx": { "version": "4.23.13", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", @@ -2237,6 +2454,18 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -2342,6 +2571,18 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } } } } diff --git a/back/package.json b/back/package.json index 20ae85c..ea2a86f 100644 --- a/back/package.json +++ b/back/package.json @@ -17,15 +17,20 @@ "license": "ISC", "description": "", "dependencies": { + "bcrypt": "^6.0.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", "knex": "^3.3.0", - "pg": "^8.23.0" + "pg": "^8.23.0", + "yup": "^1.7.1" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^26.4.0", "@types/pg": "^8.23.1", "tsx": "^4.23.13", diff --git a/back/src/controllers/authcontroller.ts b/back/src/controllers/authcontroller.ts new file mode 100644 index 0000000..2271af3 --- /dev/null +++ b/back/src/controllers/authcontroller.ts @@ -0,0 +1,27 @@ +import type { Request, Response } from 'express'; +import { ValidationError } from 'yup'; +import { AuthService } from '../services/authService.js'; +import { authSchema } from '../yup/authYup.js'; + +export class AuthController { + static async login(req: Request, res: Response) { + try { + const dados = await authSchema.validate(req.body, { + abortEarly: false, + stripUnknown: true, + }); + + const resultado = await AuthService.login(dados); + if (!resultado) { + return res.status(401).json({ message: 'Credenciais inválidas' }); + } + return res.status(200).json(resultado); + } catch (err) { + if (err instanceof ValidationError) { + return res.status(400).json({ message: 'Dados inválidos', errors: err.errors }); + } + console.error(err); + return res.status(500).json({ message: 'Erro interno do servidor' }); + } + } +} \ No newline at end of file diff --git a/back/src/database/connection/DatabaseConnection.ts b/back/src/database/connection/DatabaseConnection.ts new file mode 100644 index 0000000..e69de29 diff --git a/back/src/interfaces/usuarioInterface.ts b/back/src/interfaces/usuarioInterface.ts new file mode 100644 index 0000000..e3b7b8d --- /dev/null +++ b/back/src/interfaces/usuarioInterface.ts @@ -0,0 +1,7 @@ +export interface Usuario { + id: number; + nome: string; + email: string; + celular: string; + senha: string; +} \ No newline at end of file diff --git a/back/src/middleware/authMiddleware.ts b/back/src/middleware/authMiddleware.ts new file mode 100644 index 0000000..59770be --- /dev/null +++ b/back/src/middleware/authMiddleware.ts @@ -0,0 +1,30 @@ +import { NextFunction, Request, Response } from 'express'; +import jwt, { JwtPayload } from 'jsonwebtoken'; +import { getJwtSecret } from '../config/auth'; + +export function authMiddleware(req: Request, res: Response, next: NextFunction) { + const authorization = req.headers.authorization; + + if (!authorization) { + return res.status(401).json({ message: 'Token não informado' }); + } + + const [esquema, token] = authorization.split(' '); + + if (esquema !== 'Bearer' || !token) { + return res.status(401).json({ message: 'Token mal formatado' }); + } + + const secret = getJwtSecret(); + + try { + const payload = jwt.verify(token, secret, { algorithms: ['HS256'] }) as JwtPayload; + + req.user = { id: Number(payload.sub) }; + + return next(); + } catch (err) { + const message = err instanceof jwt.TokenExpiredError ? 'Token expirado' : 'Token inválido'; + return res.status(401).json({ message }); + } +} \ No newline at end of file diff --git a/back/src/routes/authRoutes.ts b/back/src/routes/authRoutes.ts new file mode 100644 index 0000000..129ff54 --- /dev/null +++ b/back/src/routes/authRoutes.ts @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { AuthController } from '../controllers/authcontroller.js'; + +const authRoutes = Router(); + +authRoutes.post('/auth/login', AuthController.login); + +export default authRoutes; \ No newline at end of file diff --git a/back/src/services/authService.ts b/back/src/services/authService.ts new file mode 100644 index 0000000..b44b98b --- /dev/null +++ b/back/src/services/authService.ts @@ -0,0 +1,38 @@ +import bcrypt from 'bcrypt'; +import jwt, { type SignOptions } from 'jsonwebtoken'; +import db from '../database/connection'; +import { getJwtSecret, JWT_EXPIRES_IN } from '../config/auth'; +import type { AuthInput } from '../yup/authYup.js'; +import type { Usuario } from '../interfaces/usuarioInterface.js'; + + +const DUMMY_HASH = bcrypt.hashSync('senha-invalida', 10); + +export class AuthService { + static async login({ email, celular, senha }: AuthInput) { + const usuario = await db('usuarios') + .where(email ? { email } : { celular }) + .first(); + + const senhaCorreta = await bcrypt.compare(senha, usuario?.senha ?? DUMMY_HASH); + + if (!usuario || !senhaCorreta) { + return null; + } + + const token = jwt.sign({ sub: String(usuario.id) }, getJwtSecret(), { + algorithm: 'HS256', + expiresIn: JWT_EXPIRES_IN as SignOptions['expiresIn'], + }); + + return { + token, + usuario: { + id: usuario.id, + nome: usuario.nome, + email: usuario.email, + celular: usuario.celular, + }, + }; + } +} \ No newline at end of file diff --git a/back/src/yup/authYup.ts b/back/src/yup/authYup.ts new file mode 100644 index 0000000..e3015d0 --- /dev/null +++ b/back/src/yup/authYup.ts @@ -0,0 +1,24 @@ +import * as yup from 'yup'; + +export const authSchema = yup + .object({ + email: yup + .string() + .trim() + .lowercase() + .email('E-mail inválido'), + celular: yup + .string() + .transform((valor) => + typeof valor === 'string' ? valor.replace(/\D/g, '') : valor, + ) + .matches(/^\d{10,13}$/, 'Celular inválido'), + senha: yup.string().required('A senha é obrigatória'), + }) + .test( + 'email-ou-celular', + 'Informe o e-mail ou o celular', + (valor) => Boolean(valor?.email || valor?.celular), + ); + +export type AuthInput = yup.InferType; \ No newline at end of file From fe2a8ebf4ae95173213a2091755efe7fabec9dd3 Mon Sep 17 00:00:00 2001 From: Rafael do Amaral Costa Date: Sun, 20 Sep 2026 21:35:10 -0300 Subject: [PATCH 2/3] feat: knex config e connection --- .../database/connection/DatabaseConnection.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/back/src/database/connection/DatabaseConnection.ts b/back/src/database/connection/DatabaseConnection.ts index e69de29..daf1de4 100644 --- a/back/src/database/connection/DatabaseConnection.ts +++ b/back/src/database/connection/DatabaseConnection.ts @@ -0,0 +1,49 @@ +import knex, { Knex } from 'knex'; +import DatabaseConfig from '../config/DatabaseConfig'; + +/** + * Classe responsável pela configuração e gerenciamento da conexão com o banco de dados. + */ +class DatabaseConnection { + private static instance: Knex | null = null; + + /** + * Retorna a instância do Knex. + * @returns {Knex} Instância do Knex para interagir com o banco de dados. + */ + public static getInstance(): Knex { + if (!this.instance) { + this.instance = knex(DatabaseConfig.getConfig()); + } + return this.instance; + } + + /** + * Testa a conexão com o banco de dados. + * @returns {Promise} `true` se a conexão for bem-sucedida, `false` caso contrário. + */ + public static async testConnection(): Promise { + try { + const connection = this.getInstance(); + await connection.raw('SELECT 1'); + console.log('🎲 \tConexão com banco de dados estabelecida!!'); + return true; + } catch (error) { + console.error('❌ \tErro ao se conectar ao banco de dados:', error); + return false; + } + } + + /** + * Fecha a conexão com o banco de dados, caso esteja ativa. + * @returns {Promise} + */ + public static async closeConnection(): Promise { + if (this.instance) { + await this.instance.destroy(); + this.instance = null; + } + } +} + +export default DatabaseConnection; \ No newline at end of file From 3143fb276f3686f939c4ad30aa431ce892b73dee Mon Sep 17 00:00:00 2001 From: Rafael do Amaral Costa Date: Sun, 20 Sep 2026 22:41:53 -0300 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20retornando=20o=20public=5Fid=20no=20?= =?UTF-8?q?hash=20e=20altera=C3=A7=C3=B5es=20no=20tipo=20do=20public=5Fid?= =?UTF-8?q?=20para=20uuid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/.env.example | 5 +- back/src/database/config/DatabaseConfig.ts | 56 +++++++++++++++++++ .../database/connection/DatabaseConnection.ts | 3 +- .../migrations/20260912221551_campanhas.ts | 8 +-- .../migrations/20260912221614_vaquinhas.ts | 19 +++---- .../migrations/20260912221623_rifas.ts | 7 +-- back/src/interfaces/usuarioInterface.ts | 2 +- back/src/middleware/authMiddleware.ts | 20 +++++-- back/src/services/authService.ts | 27 ++++----- 9 files changed, 103 insertions(+), 44 deletions(-) create mode 100644 back/src/database/config/DatabaseConfig.ts diff --git a/back/.env.example b/back/.env.example index c49a529..f8c4e86 100644 --- a/back/.env.example +++ b/back/.env.example @@ -8,4 +8,7 @@ DB_PORT= DB_USER= DB_PASSWORD= DB_NAME= -DATABASE_URL= \ No newline at end of file +DATABASE_URL= + +JWT_EXPIRES_IN= +JWT_SECRET= \ No newline at end of file diff --git a/back/src/database/config/DatabaseConfig.ts b/back/src/database/config/DatabaseConfig.ts new file mode 100644 index 0000000..cac8686 --- /dev/null +++ b/back/src/database/config/DatabaseConfig.ts @@ -0,0 +1,56 @@ +import type { Knex } from 'knex'; + +class DatabaseConfig { + private static env(nome: string): string { + const valor = process.env[nome]; + + if (!valor) { + throw new Error(`A variável de ambiente ${nome} não está definida`); + } + + return valor; + } + + private static numero(nome: string, padrao: number): number { + const bruto = process.env[nome]; + + if (bruto === undefined || bruto === '') { + return padrao; + } + + const valor = Number(bruto); + + if (!Number.isInteger(valor) || valor <= 0) { + throw new Error(`A variável de ambiente ${nome} deve ser um número inteiro positivo`); + } + + return valor; + } + + /** + * Retorna a configuração do Knex para PostgreSQL. + * @returns {Knex.Config} Configuração pronta para ser passada ao `knex()`. + */ + public static getConfig(): Knex.Config { + const connection: Knex.Config['connection'] = process.env.DATABASE_URL + ? process.env.DATABASE_URL + : { + host: DatabaseConfig.env('DB_HOST'), + port: DatabaseConfig.numero('DB_PORT', 5432), + user: DatabaseConfig.env('DB_USER'), + password: DatabaseConfig.env('DB_PASSWORD'), + database: DatabaseConfig.env('DB_NAME'), + }; + + return { + client: 'pg', + connection, + pool: { + min: DatabaseConfig.numero('DB_POOL_MIN', 2), + max: DatabaseConfig.numero('DB_POOL_MAX', 10), + }, + }; + } +} + +export default DatabaseConfig; \ No newline at end of file diff --git a/back/src/database/connection/DatabaseConnection.ts b/back/src/database/connection/DatabaseConnection.ts index daf1de4..7b8ded8 100644 --- a/back/src/database/connection/DatabaseConnection.ts +++ b/back/src/database/connection/DatabaseConnection.ts @@ -1,8 +1,9 @@ import knex, { Knex } from 'knex'; -import DatabaseConfig from '../config/DatabaseConfig'; +import DatabaseConfig from '../config/DatabaseConfig.js'; /** * Classe responsável pela configuração e gerenciamento da conexão com o banco de dados. + * Funciona no modelo singlwtown para ter apenas uma instancia ativa */ class DatabaseConnection { private static instance: Knex | null = null; diff --git a/back/src/database/migrations/20260912221551_campanhas.ts b/back/src/database/migrations/20260912221551_campanhas.ts index c227950..bf15f13 100644 --- a/back/src/database/migrations/20260912221551_campanhas.ts +++ b/back/src/database/migrations/20260912221551_campanhas.ts @@ -1,4 +1,4 @@ -import { Knex } from "knex"; +import type { Knex } from "knex"; export async function up(knex: Knex): Promise { return knex.schema.createTable("campaigns", (table: Knex.CreateTableBuilder) => { @@ -8,13 +8,13 @@ export async function up(knex: Knex): Promise { table.string("title", 255).notNullable(); table.text("description").nullable(); table.string("pix_key", 255).notNullable(); - table.string("visibility", 20).notNullable(); + table.boolean("ispublic").defaultTo(true); table.string("status", 20).notNullable(); table.string("category", 20).notNullable(); table.date("start_date").notNullable(); table.date("end_date").notNullable(); - - table.integer("organizer_id").unsigned().notNullable().references("id").inTable("organizers").onDelete("CASCADE"); + table.enum('type', ['crowdfundings', 'raffles']).notNullable(); + table.integer("organizer_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE"); table.timestamp("created_at").defaultTo(knex.fn.now()); }); diff --git a/back/src/database/migrations/20260912221614_vaquinhas.ts b/back/src/database/migrations/20260912221614_vaquinhas.ts index 9d20ab8..515e075 100644 --- a/back/src/database/migrations/20260912221614_vaquinhas.ts +++ b/back/src/database/migrations/20260912221614_vaquinhas.ts @@ -1,21 +1,16 @@ - - -import { Knex } from "knex"; +import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - return knex.schema.createTable("crowdfundings", (table) => { - table.increments("id").primary(); - - - table.decimal("raised_amount", 10, 2).defaultTo(0.00); + return knex.schema.createTable("crowdfundings", (table) => { + table.integer("campaign_id").primary().unsigned().notNullable().references("id").inTable("campaigns").onDelete("CASCADE"); - table.integer("campaign_id").unsigned().notNullable().references("id").inTable("campaigns").onDelete("CASCADE"); + table.decimal("raised_amount", 10, 2).defaultTo(0.00); - }); + }); } export async function down(knex: Knex): Promise { - return knex.schema.dropTableIfExists("crowdfundings"); - + return knex.schema.dropTableIfExists("crowdfundings"); + } \ No newline at end of file diff --git a/back/src/database/migrations/20260912221623_rifas.ts b/back/src/database/migrations/20260912221623_rifas.ts index 9aab965..3c7b06a 100644 --- a/back/src/database/migrations/20260912221623_rifas.ts +++ b/back/src/database/migrations/20260912221623_rifas.ts @@ -1,14 +1,11 @@ -import { Knex } from "knex"; +import type { Knex } from "knex"; export async function up(knex: Knex): Promise { return knex.schema.createTable("raffles", (table) => { - table.increments("id").primary(); + table.integer("campaign_id").primary().unsigned().notNullable().references("id").inTable("campaigns").onDelete("CASCADE"); table.string("location", 255).notNullable(); table.string("prize", 255).notNullable(); - - table.integer("campaign_id").unsigned().notNullable().references("id").inTable("campaigns").onDelete("CASCADE"); - }); } diff --git a/back/src/interfaces/usuarioInterface.ts b/back/src/interfaces/usuarioInterface.ts index e3b7b8d..da6c1bc 100644 --- a/back/src/interfaces/usuarioInterface.ts +++ b/back/src/interfaces/usuarioInterface.ts @@ -1,5 +1,5 @@ export interface Usuario { - id: number; + public_id: number; nome: string; email: string; celular: string; diff --git a/back/src/middleware/authMiddleware.ts b/back/src/middleware/authMiddleware.ts index 59770be..3475a39 100644 --- a/back/src/middleware/authMiddleware.ts +++ b/back/src/middleware/authMiddleware.ts @@ -1,6 +1,16 @@ -import { NextFunction, Request, Response } from 'express'; -import jwt, { JwtPayload } from 'jsonwebtoken'; -import { getJwtSecret } from '../config/auth'; +import type { NextFunction, Request, Response } from 'express'; +import jwt, { type JwtPayload } from 'jsonwebtoken'; +import "dotenv/config"; + +declare global { + namespace Express { + interface Request { + user?: { + id: number; + }; + } + } +} export function authMiddleware(req: Request, res: Response, next: NextFunction) { const authorization = req.headers.authorization; @@ -15,10 +25,10 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction) return res.status(401).json({ message: 'Token mal formatado' }); } - const secret = getJwtSecret(); + const secret = process.env.JWT_SECRET; try { - const payload = jwt.verify(token, secret, { algorithms: ['HS256'] }) as JwtPayload; + const payload = jwt.verify(token, String(secret), { algorithms: ['HS256'] }) as JwtPayload; req.user = { id: Number(payload.sub) }; diff --git a/back/src/services/authService.ts b/back/src/services/authService.ts index b44b98b..2c1ef75 100644 --- a/back/src/services/authService.ts +++ b/back/src/services/authService.ts @@ -1,18 +1,18 @@ import bcrypt from 'bcrypt'; +import "dotenv/config"; import jwt, { type SignOptions } from 'jsonwebtoken'; -import db from '../database/connection'; -import { getJwtSecret, JWT_EXPIRES_IN } from '../config/auth'; +import DatabaseConnection from '../database/connection/DatabaseConnection.js'; import type { AuthInput } from '../yup/authYup.js'; import type { Usuario } from '../interfaces/usuarioInterface.js'; - const DUMMY_HASH = bcrypt.hashSync('senha-invalida', 10); export class AuthService { static async login({ email, celular, senha }: AuthInput) { - const usuario = await db('usuarios') - .where(email ? { email } : { celular }) - .first(); + const db = DatabaseConnection.getInstance(); + + const filtro = email ? { email } : { celular: celular ?? '' }; + const usuario = await db('usuarios').where(filtro).first(); const senhaCorreta = await bcrypt.compare(senha, usuario?.senha ?? DUMMY_HASH); @@ -20,19 +20,16 @@ export class AuthService { return null; } - const token = jwt.sign({ sub: String(usuario.id) }, getJwtSecret(), { - algorithm: 'HS256', - expiresIn: JWT_EXPIRES_IN as SignOptions['expiresIn'], + const secret = process.env.JWT_SECRET; + + + const token = jwt.sign({ sub: String(usuario.public_id) }, String(secret), { + expiresIn: (process.env.JWT_EXPIRES_IN ?? '1d') as NonNullable, }); return { token, - usuario: { - id: usuario.id, - nome: usuario.nome, - email: usuario.email, - celular: usuario.celular, - }, + usuario: { public_id: usuario.public_id, nome: usuario.nome, email: usuario.email, celular: usuario.celular }, }; } } \ No newline at end of file