diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ece3ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +package-lock.json +build +.env \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..d944f04 --- /dev/null +++ b/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"], +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ccd725d --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "lama-template", + "version": "1.0.0", + "description": "Template para o projeto LAMA", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "ts-node ./src/index.ts" + }, + "keywords": [ + "api", + "labook" + ], + "author": "João Alves", + "license": "ISC", + "dependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/express": "^4.17.7", + "@types/jest": "^25.2.3", + "@types/jsonwebtoken": "^8.5.0", + "@types/knex": "^0.16.1", + "@types/uuid": "^8.0.0", + "bcryptjs": "^2.4.3", + "dotenv": "^8.2.0", + "express": "^4.17.1", + "jsonwebtoken": "^8.5.1", + "jest": "^26.0.1", + "knex": "^0.21.2", + "moment": "^2.27.0", + "mysql": "^2.18.1", + "ts-jest": "^26.1.0", + "ts-node": "^8.10.2", + "typescript": "^3.9.6", + "uuid": "^8.2.0" + } +} diff --git a/src/business/BandBusiness.ts b/src/business/BandBusiness.ts new file mode 100644 index 0000000..d8b03b8 --- /dev/null +++ b/src/business/BandBusiness.ts @@ -0,0 +1,37 @@ +import { UserDatabase } from "../data/UserDatabase"; +import { BaseError, InvalidName } from "../error/BaseError"; +import { BandInputDTO } from "../model/Band"; +import { IdGenerator } from "../services/IdGenerator"; + +export class BandBusiness { + + public createBand=async(input: BandInputDTO)=> { + try { + const {name,music_genre,responsible } = input + + if (!name || !music_genre || !responsible ) { + + throw new BaseError( + 400, + 'Necessário preencher os campos "name", "music_genre" e "responsible"' + ); + } + // if (Band.name.length < 3) { + // throw new InvalidName(); + // } + const idGenerator = new IdGenerator(); + const id = idGenerator.generate(); + + const userDatabase = new UserDatabase(); + userDatabase.createBand( + id, + name, + music_genre, + responsible + ); + +} catch (error: any) { + throw new Error(error.message); + } + } +} diff --git a/src/business/UserBusiness.ts b/src/business/UserBusiness.ts new file mode 100644 index 0000000..b9b0488 --- /dev/null +++ b/src/business/UserBusiness.ts @@ -0,0 +1,90 @@ +import { UserInputDTO, LoginInputDTO } from "../model/User"; +import { UserDatabase } from "../data/UserDatabase"; +import { IdGenerator } from "../services/IdGenerator"; +import { HashManager } from "../services/HashManager"; +import { Authenticator } from "../services/Authenticator"; +import { + BaseError, + InvalidEmail, + InvalidName, + InvalidPassword, + UserNotFound, +} from "../error/BaseError"; + +export class UserBusiness { + async createUser(user: UserInputDTO): Promise { + try { + if (!user.name || !user.email || !user.password || !user.role) { + throw new BaseError( + 400, + 'Necessário preencher os campos "name", "email" e "password"' + ); + } + if (user.name.length < 3) { + throw new InvalidName(); + } + if (!user.email.includes("@")) { + throw new InvalidEmail(); + } + if (user.password.length < 8) { + throw new InvalidPassword(); + } + const idGenerator = new IdGenerator(); + const id = idGenerator.generate(); + + const hashManager = new HashManager(); + const hashPassword = await hashManager.hash(user.password); + + const userDatabase = new UserDatabase(); + await userDatabase.createUser( + id, + user.email, + user.name, + hashPassword, + user.role + ); + + const authenticator = new Authenticator(); + const accessToken = authenticator.generateToken({ id, role: user.role }); + + return accessToken; + } catch (error: any) { + throw new Error(error.message); + } + } + + async getUserByEmail(user: LoginInputDTO): Promise { + try { + if (!user.email || !user.password) { + throw new BaseError(400, "Necessário preencher todos os campos"); + } + if (!user.email.includes("@")) { + throw new InvalidEmail(); + } + const userDatabase = new UserDatabase(); + const userFromDB = await userDatabase.getUserByEmail(user.email); + + if (!userFromDB) { + throw new UserNotFound(); + } + + const hashManager = new HashManager(); + const hashCompare = await hashManager.compare( + user.password, + userFromDB.getPassword() + ); + if (!hashCompare) { + throw new InvalidPassword(); + } + const authenticator = new Authenticator(); + const accessToken = authenticator.generateToken({ + id: userFromDB.getId(), + role: userFromDB.getRole(), + }); + + return accessToken; + } catch (error: any) { + throw new Error(error.message); + } + } +} diff --git a/src/controller/BandController.ts b/src/controller/BandController.ts new file mode 100644 index 0000000..13c3c90 --- /dev/null +++ b/src/controller/BandController.ts @@ -0,0 +1,34 @@ +import { BandBusiness } from "../business/BandBusiness"; +import { BandInputDTO } from "../model/Band"; +import { Request, Response } from "express"; + +const bandBusiness = new BandBusiness() + +export class BandController{ + public createBand=async(req: Request, + res: Response)=> { + try { + + const{ + name, + music_genre, + responsible + } = req.body + + const input:BandInputDTO = { + name, + music_genre, + responsible, + id: "" + } + + + await bandBusiness.createBand(input) + + res.send('Banda criada com sucesso!') + + } catch (err: any) { + res.status(err.statusCode || 400).send(err.message || err.sqlMessage); + } + } +} \ No newline at end of file diff --git a/src/controller/UserController.ts b/src/controller/UserController.ts new file mode 100644 index 0000000..dfb9601 --- /dev/null +++ b/src/controller/UserController.ts @@ -0,0 +1,50 @@ +import { Request, Response } from "express"; +import { UserInputDTO, LoginInputDTO} from "../model/User"; +import { UserBusiness } from "../business/UserBusiness"; +import { BaseDatabase } from "../data/BaseDatabase"; + +export class UserController { + async signup(req: Request, res: Response) { + try { + + const input: UserInputDTO = { + email: req.body.email, + name: req.body.name, + password: req.body.password, + role: req.body.role + } + + const userBusiness = new UserBusiness(); + const token = await userBusiness.createUser(input); + + res.status(200).send({ token }); + + } catch (error: any) { + res.status(400).send({ error: error.message }); + } + + await BaseDatabase.destroyConnection(); + } + + async login(req: Request, res: Response) { + + try { + + const loginData: LoginInputDTO = { + email: req.body.email, + password: req.body.password + }; + + const userBusiness = new UserBusiness(); + const token = await userBusiness.getUserByEmail(loginData); + + res.status(200).send({ token }); + + } catch (error: any) { + res.status(400).send({ error: error.message }); + } + + await BaseDatabase.destroyConnection(); + } + +} \ No newline at end of file diff --git a/src/data/BandDatabase.ts b/src/data/BandDatabase.ts new file mode 100644 index 0000000..51a8381 --- /dev/null +++ b/src/data/BandDatabase.ts @@ -0,0 +1,37 @@ +import { Band } from "../model/Band"; +import { BaseDatabase } from "./BaseDatabase"; + +export class BandDatabase extends BaseDatabase { + + private static TABLE_NAME = "LAMA_BANDAS"; + + public async createBand( + id: string, + name: string, + music_genre: string, + responsible: string, + ): Promise { + try { + await this.getConnection() + .insert({ + id, + name, + music_genre, + responsible + }) + .into(BandDatabase.TABLE_NAME); + } catch (error: any) { + throw new Error(error.sqlMessage || error.message); + } + } +} + +// public async getBandByName(name: string): Promise { +// const result = await this.getConnection() +// .select("*") +// .from(UserDatabase.TABLE_NAME) +// .where({ name }); + +// return Band.toUserBand(result[0]); +// } + diff --git a/src/data/BaseDatabase.ts b/src/data/BaseDatabase.ts new file mode 100644 index 0000000..927ebd7 --- /dev/null +++ b/src/data/BaseDatabase.ts @@ -0,0 +1,32 @@ +import knex from "knex"; +import Knex from "knex"; + + +export abstract class BaseDatabase { + + private static connection: Knex | null = null; + + protected getConnection(): Knex{ + if(!BaseDatabase.connection){ + BaseDatabase.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_NAME, + }, + }); + } + + return BaseDatabase.connection; + } + + public static async destroyConnection(): Promise{ + if(BaseDatabase.connection){ + await BaseDatabase.connection.destroy(); + BaseDatabase.connection = null; + } + } +} \ No newline at end of file diff --git a/src/data/Joy-LAMA10-v2.4-beta.3.zip b/src/data/Joy-LAMA10-v2.4-beta.3.zip new file mode 100644 index 0000000..56e59c9 Binary files /dev/null and b/src/data/Joy-LAMA10-v2.4-beta.3.zip differ diff --git a/src/data/UserDatabase.ts b/src/data/UserDatabase.ts new file mode 100644 index 0000000..80081c1 --- /dev/null +++ b/src/data/UserDatabase.ts @@ -0,0 +1,42 @@ +import { BaseDatabase } from "./BaseDatabase"; +import { User } from "../model/User"; + +export class UserDatabase extends BaseDatabase { + createBand(id: string, name: string, music_genre: string, responsible: string) { + throw new Error("Method not implemented."); + } + + private static TABLE_NAME = "LAMA_USUARIOS"; + + public async createUser( + id: string, + email: string, + name: string, + password: string, + role: string + ): Promise { + try { + await this.getConnection() + .insert({ + id, + email, + name, + password, + role + }) + .into(UserDatabase.TABLE_NAME); + } catch (error: any) { + throw new Error(error.sqlMessage || error.message); + } + } + + public async getUserByEmail(email: string): Promise { + const result = await this.getConnection() + .select("*") + .from(UserDatabase.TABLE_NAME) + .where({ email }); + + return User.toUserModel(result[0]); + } + +} diff --git a/src/error/BaseError.ts b/src/error/BaseError.ts new file mode 100644 index 0000000..8b2f915 --- /dev/null +++ b/src/error/BaseError.ts @@ -0,0 +1,25 @@ +export class BaseError extends Error { + constructor(public code: number, message: string) { + super(message); + } +} +export class InvalidEmail extends BaseError { + constructor() { + super(400, "Email Inválido"); + } +} +export class InvalidName extends BaseError { + constructor() { + super(400, "Nome inválido"); + } +} +export class InvalidPassword extends BaseError { + constructor() { + super(400, "Senha inválida"); + } +} +export class UserNotFound extends BaseError { + constructor() { + super(404, "Usuário não encontrado"); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3d82c68 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,19 @@ +import dotenv from "dotenv"; +import {AddressInfo} from "net"; +import express from "express"; +import { userRouter } from "./routes/userRouter"; +dotenv.config(); +const app = express(); + +app.use(express.json()); + +app.use("/user", userRouter); + +const server = app.listen(3000, () => { + if (server) { + const address = server.address() as AddressInfo; + console.log(`Servidor rodando em http://localhost:${address.port}`); + } else { + console.error(`Falha ao rodar o servidor.`); + } + }); \ No newline at end of file diff --git a/src/model/Band.ts b/src/model/Band.ts new file mode 100644 index 0000000..addb282 --- /dev/null +++ b/src/model/Band.ts @@ -0,0 +1,60 @@ +// export class Band{ +// static toUserBand(arg0: any): Band | PromiseLike { +// throw new Error("Method not implemented."); +// } +// constructor( +// private id: string, +// private name: string, +// private music_genre: string, +// private responsible: string, + +// ){} + +// getId(){ +// return this.id; +// } + +// getName(){ +// return this.name +// } + +// getMusicGenre(){ +// return this.music_genre; +// } + +// getResponsible(){ +// return this.responsible +// } + +// setId(id: string){ +// this.id = id; +// } + +// setName(name: string){ +// this.name = name; +// } + +// } +export interface BandInputDTO{ + id:string; + name: string; + music_genre: string; + responsible: string; +} + +// static toBandModel(Band: any): Band { +// return new Band(band.id, band.name, band.music_genre, band.responsible); +// } + +export type Band = { + id: string, + name: string, + music_genre: string, + responsible: string +} + +export interface GetBandInputDTO { + id: string, + name: string, + +} \ No newline at end of file diff --git a/src/model/User.ts b/src/model/User.ts new file mode 100644 index 0000000..9143a6a --- /dev/null +++ b/src/model/User.ts @@ -0,0 +1,83 @@ +export class User{ + constructor( + private id: string, + private name: string, + private email: string, + private password: string, + private role: UserRole + ){} + + getId(){ + return this.id; + } + + getName(){ + return this.name + } + + getEmail(){ + return this.email; + } + + getPassword(){ + return this.password; + } + + getRole(){ + return this.role; + } + + setId(id: string){ + this.id = id; + } + + setName(name: string){ + this.name = name; + } + + setEmail(email: string){ + this.email = email; + } + + setPassword(password: string){ + this.password = password; + } + + setRole(role: UserRole){ + this.role = role; + } + + static stringToUserRole(input: string): UserRole{ + switch (input) { + case "NORMAL": + return UserRole.NORMAL; + case "ADMIN": + return UserRole.ADMIN; + default: + throw new Error("Invalid user role"); + } + } + + static toUserModel(user: any): User { + return new User(user.id, user.name, user.email, user.password, User.stringToUserRole(user.role)); + } + + +} + +export interface UserInputDTO{ + email: string; + password: string; + name: string; + role: string; +} + +export interface LoginInputDTO{ + email: string; + password: string; +} + +export enum UserRole{ + NORMAL = "NORMAL", + ADMIN = "ADMIN" +} \ No newline at end of file diff --git a/src/routes/userRouter.ts b/src/routes/userRouter.ts new file mode 100644 index 0000000..1b56d3a --- /dev/null +++ b/src/routes/userRouter.ts @@ -0,0 +1,10 @@ +import express from "express"; +import { UserController } from "../controller/UserController"; + + +export const userRouter = express.Router(); + +const userController = new UserController(); + +userRouter.post("/signup", userController.signup); +userRouter.post("/login", userController.login); \ No newline at end of file diff --git a/src/services/Authenticator.ts b/src/services/Authenticator.ts new file mode 100644 index 0000000..d21c80d --- /dev/null +++ b/src/services/Authenticator.ts @@ -0,0 +1,32 @@ +import * as jwt from "jsonwebtoken"; + +export class Authenticator { + public generateToken(input: AuthenticationData, + expiresIn: string = process.env.ACCESS_TOKEN_EXPIRES_IN!): string { + const token = jwt.sign( + { + id: input.id, + role: input.role + }, + process.env.JWT_KEY as string, + { + expiresIn, + } + ); + return token; + } + + public getData(token: string): AuthenticationData { + const payload = jwt.verify(token, process.env.JWT_KEY as string) as any; + const result = { + id: payload.id, + role: payload.role + }; + return result; + } +} + +interface AuthenticationData { + id: string; + role?: string; +} \ No newline at end of file diff --git a/src/services/HashManager.ts b/src/services/HashManager.ts new file mode 100644 index 0000000..45704fb --- /dev/null +++ b/src/services/HashManager.ts @@ -0,0 +1,17 @@ +import * as bcrypt from "bcryptjs"; + + +export class HashManager { + + public async hash(text: string): Promise { + const rounds = 12; + const salt = await bcrypt.genSalt(rounds); + const result = await bcrypt.hash(text, salt); + return result; + } + + public async compare(text: string, hash: string): Promise{ + return await bcrypt.compare(text, hash); + } + +} \ No newline at end of file diff --git a/src/services/IdGenerator.ts b/src/services/IdGenerator.ts new file mode 100644 index 0000000..8dc2b8a --- /dev/null +++ b/src/services/IdGenerator.ts @@ -0,0 +1,8 @@ +import { v4 } from "uuid"; + +export class IdGenerator{ + + generate(): string{ + return v4(); + } +} \ No newline at end of file diff --git a/tables.sql b/tables.sql new file mode 100644 index 0000000..48f79f7 --- /dev/null +++ b/tables.sql @@ -0,0 +1,30 @@ +-- Active: 1653957237277@@35.226.146.116@3306@joy-420035-vinicius-toigo +CREATE TABLE IF NOT EXISTS LAMA_USUARIOS ( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + role VARCHAR(255) NOT NULL DEFAULT "NORMAL" +); + +SELECT * FROM `LAMA_USUARIOS`; + +CREATE TABLE IF NOT EXISTS LAMA_SHOWS ( + id VARCHAR(255) PRIMARY KEY, + week_day VARCHAR(255) NOT NULL, + start_time INT NOT NULL, + end_time INT NOT NULL, + band_id VARCHAR(255) NOT NULL, + FOREIGN KEY(band_id) REFERENCES LAMA_BANDAS(id) +); + +SELECT * FROM `LAMA_SHOWS`; + +CREATE TABLE IF NOT EXISTS LAMA_BANDAS ( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL, + music_genre VARCHAR(255) NOT NULL, + responsible VARCHAR(255) UNIQUE NOT NULL +); + +SELECT * FROM `LAMA_BANDAS`; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..54d53fd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,66 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* 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": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* 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 */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +}