diff --git a/semana21/aula64/template-testes-backend-feito/.rest b/semana21/aula64/template-testes-backend-feito/.rest new file mode 100644 index 0000000..145d378 --- /dev/null +++ b/semana21/aula64/template-testes-backend-feito/.rest @@ -0,0 +1,21 @@ +### CADASTRO + +POST http://localhost:3003/users/signup +Content-Type: application/json + +{ + "name": "Alice", + "email": "alice@lbn.com", + "password": "123456", + "role":"ADMIN" +} + +### LOGIN + +POST http://localhost:3003/users/login +Content-Type: application/json + +{ + "email": "alice@lbn.com", + "password": "123456" +} \ No newline at end of file diff --git a/semana21/aula64/template-testes-backend-feito/.vscode/launch.json b/semana21/aula64/template-testes-backend-feito/.vscode/launch.json new file mode 100644 index 0000000..2068be4 --- /dev/null +++ b/semana21/aula64/template-testes-backend-feito/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/build/src/index.js", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/semana22/amaro/.gitignore b/semana22/amaro/.gitignore new file mode 100644 index 0000000..8ece3ba --- /dev/null +++ b/semana22/amaro/.gitignore @@ -0,0 +1,4 @@ +node_modules +package-lock.json +build +.env \ No newline at end of file diff --git a/semana22/amaro/package.json b/semana22/amaro/package.json new file mode 100644 index 0000000..a5d06d8 --- /dev/null +++ b/semana22/amaro/package.json @@ -0,0 +1,34 @@ +{ + "name": "amaro", + "version": "1.0.0", + "description": "### Sobre o desafio", + "main": "index.js", + "scripts": { + "dev": "ts-node-dev ./src/index.ts", + "start": "tsc && node ./build/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/knex": "^0.16.1", + "@types/uuid": "^8.3.4", + "typescript": "^4.5.4" + }, + "dependencies": { + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^8.5.8", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^14.2.0", + "express": "^4.17.2", + "jsonwebtoken": "^8.5.1", + "knex": "^1.0.1", + "mysql": "^2.18.1", + "ts-node-dev": "^1.1.8", + "uuid": "^8.3.2" + } +} diff --git a/semana22/amaro/readme.md b/semana22/amaro/readme.md new file mode 100644 index 0000000..1bb882c --- /dev/null +++ b/semana22/amaro/readme.md @@ -0,0 +1,34 @@ +### Desafio back-end AMARO + +### Sobre o desafio + +- Criação de API para cadastro e consulta de produtos +- Você precisa criar uma API com os seguintes requisitos: + +#### End-point para inserção de dados + +- O cliente poderá enviá-los em arquivos json ou xml e a API deverá inserir no banco de dados. +- Escolha o banco de dados que achar melhor. + +### End-point para consulta destes produtos + +-Pode ser consultado por: id, nome ou tags. Caso a consulta seja por uma tag ou nome, deverá listar todos os produtos com aquela respectiva busca, poderá ser feito em um ou mais end-points. + +### Requisitos Obrigatórios + +- Ter uma cobertura de teste relativamente boa, a maior que você conseguir. +- Usar NodeJS +- Criar um cache para consulta. + +### PLUS - Não necessário + +- Colocar uma autenticação JWT. + +### Orientações + +- Procure fazer uma API sucinta. +- Os arquivos (json, xml) junto com o formato que o cliente irá enviar estão no repositório. +- Pense em escalabilidade, pode ser uma quantidade muito grande de dados. +- Coloque isso em um repositório GIT. +- Colocar as orientações de setup no README do seu repositório. +- Boa sorte! diff --git a/semana22/amaro/src/business/ProductBusiness/ProductBusiness.ts b/semana22/amaro/src/business/ProductBusiness/ProductBusiness.ts new file mode 100644 index 0000000..d715759 --- /dev/null +++ b/semana22/amaro/src/business/ProductBusiness/ProductBusiness.ts @@ -0,0 +1,64 @@ +import { + CreateProductDTO, + GetProductByNameDTO, + Product, +} from "../../entities/Product"; +import { + AuthenticationData, + Authenticator, +} from "../../services/Authenticator"; +import { IdGenerator } from "../../services/IdGenerator"; +import { ProductDatabase } from "../../data/ProductDataBase/ProductDatabase"; + +export class ProductBusiness { + async registerProduct(input: CreateProductDTO): Promise { + try { + const tokenManager = new Authenticator(); + + if (!input.name || !input.price || !input.image_url) { + throw new Error('"Name", "Price" and "image_url" must be provided'); + } + + if (!input.token) { + throw new Error("A jwt must be provided"); + } + + const tokenData: AuthenticationData = tokenManager.getTokenData( + input.token + ); + + const idGenerator = new IdGenerator(); + const id: string = idGenerator.generateId(); + + const product: Product = { + id, + user_id: tokenData.id, + name: input.name, + price: input.price, + image_url: input.image_url, + }; + + await new ProductDatabase().registerProduct(product); + } catch (error: any) { + throw new Error(error.message); + } + } + + async getProductByName( + input: GetProductByNameDTO + ): Promise { + try { + const product: Product = await new ProductDatabase().getProductByName( + input.name + ); + + if (!product) { + throw new Error("Product not found"); + } + + return product; + } catch (error: any) { + throw new Error(error.message); + } + } +} diff --git a/semana22/amaro/src/business/UserBusiness/UserBusiness.ts b/semana22/amaro/src/business/UserBusiness/UserBusiness.ts new file mode 100644 index 0000000..d9d4ae0 --- /dev/null +++ b/semana22/amaro/src/business/UserBusiness/UserBusiness.ts @@ -0,0 +1,64 @@ +import { SignupInputDTO, LoginInputDTO, User } from "../../entities/User"; +import { IdGenerator } from "../../services/IdGenerator"; +import { HashManager } from "../../services/HashManager"; +import { Authenticator } from "../../services/Authenticator"; +import { UserDatabase } from "../../data/UserDataBase/UserDataBase"; + +export class UserBusiness { + async signup(input: SignupInputDTO): Promise { + try { + if (!input.name || !input.email || !input.password) { + throw new Error('"name", "email" and "password" must be provided'); + } + + const idGenerator = new IdGenerator(); + const id: string = idGenerator.generateId(); + + const hashManager = new HashManager(); + const cypherPassword = await hashManager.hash(input.password); + + const user: User = { + id, + name: input.name, + email: input.email, + password: cypherPassword, + }; + + const userDataBase = new UserDatabase(); + await userDataBase.insertUser(user); + + const tokenManager = new Authenticator(); + const token: string = tokenManager.generate({ id }); + + return token; + } catch (error) {} + } + + async login(input: LoginInputDTO): Promise { + try { + if (!input.email || !input.password) { + throw new Error('"email" and "password" must be provided'); + } + + const userDataBase = new UserDatabase(); + const user: User = await userDataBase.getUserByEmail(input.email); + + const hashManager = new HashManager(); + const passwordIsCorrect: boolean = await hashManager.compare( + input.password, + user.password + ); + + if (!user || !passwordIsCorrect) { + throw new Error("Invalid credentials"); + } + + const tokenManager = new Authenticator(); + const token: string = await tokenManager.generate({ id: user.id }); + + return token; + } catch (error: any) { + throw new Error(error.message); + } + } +} diff --git a/semana22/amaro/src/controller/ProductController/ProductController.ts b/semana22/amaro/src/controller/ProductController/ProductController.ts new file mode 100644 index 0000000..e3e2bf2 --- /dev/null +++ b/semana22/amaro/src/controller/ProductController/ProductController.ts @@ -0,0 +1,52 @@ +import { Request, Response } from "express"; +import { + CreateProductDTO, + GetProductByNameDTO, + Product, +} from "../../entities/Product"; +import { ProductBusiness } from "../../business/ProductBusiness/ProductBusiness"; + +export class ProductController { + async registerProduct(req: Request, res: Response): Promise { + try { + const message = "Success!"; + + const token: string = req.headers.authorization as string; + + const input: CreateProductDTO = { + name: req.body.name, + price: req.body.price, + image_url: req.body.image_url, + token, + }; + + await new ProductBusiness().registerProduct(input); + + res.status(201).send({ message }); + } catch (error: any) { + const message = error.SqlMessage || error.message; + + res.send({ message }); + } + } + + async getProductByName(req: Request, res: Response): Promise { + try { + const message = "Success!"; + + const input: GetProductByNameDTO = { + name: req.params.name, + }; + + const product: Product | undefined = + await new ProductBusiness().getProductByName(input); + + res.status(200).send({ message, product }); + } catch (error: any) { + let message = error.sqlMessage || error.message; + res.statusCode = 400; + + res.send({ message }); + } + } +} diff --git a/semana22/amaro/src/controller/UserController/UserController.ts b/semana22/amaro/src/controller/UserController/UserController.ts new file mode 100644 index 0000000..f214f3f --- /dev/null +++ b/semana22/amaro/src/controller/UserController/UserController.ts @@ -0,0 +1,46 @@ +import { Request, Response } from "express"; +import { SignupInputDTO, LoginInputDTO } from "../../entities/User"; +import { UserBusiness } from "../../business/UserBusiness/UserBusiness"; + +export class UserController { + async signup(req: Request, res: Response): Promise { + try { + const message = "Success!"; + + const input: SignupInputDTO = { + name: req.body.name, + email: req.body.email, + password: req.body.password, + }; + + const userBusiness = new UserBusiness(); + const token = await userBusiness.signup(input); + + res.status(201).send({ message, token }); + } catch (error: any) { + const message = error.SqlMessage || error.message; + + res.send({ message }); + } + } + + async loginUser(req: Request, res: Response) { + try { + const message = "Success!"; + + const input: LoginInputDTO = { + email: req.body.email, + password: req.body.password, + }; + + const token = await new UserBusiness().login(input); + + res.status(200).send({ message, token }); + } catch (error: any) { + let message = error.sqlMessage || error.message; + res.statusCode = 400; + + res.send({ message }); + } + } +} diff --git a/semana22/amaro/src/controller/app.ts b/semana22/amaro/src/controller/app.ts new file mode 100644 index 0000000..7a745f2 --- /dev/null +++ b/semana22/amaro/src/controller/app.ts @@ -0,0 +1,11 @@ +import express from "express"; +import cors from "cors"; + +export const app = express(); + +app.use(express.json()); +app.use(cors()); + +app.listen(3003, () => { + console.log("Server runin on port 3003"); +}); diff --git a/semana22/amaro/src/controller/routes/productRouter.ts b/semana22/amaro/src/controller/routes/productRouter.ts new file mode 100644 index 0000000..52f43dd --- /dev/null +++ b/semana22/amaro/src/controller/routes/productRouter.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; +import { ProductController } from "../ProductController/ProductController"; + +export const productRouter = Router(); + +const productController = new ProductController(); + +productRouter.post("/", productController.registerProduct); +productRouter.get("/:name", productController.getProductByName); diff --git a/semana22/amaro/src/controller/routes/userRouter.ts b/semana22/amaro/src/controller/routes/userRouter.ts new file mode 100644 index 0000000..2d51b77 --- /dev/null +++ b/semana22/amaro/src/controller/routes/userRouter.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; +import { UserController } from "../UserController/UserController"; + +export const userRouter = Router(); + +const userController = new UserController(); + +userRouter.post("/signup", userController.signup); +userRouter.post("/login", userController.loginUser); diff --git a/semana22/amaro/src/data/BaseDatabase.ts b/semana22/amaro/src/data/BaseDatabase.ts new file mode 100644 index 0000000..9a5ac4f --- /dev/null +++ b/semana22/amaro/src/data/BaseDatabase.ts @@ -0,0 +1,15 @@ +import knex from "knex"; + +export class BaseDatabase { + protected connection = knex({ + client: "mysql", + connection: { + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_SCHEMA, + port: 3306, + multipleStatements: true, + }, + }); +} diff --git a/semana22/amaro/src/data/ProductDataBase/ProductDatabase.ts b/semana22/amaro/src/data/ProductDataBase/ProductDatabase.ts new file mode 100644 index 0000000..92c3543 --- /dev/null +++ b/semana22/amaro/src/data/ProductDataBase/ProductDatabase.ts @@ -0,0 +1,33 @@ +import { BaseDatabase } from "../BaseDatabase"; +import { Product, toProductModel } from "../../entities/Product"; +import dotenv from "dotenv"; + +dotenv.config(); + +export class ProductDatabase extends BaseDatabase { + async registerProduct(product: Product) { + try { + await this.connection("amaro_products").insert({ + id: product.id, + name: product.name, + price: product.price, + image_url: product.image_url, + user_id: product.user_id, + }); + } catch (error: any) { + throw new Error(error.sqlMessage || error.message); + } + } + + async getProductByName(name: string): Promise { + try { + const result = await this.connection("amaro_products") + .select("*") + .where({ name }); + + return toProductModel(result[0]); + } catch (error: any) { + throw new Error(error.sqlMessage || error.message); + } + } +} diff --git a/semana22/amaro/src/data/UserDataBase/UserDataBase.ts b/semana22/amaro/src/data/UserDataBase/UserDataBase.ts new file mode 100644 index 0000000..4bcf444 --- /dev/null +++ b/semana22/amaro/src/data/UserDataBase/UserDataBase.ts @@ -0,0 +1,34 @@ +import { BaseDatabase } from "../BaseDatabase"; +import { User, toUserModel } from "../../entities/User"; +import dotenv from "dotenv"; + +dotenv.config(); + +export class UserDatabase extends BaseDatabase { + async insertUser(user: User) { + try { + await this.connection("amaro_users").insert({ + id: user.id, + name: user.name, + email: user.email, + password: user.password, + }); + } catch (error: any) { + throw new Error(error.sqlMessage || error.message); + } + } + + async getUserByEmail(email: string): Promise { + try { + const result: any = await this.connection("amaro_users") + .select("*") + .where({ email }); + + const user = toUserModel(result[0]); + + return user; + } catch (error: any) { + throw new Error(error.sqlMessage || error.message); + } + } +} diff --git a/semana22/amaro/src/entities/Product.ts b/semana22/amaro/src/entities/Product.ts new file mode 100644 index 0000000..b07b1c8 --- /dev/null +++ b/semana22/amaro/src/entities/Product.ts @@ -0,0 +1,34 @@ +export interface Product { + id: string; + name: string; + price: number; + image_url: string; + user_id: string; +} + +export interface CreateProductDTO { + name: string; + price: number; + image_url: string; + token: string; +} + +export interface GetProductByIdInputDTO { + id: string; +} + +export interface GetProductByNameDTO { + name: string; +} + +export function toProductModel(obj: any): Product { + return ( + obj && { + id: obj.id, + name: obj.name, + price: obj.price, + image_url: obj.image_url, + authorId: obj.authorId, + } + ); +} diff --git a/semana22/amaro/src/entities/User.ts b/semana22/amaro/src/entities/User.ts new file mode 100644 index 0000000..e453358 --- /dev/null +++ b/semana22/amaro/src/entities/User.ts @@ -0,0 +1,28 @@ +export interface User { + id: string; + name: string; + email: string; + password: string; +} + +export interface SignupInputDTO { + name: string; + email: string; + password: string; +} + +export interface LoginInputDTO { + email: string; + password: string; +} + +export function toUserModel(obj: any): User { + return ( + obj && { + id: obj.id, + email: obj.email, + name: obj.name, + password: obj.password, + } + ); +} diff --git a/semana22/amaro/src/index.ts b/semana22/amaro/src/index.ts new file mode 100644 index 0000000..1fb1382 --- /dev/null +++ b/semana22/amaro/src/index.ts @@ -0,0 +1,6 @@ +import { app } from "./controller/app"; +import { userRouter } from "./controller/routes/userRouter"; +import { productRouter } from "./controller/routes/productRouter"; + +app.use("/user", userRouter); +app.use("/product", productRouter); diff --git a/semana22/amaro/src/migrations.ts b/semana22/amaro/src/migrations.ts new file mode 100644 index 0000000..1a96bd9 --- /dev/null +++ b/semana22/amaro/src/migrations.ts @@ -0,0 +1,24 @@ +import { BaseDatabase } from "./data/BaseDatabase"; + +export class BaseDatabaseMigrations extends BaseDatabase { + baseDatabase = this.connection + .raw( + `CREATE TABLE IF NOT EXISTS amaro_users ( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL + ); + + CREATE TABLE IF NOT EXISTS amaro_products ( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + price FLOAT NOT NULL, + image_url VARCHAR(255) NOT NULL, + user_id VARCHAR(255), + FOREIGN KEY(user_id) REFERENCES amaro_users(id) + );` + ) + .then(console.log) + .catch(console.log); +} diff --git a/semana22/amaro/src/services/Authenticator.ts b/semana22/amaro/src/services/Authenticator.ts new file mode 100644 index 0000000..9cd705a --- /dev/null +++ b/semana22/amaro/src/services/Authenticator.ts @@ -0,0 +1,20 @@ +import * as jwt from "jsonwebtoken"; + +export interface AuthenticationData { + id: string; +} + +export class Authenticator { + generate(input: AuthenticationData): string { + const token = jwt.sign(input, process.env.JWT_KEY as any, { + expiresIn: process.env.ACCESS_TOKEN_EXPIRES_IN, + }); + + return token; + } + + getTokenData(token: string): AuthenticationData { + const data = jwt.verify(token, process.env.JWT_KEY as any); + return data as AuthenticationData; + } +} diff --git a/semana22/amaro/src/services/HashManager.ts b/semana22/amaro/src/services/HashManager.ts new file mode 100644 index 0000000..1c745d2 --- /dev/null +++ b/semana22/amaro/src/services/HashManager.ts @@ -0,0 +1,13 @@ +import * as bcrypt from "bcryptjs"; + +export class HashManager { + async hash(text: string): Promise { + const rounds = Number(process.env.BCRYPT_COST); + const salt = await bcrypt.genSalt(rounds); + return bcrypt.hash(text, salt); + } + + async compare(text: string, hash: string): Promise { + return bcrypt.compare(text, hash); + } +} diff --git a/semana22/amaro/src/services/IdGenerator.ts b/semana22/amaro/src/services/IdGenerator.ts new file mode 100644 index 0000000..3a8b784 --- /dev/null +++ b/semana22/amaro/src/services/IdGenerator.ts @@ -0,0 +1,5 @@ +import { v4 } from "uuid"; + +export class IdGenerator { + generateId = (): string => v4(); +} diff --git a/semana22/amaro/tsconfig.json b/semana22/amaro/tsconfig.json new file mode 100644 index 0000000..3157e37 --- /dev/null +++ b/semana22/amaro/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true /* Generates corresponding '.map' file. */, + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./build" /* Redirect output structure to the directory. */, + "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + "removeComments": true /* Do not emit comments to output. */, + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +}