From cd89b30bc3918492820900bf7cfb21778a333443 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 19 Oct 2022 11:59:13 -0300 Subject: [PATCH] :tada: Commit Inicial --- .../rodada-case-2/amaro-backend/.gitignore | 5 + .../amaro-backend/jest.config.js | 8 + .../rodada-case-2/amaro-backend/package.json | 40 +++++ .../rodada-case-2/amaro-backend/requests.rest | 21 +++ .../src/business/PingBusiness.ts | 9 ++ .../src/business/UserBusiness.ts | 136 +++++++++++++++++ .../src/controller/PingController.ts | 22 +++ .../src/controller/UserController.ts | 47 ++++++ .../src/database/BaseDatabase.ts | 18 +++ .../src/database/UserDatabase.ts | 39 +++++ .../src/database/migrations/Migrations.ts | 52 +++++++ .../src/database/migrations/data.ts | 140 ++++++++++++++++++ .../src/errors/AuthenticationError.ts | 9 ++ .../src/errors/AuthorizationError.ts | 9 ++ .../amaro-backend/src/errors/BaseError.ts | 8 + .../amaro-backend/src/errors/ConflictError.ts | 9 ++ .../amaro-backend/src/errors/NotFoundError.ts | 9 ++ .../amaro-backend/src/errors/ParamsError.ts | 9 ++ .../src/errors/UnprocessableError.ts | 9 ++ .../rodada-case-2/amaro-backend/src/index.ts | 19 +++ .../amaro-backend/src/models/Products.ts | 129 ++++++++++++++++ .../amaro-backend/src/models/Users.ts | 83 +++++++++++ .../amaro-backend/src/router/pingRouter.ts | 11 ++ .../amaro-backend/src/router/userRouter.ts | 21 +++ .../src/services/Authenticator.ts | 37 +++++ .../amaro-backend/src/services/HashManager.ts | 15 ++ .../amaro-backend/src/services/IdGenerator.ts | 7 + .../tests/UserBusinessTests/login.test.ts | 66 +++++++++ .../tests/UserBusinessTests/signup.test.ts | 27 ++++ .../tests/mocks/AuthenticatorMock.ts | 37 +++++ .../tests/mocks/HashManagerMock.ts | 17 +++ .../tests/mocks/IdGeneratorMock.ts | 5 + .../tests/mocks/UserDatabaseMock.ts | 49 ++++++ .../rodada-case-2/amaro-backend/tsconfig.json | 16 ++ 34 files changed, 1138 insertions(+) create mode 100644 modulo6/rodada-case-2/amaro-backend/.gitignore create mode 100644 modulo6/rodada-case-2/amaro-backend/jest.config.js create mode 100644 modulo6/rodada-case-2/amaro-backend/package.json create mode 100644 modulo6/rodada-case-2/amaro-backend/requests.rest create mode 100644 modulo6/rodada-case-2/amaro-backend/src/business/PingBusiness.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/business/UserBusiness.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/controller/PingController.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/controller/UserController.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/database/BaseDatabase.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/database/UserDatabase.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/database/migrations/Migrations.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/database/migrations/data.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/errors/AuthenticationError.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/errors/AuthorizationError.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/errors/BaseError.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/errors/ConflictError.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/errors/NotFoundError.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/errors/ParamsError.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/errors/UnprocessableError.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/index.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/models/Products.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/models/Users.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/router/pingRouter.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/router/userRouter.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/services/Authenticator.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/services/HashManager.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/src/services/IdGenerator.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/tests/UserBusinessTests/login.test.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/tests/UserBusinessTests/signup.test.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/tests/mocks/AuthenticatorMock.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/tests/mocks/HashManagerMock.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/tests/mocks/IdGeneratorMock.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/tests/mocks/UserDatabaseMock.ts create mode 100644 modulo6/rodada-case-2/amaro-backend/tsconfig.json diff --git a/modulo6/rodada-case-2/amaro-backend/.gitignore b/modulo6/rodada-case-2/amaro-backend/.gitignore new file mode 100644 index 0000000..b55dc4a --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/.gitignore @@ -0,0 +1,5 @@ +.env +node_modules +build +.DS_STORE +coverage \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/jest.config.js b/modulo6/rodada-case-2/amaro-backend/jest.config.js new file mode 100644 index 0000000..a5062c6 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + roots: ["/tests"], + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/package.json b/modulo6/rodada-case-2/amaro-backend/package.json new file mode 100644 index 0000000..478c292 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/package.json @@ -0,0 +1,40 @@ +{ + "name": "template-backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node ./build/src/index.js", + "build": "tsc", + "dev": "ts-node-dev ./src/index.ts", + "migrations": "tsc && node ./build/src/database/migrations/Migrations.js", + "test": "jest" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.14", + "@types/jest": "^29.0.3", + "@types/jsonwebtoken": "^8.5.9", + "@types/knex": "^0.16.1", + "@types/node": "^18.7.23", + "@types/uuid": "^8.3.4", + "jest": "^29.1.1", + "ts-jest": "^29.0.2", + "ts-node-dev": "^2.0.0", + "typescript": "^4.8.4" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.0.2", + "express": "^4.18.1", + "jsonwebtoken": "^8.5.1", + "knex": "^2.3.0", + "mysql": "^2.18.1", + "uuid": "^9.0.0" + } +} diff --git a/modulo6/rodada-case-2/amaro-backend/requests.rest b/modulo6/rodada-case-2/amaro-backend/requests.rest new file mode 100644 index 0000000..74af2c8 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/requests.rest @@ -0,0 +1,21 @@ +### Ping +GET http://localhost:3003/ping + +### Signup +POST http://localhost:3003/users/signup +Content-Type: application/json + +{ + "name": "alice", + "email": "alice@gmail.com", + "password": "alice99" +} + +### Login +POST http://localhost:3003/users/login +Content-Type: application/json + +{ + "email": "astrodev@gmail.com", + "password": "bananinha" +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/business/PingBusiness.ts b/modulo6/rodada-case-2/amaro-backend/src/business/PingBusiness.ts new file mode 100644 index 0000000..d426790 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/business/PingBusiness.ts @@ -0,0 +1,9 @@ +export class PingBusiness { + public ping = async () => { + const response = { + message: "Pong!" + } + + return response + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/business/UserBusiness.ts b/modulo6/rodada-case-2/amaro-backend/src/business/UserBusiness.ts new file mode 100644 index 0000000..c04a34d --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/business/UserBusiness.ts @@ -0,0 +1,136 @@ +import { UserDatabase } from "../database/UserDatabase" +import { NotFoundError } from "../errors/NotFoundError" +import { ConflictError} from "../errors/ConflictError" +import { ParamsError} from "../errors/ParamsError" +import { ILoginInputDTO, ILoginOutputDTO, ISignupInputDTO, ISignupOutputDTO, User, USER_ROLES } from "../models/Users" +import { Authenticator, ITokenPayload } from "../services/Authenticator" +import { HashManager } from "../services/HashManager" +import { IdGenerator } from "../services/IdGenerator" +import { AuthenticationError } from "../errors/AuthenticationError" + +export class UserBusiness { + constructor( + private userDatabase: UserDatabase, + private idGenerator: IdGenerator, + private hashManager: HashManager, + private authenticator: Authenticator + ) {} + + public signup = async (input: ISignupInputDTO): Promise => { + const { name, email, password } = input + + if (typeof name !== "string") { + throw new ParamsError("Parâmetro 'name' inválido: deve ser uma string") + } + + if (typeof email !== "string") { + throw new ParamsError("Parâmetro 'email' inválido: deve ser uma string") + } + + if (typeof password !== "string") { + throw new ParamsError("Parâmetro 'password' inválido: deve ser uma string") + } + + if (name.length < 3) { + throw new ParamsError("Parâmetro 'name' inválido: mínimo de 3 caracteres") + } + + if (password.length < 6) { + throw new ParamsError("Parâmetro 'password' inválido: mínimo de 6 caracteres") + } + + if (!email.match(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g)) { + throw new ParamsError("Parâmetro 'email' inválido") + } + + const isEmailAlreadyExists = await this.userDatabase.findByEmail(email) + + if (isEmailAlreadyExists) { + throw new ConflictError("Email já cadastrado") + } + + const id = this.idGenerator.generate() + const hashedPassword = await this.hashManager.hash(password) + + const user = new User( + id, + name, + email, + hashedPassword, + USER_ROLES.NORMAL + ) + + await this.userDatabase.createUser(user) + + const payload: ITokenPayload = { + id: user.getId(), + role: user.getRole() + } + + const token = this.authenticator.generateToken(payload) + + const response: ISignupOutputDTO = { + message: "Cadastro realizado com sucesso", + token + } + + return response + } + + public login = async (input: ILoginInputDTO): Promise => { + const { email, password } = input + + if (typeof email !== "string") { + throw new ParamsError("Parâmetro 'email' inválido") + } + + if (typeof password !== "string") { + throw new ParamsError("Parâmetro 'password' inválido") + } + + if (password.length < 6) { + throw new ParamsError("Parâmetro 'password' inválido: mínimo de 6 caracteres") + } + + if (!email.match(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g)) { + throw new ParamsError("Parâmetro 'email' inválido") + } + + const userDB = await this.userDatabase.findByEmail(email) + + if (!userDB) { + throw new NotFoundError("Email não cadastrado") + } + + const user = new User( + userDB.id, + userDB.name, + userDB.email, + userDB.password, + userDB.role + ) + + const isPasswordCorrect = await this.hashManager.compare( + password, + user.getPassword() + ) + + if (!isPasswordCorrect) { + throw new AuthenticationError("Password incorreto") + } + + const payload: ITokenPayload = { + id: user.getId(), + role: user.getRole() + } + + const token = this.authenticator.generateToken(payload) + + const response: ILoginOutputDTO = { + message: "Login realizado com sucesso", + token + } + + return response + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/controller/PingController.ts b/modulo6/rodada-case-2/amaro-backend/src/controller/PingController.ts new file mode 100644 index 0000000..9fb815b --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/controller/PingController.ts @@ -0,0 +1,22 @@ +import { Request, Response } from "express" +import { PingBusiness } from "../business/PingBusiness" +import { BaseError } from "../errors/BaseError" + +export class PingController { + constructor( + private pingBusiness: PingBusiness + ) {} + + public ping = async (req: Request, res: Response) => { + try { + const response = await this.pingBusiness.ping() + res.status(200).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado no endpoint ping" }) + } + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/controller/UserController.ts b/modulo6/rodada-case-2/amaro-backend/src/controller/UserController.ts new file mode 100644 index 0000000..7cd98be --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/controller/UserController.ts @@ -0,0 +1,47 @@ +import { Request, Response } from "express"; +import { UserBusiness } from "../business/UserBusiness"; +import { BaseError } from "../errors/BaseError"; +import { ILoginInputDTO, ISignupInputDTO } from "../models/Users"; + +export class UserController { + constructor( + private userBusiness: UserBusiness + ) {} + + public signup = async (req: Request, res: Response) => { + try { + const input: ISignupInputDTO = { + name: req.body.name, + email: req.body.email, + password: req.body.password + } + + const response = await this.userBusiness.signup(input) + res.status(201).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado ao cadastrar usuário" }) + } + } + + public login = async (req: Request, res: Response) => { + try { + const input: ILoginInputDTO = { + email: req.body.email, + password: req.body.password + } + + const response = await this.userBusiness.login(input) + res.status(200).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado ao cadastrar usuário" }) + } + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/database/BaseDatabase.ts b/modulo6/rodada-case-2/amaro-backend/src/database/BaseDatabase.ts new file mode 100644 index 0000000..bb1d2c5 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/database/BaseDatabase.ts @@ -0,0 +1,18 @@ +import knex from "knex" +import dotenv from "dotenv" + +dotenv.config() + +export abstract class BaseDatabase { + protected static connection = knex({ + client: "mysql", + connection: { + host: process.env.DB_HOST, + port: 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + multipleStatements: true + }, + }) +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/database/UserDatabase.ts b/modulo6/rodada-case-2/amaro-backend/src/database/UserDatabase.ts new file mode 100644 index 0000000..c3f31e6 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/database/UserDatabase.ts @@ -0,0 +1,39 @@ +import { IUserDB, User } from "../models/Users" +import { BaseDatabase } from "./BaseDatabase" + +export class UserDatabase extends BaseDatabase { + + public static TABLE_USERS = "Template_Users" + public static TABLE_PRODUCTS = "Am_Products" + public static TABLE_TAGS = "Am_Tags" + public static TABLE_TAGS_PRODUCTS = "Am_Tags_Products" + + public toUserDBModel = (user: User): IUserDB => { + const userDB: IUserDB = { + id: user.getId(), + name: user.getName(), + email: user.getEmail(), + password: user.getPassword(), + role: user.getRole() + } + + return userDB + } + + public findByEmail = async (email: string): Promise => { + const result: IUserDB[] = await BaseDatabase + .connection(UserDatabase.TABLE_USERS) + .select() + .where({ email }) + + return result[0] + } + + public createUser = async (user: User): Promise => { + const userDB = this.toUserDBModel(user) + + await BaseDatabase + .connection(UserDatabase.TABLE_USERS) + .insert(userDB) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/database/migrations/Migrations.ts b/modulo6/rodada-case-2/amaro-backend/src/database/migrations/Migrations.ts new file mode 100644 index 0000000..ca53184 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/database/migrations/Migrations.ts @@ -0,0 +1,52 @@ +import { BaseDatabase } from "../BaseDatabase" +import { UserDatabase } from "../UserDatabase" +import { users } from "./data" + + +class Migrations extends BaseDatabase { + execute = async () => { + try { + console.log("Creating tables...") + await this.createTables() + console.log("Tables created successfully.") + + console.log("Populating tables...") + await this.insertData() + console.log("Tables populated successfully.") + + console.log("Migrations completed.") + } catch (error) { + console.log("FAILED! Error in migrations...") + if (error instanceof Error) { + console.log(error.message) + } + } finally { + console.log("Ending connection...") + BaseDatabase.connection.destroy() + console.log("Connection closed graciously.") + } + } + + createTables = async () => { + await BaseDatabase.connection.raw(` + DROP TABLE IF EXISTS ${UserDatabase.TABLE_USERS}; + + CREATE TABLE IF NOT EXISTS ${UserDatabase.TABLE_USERS}( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + role ENUM("NORMAL", "ADMIN") DEFAULT "NORMAL" NOT NULL + ); + `) + } + + insertData = async () => { + await BaseDatabase + .connection(UserDatabase.TABLE_USERS) + .insert(users) + } +} + +const migrations = new Migrations() +migrations.execute() \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/database/migrations/data.ts b/modulo6/rodada-case-2/amaro-backend/src/database/migrations/data.ts new file mode 100644 index 0000000..0692dfb --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/database/migrations/data.ts @@ -0,0 +1,140 @@ +import { IUserDB, USER_ROLES } from "../../models/Users" +import { IProductsDB } from "../../models/Products" + +export const products: IProductsDB[] = [ + { + id: "8371", + name: "VESTIDO TRICOT CHEVRON", + tags: ["balada", "neutro", "delicado", "festa"] + }, + { + id: "8367", + name: "VESTIDO MOLETOM COM CAPUZ MESCLA", + tags: ["casual", "metal", "metal"] + }, + { + id: "8363", + name: "VESTIDO CURTO MANGA LONGA LUREX", + tags: ["colorido", "metal", "delicado", "estampas", "passeio"] + }, + { + id: "8360", + name: "VESTIDO FEMININO CANELADO", + tags: ["workwear", "viagem", "descolado"] + }, + { + id: "8358", + name: "VESTIDO REGATA FEMININO COM GOLA", + tags: ["moderno", "inverno", "liso", "basics"] + }, + { + id: "8314", + name: "VESTIDO PLISSADO ACINTURADO", + tags: ["casual", "viagem", "delicado"] + }, + { + id: "8311", + name: "VESTIDO SLIPDRESS CETIM", + tags: ["balada", "metal", "boho", "descolado", "passeio"] + }, + { + id: "8310", + name: "VESTIDO CURTO PONTO ROMA MANGA", + tags: ["casual", "metal", "delicado", "descolado", "elastano", "estampas"] + }, + { + id: "8309", + name: "VESTIDO MOLETOM COM CAPUZ", + tags: ["inverno", "liso", "casual", "descolado"] + }, + { + id: "8301", + name: "VESTIDO LONGO CREPE MANGA COMPRIDA", + tags: ["casual", "metal", "delicado", "descolado"] + }, + { + id: "8300", + name: "VESTIDO MALHA COM FENDA", + tags: ["balada", "metal", "estampas", "moderno"] + }, + { + id: "8293", + name: "VESTIDO CURTO VELUDO RECORTE GOLA", + tags: ["colorido", "viagem", "delicado", "descolado", "inverno"] + }, + { + id: "8291", + name: "VESTIDO MANGA COMPRIDA COSTAS", + tags: ["inverno", "estampas", "delicado", "descolado", "casual", "passeio", "basics"] + }, + { + id: "8264", + name: "VESTIDO CURTO VELUDO CRISTAL", + tags: ["casual", "viagem", "boho", "neutro", "festa"] + }, + { + id: "8119", + name: "VESTIDO BABADOS KNIT", + tags: ["moderno", "metal", "descolado", "elastano", "festa", "colorido"] + }, + { + id: "8110", + name: "VESTIDO CUT OUT TRICOT", + tags: ["casual", "colorido", "delicado", "descolado", "viagem", "inverno"] + }, + { + id: "8109", + name: "VESTIDO BABADOS HORIZONTAIS", + tags: ["moderno", "boho", "festa", "descolado", "colorido"] + }, + { + id: "8104", + name: "VESTIDO BABADO TURTLENECK", + tags: ["casual", "metal", "delicado", "neutro", "basics", "inverno", "viagem"] + }, + { + id: "8091", + name: "VESTIDO MIDI VELUDO DECOTADO", + tags: ["couro", "veludo", "passeio", "viagem"] + }, + { + id: "8083", + name: "VESTIDO LONGO ESTAMPADO", + tags: ["couro", "estampado", "passeio", "viagem"] + }, + { + id: "8080", + name: "VESTIDO CURTO RENDA VISCOSE", + tags: ["neutro", "workwear", "moderno", "descolado", "liso", "elastano"] + }, + { + id: "7613", + name: "VESTIDO LONGO BABADO", + tags: ["casual", "liso", "passeio", "colorido", "boho"] + }, + { + id: "7533", + name: "VESTIDO COTTON DOUBLE", + tags: ["balada", "liso", "moderno", "descolado"] + }, + { + id: "7518", + name: "VESTIDO CAMISETA FANCY", + tags: ["casual", "liso"] + }, + { + id: "7516", + name: "VESTIDO WRAP FLEUR", + tags: ["neutro", "liso", "basics", "viagem"] + } +] + +export const users: IUserDB[] = [ + { + id: "101", + name: "Astrodev", + email: "astrodev@gmail.com", + password: "$2a$12$RBAWOHpUvGTE.MEeIohAzec9tlVqtNA/x2PMPt/Hrt0vI437cQdJC", // bananinha + role: USER_ROLES.ADMIN + } +] \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/errors/AuthenticationError.ts b/modulo6/rodada-case-2/amaro-backend/src/errors/AuthenticationError.ts new file mode 100644 index 0000000..0a5bfac --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/errors/AuthenticationError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class AuthenticationError extends BaseError { + constructor( + message: string = "Credenciais inválidas" + ) { + super(401, message) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/errors/AuthorizationError.ts b/modulo6/rodada-case-2/amaro-backend/src/errors/AuthorizationError.ts new file mode 100644 index 0000000..6b9f2a0 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/errors/AuthorizationError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class AuthorizationError extends BaseError { + constructor( + message: string = "Permissão insuficiente" + ) { + super(403, message) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/errors/BaseError.ts b/modulo6/rodada-case-2/amaro-backend/src/errors/BaseError.ts new file mode 100644 index 0000000..1dae99f --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/errors/BaseError.ts @@ -0,0 +1,8 @@ +export class BaseError extends Error { + constructor( + public statusCode: number, + message: string + ) { + super(message) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/errors/ConflictError.ts b/modulo6/rodada-case-2/amaro-backend/src/errors/ConflictError.ts new file mode 100644 index 0000000..e5934ef --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/errors/ConflictError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class ConflictError extends BaseError { + constructor( + message: string = "Recurso já existe" + ) { + super(409, message) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/errors/NotFoundError.ts b/modulo6/rodada-case-2/amaro-backend/src/errors/NotFoundError.ts new file mode 100644 index 0000000..697098f --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/errors/NotFoundError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class NotFoundError extends BaseError { + constructor( + message: string = "Recurso não encontrado" + ) { + super(404, message) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/errors/ParamsError.ts b/modulo6/rodada-case-2/amaro-backend/src/errors/ParamsError.ts new file mode 100644 index 0000000..b43539b --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/errors/ParamsError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class ParamsError extends BaseError { + constructor( + message: string = "Parâmetros inválidos ou faltando" + ) { + super(400, message) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/errors/UnprocessableError.ts b/modulo6/rodada-case-2/amaro-backend/src/errors/UnprocessableError.ts new file mode 100644 index 0000000..85e971e --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/errors/UnprocessableError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class UnprocessableError extends BaseError { + constructor( + message: string = "Parâmetros válidos, porém com erros de semântica" + ) { + super(422, message) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/index.ts b/modulo6/rodada-case-2/amaro-backend/src/index.ts new file mode 100644 index 0000000..711346e --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/index.ts @@ -0,0 +1,19 @@ +import express from 'express' +import cors from 'cors' +import dotenv from "dotenv" + +import { pingRouter } from './router/pingRouter' +import { userRouter } from './router/userRouter' + +dotenv.config() + +const app = express() +app.use(express.json()) +app.use(cors()) + +app.listen(process.env.PORT || 3003, () => { + console.log(`Servidor rodando na porta ${process.env.PORT || 3003}`) +}) + +app.use("/ping", pingRouter) +app.use("/users", userRouter) \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/models/Products.ts b/modulo6/rodada-case-2/amaro-backend/src/models/Products.ts new file mode 100644 index 0000000..8e989b6 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/models/Products.ts @@ -0,0 +1,129 @@ + + export interface IProductDB { + id: string, + name: string +} + +export interface IProductsDB { + id: string, + name: string, + tags: string[] +} + +export interface ITagsProductsDB { + id: string, + product_id: string, + tag_id: string +} + +export interface ITagDB { + id: string, + tag: string +} + + +export class Product { + constructor( + private id: string, + private name: string, + private price: number, + private tags: string[] = [] + ) {} + + public getId = () => { + return this.id + } + + public getName = () => { + return this.name + } + + public getPrice = () => { + return this.price + } + + public getTags = () => { + return this.price + } + + public setId = (newId: string) => { + this.id = newId + } + + public setName = (newName: string) => { + this.name = newName + } + + public setPrice = (newPrice: number) => { + this.price = newPrice + } + + public setTags = (newTags: string[]) => { + this.tags = [...newTags] + } + +} + +export interface IGetProductosInputDTO { + order: string, + sort: string, + limit: string, + page: string +} + +export interface IGetProductosDB { + order: string, + sort: string, + limit: number, + page: number, + offset: number +} + +export interface ISearchProductosDB { + search: string + order: string, + sort: string, + limit: number, + page: number, + offset: number +} +export interface ISearchProductosInputDB { + search: string, + order: string, + sort: string, + limit: string, + page: string +} + +export interface ICreateTagsProdInput { + id: string, + product_id: string, + tag_id: string +} +export interface ICreateProductInput { + token: string + name: string, + tags: string[], + price: number +} + +export interface IDeleteProductoInput { + token: string, + idToDelete: string +} + + +export interface IGetAllProductsOutputDTO { + message: string + products: any +} + +export interface IAllGetsOutputDTO { + message: string + products: any +} + +export interface IDeletePostInputDTO { + token: string, + idToDelete: string +} diff --git a/modulo6/rodada-case-2/amaro-backend/src/models/Users.ts b/modulo6/rodada-case-2/amaro-backend/src/models/Users.ts new file mode 100644 index 0000000..ed49c1c --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/models/Users.ts @@ -0,0 +1,83 @@ +export enum USER_ROLES { + NORMAL = "NORMAL", + ADMIN = "ADMIN" +} + +export interface IUserDB { + id: string, + name: string, + email: string, + password: string, + role: USER_ROLES +} + +export class User { + constructor( + private id: string, + private name: string, + private email: string, + private password: string, + private role: USER_ROLES + ) {} + + public getId = () => { + return this.id + } + + public getName = () => { + return this.name + } + + public getEmail = () => { + return this.email + } + + public getPassword = () => { + return this.password + } + + public getRole = () => { + return this.role + } + + public setId = (newId: string) => { + this.id = newId + } + + public setName = (newName: string) => { + this.name = newName + } + + public setEmail = (newEmail: string) => { + this.email = newEmail + } + + public setPassword = (newPassword: string) => { + this.password = newPassword + } + + public setRole = (newRole: USER_ROLES) => { + this.role = newRole + } +} + +export interface ISignupInputDTO { + name: string, + email: string, + password: string +} + +export interface ISignupOutputDTO { + message: string, + token: string +} + +export interface ILoginInputDTO { + email: string, + password: string +} + +export interface ILoginOutputDTO { + message: string, + token: string +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/router/pingRouter.ts b/modulo6/rodada-case-2/amaro-backend/src/router/pingRouter.ts new file mode 100644 index 0000000..3b328b6 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/router/pingRouter.ts @@ -0,0 +1,11 @@ +import { Router } from 'express' +import { PingBusiness } from '../business/PingBusiness' +import { PingController } from '../controller/PingController' + +export const pingRouter = Router() + +const pingController = new PingController( + new PingBusiness() +) + +pingRouter.get("/", pingController.ping) \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/router/userRouter.ts b/modulo6/rodada-case-2/amaro-backend/src/router/userRouter.ts new file mode 100644 index 0000000..4548e60 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/router/userRouter.ts @@ -0,0 +1,21 @@ +import { Router } from 'express' +import { UserBusiness } from '../business/UserBusiness' +import { UserController } from '../controller/UserController' +import { UserDatabase } from '../database/UserDatabase' +import { Authenticator } from '../services/Authenticator' +import { HashManager } from '../services/HashManager' +import { IdGenerator } from '../services/IdGenerator' + +export const userRouter = Router() + +const userController = new UserController( + new UserBusiness( + new UserDatabase(), + new IdGenerator(), + new HashManager(), + new Authenticator() + ) +) + +userRouter.post("/signup", userController.signup) +userRouter.post("/login", userController.login) \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/services/Authenticator.ts b/modulo6/rodada-case-2/amaro-backend/src/services/Authenticator.ts new file mode 100644 index 0000000..faeaee3 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/services/Authenticator.ts @@ -0,0 +1,37 @@ +import jwt from 'jsonwebtoken' +import dotenv from "dotenv" +import { USER_ROLES } from '../models/Users' + +dotenv.config() + +export interface ITokenPayload { + id: string, + role: USER_ROLES +} + +export class Authenticator { + generateToken = (payload: ITokenPayload): string => { + const token = jwt.sign( + payload, + process.env.JWT_KEY as string, + { + expiresIn: process.env.JWT_EXPIRES_IN + } + ) + + return token + } + + getTokenPayload = (token: string): ITokenPayload | null => { + try { + const payload = jwt.verify( + token, + process.env.JWT_KEY as string + ) + + return payload as ITokenPayload + } catch (error) { + return null + } + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/services/HashManager.ts b/modulo6/rodada-case-2/amaro-backend/src/services/HashManager.ts new file mode 100644 index 0000000..bf54501 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/services/HashManager.ts @@ -0,0 +1,15 @@ +import bcrypt from 'bcryptjs' + +export class HashManager { + public hash = async (plaintext: string): Promise => { + const rounds = Number(process.env.BCRYPT_SALT_ROUNDS) + const salt = await bcrypt.genSalt(rounds) + const hash = await bcrypt.hash(plaintext, salt) + + return hash + } + + public compare = async (plaintext: string, hash: string): Promise => { + return bcrypt.compare(plaintext, hash) + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/src/services/IdGenerator.ts b/modulo6/rodada-case-2/amaro-backend/src/services/IdGenerator.ts new file mode 100644 index 0000000..a6ba76e --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/src/services/IdGenerator.ts @@ -0,0 +1,7 @@ +import { v4 } from 'uuid' + +export class IdGenerator { + public generate = (): string => { + return v4() + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/tests/UserBusinessTests/login.test.ts b/modulo6/rodada-case-2/amaro-backend/tests/UserBusinessTests/login.test.ts new file mode 100644 index 0000000..819501b --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/tests/UserBusinessTests/login.test.ts @@ -0,0 +1,66 @@ +import { UserBusiness } from "../../src/business/UserBusiness" +import { BaseError } from "../../src/errors/BaseError" +import { ILoginInputDTO, ISignupInputDTO } from "../../src/models/User" +import { AuthenticatorMock } from ".././mocks/AuthenticatorMock" +import { HashManagerMock } from ".././mocks/HashManagerMock" +import { IdGeneratorMock } from ".././mocks/IdGeneratorMock" +import { UserDatabaseMock } from ".././mocks/UserDatabaseMock" + +describe("Testando o método login da UserBusiness", () => { + const userBusiness = new UserBusiness( + new UserDatabaseMock(), + new IdGeneratorMock(), + new HashManagerMock(), + new AuthenticatorMock() + ) + + test("Um token é retornado quando o login é bem-sucedido", async () => { + const input: ILoginInputDTO = { + email: "astrodev@gmail.com", + password: "bananinha" + } + + const response = await userBusiness.login(input) + expect(response.message).toBe("Login realizado com sucesso") + expect(response.token).toBe("token-mock-admin") + }) + + test("Erro quando 'password' possuir menos de 6 caracteres", async () => { + expect.assertions(2) + + try { + const input: ISignupInputDTO = { + email: "fulano@gmail.com", + name: "Fulano", + password: "123" + } + + await userBusiness.signup(input) + + } catch (error) { + if (error instanceof BaseError) { + expect(error.statusCode).toBe(400) + expect(error.message).toBe("Parâmetro 'password' inválido: mínimo de 6 caracteres") + } + } + }) + + test("Erro quando 'password' for incorreto", async () => { + expect.assertions(2) + + try { + const input: ILoginInputDTO = { + email: "astrodev@gmail.com", + password: "bananinha123" + } + + await userBusiness.login(input) + + } catch (error) { + if (error instanceof BaseError) { + expect(error.statusCode).toBe(401) + expect(error.message).toBe("Password incorreto") + } + } + }) +}) \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/tests/UserBusinessTests/signup.test.ts b/modulo6/rodada-case-2/amaro-backend/tests/UserBusinessTests/signup.test.ts new file mode 100644 index 0000000..b2af2b2 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/tests/UserBusinessTests/signup.test.ts @@ -0,0 +1,27 @@ +import { UserBusiness } from "../../src/business/UserBusiness" +import { ISignupInputDTO } from "../../src/models/User" +import { AuthenticatorMock } from ".././mocks/AuthenticatorMock" +import { HashManagerMock } from ".././mocks/HashManagerMock" +import { IdGeneratorMock } from ".././mocks/IdGeneratorMock" +import { UserDatabaseMock } from ".././mocks/UserDatabaseMock" + +describe("Testando o método signup da UserBusiness", () => { + const userBusiness = new UserBusiness( + new UserDatabaseMock(), + new IdGeneratorMock(), + new HashManagerMock(), + new AuthenticatorMock() + ) + + test("Um token é retornado quando o cadastro é bem-sucedido", async () => { + const input: ISignupInputDTO = { + email: "teste@gmail.com", + name: "Teste", + password: "teste123" + } + + const response = await userBusiness.signup(input) + expect(response.message).toBe("Cadastro realizado com sucesso") + expect(response.token).toBe("token-mock-normal") + }) +}) \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/tests/mocks/AuthenticatorMock.ts b/modulo6/rodada-case-2/amaro-backend/tests/mocks/AuthenticatorMock.ts new file mode 100644 index 0000000..954cfbe --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/tests/mocks/AuthenticatorMock.ts @@ -0,0 +1,37 @@ +import { USER_ROLES } from "../../src/models/User" +import { ITokenPayload } from "../../src/services/Authenticator" + +export class AuthenticatorMock { + public generateToken = (payload: ITokenPayload): string => { + switch (payload.role) { + case USER_ROLES.ADMIN: + return "token-mock-admin" + default: + return "token-mock-normal" + } + } + + public getTokenPayload = (token: string): ITokenPayload | null => { + switch (token) { + case "token-mock-admin": + const adminPayload: ITokenPayload = { + id: "id-mock", + role: USER_ROLES.ADMIN + } + + return adminPayload + + case "token-mock-normal": + const normalPayload: ITokenPayload = { + id: "id-mock", + role: USER_ROLES.NORMAL + } + + return normalPayload + + default: + return null + } + } + +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/tests/mocks/HashManagerMock.ts b/modulo6/rodada-case-2/amaro-backend/tests/mocks/HashManagerMock.ts new file mode 100644 index 0000000..6a122d2 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/tests/mocks/HashManagerMock.ts @@ -0,0 +1,17 @@ +export class HashManagerMock { + public hash = async (plaintext: string): Promise => { + if (plaintext == "bananinha") { + return "hash-bananinha" + } + + return "hash-mock" + } + + public compare = async (plaintext: string, hash: string): Promise => { + if (plaintext == "bananinha" && hash == "hash-bananinha") { + return true + } + + return false + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/tests/mocks/IdGeneratorMock.ts b/modulo6/rodada-case-2/amaro-backend/tests/mocks/IdGeneratorMock.ts new file mode 100644 index 0000000..42e1349 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/tests/mocks/IdGeneratorMock.ts @@ -0,0 +1,5 @@ +export class IdGeneratorMock { + public generate = (): string => { + return "id-mock" + } +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/tests/mocks/UserDatabaseMock.ts b/modulo6/rodada-case-2/amaro-backend/tests/mocks/UserDatabaseMock.ts new file mode 100644 index 0000000..8c4e356 --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/tests/mocks/UserDatabaseMock.ts @@ -0,0 +1,49 @@ +import { IUserDB, User, USER_ROLES } from "../../src/models/User" +import { BaseDatabase } from "../../src/database/BaseDatabase" + +export class UserDatabaseMock extends BaseDatabase { + public static TABLE_USERS = "Labook_Users" + + public toUserDBModel = (user: User) => { + const userDB: IUserDB = { + id: user.getId(), + name: user.getName(), + email: user.getEmail(), + password: user.getPassword(), + role: user.getRole() + } + + return userDB + } + + public findByEmail = async (email: string): Promise => { + switch (email) { + case "usermock@gmail.com": + const normalUser: IUserDB = { + id: "id-mock", + name: "User Mock", + email: "usermock@gmail.com", + password: "hash-mock", + role: USER_ROLES.NORMAL + } + + return normalUser + + case "astrodev@gmail.com": + const adminUser: IUserDB = { + id: "id-mock", + name: "Astrodev", + email: "astrodev@gmail.com", + password: "hash-bananinha", + role: USER_ROLES.ADMIN + } + + return adminUser + + default: + return undefined + } + } + + public createUser = async (user: User): Promise => {} +} \ No newline at end of file diff --git a/modulo6/rodada-case-2/amaro-backend/tsconfig.json b/modulo6/rodada-case-2/amaro-backend/tsconfig.json new file mode 100644 index 0000000..b87ef5e --- /dev/null +++ b/modulo6/rodada-case-2/amaro-backend/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "commonjs", + "sourceMap": true, + "outDir": "./build", + "rootDir": "./", + "removeComments": true, + "noImplicitAny": true, + "esModuleInterop": true, + "strict": true + }, + "exclude": [ + "./tests/*" + ] +} \ No newline at end of file