diff --git a/.env b/.env new file mode 100644 index 00000000..5daf1e51 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +PORT=3003 + +DB_FILE_PATH=./src/database/nome-do-arquivo.db + +JWT_KEY=senha-de-exemplo-jwt-key +JWT_EXPIRES_IN=7d + +BCRYPT_COST=12 \ No newline at end of file diff --git a/README.md b/README.md index 266434ba..22f8e2c1 100644 --- a/README.md +++ b/README.md @@ -1,207 +1,100 @@ # Projeto Labook + +## Índice: + +- Objetivo +- Documentação da API +- Estruturação do banco de dados +- Requisitos: +- Como rodar este projeto? +- Técnologias utilizadas +- Autoria +- Próximos Passos + +## Objetivo + O Labook é uma rede social com o objetivo de promover a conexão e interação entre pessoas. Quem se cadastrar no aplicativo poderá criar e curtir publicações. -Agora que temos as bases de criação de APIs e banco de dados, o próximo nível é a implementação de segurança e códigos mais escaláveis. Veremos durante o prazo de entrega desse projeto inúmeros conceitos e formas de desenvolvimento seguindo padrões de design e arquitetura, e seu desafio será unir as funcionalidades com as boas práticas de código. - -# Conteúdos abordados -- NodeJS -- Typescript -- Express -- SQL e SQLite -- Knex -- POO -- Arquitetura em camadas -- Geração de UUID -- Geração de hashes -- Autenticação e autorização -- Roteamento -- Postman +## Documentação da API + +[Link Demonstração](https://documenter.getpostman.com/view/25825355/2s93ebUBDa) + +## Estruturação do banco de dados -# Banco de dados ![projeto-labook (2)](https://user-images.githubusercontent.com/29845719/216036534-2b3dfb48-7782-411a-bffd-36245b78594e.png) -https://dbdiagram.io/d/63d16443296d97641d7c1ae1 +## Requisitos: -# Lista de requisitos - Documentação Postman de todos os endpoints (obrigatória para correção) - Endpoints - - [ ] signup - - [ ] login - - [ ] get posts - - [ ] create post - - [ ] edit post - - [ ] delete post - - [ ] like / dislike post + + - [x] signup + - [x] login + - [x] get posts + - [x] create post + - [x] edit post + - [x] delete post + - [] like / dislike post - Autenticação e autorização - - [ ] identificação UUID - - [ ] senhas hasheadas com Bcrypt - - [ ] tokens JWT - - - Código - - [ ] POO - - [ ] Arquitetura em camadas - - [ ] Roteadores no Express + + - [x] identificação UUID + - [x] senhas hasheadas com Bcrypt + - [x] tokens JWT + +- Código + + - [x] POO + - [x] Arquitetura em camadas + - [x] Roteadores no Express - README.md -# Exemplos de requisição - -## Signup -Endpoint público utilizado para cadastro. Devolve um token jwt. -```typescript -// request POST /users/signup -// body JSON -{ - "name": "Beltrana", - "email": "beltrana@email.com", - "password": "beltrana00" -} - -// response -// status 201 CREATED -{ - token: "um token jwt" -} -``` +## Como rodar este projeto? -## Login -Endpoint público utilizado para login. Devolve um token jwt. -```typescript -// request POST /users/login -// body JSON -{ - "email": "beltrana@email.com", - "password": "beltrana00" -} - -// response -// status 200 OK -{ - token: "um token jwt" -} -``` +```bash +#Clone este repositório +$ git clone lin krepo -## Get posts -Endpoint protegido, requer um token jwt para acessá-lo. -```typescript -// request GET /posts -// headers.authorization = "token jwt" - -// response -// status 200 OK -[ - { - "id": "uma uuid v4", - "content": "Hoje vou estudar POO!", - "likes": 2, - "dislikes" 1, - "createdAt": "2023-01-20T12:11:47:000Z" - "updatedAt": "2023-01-20T12:11:47:000Z" - "creator": { - "id": "uma uuid v4", - "name": "Fulano" - } - }, - { - "id": "uma uuid v4", - "content": "kkkkkkkkkrying", - "likes": 0, - "dislikes" 0, - "createdAt": "2023-01-20T15:41:12:000Z" - "updatedAt": "2023-01-20T15:49:55:000Z" - "creator": { - "id": "uma uuid v4", - "name": "Ciclana" - } - } -] -``` +#Acesse a pasta do projeto no seu terminal +$ cd nomeDaPasta -## Create post -Endpoint protegido, requer um token jwt para acessá-lo. -```typescript -// request POST /posts -// headers.authorization = "token jwt" -// body JSON -{ - "content": "Partiu happy hour!" -} - -// response -// status 201 CREATED -``` +# Instale as dependencias +$ npm install + +# Execute a aplicação +$ npm run dev + +# A aplicação será iniciada na porta 3004, acesse pelo navegador: http://localhost:3003 -## Edit post -Endpoint protegido, requer um token jwt para acessá-lo.
-Só quem criou o post pode editá-lo e somente o conteúdo pode ser editado. -```typescript -// request PUT /posts/:id -// headers.authorization = "token jwt" -// body JSON -{ - "content": "Partiu happy hour lá no point de sempre!" -} - -// response -// status 200 OK ``` -## Delete post -Endpoint protegido, requer um token jwt para acessá-lo.
-Só quem criou o post pode deletá-lo. Admins podem deletar o post de qualquer pessoa. +## Técnologias utilizadas -```typescript -// request DELETE /posts/:id -// headers.authorization = "token jwt" +1. [Node.js](https://nodejs.org/en) +2. [TypeScript](https://www.typescriptlang.org/) +3. [Express](https://expressjs.com/) +4. [SQLite3 / SQL](https://sqlite.org/index.html) +5. [Knex](https://knexjs.org/) +6. [POO](https://pt.wikipedia.org/wiki/Programa%C3%A7%C3%A3o_orientada_a_objetos) +7. [Arquiterura em camadas](https://pt.wikipedia.org/wiki/Arquitetura_multicamada) +8. [Geração de UUID](https://pt.wikipedia.org/wiki/Identificador_%C3%BAnico_universal) +9. [Geração de hashes](https://pt.wikipedia.org/wiki/Fun%C3%A7%C3%A3o_hash_criptogr%C3%A1fica) +10. [Autenticação e autorização](https://pt.wikipedia.org/wiki/Autoriza%C3%A7%C3%A3o) +11. [Roteamento](https://acervolima.com/roteamento-em-node-js/) +12. [Postman](https://www.postman.com/) -// response -// status 200 OK -``` +## Autoria -## Like or dislike post (mesmo endpoint faz as duas coisas) - -Endpoint protegido, requer um token jwt para acessá-lo.
-Quem criou o post não pode dar like ou dislike no mesmo.

-Caso dê um like em um post que já tenha dado like, o like é desfeito.
-Caso dê um dislike em um post que já tenha dado dislike, o dislike é desfeito.

-Caso dê um like em um post que tenha dado dislike, o like sobrescreve o dislike.
-Caso dê um dislike em um post que tenha dado like, o dislike sobrescreve o like. -### Like (funcionalidade 1) -```typescript -// request PUT /posts/:id/like -// headers.authorization = "token jwt" -// body JSON -{ - "like": true -} - -// response -// status 200 OK -``` +Michelle Antunes, abril/2023. +
-### Dislike (funcionalidade 2) -```typescript -// request PUT /posts/:id/like -// headers.authorization = "token jwt" -// body JSON -{ - "like": false -} - -// response -// status 200 OK -``` +Linkedin: www.linkedin.com/in/michelle-antunes-868b24156 +
+Email: miichelleantunes@outlook.com + +## Próximos Passos -### Para entender a tabela likes_dislikes -- no SQLite, lógicas booleanas devem ser controladas via 0 e 1 (INTEGER) -- quando like valer 1 na tabela é porque a pessoa deu like no post - - na requisição like é true - -- quando like valer 0 na tabela é porque a pessoa deu dislike no post - - na requisição like é false - -- caso não exista um registro na tabela de relação, é porque a pessoa não deu like nem dislike -- caso dê like em um post que já tenha dado like, o like é removido (deleta o item da tabela) -- caso dê dislike em um post que já tenha dado dislike, o dislike é removido (deleta o item da tabela) +- [ ] Deploy +- [ ] Testes unitários diff --git a/package-lock.json b/package-lock.json index 1c170026..e2c2798e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,24 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcryptjs": "^2.4.3", "cors": "^2.8.5", + "dotenv": "^16.0.3", "express": "^4.18.2", + "jsonwebtoken": "^9.0.0", "knex": "^2.4.2", - "sqlite3": "^5.1.4" + "sqlite3": "^5.1.4", + "uuid": "^9.0.0", + "zod": "^3.21.4" }, "devDependencies": { + "@types/bcryptjs": "^2.4.2", "@types/cors": "^2.8.13", "@types/express": "^4.17.15", + "@types/jsonwebtoken": "^9.0.2", "@types/knex": "^0.16.1", "@types/node": "^18.11.18", + "@types/uuid": "^9.0.1", "ts-node-dev": "^2.0.0", "typescript": "^4.9.4" } @@ -142,6 +150,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -193,6 +207,15 @@ "@types/range-parser": "*" } }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/knex": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/@types/knex/-/knex-0.16.1.tgz", @@ -249,6 +272,12 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "node_modules/@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -432,6 +461,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -485,6 +519,11 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -712,6 +751,14 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -721,6 +768,14 @@ "xtend": "^4.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1264,6 +1319,45 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/knex": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.2.tgz", @@ -2491,6 +2585,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -2569,6 +2671,14 @@ "engines": { "node": ">=6" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -2675,6 +2785,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -2726,6 +2842,15 @@ "@types/range-parser": "*" } }, + "@types/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/knex": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/@types/knex/-/knex-0.16.1.tgz", @@ -2781,6 +2906,12 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2919,6 +3050,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2962,6 +3098,11 @@ "fill-range": "^7.0.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3132,6 +3273,11 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + }, "dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -3141,6 +3287,14 @@ "xtend": "^4.0.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3569,6 +3723,43 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "knex": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.2.tgz", @@ -4431,6 +4622,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -4494,6 +4690,11 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true + }, + "zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } } diff --git a/package.json b/package.json index 679b688e..7478a560 100644 --- a/package.json +++ b/package.json @@ -12,17 +12,25 @@ "author": "", "license": "ISC", "devDependencies": { + "@types/bcryptjs": "^2.4.2", "@types/cors": "^2.8.13", "@types/express": "^4.17.15", + "@types/jsonwebtoken": "^9.0.2", "@types/knex": "^0.16.1", "@types/node": "^18.11.18", + "@types/uuid": "^9.0.1", "ts-node-dev": "^2.0.0", "typescript": "^4.9.4" }, "dependencies": { + "bcryptjs": "^2.4.3", "cors": "^2.8.5", + "dotenv": "^16.0.3", "express": "^4.18.2", + "jsonwebtoken": "^9.0.0", "knex": "^2.4.2", - "sqlite3": "^5.1.4" + "sqlite3": "^5.1.4", + "uuid": "^9.0.0", + "zod": "^3.21.4" } } diff --git a/src/business/PostBusiness.ts b/src/business/PostBusiness.ts new file mode 100644 index 00000000..565242bd --- /dev/null +++ b/src/business/PostBusiness.ts @@ -0,0 +1,169 @@ +import { PostDatabase } from "../database/PostDatabase"; +import { + CreatePostInputDTO, + CreatePostOutputDTO, +} from "../dtos/Post/createPost.dto"; +import { + DeletePostInputDTO, + DeletePostOutputDTO, +} from "../dtos/Post/delete.dto"; +import { EditPostInputDTO, EditPostOutputDTO } from "../dtos/Post/editPost.dto"; +import { GetPostsInputDTO, GetPostsOutputDTO } from "../dtos/Post/getPosts.dto"; + +import { + LikeOrDislikePostInputDTO, + LikeOrDislikePostOutputDTO, +} from "../dtos/Post/likeOrdeslikePost.dto"; +import { NotFoundError } from "../errors/NotFoundError"; + +import { LikeDislikeDB, Post, PostDB, POST_LIKE } from "../models/Posts"; +import { USER_ROLES } from "../models/User"; +import { TokenManager } from "../services/TokenManager"; + +import { IdGenerator } from "../services/IdGenerator"; +import { BadRequestError } from "../errors/BadRequestError"; + +export class PostBusiness { + constructor( + private postDatabase: PostDatabase, + private idGenerator: IdGenerator, + private tokenManeger: TokenManager + ) {} + + public getPost = async ( + input: GetPostsInputDTO + ): Promise => { + const { token } = input; + + const payload = this.tokenManeger.getPayload(token); + + if (!payload) { + throw new BadRequestError("Token inválido."); + } + + const postsWithCreatorName = + await this.postDatabase.findPostsWithCreatorName(); + + const posts = postsWithCreatorName.map((postWithCreatorName) => { + const post = new Post( + postWithCreatorName.id, + postWithCreatorName.content, + postWithCreatorName.likes, + postWithCreatorName.dislikes, + postWithCreatorName.created_at, + postWithCreatorName.updated_at, + postWithCreatorName.creator_id, + postWithCreatorName.creator_name + ); + return post.toBusinessModel(); + }); + + const output: GetPostsOutputDTO = posts; + + return output; + }; + + public postPost = async ( + input: CreatePostInputDTO + ): Promise => { + const { token, content } = input; + + const payload = this.tokenManeger.getPayload(token); + + if (!payload) { + throw new BadRequestError("Token inválido"); + } + + const id = this.idGenerator.generate(); + + const newPost = new Post( + id, + content, + 0, + 0, + new Date().toString(), + new Date().toString(), + payload.id, + payload.name + ); + + const newPostDB = newPost.toDBModel(); + await this.postDatabase.createPost(newPostDB); + + const output: CreatePostOutputDTO = undefined; + + return output; + }; + + public putPost = async ( + input: EditPostInputDTO + ): Promise => { + const { token, idToEdit, content } = input; + + const payload = this.tokenManeger.getPayload(token); + + if (!payload) { + throw new BadRequestError("Token inválido"); + } + + const postDBExists = await this.postDatabase.findPostById(idToEdit); + + if (!postDBExists) { + throw new NotFoundError("Post id not found"); + } + + if (postDBExists.creator_id !== payload.id) { + throw new BadRequestError("Only the creator of the post can edit it"); + } + + const post = new Post( + postDBExists.id, + postDBExists.content, + postDBExists.likes, + postDBExists.dislikes, + postDBExists.created_at, + postDBExists.updated_at, + postDBExists.creator_id, + payload.name + ); + + post.setContent(content); + + const updatedPostDB = post.toDBModel(); + await this.postDatabase.editPost(updatedPostDB); + + const output: EditPostOutputDTO = undefined; + + return output; + }; + + public deletePost = async ( + input: DeletePostInputDTO + ): Promise => { + const { token, idToDelete } = input; + + const payload = this.tokenManeger.getPayload(token); + + if (!payload) { + throw new BadRequestError("Token inválido"); + } + + const postDBExists = await this.postDatabase.findPostById(idToDelete); + + if (!postDBExists) { + throw new NotFoundError("Post-Is não existe"); + } + + if (payload.role !== USER_ROLES.ADMIN) { + if (payload.id !== postDBExists.creator_id) { + throw new BadRequestError("Somente quem criou o post pode deletá-lo"); + } + } + + await this.postDatabase.removePost(idToDelete); + + const output: DeletePostOutputDTO = undefined; + + return output; + }; +} diff --git a/src/business/UserBusiness.ts b/src/business/UserBusiness.ts new file mode 100644 index 00000000..b925904b --- /dev/null +++ b/src/business/UserBusiness.ts @@ -0,0 +1,103 @@ +import { UserDatabase } from "../database/UserDatabase"; +import { LoginInputDTO, LoginOutputDTO } from "../dtos/User/login.dto"; +import { SignupInputDTO, SignupOutputDTO } from "../dtos/User/signup.dto"; +import { BadRequestError } from "../errors/BadRequestError"; + +import { NotFoundError } from "../errors/NotFoundError"; +import { TokenPayload, User, USER_ROLES } from "../models/User"; + +import { HashManager } from "../services/HashManeger"; +import { IdGenerator } from "../services/IdGenerator"; +import { TokenManager } from "../services/TokenManager"; + +export class UserBusiness { + constructor( + private userDatabase: UserDatabase, + private idGenerator: IdGenerator, + private tokenManeger: TokenManager, + private hashManeger: HashManager + ) {} + + public signup = async (input: SignupInputDTO) => { + const { name, email, password } = input; + + const userBDExists = await this.userDatabase.findUserByEmail(email); + + if (userBDExists) { + throw new BadRequestError("Esse e-mail já foi cadastrado"); + } + + const id = this.idGenerator.generate(); + const hashedPassword = await this.hashManeger.hash(password); + + const newUser = new User( + id, + name, + email, + hashedPassword, + USER_ROLES.NORMAL, + new Date().toISOString() + ); + + const newUserDB = newUser.toDBModel(); + await this.userDatabase.postUser(newUserDB); + + const payload: TokenPayload = { + id: newUser.getId(), + name: newUser.getName(), + role: newUser.getRole(), + }; + + const token = this.tokenManeger.createToken(payload); + + const output: SignupOutputDTO = { + token, + }; + + return output; + }; + + public userLogin = async (input: LoginInputDTO) => { + const { email, password } = input; + + const userBDExists = await this.userDatabase.findUserByEmail(email); + + if (!userBDExists) { + throw new NotFoundError("Email não encontrado"); + } + + const user = new User( + userBDExists.id, + userBDExists.name, + userBDExists.email, + userBDExists.password, + userBDExists.role, + userBDExists.created_at + ); + + const hashedPassword = userBDExists.password; + + const isCorrectPassword = await this.hashManeger.compare( + password, + hashedPassword + ); + + if (!isCorrectPassword) { + throw new BadRequestError("Email ou senha incorretos"); + } + + const payload: TokenPayload = { + id: user.getId(), + name: user.getName(), + role: user.getRole(), + }; + + const token = this.tokenManeger.createToken(payload); + + const output: LoginOutputDTO = { + token, + }; + + return output; + }; +} diff --git a/src/controller/PostController.ts b/src/controller/PostController.ts new file mode 100644 index 00000000..b68a6faa --- /dev/null +++ b/src/controller/PostController.ts @@ -0,0 +1,104 @@ +import { Request, Response } from "express"; +import { ZodError } from "zod"; +import { PostBusiness } from "../business/PostBusiness"; +import { CreatePostSchema } from "../dtos/Post/createPost.dto"; +import { DeletePostSchema } from "../dtos/Post/delete.dto"; +import { EditPostSchema } from "../dtos/Post/editPost.dto"; +import { GetPostsSchema } from "../dtos/Post/getPosts.dto"; + +import { BaseError } from "../errors/BaseError"; + +export class PostControlers { + constructor(private postBusiness: PostBusiness) {} + + public getPosts = async (req: Request, res: Response) => { + try { + const input = GetPostsSchema.parse({ + token: req.headers.authorization, + }); + const output = await this.postBusiness.getPost(input); + + res.status(200).send(output); + } catch (error) { + console.log(error); + + if (error instanceof ZodError) { + res.status(400).send(error.issues); + } else if (error instanceof BaseError) { + res.status(error.statusCode).send(error.message); + } else { + res.status(500).send("Erro inesperado"); + } + } + }; + + public postPost = async (req: Request, res: Response) => { + try { + const input = CreatePostSchema.parse({ + token: req.headers.authorization, + content: req.body.content, + }); + + const output = await this.postBusiness.postPost(input); + + res.status(201).send(output); + } catch (error) { + console.log(error); + + if (error instanceof ZodError) { + res.status(400).send(error.issues); + } else if (error instanceof BaseError) { + res.status(error.statusCode).send(error.message); + } else { + res.status(500).send("Erro inesperado"); + } + } + }; + + public putPost = async (req: Request, res: Response) => { + try { + const input = EditPostSchema.parse({ + token: req.headers.authorization, + idToEdit: req.params.id, + content: req.body.content, + }); + + const output = await this.postBusiness.putPost(input); + + res.status(200).send(output); + } catch (error) { + console.log(error); + + if (error instanceof ZodError) { + res.status(400).send(error.issues); + } else if (error instanceof BaseError) { + res.status(error.statusCode).send(error.message); + } else { + res.status(500).send("Erro inesperado"); + } + } + }; + + public deletePosts = async (req: Request, res: Response) => { + try { + const input = DeletePostSchema.parse({ + token: req.headers.authorization, + idToDelete: req.params.id, + }); + + const output = await this.postBusiness.deletePost(input); + + res.status(200).send(output); + } catch (error) { + console.log(error); + + if (error instanceof ZodError) { + res.status(400).send(error.issues); + } else if (error instanceof BaseError) { + res.status(error.statusCode).send(error.message); + } else { + res.status(500).send("Erro inesperado"); + } + } + }; +} diff --git a/src/controller/UserController.ts b/src/controller/UserController.ts new file mode 100644 index 00000000..5ea87088 --- /dev/null +++ b/src/controller/UserController.ts @@ -0,0 +1,57 @@ +import { Request, Response } from "express"; +import { ZodError } from "zod"; +import { UserBusiness } from "../business/UserBusiness"; +import { LoginSchema } from "../dtos/User/login.dto"; +import { SignupSchema } from "../dtos/User/signup.dto"; +import { BaseError } from "../errors/BaseError"; + +export class UserController { + constructor(private userBusiness: UserBusiness) {} + + public signup = async (req: Request, res: Response) => { + try { + const input = SignupSchema.parse({ + name: req.body.name, + email: req.body.email, + password: req.body.password, + }); + + const output = await this.userBusiness.signup(input); + + res.status(201).send(output); + } catch (error) { + console.log(error); + + if (error instanceof ZodError) { + res.status(400).send(error.issues); + } else if (error instanceof BaseError) { + res.status(error.statusCode).send(error.message); + } else { + res.status(500).send("Erro inesperado"); + } + } + }; + + public login = async (req: Request, res: Response) => { + try { + const input = LoginSchema.parse({ + email: req.body.email, + password: req.body.password, + }); + + const output = await this.userBusiness.userLogin(input); + + res.status(200).send(output); + } catch (error) { + console.log(error); + + if (error instanceof ZodError) { + res.status(400).send(error.issues); + } else if (error instanceof BaseError) { + res.status(error.statusCode).send(error.message); + } else { + res.status(500).send("Erro inesperado"); + } + } + }; +} diff --git a/src/database/BaseDatabase.ts b/src/database/BaseDatabase.ts index f8cc53d5..9b443942 100644 --- a/src/database/BaseDatabase.ts +++ b/src/database/BaseDatabase.ts @@ -1,10 +1,13 @@ import { knex } from "knex"; +import dotenv from "dotenv"; + +dotenv.config(); export abstract class BaseDatabase { protected static connection = knex({ client: "sqlite3", connection: { - filename: "./src/database/projetoBack_End.db", + filename: process.env.DB_FILE_PATH as string, }, useNullAsDefault: true, pool: { diff --git a/src/database/PostDatabase.ts b/src/database/PostDatabase.ts new file mode 100644 index 00000000..a83f467d --- /dev/null +++ b/src/database/PostDatabase.ts @@ -0,0 +1,86 @@ +import { + LikeDislikeDB, + PostDB, + PostDBWithCreatorName, + POST_LIKE, +} from "../models/Posts"; +import { BaseDatabase } from "./BaseDatabase"; +import { UserDatabase } from "./UserDatabase"; + +export class PostDatabase extends BaseDatabase { + public static TABLE_POSTS = "posts"; + public static TABLE_LIKES_DISLIKES = "likes_dislikes"; + + public findPostsWithCreatorName = async (): Promise< + PostDBWithCreatorName[] + > => { + const result: PostDB[] = await BaseDatabase.connection( + PostDatabase.TABLE_POSTS + ) + .select( + `${PostDatabase.TABLE_POSTS}.id`, + `${PostDatabase.TABLE_POSTS}.creator_id`, + `${PostDatabase.TABLE_POSTS}.content`, + `${PostDatabase.TABLE_POSTS}.likes`, + `${PostDatabase.TABLE_POSTS}.dislikes`, + `${PostDatabase.TABLE_POSTS}.created_at`, + `${PostDatabase.TABLE_POSTS}.updated_at`, + `${UserDatabase.TABLE_USERS}.name as creator_name` + ) + .join( + `${UserDatabase.TABLE_USERS}`, + `${PostDatabase.TABLE_POSTS}.creator_id`, + "=", + `${UserDatabase.TABLE_USERS}.id` + ); + return result as PostDBWithCreatorName[]; + }; + + public createPost = async (newPost: PostDB): Promise => { + await BaseDatabase.connection(PostDatabase.TABLE_POSTS).insert(newPost); + }; + + public findPostById = async (id: string): Promise => { + const [postDB]: PostDB[] | undefined[] = await BaseDatabase.connection( + PostDatabase.TABLE_POSTS + ).where({ id }); + + return postDB; + }; + + public editPost = async (newPost: PostDB): Promise => { + await BaseDatabase.connection(PostDatabase.TABLE_POSTS) + .update(newPost) + .where({ id: newPost.id }); + }; + + public removePost = async (id: string): Promise => { + await BaseDatabase.connection(PostDatabase.TABLE_POSTS).del().where({ id }); + }; + + public findPostsWithCreatorNameById = async ( + id: string + ): Promise => { + const [result] = await BaseDatabase.connection(PostDatabase.TABLE_POSTS) + .select( + `${PostDatabase.TABLE_POSTS}.id`, + `${PostDatabase.TABLE_POSTS}.creator_id`, + `${PostDatabase.TABLE_POSTS}.content`, + `${PostDatabase.TABLE_POSTS}.likes`, + `${PostDatabase.TABLE_POSTS}.dislikes`, + `${PostDatabase.TABLE_POSTS}.created_at`, + `${PostDatabase.TABLE_POSTS}.updated_at`, + `${UserDatabase.TABLE_USERS}.name as creator_name` + ) + .join( + `${UserDatabase.TABLE_USERS}`, + `${PostDatabase.TABLE_POSTS}.creator_id`, + "=", + `${UserDatabase.TABLE_USERS}.id` + ) + .where({ + [`${PostDatabase.TABLE_POSTS}.id`]: id, + }); + return result as PostDBWithCreatorName | undefined; + }; +} diff --git a/src/database/UserDatabase.ts b/src/database/UserDatabase.ts new file mode 100644 index 00000000..13791fd7 --- /dev/null +++ b/src/database/UserDatabase.ts @@ -0,0 +1,17 @@ +import { UserDB } from "../models/User"; +import { BaseDatabase } from "./BaseDatabase"; + +export class UserDatabase extends BaseDatabase { + public static TABLE_USERS = "users"; + + public async postUser(newUser: UserDB): Promise { + await BaseDatabase.connection(UserDatabase.TABLE_USERS).insert(newUser); + } + + public async findUserByEmail(email: string): Promise { + const [userDB]: UserDB[] | undefined[] = await BaseDatabase.connection( + UserDatabase.TABLE_USERS + ).where({ email }); + return userDB; + } +} diff --git a/src/database/projetoBack_End.sql b/src/database/projetoBack_End.sql index b3df6a74..9a095d49 100644 --- a/src/database/projetoBack_End.sql +++ b/src/database/projetoBack_End.sql @@ -1,34 +1,59 @@ --- Active: 1682949688943@@127.0.0.1@3306 -CREATE TABLE users ( - id TEXT PRIMARY KEY UNIQUE NOT NULL, - name TEXT NOT NULL, - email TEXT NOT NULL, - password TEXT NOT NULL, - created_at TEXT DEFAULT (DATETIME()) NOT NULL -); +-- Active: 1683738400790@@127.0.0.1@3306 + +CREATE TABLE + users ( + id TEXT PRIMARY KEY UNIQUE NOT NULL, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + role TEXT NOT NULL, + created_at TEXT DEFAULT (DATETIME()) NOT NULL + ); CREATE TABLE posts ( id TEXT PRIMARY KEY UNIQUE NOT NULL, + creator_id TEXT NOT NULL, content TEXT NOT NULL, likes INTEGER DEFAULT(0) NOT NULL, dislikes INTEGER DEFAULT(0) NOT NULL, created_at TEXT DEFAULT (DATETIME()) NOT NULL, updated_at TEXT DEFAULT (DATETIME()) NOT NULL, - creator_id TEXT NOT NULL, - FOREIGN KEY (creator_id) REFERENCES users(id) - ); - CREATE TABLE - likes_dislikes ( - user_id TEXT NOT NULL, - post_id TEXT NOT NULL, - like INTEGER DEFAULT (0) NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id), - FOREIGN KEY (post_id) REFERENCES posts (id) + FOREIGN KEY (creator_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE ); - -SELECT * FROM users; + + +SELECT * FROM users; SELECT * FROM posts; -SELECT * FROM likes_dislikes; +DROP TABLE users; +DROP TABLE posts; + + +INSERT INTO + users (id, name, email, password, role) +VALUES + + -- tipo ADMIN e senha = astrodev99 + ( + 'u003', + 'Astrodev', + 'astrodev@email.com', + '$2a$12$lHyD.hKs3JDGu2nIbBrxYujrnfIX5RW5oq/B41HCKf7TSaq9RgqJ.', + 'ADMIN' + ); + +INSERT INTO + posts (id, creator_id, content) +VALUES ( + 'p001', + 'u003', + 'Olá' + ); + + +INSERT INTO + likes_dislikes (user_id, post_id, like) +VALUES ('u003', 'p001', 1), ('u003', 'p002', 0); + diff --git a/src/dtos/Post/createPost.dto.ts b/src/dtos/Post/createPost.dto.ts new file mode 100644 index 00000000..31d73ed0 --- /dev/null +++ b/src/dtos/Post/createPost.dto.ts @@ -0,0 +1,15 @@ +import z from "zod"; + +export interface CreatePostInputDTO { + token: string; + content: string; +} + +export type CreatePostOutputDTO = undefined; + +export const CreatePostSchema = z + .object({ + token: z.string().min(1), + content: z.string().min(1), + }) + .transform((data) => data as CreatePostInputDTO); diff --git a/src/dtos/Post/delete.dto.ts b/src/dtos/Post/delete.dto.ts new file mode 100644 index 00000000..df37d234 --- /dev/null +++ b/src/dtos/Post/delete.dto.ts @@ -0,0 +1,15 @@ +import z from "zod"; + +export interface DeletePostInputDTO { + idToDelete: string; + token: string; +} + +export type DeletePostOutputDTO = undefined; + +export const DeletePostSchema = z + .object({ + idToDelete: z.string().min(1), + token: z.string().min(1), + }) + .transform((data) => data as DeletePostInputDTO); diff --git a/src/dtos/Post/editPost.dto.ts b/src/dtos/Post/editPost.dto.ts new file mode 100644 index 00000000..4e5627f8 --- /dev/null +++ b/src/dtos/Post/editPost.dto.ts @@ -0,0 +1,17 @@ +import z from "zod"; + +export interface EditPostInputDTO { + idToEdit: string; + token: string; + content: string; +} + +export type EditPostOutputDTO = undefined; + +export const EditPostSchema = z + .object({ + idToEdit: z.string().min(1), + token: z.string().min(1), + content: z.string().min(1), + }) + .transform((data) => data as EditPostInputDTO); diff --git a/src/dtos/Post/getPosts.dto.ts b/src/dtos/Post/getPosts.dto.ts new file mode 100644 index 00000000..51b68ea0 --- /dev/null +++ b/src/dtos/Post/getPosts.dto.ts @@ -0,0 +1,14 @@ +import z from "zod"; +import { PostModel } from "../../models/Posts"; + +export interface GetPostsInputDTO { + token: string; +} + +export type GetPostsOutputDTO = PostModel[]; + +export const GetPostsSchema = z + .object({ + token: z.string().min(1), + }) + .transform((data) => data as GetPostsInputDTO); diff --git a/src/dtos/User/login.dto.ts b/src/dtos/User/login.dto.ts new file mode 100644 index 00000000..53c671e7 --- /dev/null +++ b/src/dtos/User/login.dto.ts @@ -0,0 +1,17 @@ +import z from "zod"; + +export interface LoginInputDTO { + email: string; + password: string; +} + +export interface LoginOutputDTO { + token: string; +} + +export const LoginSchema = z + .object({ + email: z.string().email(), + password: z.string().min(6), + }) + .transform((data) => data as LoginInputDTO); diff --git a/src/dtos/User/signup.dto.ts b/src/dtos/User/signup.dto.ts new file mode 100644 index 00000000..fa35c3aa --- /dev/null +++ b/src/dtos/User/signup.dto.ts @@ -0,0 +1,19 @@ +import z from "zod"; + +export interface SignupInputDTO { + name: string; + email: string; + password: string; +} + +export interface SignupOutputDTO { + token: string; +} + +export const SignupSchema = z + .object({ + name: z.string().min(2), + email: z.string().email(), + password: z.string().min(6), + }) + .transform((data) => data as SignupInputDTO); diff --git a/src/index.ts b/src/index.ts index b543ee71..572d0735 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,19 @@ import express from "express"; import cors from "cors"; +import dotenv from "dotenv"; +import { postRouter } from "./router/postRouter"; +import { userRouter } from "./router/userRouter"; -// import { userRouter } from "./router/userRouter"; -// import { accountRouter } from "./router/accountRouter"; +dotenv.config(); const app = express(); app.use(cors()); app.use(express.json()); -app.listen(3003, () => { - console.log(`Servidor rodando na porta ${3003}`); +app.listen(Number(process.env.PORT) || 3003, () => { + console.log(`server on port ${Number(process.env.PORT) || 3003}`); }); + +app.use("/posts", postRouter); +app.use("/users", userRouter); diff --git a/src/models/Posts.ts b/src/models/Posts.ts new file mode 100644 index 00000000..2b9122b6 --- /dev/null +++ b/src/models/Posts.ts @@ -0,0 +1,146 @@ +export interface PostDB { + id: string; + creator_id: string; + content: string; + likes: number; + dislikes: number; + created_at: string; + updated_at: string; +} + +export interface PostDBWithCreatorName { + id: string; + creator_id: string; + content: string; + likes: number; + dislikes: number; + created_at: string; + updated_at: string; + creator_name: string; +} + +export interface PostModel { + id: string; + content: string; + likes: number; + dislikes: number; + created_at: string; + updated_at: string; + creator: { + id: string; + name: string; + }; +} + +export interface LikeDislikeDB { + user_id: string; + post_id: string; + like: number; +} + +export enum POST_LIKE { + ALREADY_LIKED = "ALREADY_LIKED", + ALREADY_DISLIKED = "ALREADY_DISLIKED", +} + +export class Post { + constructor( + private id: string, + private content: string, + private likes: number, + private dislikes: number, + private createdAt: string, + private updatedAt: string, + private creatorId: string, + private creatorName: string + ) {} + + public getId(): string { + return this.id; + } + + public getContent(): string { + return this.content; + } + public setContent(value: string) { + this.content = value; + } + + public getLikes(): number { + return this.likes; + } + public setLikes(value: number) { + this.likes = value; + } + public addLike = (): void => { + this.likes++; + }; + public removeLike = (): void => { + this.likes--; + }; + + public getDislikes(): number { + return this.dislikes; + } + public setDislikes(value: number) { + this.dislikes = value; + } + public addDislike = (): void => { + this.dislikes++; + }; + public removeDislike = (): void => { + this.dislikes--; + }; + + public getCreatedAt(): string { + return this.createdAt; + } + + public getUdatedAt(): string { + return this.updatedAt; + } + public setUpdatedAt(value: string) { + this.updatedAt = value; + } + + public getCreatorId(): string { + return this.creatorId; + } + public setCreatorId(value: string) { + this.creatorId = value; + } + + public getCreatorName(): string { + return this.creatorName; + } + public setCreatorName(value: string) { + this.creatorName = value; + } + + public toDBModel(): PostDB { + return { + id: this.id, + creator_id: this.creatorId, + content: this.content, + likes: this.likes, + dislikes: this.dislikes, + created_at: this.createdAt, + updated_at: this.updatedAt, + }; + } + + public toBusinessModel(): PostModel { + return { + id: this.id, + content: this.content, + likes: this.likes, + dislikes: this.dislikes, + created_at: this.createdAt, + updated_at: this.updatedAt, + creator: { + id: this.creatorId, + name: this.creatorName, + }, + }; + } +} diff --git a/src/models/User.ts b/src/models/User.ts new file mode 100644 index 00000000..168c684b --- /dev/null +++ b/src/models/User.ts @@ -0,0 +1,95 @@ +export enum USER_ROLES { + NORMAL = "NORMAL", + ADMIN = "ADMIN", +} + +export interface TokenPayload { + id: string; + name: string; + role: USER_ROLES; +} + +export interface UserDB { + id: string; + name: string; + email: string; + password: string; + role: USER_ROLES; + created_at: string; +} + +export interface UserModel { + id: string; + name: string; + email: string; + role: USER_ROLES; + createdAt: string; +} + +export class User { + constructor( + private id: string, + private name: string, + private email: string, + private password: string, + private role: USER_ROLES, + private createdAt: string + ) {} + + public getId(): string { + return this.id; + } + + public getName(): string { + return this.name; + } + public setName(value: string) { + this.name = value; + } + + public getEmail(): string { + return this.email; + } + public setEmail(value: string) { + this.email = value; + } + + public getPassaword(): string { + return this.password; + } + public setPassword(value: string) { + this.password = value; + } + + public getRole(): USER_ROLES { + return this.role; + } + public setRole(value: USER_ROLES) { + this.role = value; + } + + public getCreatedAt(): string { + return this.createdAt; + } + + public toDBModel(): UserDB { + return { + id: this.id, + name: this.name, + email: this.email, + password: this.password, + role: this.role, + created_at: this.createdAt, + }; + } + + public toBusinessModel(): UserModel { + return { + id: this.id, + name: this.name, + email: this.email, + role: this.role, + createdAt: this.createdAt, + }; + } +} diff --git a/src/router/PostRouter.ts b/src/router/PostRouter.ts index e69de29b..f3dbd089 100644 --- a/src/router/PostRouter.ts +++ b/src/router/PostRouter.ts @@ -0,0 +1,17 @@ +import express from "express"; +import { PostBusiness } from "../business/PostBusiness"; +import { PostControlers } from "../controller/PostController"; +import { PostDatabase } from "../database/PostDatabase"; +import { IdGenerator } from "../services/IdGenerator"; +import { TokenManager } from "../services/TokenManager"; + +export const postRouter = express.Router(); + +const postController = new PostControlers( + new PostBusiness(new PostDatabase(), new IdGenerator(), new TokenManager()) +); + +postRouter.get("/", postController.getPosts); +postRouter.post("/", postController.postPost); +postRouter.put("/:id", postController.putPost); +postRouter.delete("/:id", postController.deletePosts); diff --git a/src/router/UserRouter.ts b/src/router/UserRouter.ts index e69de29b..68b49017 100644 --- a/src/router/UserRouter.ts +++ b/src/router/UserRouter.ts @@ -0,0 +1,21 @@ +import express from "express"; +import { UserBusiness } from "../business/UserBusiness"; +import { UserController } from "../controller/UserController"; +import { UserDatabase } from "../database/UserDatabase"; +import { HashManager } from "../services/HashManeger"; +import { IdGenerator } from "../services/IdGenerator"; +import { TokenManager } from "../services/TokenManager"; + +export const userRouter = express.Router(); + +const userController = new UserController( + new UserBusiness( + new UserDatabase(), + new IdGenerator(), + new TokenManager(), + new HashManager() + ) +); + +userRouter.post("/signup", userController.signup); +userRouter.post("/login", userController.login); diff --git a/src/services/HashManeger.ts b/src/services/HashManeger.ts new file mode 100644 index 00000000..8e5576e2 --- /dev/null +++ b/src/services/HashManeger.ts @@ -0,0 +1,21 @@ +import bcrypt from "bcryptjs"; +import dotenv from "dotenv"; + +dotenv.config(); + +export class HashManager { + public hash = async (plaintext: string): Promise => { + const rounds = Number(process.env.BCRYPT_COST); + 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); + }; +} diff --git a/src/services/IdGenerator.ts b/src/services/IdGenerator.ts new file mode 100644 index 00000000..5f327f66 --- /dev/null +++ b/src/services/IdGenerator.ts @@ -0,0 +1,7 @@ +import { v4 } from "uuid"; + +export class IdGenerator { + public generate = (): string => { + return v4(); + }; +} diff --git a/src/services/TokenManager.ts b/src/services/TokenManager.ts new file mode 100644 index 00000000..306d2525 --- /dev/null +++ b/src/services/TokenManager.ts @@ -0,0 +1,24 @@ +import jwt from "jsonwebtoken"; +import dotenv from "dotenv"; +import { TokenPayload } from "../models/User"; + +dotenv.config(); + +export class TokenManager { + public createToken = (payload: TokenPayload): string => { + const token = jwt.sign(payload, process.env.JWT_KEY as string, { + expiresIn: process.env.JWT_EXPIRES_IN, + }); + return token; + }; + + public getPayload = (token: string): TokenPayload | null => { + try { + const payload = jwt.verify(token, process.env.JWT_KEY as string); + + return payload as TokenPayload; + } catch (error) { + return null; + } + }; +} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index e69de29b..00000000