From b76f460596b8b2b7b5e8f6c795712190e7865ca3 Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:49:58 -0300 Subject: [PATCH 01/11] =?UTF-8?q?:bugfix;=20Corrigindo=20erro=20de=20passw?= =?UTF-8?q?ordHash=20duplicado=20e=20atualizando=20vers=C3=A3o=20do=20axio?= =?UTF-8?q?s=20para=20ser=20compativel=20com=20o=20tsconfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/npm-publish.yml | 50 +++++++++++++++++++++++ backend/package-lock.json | 2 +- backend/package.json | 2 +- backend/src/controllers/authController.ts | 5 +-- backend/src/models/User.ts | 2 + package-lock.json | 6 --- 6 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/npm-publish.yml delete mode 100644 package-lock.json diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..f7e89f8 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,50 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages + +name: Node.js package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + - run: npm ci + - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.npm_token}} + + publish-gpr: + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + registry-url: $registry-url(npm) + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/backend/package-lock.json b/backend/package-lock.json index bcbada7..3d96b36 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@types/bcrypt": "^5.0.2", "@types/validator": "^13.12.2", - "axios": "^1.7.7", + "axios": "^1.7.9", "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", diff --git a/backend/package.json b/backend/package.json index 463cef0..657ca29 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,7 +15,7 @@ "dependencies": { "@types/bcrypt": "^5.0.2", "@types/validator": "^13.12.2", - "axios": "^1.7.7", + "axios": "^1.7.9", "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 073ee68..9ca0833 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -60,8 +60,7 @@ export const registerUser = async (req: Request, res: Response, next: NextFuncti return handleValidationError(res, 'A senha é obrigatória e deve ter entre 8 e 128 caracteres e as senhas devem coincidir.'); } - const hashedPassword = await bcrypt.hash(password, 10); - const user = new User({ username, email, password: hashedPassword }); + const user = new User({ username, email, password }); await user.save(); console.log(`Usuário registrado: ${user.id}`); @@ -91,7 +90,7 @@ export const loginUser = async (req: Request, res: Response, next: NextFunction) // Verifica se o usuário está bloqueado if (user.isLocked()) { - return handleValidationError(res, 'Conta bloqueada devido a várias tentativas de login. Tente novamente mais tarde.'); + return handleValidationError(res, 'Conta bloqueada devido a várias tentativas de login. Tente novamente mais tarde.'); } const isMatch = await user.comparePassword(password); diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 5d516d5..e19c86f 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -67,6 +67,8 @@ UserSchema.pre('save', async function (next) { next(); }); +console.log('Hash da senha (UserSchema):', UserSchema); + // Método para comparar a senha informada com o hash armazenado UserSchema.methods.comparePassword = async function (candidatePassword: string): Promise { return bcrypt.compare(candidatePassword, this.password); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index d95ae97..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "SnapSnippet", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} From e85e530da621978c39cd6034fc1372025476c424 Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 01:21:52 -0300 Subject: [PATCH 02/11] =?UTF-8?q?refatora=20a=20fun=C3=A7=C3=A3o=20de=20lo?= =?UTF-8?q?gin=20para=20utilizar=20um=20servi=C3=A7o=20dedicado,=20simplif?= =?UTF-8?q?icando=20a=20l=C3=B3gica=20de=20autentica=C3=A7=C3=A3o=20e=20me?= =?UTF-8?q?lhorando=20a=20legibilidade=20do=20c=C3=B3digo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/authController.ts | 31 ----------------------- 1 file changed, 31 deletions(-) diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 50beddc..c49734f 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -15,37 +15,6 @@ export const registerUser = async (req: Request, res: Response, next: NextFuncti export const loginUser = async (req: Request, res: Response, next: NextFunction): Promise => { try { const { email, password } = req.body; - - if (!email || !password) { - return handleValidationError(res, 'Todos os campos são obrigatórios'); - } - - const user = await User.findOne({ email: { $eq: email } }).select('+password'); // Inclui a senha na consulta - if (!user) { - return handleValidationError(res, 'Credenciais inválidas'); - } - - // Verifica se o usuário está bloqueado - if (user.isLocked()) { - return handleValidationError(res, 'Conta bloqueada devido a várias tentativas de login. Tente novamente mais tarde.'); - } - - const isMatch = await user.comparePassword(password); - if (!isMatch) { - await user.incrementLoginAttempts(); // Incrementa tentativas de login - return handleValidationError(res, 'Credenciais inválidas'); - } - - // Login bem-sucedido: redefinir tentativas de login - user.loginAttempts = 0; - user.lockUntil = null; // Remove bloqueios, caso tenha - await user.save(); - - console.log(`Usuário logado: ${user.id}`); - - const accessToken = generateAccessToken(user); - const refreshToken = generateRefreshToken(user); - const { accessToken, refreshToken } = await loginUserService(email, password); res.json({ accessToken, refreshToken }); } catch (error: any) { From 88a2888d33e5b080a6c1c63663b8bd3c4bbe51cd Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:59:43 -0300 Subject: [PATCH 03/11] Remove commented-out code and clean up UserSchema logging --- backend/src/models/Category.ts | 15 --------------- backend/src/models/Snippet.ts | 2 +- backend/src/models/User.ts | 2 -- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/backend/src/models/Category.ts b/backend/src/models/Category.ts index d003b39..e69de29 100644 --- a/backend/src/models/Category.ts +++ b/backend/src/models/Category.ts @@ -1,15 +0,0 @@ -// // Modelo de categoria para os snippets - -// export interface Category { -// name: string; -// description: string; -// color: string; -// } - -// // Modelo de snippet - -// export interface Snippet { -// id: string; -// title: string; -// description: string; - diff --git a/backend/src/models/Snippet.ts b/backend/src/models/Snippet.ts index 659b633..fcc1cca 100644 --- a/backend/src/models/Snippet.ts +++ b/backend/src/models/Snippet.ts @@ -65,7 +65,7 @@ const SnippetSchema = new Schema( }, sharedLink: { type: String, - default: null, // Indica que o snippet não é compartilhado inicialmente + // default: null, // Indica que o snippet não é compartilhado inicialmente unique: true, sparse: true, // Permite valores nulos e únicos }, diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 25786fc..32b4bfa 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -68,8 +68,6 @@ UserSchema.pre('save', async function (next) { next(); }); -console.log('Hash da senha (UserSchema):', UserSchema); - // Método para comparar a senha informada com o hash armazenado UserSchema.methods.comparePassword = async function (candidatePassword: string): Promise { return bcrypt.compare(candidatePassword, this.password); From d2836998fab03ad7f17114054d296364a49bbccf Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:01:24 -0300 Subject: [PATCH 04/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20:recycle:=20Adiciona?= =?UTF-8?q?r=20reposit=C3=B3rios=20de=20snippets=20e=20atualizar=20coment?= =?UTF-8?q?=C3=A1rios=20em=20fun=C3=A7=C3=B5es=20existentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/repositories/snippetRepository.ts | 30 +++++++++++++++++++ backend/src/repositories/tokenRepository.ts | 2 +- backend/src/repositories/userRepository.ts | 4 +-- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 backend/src/repositories/snippetRepository.ts diff --git a/backend/src/repositories/snippetRepository.ts b/backend/src/repositories/snippetRepository.ts new file mode 100644 index 0000000..f0739ef --- /dev/null +++ b/backend/src/repositories/snippetRepository.ts @@ -0,0 +1,30 @@ +import { Snippet } from '../models/Snippet'; + +export const createSnippet = (snippetData: any) => { + const snippet = new Snippet(snippetData); + return snippet.save(); +}; + +export const findSnippetsByUser = (userId: string) => { + return Snippet.find({ user: userId }); +}; + +export const findSnippetById = (id: string) => { + return Snippet.findById(id); +}; + +export const updateSnippetById = (id: string, updateData: any) => { + return Snippet.findByIdAndUpdate(id, { $set: updateData }, { new: true, runValidators: true }); +}; + +export const deleteSnippetById = (id: string) => { + return Snippet.findByIdAndDelete(id); +}; + +export const findFavoriteSnippetsByUser = (userId: string) => { + return Snippet.find({ user: userId, favorite: true }); +}; + +export const findSnippetBySharedLink = (link: string) => { + return Snippet.findOne({ sharedLink: link }); +}; diff --git a/backend/src/repositories/tokenRepository.ts b/backend/src/repositories/tokenRepository.ts index 476ce50..0fe5a10 100644 --- a/backend/src/repositories/tokenRepository.ts +++ b/backend/src/repositories/tokenRepository.ts @@ -2,7 +2,7 @@ import { Token, IToken } from '../models/Token'; export async function createResetToken(userId: string, hashedToken: string, sessionId: string, expiresAt: number): Promise { const token = new Token({ userId, token: hashedToken, sessionId, expiresAt }); - return token.save(); + return token.save(); // Salva no bd } export async function findValidResetToken(hashedToken: string): Promise { diff --git a/backend/src/repositories/userRepository.ts b/backend/src/repositories/userRepository.ts index 11d4758..5e47940 100644 --- a/backend/src/repositories/userRepository.ts +++ b/backend/src/repositories/userRepository.ts @@ -2,7 +2,7 @@ import { Types } from 'mongoose'; import { User, IUser } from '../models/User'; export async function findUserByEmail(email: string): Promise { - return User.findOne({ email: { $eq: email } }).select('+password'); + return User.findOne({ email: { $eq: email } }).select('+password'); // busca user por email } export async function findUserByUsername(username: string): Promise { @@ -15,5 +15,5 @@ export async function createUser(username: string, email: string, hashedPassword } export async function findUserById(userId: string | Types.ObjectId): Promise { - return User.findById(userId); + return User.findById(userId); // busca user por id } From 07daa25aa0488c10395183805eaf1b9181684177 Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:01:51 -0300 Subject: [PATCH 05/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20:recycle:=20?= =?UTF-8?q?=E2=9C=A8=20Adicionar=20servi=C3=A7o=20de=20snippets=20e=20refa?= =?UTF-8?q?torar=20imports=20no=20authService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/authController.ts | 7 +- backend/src/controllers/snippetController.ts | 168 +++--------------- backend/src/services/auth/authService.ts | 56 +++--- .../src/services/snnipets/snippetService.ts | 81 +++++++++ 4 files changed, 148 insertions(+), 164 deletions(-) create mode 100644 backend/src/services/snnipets/snippetService.ts diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index c49734f..9768420 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -1,5 +1,10 @@ import { Request, Response, NextFunction } from 'express'; -import { registerUserService, loginUserService, forgotPasswordService, resetPasswordService } from '../services/auth/authService'; +import { + registerUserService, + loginUserService, + forgotPasswordService, + resetPasswordService +} from '../services/auth/authService'; import { handleValidationError } from '../utils/validationUtils'; export const registerUser = async (req: Request, res: Response, next: NextFunction): Promise => { diff --git a/backend/src/controllers/snippetController.ts b/backend/src/controllers/snippetController.ts index 7985b31..575fc5d 100644 --- a/backend/src/controllers/snippetController.ts +++ b/backend/src/controllers/snippetController.ts @@ -1,204 +1,106 @@ -// src/controllers/snippetController.ts - import { Request, Response, NextFunction } from 'express'; -import { Snippet } from '../models/Snippet'; -import { GitHubApiService } from '../services/github/GitHubApiService'; -import { v4 as uuidv4 } from 'uuid'; -// import { validationResult } from 'express-validator'; +import * as snippetService from '../services/snnipets/snippetService'; import { handleValidationError } from '../utils/validationUtils'; +import { Snippet } from '../models/Snippet'; -// Cria um novo snippet export const createSnippet = async (req: Request, res: Response, next: NextFunction) => { try { - const { title, description, language, tags, code } = req.body; - - // Validações adicionais de segurança - if (typeof code !== 'string' || code.length === 0) { - return handleValidationError(res, 'Código inválido ou ausente.'); - } - - if (!req.user) { - return handleValidationError(res, 'Usuário não autenticado.'); - } - - const snippet = new Snippet({ - title, - description, - language, - tags, - code, - favorite: false, - user: req.user.id, - }); - - if (snippet.title === '' || snippet.title === undefined || snippet.title === null) { // Adiciona um título padrão - snippet.title = 'Snippet sem título'; - } - - if (snippet.description === '' || snippet.description === undefined || snippet.description === null) { - snippet.description = 'Snippet sem descrição'; - } - - if (snippet.language === '' || snippet.language === undefined || snippet.language === null) { - snippet.language = 'text'; - } - - if (snippet.tags === '' as unknown as Array || snippet.tags === undefined || snippet.tags === null) { - snippet.tags = []; // nesse caso essa array é vazia - } - - if (snippet.code === '' || snippet.code === undefined || snippet.code === null) { - return handleValidationError(res, 'Código inválido ou ausente.'); - } - - await snippet.save(); + if (!req.user) throw new Error('Usuário não autenticado.'); + const snippet = await snippetService.createNewSnippet(req.body, req.user.id); res.status(201).json(snippet); } catch (error) { - console.error('Erro ao criar snippet:', error); - next(error); // Propaga o erro para o middleware de tratamento de erros + next(error); } }; -// Busca todos os snippets do usuário export const fetchMySnippets = async (req: Request, res: Response, next: NextFunction) => { try { - if (!req.user) { - return handleValidationError(res, 'Usuário não autenticado.'); - } - const snippets = await Snippet.find({ user: req.user.id }); + if (!req.user) throw new Error('Usuário não autenticado.'); + const snippets = await snippetService.getUserSnippets(req.user.id); res.json(snippets); } catch (error) { - console.error('Erro ao buscar snippets do usuário:', error); next(error); } }; -// Retorna um snippet específico pelo ID export const getSnippet = async (req: Request, res: Response, next: NextFunction) => { try { - const snippet = await Snippet.findById(req.params.id); + const snippet = await snippetService.getSnippetById(req.params.id); if (!snippet) return handleValidationError(res, 'Snippet não encontrado.'); - res.json(snippet); } catch (error) { - console.error('Erro ao buscar snippet:', error); next(error); } }; -// Atualiza um snippet pelo ID export const updateSnippet = async (req: Request, res: Response, next: NextFunction) => { try { - const { title, description, language, tags, code, favorite } = req.body; - - // Validate input data - if (typeof title !== 'string' || - typeof description !== 'string' || - typeof language !== 'string' || - !Array.isArray(tags) || - typeof code !== 'string' || - typeof favorite !== 'boolean') { - return handleValidationError(res, 'Dados inválidos.'); - } - - const snippet = await Snippet.findByIdAndUpdate( - req.params.id, - { $set: { title, description, language, tags, code, favorite } }, - { new: true, runValidators: true } - ); - + const snippet = await snippetService.updateSnippet(req.params.id, req.body); if (!snippet) return handleValidationError(res, 'Snippet não encontrado.'); - res.json(snippet); } catch (error) { - console.error('Erro ao atualizar snippet:', error); next(error); } }; -// Deleta um snippet pelo ID export const deleteSnippet = async (req: Request, res: Response, next: NextFunction) => { try { - const snippet = await Snippet.findByIdAndDelete(req.params.id); - if (!snippet) return handleValidationError(res, 'Snippet não encontrado.'); - + await snippetService.deleteSnippet(req.params.id); res.status(204).send(); } catch (error) { - console.error('Erro ao deletar snippet:', error); next(error); } }; -// Busca snippets favoritos do usuário export const fetchMySnippetsFavorite = async (req: Request, res: Response, next: NextFunction) => { try { - if (!req.user) { - return handleValidationError(res, 'Usuário não autenticado.'); - } - const snippets = await Snippet.find({ user: req.user.id, favorite: true }); + if (!req.user) throw new Error('Usuário não autenticado.'); + const snippets = await snippetService.getFavoriteSnippets(req.user.id); res.json(snippets); } catch (error) { - console.error('Erro ao buscar snippets favoritos do usuário:', error); next(error); } -} +}; -// Marca ou desmarca um snippet como favorito export const markFavorite = async (req: Request, res: Response, next: NextFunction) => { try { - const snippet = await Snippet.findById(req.params.id); - if (!snippet) return handleValidationError(res, 'Snippet não encontrado.'); - - snippet.favorite = !snippet.favorite; - await snippet.save(); - + const snippet = await snippetService.toggleFavorite(req.params.id); res.json(snippet); } catch (error) { - console.error('Erro ao marcar snippet como favorito:', error); next(error); } }; -// Busca snippets públicos usando um serviço externo opcional (ex: GitHub Gist) export const fetchPublicSnippets = async (req: Request, res: Response, next: NextFunction) => { try { - const query = req.query.query as string; - const snippets = await GitHubApiService.fetchPublicSnippets(query); + const snippets = await snippetService.fetchPublicSnippets(req.query.query as string); res.json(snippets); } catch (error) { - console.error('Erro ao buscar snippets públicos:', error); next(error); } }; export const shareSnippet = async (req: Request, res: Response, next: NextFunction) => { try { - const snippet = await Snippet.findById(req.params.id); - if (!snippet) return handleValidationError(res, 'Snippet não encontrado.'); - - // Verifica se o snippet pertence ao usuário autenticado - if (snippet.user.toString() !== req.user?.id) { - return handleValidationError(res, 'Snippet não pertence ao usuário autenticado.'); - } - - if (!snippet.sharedLink) { - const uniqueLink = `${req.protocol}://${req.get('host')}/api/snippets/shared/${uuidv4()}`; - snippet.sharedLink = uniqueLink; - await snippet.save(); - } - - res.json({ link: snippet.sharedLink }); - console.log('Snippet compartilhado:', snippet); + const host = req.get('host'); + if (!host) throw new Error('Host não encontrado.'); + if (!req.user?.id) throw new Error('Usuário não autenticado.'); + const link = await snippetService.shareSnippet(req.params.id, req.user.id, req.protocol, host); + res.json({ link }); } catch (error) { - console.error('Erro ao compartilhar snippet:', error); next(error); } }; export const fetchSharedSnippet = async (req: Request, res: Response, next: NextFunction) => { try { - const link = `${req.protocol}://${req.get('host')}/api/snippets/shared/${req.params.link}`; - const snippet = await Snippet.findOne({ sharedLink: link }); + const { link } = req.params; + + // Busca apenas pelo UUID no campo sharedLink + const snippet = await Snippet.findOne({ + sharedLink: { $regex: `${link}$` }, // Busca o UUID no final do campo sharedLink + }); + if (!snippet) { return res.status(404).json({ message: 'Snippet compartilhado não encontrado.' }); } @@ -216,19 +118,3 @@ export const fetchSharedSnippet = async (req: Request, res: Response, next: Next next(error); } }; - -// deleta link compartilhado -// export const deleteSharedLink = async (req: Request, res: Response, next: NextFunction) => { -// try { -// const snippet = await Snippet.findById(req.params.id); -// if (!snippet) return handleValidationError(res, 'Snippet não encontrado.'); - -// snippet.sharedLink = null; -// await snippet.save(); - -// res.json({ message: 'Link compartilhado removido.' }); -// } catch (error) { -// console.error('Erro ao deletar link compartilhado:', error); -// next(error); -// } -// }; diff --git a/backend/src/services/auth/authService.ts b/backend/src/services/auth/authService.ts index 51e97b9..c2945f6 100644 --- a/backend/src/services/auth/authService.ts +++ b/backend/src/services/auth/authService.ts @@ -1,12 +1,24 @@ -import bcrypt from 'bcrypt'; -import crypto from 'crypto'; -import { Schema, model, Document, Types } from 'mongoose'; -import { IUser } from '../../models/User'; +import crypto from 'crypto'; // gerar tokens aleatórios import { IToken } from '../../models/Token'; -import { findUserByEmail, findUserByUsername, createUser, findUserById } from '../../repositories/userRepository'; -import { createResetToken, findValidResetToken, deleteResetToken } from '../../repositories/tokenRepository'; -import { validateEmail, validatePassword } from '../../utils/validationUtils'; -import { generateAccessToken, generateRefreshToken } from '../../utils/tokenUtils'; +import { + findUserByEmail, + findUserByUsername, + createUser, + findUserById +} from '../../repositories/userRepository'; // Importa as funções de manipulação de usuários +import { + createResetToken, + findValidResetToken, + deleteResetToken +} from '../../repositories/tokenRepository'; // Importa as funções de manipulação de tokens +import { + validateEmail, + validatePassword +} from '../../utils/validationUtils'; +import { + generateAccessToken, + generateRefreshToken +} from '../../utils/tokenUtils'; import { sendEmail } from '../../config/sendEmail'; export async function registerUserService(username: string, email: string, password: string, confirmPassword: string): Promise<{ message: string, accessToken: string, refreshToken: string }> { @@ -64,9 +76,9 @@ export async function loginUserService(email: string, password: string): Promise throw new Error('Credenciais inválidas'); } - user.loginAttempts = 0; - user.lockUntil = null; - await user.save(); + user.loginAttempts = 0; // Reset login attempts + user.lockUntil = null; // Remove lock on account caso exista um lock + await user.save(); // salva no bd const accessToken = generateAccessToken(user); const refreshToken = generateRefreshToken(user); @@ -84,12 +96,12 @@ export async function forgotPasswordService(email: string, baseUrl: string): Pro throw new Error('Usuário não encontrado'); } - const resetToken = crypto.randomBytes(32).toString('hex'); - const hashedToken = crypto.createHash('sha256').update(resetToken).digest('hex'); - const sessionId = crypto.randomBytes(16).toString('hex'); - const expiresAt = Date.now() + 20 * 60 * 1000; + const resetToken = crypto.randomBytes(32).toString('hex'); // Gera um token aleatório + const hashedToken = crypto.createHash('sha256').update(resetToken).digest('hex'); // hash do token + const sessionId = crypto.randomBytes(16).toString('hex'); // Gera um id de sessão aleatório + const expiresAt = Date.now() + 20 * 60 * 1000; // 20 minutos - await createResetToken(user._id.toString(), hashedToken, sessionId, expiresAt); + await createResetToken(user._id.toString(), hashedToken, sessionId, expiresAt); // Salva o token no bd const resetLink = `${baseUrl}/reset-password/${resetToken}`; @@ -147,8 +159,8 @@ export async function resetPasswordService(token: string, password: string, conf throw new Error('A senha é obrigatória e deve ter entre 8 e 128 caracteres e as senhas devem coincidir.'); } - const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); - const tokenDoc: IToken | null = await findValidResetToken(hashedToken); + const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); // hash do token + const tokenDoc: IToken | null = await findValidResetToken(hashedToken); // Busca o token no bd if (!tokenDoc) { throw new Error('Token inválido ou expirado'); @@ -159,19 +171,19 @@ export async function resetPasswordService(token: string, password: string, conf throw new Error('Usuário não encontrado'); } - user.password = password; + user.password = password; // Atualiza a senha do usuário user.loginAttempts = 0; user.lockUntil = null; - await user.save(); + await user.save(); // Salva no bd - await deleteResetToken(tokenDoc._id.toString()); + await deleteResetToken(tokenDoc._id.toString()); // Deleta o token do bd const accessToken = generateAccessToken(user); const refreshToken = generateRefreshToken(user); return { message: 'Senha redefinida com sucesso', - redirect: './', + redirect: './', // Redireciona para a página inicial accessToken, refreshToken }; diff --git a/backend/src/services/snnipets/snippetService.ts b/backend/src/services/snnipets/snippetService.ts new file mode 100644 index 0000000..67d78af --- /dev/null +++ b/backend/src/services/snnipets/snippetService.ts @@ -0,0 +1,81 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + createSnippet, + findSnippetsByUser, + findSnippetById, + updateSnippetById, + deleteSnippetById, + findFavoriteSnippetsByUser, + findSnippetBySharedLink, +} from '../../repositories/snippetRepository'; +import { GitHubApiService } from '../github/GitHubApiService'; + +export const createNewSnippet = async (data: any, userId: string) => { + const { title, description, language, tags, code } = data; + + // Adiciona valores padrão + const snippetData = { + title: title || 'Snippet sem título', + description: description || 'Snippet sem descrição', + language: language || 'text', + tags, + code, + favorite: false, + user: userId, + }; + + return createSnippet(snippetData); +}; + +export const getUserSnippets = (userId: string) => { + return findSnippetsByUser(userId); +}; + +export const getSnippetById = (id: string) => { + return findSnippetById(id); +}; + +export const updateSnippet = (id: string, updateData: any) => { + return updateSnippetById(id, updateData); +}; + +export const deleteSnippet = (id: string) => { + return deleteSnippetById(id); +}; + +export const getFavoriteSnippets = (userId: string) => { + return findFavoriteSnippetsByUser(userId); +}; + +export const toggleFavorite = async (id: string) => { + const snippet = await findSnippetById(id); + if (!snippet) throw new Error('Snippet não encontrado.'); + + snippet.favorite = !snippet.favorite; + return snippet.save(); +}; + +export const fetchPublicSnippets = (query: string) => { + return GitHubApiService.fetchPublicSnippets(query); +}; + +export const shareSnippet = async (id: string, userId: string, protocol: string, host: string) => { + const snippet = await findSnippetById(id); + if (!snippet) throw new Error('Snippet não encontrado.'); + + if (snippet.user.toString() !== userId) { + throw new Error('Snippet não pertence ao usuário autenticado.'); + } + + if (!snippet.sharedLink) { + const uniqueLink = `${protocol}://${host}/api/snippets/shared/${uuidv4()}`; + snippet.sharedLink = uniqueLink; + await snippet.save(); + } + + return snippet.sharedLink; +}; + +export const getSharedSnippet = (link: string) => { + return findSnippetBySharedLink(link); +}; From e8d73a99dbd800327a9a31be0d2720cfe43fa777 Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:02:19 -0300 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=A7=B9=20:broom:=20Remove=20c=C3=B3?= =?UTF-8?q?digo=20comentado=20da=20fun=C3=A7=C3=A3o=20de=20valida=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20snippets=20em=20validationUtils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/utils/validationUtils.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/utils/validationUtils.ts b/backend/src/utils/validationUtils.ts index d923c94..b69bf8b 100644 --- a/backend/src/utils/validationUtils.ts +++ b/backend/src/utils/validationUtils.ts @@ -13,3 +13,15 @@ export function validateEmail(email: string): boolean { export function validatePassword(password: string, confirmPassword: string): boolean { return password.length >= 8 && password.length <= 128 && password === confirmPassword; } + +// export function validateSnippetData (data: any): boolean => { +// const { title, description, language, tags, code } = data; +// return ( +// typeof title === 'string' && +// typeof description === 'string' && +// typeof language === 'string' && +// Array.isArray(tags) && +// typeof code === 'string' && +// code.length > 0 +// ); +// }; From 42c2cb5c43f1786dee90d5f47ad0ceaa33059f7b Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:08:52 -0300 Subject: [PATCH 07/11] =?UTF-8?q?:recycle:=09refactor=20=E2=9C=A8=20Adicio?= =?UTF-8?q?nar=20controlador=20de=20categorias=20com=20opera=C3=A7=C3=B5es?= =?UTF-8?q?=20CRUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/categoryController.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 backend/src/controllers/categoryController.ts diff --git a/backend/src/controllers/categoryController.ts b/backend/src/controllers/categoryController.ts new file mode 100644 index 0000000..841a1f0 --- /dev/null +++ b/backend/src/controllers/categoryController.ts @@ -0,0 +1,66 @@ +import { Request, Response, NextFunction } from 'express'; +import * as categoryService from '../services/snnipets/categoryService'; + +export const createCategory = async (req: Request, res: Response, next: NextFunction) => { + try { + const { name, description } = req.body; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ message: 'Usuário não autenticado.' }); + } + + const category = await categoryService.createNewCategory(name, description, userId); + res.status(201).json(category); + } catch (error) { + next(error); + } +}; + +export const getCategories = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ message: 'Usuário não autenticado.' }); + } + + const categories = await categoryService.getUserCategories(userId); + res.json(categories); + } catch (error) { + next(error); + } +}; + +export const updateCategory = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const { name, description } = req.body; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ message: 'Usuário não autenticado.' }); + } + + const category = await categoryService.updateCategory(id, name, description, userId); + res.json(category); + } catch (error) { + next(error); + } +}; + +export const deleteCategory = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ message: 'Usuário não autenticado.' }); + } + + const result = await categoryService.removeCategory(id, userId); + res.json(result); + } catch (error) { + next(error); + } +}; From 886a9bdf5b1aa0adff2c2a0a4531d77a78af2e87 Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:09:31 -0300 Subject: [PATCH 08/11] =?UTF-8?q?=E2=9C=A8=20feat:=20Adicionar=20modelo=20?= =?UTF-8?q?de=20Categoria=20e=20atualizar=20modelo=20de=20Snippet=20para?= =?UTF-8?q?=20incluir=20refer=C3=AAncia=20=C3=A0=20categoria?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/models/Category.ts | 33 +++++++++++++++++++++++++++++++++ backend/src/models/Snippet.ts | 8 +++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/backend/src/models/Category.ts b/backend/src/models/Category.ts index e69de29..bda69b6 100644 --- a/backend/src/models/Category.ts +++ b/backend/src/models/Category.ts @@ -0,0 +1,33 @@ +import { Schema, model, Document } from 'mongoose'; + +export interface ICategory extends Document { + name: string; + user: Schema.Types.ObjectId; // Referência ao usuário + _id: Schema.Types.ObjectId; // Referência ao ID da categoria + description: string; + createdAt: Date; +} + +const CategorySchema = new Schema({ + name: { + type: String, + required: [true, 'Nome da categoria é obrigatório.'], + trim: true, + minLength: [3, 'Nome da categoria deve ter no mínimo 3 caracteres.'], + maxLength: [50, 'Nome da categoria deve ter no máximo 50 caracteres.'], + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + }, + description: { + type: String, + trim: true, + maxLength: [255, 'Descrição da categoria deve ter no máximo 255 caracteres.'], + }, + }, + { timestamps: true } +); + +export const Category = model('Category', CategorySchema); diff --git a/backend/src/models/Snippet.ts b/backend/src/models/Snippet.ts index fcc1cca..aff66a0 100644 --- a/backend/src/models/Snippet.ts +++ b/backend/src/models/Snippet.ts @@ -9,7 +9,8 @@ export interface ISnippet extends Document { user: Schema.Types.ObjectId; code: string; favorite: boolean; - sharedLink: string | null; // Novo campo + category: Schema.Types.ObjectId | null; + sharedLink: string | null; createdAt: Date; } @@ -63,6 +64,11 @@ const SnippetSchema = new Schema( type: Boolean, default: false, }, + category: { + type: Schema.Types.ObjectId, + ref: 'Category', + required: false, + }, sharedLink: { type: String, // default: null, // Indica que o snippet não é compartilhado inicialmente From 91ec8d72cc65635b43b13f571d86f36383828ae2 Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:09:47 -0300 Subject: [PATCH 09/11] =?UTF-8?q?=E2=9C=A8=20feat:=20Adicionar=20reposit?= =?UTF-8?q?=C3=B3rio=20de=20categorias=20e=20rotas=20para=20opera=C3=A7?= =?UTF-8?q?=C3=B5es=20CRUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/repositories/categoryRepository.ts | 21 +++++++++++++++++++ backend/src/routes/snippetRoutes.ts | 17 +++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 backend/src/repositories/categoryRepository.ts diff --git a/backend/src/repositories/categoryRepository.ts b/backend/src/repositories/categoryRepository.ts new file mode 100644 index 0000000..2be994e --- /dev/null +++ b/backend/src/repositories/categoryRepository.ts @@ -0,0 +1,21 @@ +import { Category } from "../models/Category"; + +export const createCategory = (name: string, userId: string, description: string) => { + return Category.create({ name, user: userId, description }); +}; + +export const findCategoriesByUser = (userId: string) => { + return Category.find({ user: userId }); +}; + +export const findCategoryById = (id: string) => { + return Category.findById(id); +}; + +export const updateCategoryById = (id: string, name: string, description: string) => { + return Category.findByIdAndUpdate(id, { $set: { name, description } }, { new: true, runValidators: true }); +}; + +export const deleteCategory = (id: string, userId: string) => { + return Category.findOneAndDelete({ _id: id, user: userId }); +} diff --git a/backend/src/routes/snippetRoutes.ts b/backend/src/routes/snippetRoutes.ts index 3ca3fdf..24a82fa 100644 --- a/backend/src/routes/snippetRoutes.ts +++ b/backend/src/routes/snippetRoutes.ts @@ -14,6 +14,7 @@ import { shareSnippet, // deleteSharedLink, } from '../controllers/snippetController'; +import { createCategory, getCategories, updateCategory, deleteCategory } from '../controllers/categoryController'; import { authenticatedLimiter, limiter } from '../utils/rateLimiting'; const router = Router(); @@ -47,6 +48,22 @@ router.get('/shared/:link', limiter, (req, res, next) => { fetchSharedSnippet(req, res, next).catch(next); }); // Busca snippets compartilhados com o usuário +router.post('/categories', authenticatedLimiter, validateToken, (req, res, next) => { + createCategory(req, res, next).catch(next); +}); // Criação de uma nova categoria + +router.get('/categories', authenticatedLimiter, validateToken, (req, res, next) => { + getCategories(req, res, next).catch(next); +}); // Busca categorias do usuário + +router.put('/categories/:id', authenticatedLimiter, validateToken, (req, res, next) => { + updateCategory(req, res, next).catch(next); +}); // Atualização de uma categoria existente + +router.delete('/categories/:id', authenticatedLimiter, validateToken, (req, res, next) => { + deleteCategory(req, res, next).catch(next); +}); // Exclusão de uma categoria + router.post('/:id/share', authenticatedLimiter, validateToken, (req, res, next) => { shareSnippet(req, res, next).catch(next); }); // Compartilha um snippet From 6e228e264b407ac83c0f9f5e1ba960ff8fe5b325 Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:09:57 -0300 Subject: [PATCH 10/11] =?UTF-8?q?=E2=9C=A8=20feat:=20Adicionar=20servi?= =?UTF-8?q?=C3=A7o=20de=20categorias=20com=20opera=C3=A7=C3=B5es=20de=20cr?= =?UTF-8?q?ia=C3=A7=C3=A3o,=20atualiza=C3=A7=C3=A3o,=20remo=C3=A7=C3=A3o?= =?UTF-8?q?=20e=20recupera=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/snnipets/categoryService.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 backend/src/services/snnipets/categoryService.ts diff --git a/backend/src/services/snnipets/categoryService.ts b/backend/src/services/snnipets/categoryService.ts new file mode 100644 index 0000000..9f738cc --- /dev/null +++ b/backend/src/services/snnipets/categoryService.ts @@ -0,0 +1,87 @@ +import * as categoryRepository from '../../repositories/categoryRepository'; + +export const createNewCategory = async (name: string, description: string, userId: string) => { + if (!name) { + throw new Error('O nome da categoria é obrigatório.'); + } + + if (name.length < 3 || name.length > 50) { + throw new Error('O nome da categoria deve ter entre 3 e 50 caracteres.'); + } + + if (description && description.length > 255) { + throw new Error('A descrição da categoria deve ter no máximo 255 caracteres.'); + } + + // Verificar se a categoria já existe para o usuário + const existingCategories = await categoryRepository.findCategoriesByUser(userId); + const isDuplicate = existingCategories.some((category) => category.name === name); + + if (isDuplicate) { + throw new Error('Uma categoria com esse nome já existe.'); + } + + return categoryRepository.createCategory(name, userId, description); +}; + +export const getUserCategories = (userId: string) => { + if (!userId) { + throw new Error('ID do usuário é obrigatório.'); + } + return categoryRepository.findCategoriesByUser(userId); +}; + +export const updateCategory = async ( + id: string, + name: string, + description: string, + userId: string +) => { + if (!name) { + throw new Error('O nome da categoria é obrigatório.'); + } + + if (name.length < 3 || name.length > 50) { + throw new Error('O nome da categoria deve ter entre 3 e 50 caracteres.'); + } + + if (description && description.length > 255) { + throw new Error('A descrição da categoria deve ter no máximo 255 caracteres.'); + } + + // Garantir que não exista outra categoria com o mesmo nome para o usuário + const categories = await categoryRepository.findCategoriesByUser(userId); + const isDuplicate = categories.some((category) => category.name === name && category._id.toString() !== id); + + if (isDuplicate) { + throw new Error('Uma categoria com esse nome já existe.'); + } + + return categoryRepository.updateCategoryById(id, name, description); +}; + +export const removeCategory = async (id: string, userId: string) => { + const category = await categoryRepository.findCategoryById(id); + if (!category) { + throw new Error('Categoria não encontrada.'); + } + + if (category.user.toString() !== userId) { + throw new Error('Você não tem permissão para deletar esta categoria.'); + } + + return categoryRepository.deleteCategory(id, userId); +}; + +export const getCategoryById = async (id: string, userId: string) => { + const category = await categoryRepository.findCategoryById(id); + if (!category) { + throw new Error('Categoria não encontrada.'); + } + + if (category.user.toString() !== userId) { + throw new Error('Você não tem permissão para visualizar esta categoria.'); + } + + return category; +}; From e36e56673a72eef7907b7706571648ffe70bdb1f Mon Sep 17 00:00:00 2001 From: Jonh Alex <122692601+Jonhvmp@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:16:52 -0300 Subject: [PATCH 11/11] Fix code scanning alert no. 41: Database query built from user-controlled sources Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- backend/src/repositories/snippetRepository.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/repositories/snippetRepository.ts b/backend/src/repositories/snippetRepository.ts index f0739ef..103fbfb 100644 --- a/backend/src/repositories/snippetRepository.ts +++ b/backend/src/repositories/snippetRepository.ts @@ -14,7 +14,15 @@ export const findSnippetById = (id: string) => { }; export const updateSnippetById = (id: string, updateData: any) => { - return Snippet.findByIdAndUpdate(id, { $set: updateData }, { new: true, runValidators: true }); + // Validate updateData to ensure it only contains allowed fields + const allowedFields = ['title', 'description', 'language', 'tags', 'code', 'favorite']; + const sanitizedData: any = {}; + for (const key in updateData) { + if (allowedFields.includes(key)) { + sanitizedData[key] = updateData[key]; + } + } + return Snippet.findByIdAndUpdate(id, { $set: sanitizedData }, { new: true, runValidators: true }); }; export const deleteSnippetById = (id: string) => {