├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── assets └── logo-arcanimal.png ├── docker-compose.yml ├── exemplo.env ├── nest-cli.json ├── nodemon.json ├── package.json ├── prisma ├── migrations │ ├── 20240512141956_add_database │ │ └── migration.sql │ ├── 20240512203347_new_columns_for_shelter │ │ └── migration.sql │ ├── 20240512205738_add_owner_to_shelter │ │ └── migration.sql │ ├── 20240514004250_create_refresh_token │ │ └── migration.sql │ ├── 20240514021432_clear │ │ └── migration.sql │ ├── 20240516174924_turn_shelter_email_optional │ │ └── migration.sql │ ├── 20240516181111_ │ │ └── migration.sql │ ├── 20240516181631_add_spaces │ │ └── migration.sql │ └── migration_lock.toml ├── prisma.service.d.ts ├── prisma.service.js ├── prisma.service.js.map ├── prisma.service.ts ├── schema.prisma └── seed │ ├── seed.ts │ └── user.seed.ts ├── src ├── app.controller.ts ├── app.module.ts ├── auth │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── auth.service.ts │ ├── dto │ │ ├── login.dto.ts │ │ └── token.dto.ts │ ├── guards │ │ └── local-auth.guard.ts │ ├── interface │ │ └── login.interface.ts │ └── strategies │ │ ├── jwt.strategy.ts │ │ └── local.strategy.ts ├── decorators │ └── get-user-id.decorator.ts ├── enums │ └── role.enum.ts ├── images │ ├── images.module.ts │ └── images.service.ts ├── mail │ ├── dto │ │ └── send-email.dto.ts │ ├── mail.module.ts │ ├── mail.service.ts │ └── templates │ │ ├── recover-password.hbs │ │ └── welcome.hbs ├── main.ts ├── pet │ ├── dto │ │ ├── create-pet.dto.ts │ │ ├── read-pet.dto.ts │ │ └── update-pet.dto.ts │ ├── pet.controller.ts │ ├── pet.module.ts │ └── pet.service.ts ├── shelter │ ├── dto │ │ ├── create-shelter.dto.ts │ │ ├── read-shelter.dto.ts │ │ └── update-shelter.dto.ts │ ├── shelter-csv-parser.ts │ ├── shelter.controller.ts │ ├── shelter.module.ts │ └── shelter.service.ts ├── user │ ├── dto │ │ ├── create-user.dto.ts │ │ ├── read-user.dto.ts │ │ └── update-user.dto.ts │ ├── user.controller.ts │ ├── user.module.ts │ └── user.service.ts ├── utils │ └── generateRandomPassword.ts └── validation │ └── user-validation.service.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | npm-debug.log 4 | .DS_Store 5 | .git 6 | .gitignore 7 | tsconfig.build.tsbuildinfo -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | yarn.lock 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | pnpm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # OS 17 | .DS_Store 18 | 19 | # Tests 20 | /coverage 21 | /.nyc_output 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | 39 | # dotenv environment variable files 40 | .env 41 | .env.development.local 42 | .env.test.local 43 | .env.production.local 44 | .env.local 45 | 46 | # temp directory 47 | .temp 48 | .tmp 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Diagnostic reports (https://nodejs.org/api/report.html) 57 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 58 | /prisma/migrations -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | WORKDIR /usr/src/app 3 | 4 | COPY package*.json ./ 5 | COPY yarn.lock ./ 6 | COPY . . 7 | COPY .env ./.env 8 | 9 | RUN yarn install 10 | RUN yarn generate 11 | RUN yarn build 12 | 13 | EXPOSE 8000 14 | ENV NODE_ENV production 15 | CMD ["yarn" , "start:migrate:prod"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ yarn install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ yarn run start 40 | 41 | # watch mode 42 | $ yarn run start:dev 43 | 44 | # production mode 45 | $ yarn run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ yarn run test 53 | 54 | # e2e tests 55 | $ yarn run test:e2e 56 | 57 | # test coverage 58 | $ yarn run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /assets/logo-arcanimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theleba/arcanimal-api/e4f5bb70606abc987ae0ec230955e0fc9c9e2456/assets/logo-arcanimal.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | container_name: arcanimal_db 4 | image: postgres:16 5 | ports: 6 | - "9668:5432" 7 | environment: 8 | - POSTGRES_USER=${POSTGRES_USER} 9 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 10 | - POSTGRES_DB=${POSTGRES_DB} 11 | restart: always 12 | networks: 13 | - arcanimal_network 14 | volumes: 15 | - postgres_data:/var/lib/postgresql/data 16 | env_file: 17 | - .env 18 | 19 | app: 20 | container_name: arcanimal_app 21 | build: 22 | context: . 23 | dockerfile: Dockerfile 24 | ports: 25 | - "8000:8000" 26 | environment: 27 | - DATABASE_URL=${DATABASE_URL} 28 | - JWT_SECRET=${JWT_SECRET} 29 | networks: 30 | - arcanimal_network 31 | volumes: 32 | - ./src:/usr/src/app/src 33 | - ./uploads:/usr/src/app/uploads 34 | depends_on: 35 | - db 36 | restart: always 37 | env_file: 38 | - .env 39 | 40 | networks: 41 | arcanimal_network: 42 | 43 | volumes: 44 | postgres_data: -------------------------------------------------------------------------------- /exemplo.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET="teste" 2 | POSTGRES_USER='admin' 3 | POSTGRES_PASSWORD='admin' 4 | POSTGRES_DB='seu_db' 5 | DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" 6 | SMTP_HOST=smtp.example.com 7 | SMTP_PORT=587 8 | SMTP_USER=your-email@example.com 9 | SMTP_PASS=your-password 10 | JWT_EXPIRE=30d 11 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "assets": ["assets/**/*.*"], 8 | "watchAssets": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "nest start --watch" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arcanimal-api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/src/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "migrate": "npx prisma migrate dev", 17 | "migrate:prod": "npx prisma migrate deploy", 18 | "generate": "npx prisma generate", 19 | "seed": "npx prisma db seed", 20 | "start:migrate:prod": "yarn migrate:prod && yarn generate && yarn seed && yarn start:prod" 21 | }, 22 | "prisma": { 23 | "seed": "ts-node prisma/seed/seed.ts" 24 | }, 25 | "dependencies": { 26 | "@nestjs-modules/mailer": "^2.0.2", 27 | "@nestjs/common": "^10.0.0", 28 | "@nestjs/config": "^3.2.2", 29 | "@nestjs/core": "^10.0.0", 30 | "@nestjs/jwt": "^10.2.0", 31 | "@nestjs/passport": "^10.0.3", 32 | "@nestjs/platform-express": "^10.0.0", 33 | "@nestjs/swagger": "^7.3.1", 34 | "@prisma/client": "^5.13.0", 35 | "prisma": "^5.14.0", 36 | "@types/file-type": "^10.9.1", 37 | "@types/passport-jwt": "^4.0.1", 38 | "@types/xlsx": "^0.0.36", 39 | "bcrypt": "^5.1.1", 40 | "bcryptjs": "^2.4.3", 41 | "class-transformer": "^0.5.1", 42 | "class-validator": "^0.14.1", 43 | "file-type": "^19.0.0", 44 | "handlebars": "^4.7.8", 45 | "jsonwebtoken": "^9.0.2", 46 | "nodemailer": "^6.9.13", 47 | "papaparse": "^5.4.1", 48 | "passport": "^0.7.0", 49 | "passport-jwt": "^4.0.1", 50 | "passport-local": "^1.0.0", 51 | "reflect-metadata": "^0.2.0", 52 | "rxjs": "^7.8.1", 53 | "swagger-ui-express": "^5.0.0", 54 | "xlsx": "^0.18.5" 55 | }, 56 | "devDependencies": { 57 | "@nestjs/cli": "^10.0.0", 58 | "@nestjs/schematics": "^10.0.0", 59 | "@nestjs/testing": "^10.0.0", 60 | "@types/bcryptjs": "^2.4.6", 61 | "@types/express": "^4.17.17", 62 | "@types/jest": "^29.5.2", 63 | "@types/multer": "^1.4.11", 64 | "@types/node": "^20.3.1", 65 | "@types/papaparse": "^5.3.14", 66 | "@types/supertest": "^6.0.0", 67 | "@typescript-eslint/eslint-plugin": "^6.0.0", 68 | "@typescript-eslint/parser": "^6.0.0", 69 | "eslint": "^9.2.0", 70 | "eslint-config-prettier": "^9.1.0", 71 | "eslint-plugin-prettier": "^5.1.3", 72 | "jest": "^29.5.0", 73 | "nodemon": "^3.1.0", 74 | "prettier": "^3.2.5", 75 | "source-map-support": "^0.5.21", 76 | "supertest": "^6.3.3", 77 | "ts-jest": "^29.1.0", 78 | "ts-loader": "^9.4.3", 79 | "ts-node": "^10.9.1", 80 | "tsconfig-paths": "^4.2.0", 81 | "typescript": "^5.1.3" 82 | }, 83 | "jest": { 84 | "moduleFileExtensions": [ 85 | "js", 86 | "json", 87 | "ts" 88 | ], 89 | "rootDir": "src", 90 | "testRegex": ".*\\.spec\\.ts$", 91 | "transform": { 92 | "^.+\\.(t|j)s$": "ts-jest" 93 | }, 94 | "collectCoverageFrom": [ 95 | "**/*.(t|j)s" 96 | ], 97 | "coverageDirectory": "../coverage", 98 | "testEnvironment": "node" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /prisma/migrations/20240512141956_add_database/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Role" AS ENUM ('admin', 'volunteer'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Pet" ( 6 | "id" SERIAL NOT NULL, 7 | "location" TEXT NOT NULL, 8 | "contact" TEXT NOT NULL, 9 | "gender" TEXT NOT NULL, 10 | "breed" TEXT NOT NULL, 11 | "size" TEXT NOT NULL, 12 | "type" TEXT NOT NULL, 13 | "color" TEXT NOT NULL, 14 | "name" TEXT NOT NULL, 15 | "url" TEXT NOT NULL, 16 | "found" BOOLEAN NOT NULL, 17 | "details" TEXT NOT NULL, 18 | "shelterId" INTEGER, 19 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | "updatedAt" TIMESTAMP(3) NOT NULL, 21 | "updatedBy" INTEGER NOT NULL, 22 | 23 | CONSTRAINT "Pet_pkey" PRIMARY KEY ("id") 24 | ); 25 | 26 | -- CreateTable 27 | CREATE TABLE "User" ( 28 | "id" SERIAL NOT NULL, 29 | "name" TEXT NOT NULL, 30 | "phone" TEXT NOT NULL, 31 | "email" TEXT NOT NULL, 32 | "password" TEXT NOT NULL, 33 | "role" "Role" NOT NULL, 34 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 35 | "updatedAt" TIMESTAMP(3) NOT NULL, 36 | "updatedBy" INTEGER NOT NULL, 37 | 38 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 39 | ); 40 | 41 | -- CreateTable 42 | CREATE TABLE "Shelter" ( 43 | "id" SERIAL NOT NULL, 44 | "location" TEXT NOT NULL, 45 | "name" TEXT NOT NULL, 46 | "email" TEXT NOT NULL, 47 | "phone" TEXT NOT NULL, 48 | "capacity" INTEGER NOT NULL, 49 | "occupation" INTEGER NOT NULL, 50 | "description" TEXT, 51 | "needs" TEXT[], 52 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 53 | "updatedAt" TIMESTAMP(3) NOT NULL, 54 | "updatedBy" INTEGER NOT NULL, 55 | 56 | CONSTRAINT "Shelter_pkey" PRIMARY KEY ("id") 57 | ); 58 | 59 | -- CreateTable 60 | CREATE TABLE "Image" ( 61 | "id" SERIAL NOT NULL, 62 | "filePath" TEXT NOT NULL, 63 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 64 | "updatedAt" TIMESTAMP(3) NOT NULL, 65 | 66 | CONSTRAINT "Image_pkey" PRIMARY KEY ("id") 67 | ); 68 | 69 | -- CreateIndex 70 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 71 | 72 | -- CreateIndex 73 | CREATE UNIQUE INDEX "Shelter_email_key" ON "Shelter"("email"); 74 | 75 | -- AddForeignKey 76 | ALTER TABLE "Pet" ADD CONSTRAINT "Pet_shelterId_fkey" FOREIGN KEY ("shelterId") REFERENCES "Shelter"("id") ON DELETE SET NULL ON UPDATE CASCADE; 77 | -------------------------------------------------------------------------------- /prisma/migrations/20240512203347_new_columns_for_shelter/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Shelter" ADD COLUMN "address" TEXT, 3 | ADD COLUMN "other_needs" TEXT[] DEFAULT ARRAY[]::TEXT[], 4 | ALTER COLUMN "needs" SET DEFAULT ARRAY[]::TEXT[]; 5 | -------------------------------------------------------------------------------- /prisma/migrations/20240512205738_add_owner_to_shelter/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `description` on the `Shelter` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Shelter" DROP COLUMN "description", 9 | ADD COLUMN "owner" TEXT; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20240514004250_create_refresh_token/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Shelter" ALTER COLUMN "other_needs" SET NOT NULL, 3 | ALTER COLUMN "other_needs" SET DEFAULT '', 4 | ALTER COLUMN "other_needs" SET DATA TYPE TEXT; 5 | 6 | -- CreateTable 7 | CREATE TABLE "RefreshToken" ( 8 | "id" SERIAL NOT NULL, 9 | "userId" INTEGER NOT NULL, 10 | "token" TEXT NOT NULL, 11 | "expiryDate" TIMESTAMP(3) NOT NULL, 12 | 13 | CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token"); 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 21 | -------------------------------------------------------------------------------- /prisma/migrations/20240514021432_clear/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Shelter" ALTER COLUMN "other_needs" SET NOT NULL, 3 | ALTER COLUMN "other_needs" SET DEFAULT '', 4 | ALTER COLUMN "other_needs" SET DATA TYPE TEXT; 5 | -------------------------------------------------------------------------------- /prisma/migrations/20240516174924_turn_shelter_email_optional/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Shelter_email_key"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Shelter" ALTER COLUMN "email" DROP NOT NULL; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240516181111_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `capacity` on the `Shelter` table. All the data in the column will be lost. 5 | - You are about to drop the column `occupation` on the `Shelter` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Shelter" DROP COLUMN "capacity", 10 | DROP COLUMN "occupation", 11 | ADD COLUMN "city" TEXT, 12 | ADD COLUMN "owner_contact" TEXT, 13 | ADD COLUMN "spaces" INTEGER; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20240516181631_add_spaces/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `city` on the `Shelter` table. All the data in the column will be lost. 5 | - You are about to drop the column `owner_contact` on the `Shelter` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Shelter" DROP COLUMN "city", 10 | DROP COLUMN "owner_contact"; 11 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/prisma.service.d.ts: -------------------------------------------------------------------------------- 1 | import { OnModuleInit, OnModuleDestroy } from '@nestjs/common'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | export declare class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { 5 | onModuleInit(): Promise; 6 | onModuleDestroy(): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /prisma/prisma.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | Object.defineProperty(exports, "__esModule", { value: true }); 9 | exports.PrismaService = void 0; 10 | const common_1 = require("@nestjs/common"); 11 | const client_1 = require("@prisma/client"); 12 | let PrismaService = class PrismaService extends client_1.PrismaClient { 13 | async onModuleInit() { 14 | await this.$connect(); 15 | } 16 | async onModuleDestroy() { 17 | await this.$disconnect(); 18 | } 19 | }; 20 | exports.PrismaService = PrismaService; 21 | exports.PrismaService = PrismaService = __decorate([ 22 | (0, common_1.Injectable)() 23 | ], PrismaService); 24 | //# sourceMappingURL=prisma.service.js.map -------------------------------------------------------------------------------- /prisma/prisma.service.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"prisma.service.js","sourceRoot":"","sources":["prisma.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA2E;AAC3E,2CAA8C;AAGvC,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,qBAAY;IAC7C,KAAK,CAAC,YAAY;QAChB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;IAC3B,CAAC;CACF,CAAA;AARY,sCAAa;wBAAb,aAAa;IADzB,IAAA,mBAAU,GAAE;GACA,aAAa,CAQzB"} -------------------------------------------------------------------------------- /prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | @Injectable() 5 | export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { 6 | async onModuleInit() { 7 | await this.$connect(); 8 | } 9 | 10 | async onModuleDestroy() { 11 | await this.$disconnect(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | seed = "ts-node prisma/seed/seed.ts" 4 | binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x", "linux-musl", "linux-musl-openssl-3.0.x"] 5 | } 6 | 7 | datasource db { 8 | provider = "postgresql" 9 | url = env("DATABASE_URL") 10 | } 11 | 12 | model Pet { 13 | id Int @id @default(autoincrement()) 14 | location String 15 | contact String 16 | gender String 17 | breed String 18 | size String 19 | type String 20 | color String 21 | name String 22 | url String 23 | found Boolean 24 | details String 25 | shelterId Int? 26 | createdAt DateTime @default(now()) 27 | updatedAt DateTime @updatedAt 28 | updatedBy Int 29 | 30 | Shelter Shelter? @relation(fields: [shelterId], references: [id]) 31 | } 32 | 33 | model User { 34 | id Int @id @default(autoincrement()) 35 | name String 36 | phone String 37 | email String @unique 38 | password String 39 | role Role 40 | createdAt DateTime @default(now()) 41 | updatedAt DateTime @updatedAt 42 | updatedBy Int 43 | 44 | refreshTokens RefreshToken[] 45 | } 46 | 47 | model RefreshToken { 48 | id Int @id @default(autoincrement()) 49 | userId Int 50 | token String @unique 51 | expiryDate DateTime 52 | 53 | user User @relation(fields: [userId], references: [id]) 54 | } 55 | 56 | model Shelter { 57 | id Int @id @default(autoincrement()) 58 | location String 59 | address String? 60 | name String 61 | email String? 62 | phone String? 63 | owner String? 64 | occupation Int? 65 | capacity Int? 66 | needs String[] @default([]) 67 | other_needs String @default("") 68 | createdAt DateTime @default(now()) 69 | updatedAt DateTime @updatedAt 70 | updatedBy Int 71 | 72 | Pets Pet[] 73 | } 74 | 75 | model Image { 76 | id Int @id @default(autoincrement()) 77 | filePath String 78 | createdAt DateTime @default(now()) 79 | updatedAt DateTime @updatedAt 80 | } 81 | 82 | enum Role { 83 | admin 84 | volunteer 85 | } 86 | 87 | -------------------------------------------------------------------------------- /prisma/seed/seed.ts: -------------------------------------------------------------------------------- 1 | import { userSeed } from "./user.seed" 2 | import { PrismaClient } from '@prisma/client' 3 | 4 | const prisma = new PrismaClient() 5 | Promise.all([userSeed(prisma)]) 6 | .then(async () => { 7 | await prisma.$disconnect() 8 | }) 9 | .catch(async (e) => { 10 | console.error(e) 11 | await prisma.$disconnect() 12 | process.exit(1) 13 | }) -------------------------------------------------------------------------------- /prisma/seed/user.seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import * as bcrypt from 'bcryptjs' 3 | import { Role } from '../../src/enums/role.enum'; 4 | 5 | async function hashPassword(password) { 6 | const saltRounds = 10; 7 | const salt = await bcrypt.genSalt(saltRounds); 8 | const hashedPassword = await bcrypt.hash(password, salt); 9 | return hashedPassword; 10 | } 11 | 12 | export async function userSeed(prisma:PrismaClient) { 13 | const users = [ 14 | { email: 'arcanimal.dev@gmail.com', name: 'Admin ArcAnimal', password: 'ecadLEnDAyAn', role: Role.Admin, phone:"", 15 | updatedBy: 0 }, 16 | { email: 'soulebarbosa@gmail.com', name: 'Admin DEV', password: 'Teste@123', role:Role.Admin, phone:"", 17 | updatedBy: 0 }, 18 | { email: 'arcanimal.voluntarios@gmail.com', name: 'Voluntário ArcAnimal', password: 'ecadLEnDAyAn', role:Role.Volunteer, phone:"", 19 | updatedBy: 0 } 20 | ]; 21 | 22 | for (let user of users) { 23 | user.password = await hashPassword(user.password); 24 | await prisma.user.upsert({ 25 | where: { email: user.email}, 26 | update: {}, 27 | create:{...user}, 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, UseGuards } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Controller('protected') 5 | export class AppController { 6 | @UseGuards(AuthGuard('jwt')) 7 | @Get() 8 | getProtectedResource() { 9 | return { message: "You have access to the protected resource" }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from 'prisma/prisma.service'; 3 | import { ConfigModule } from '@nestjs/config'; 4 | import { AuthModule } from './auth/auth.module'; 5 | import { UserModule } from './user/user.module'; 6 | import { PetModule } from './pet/pet.module'; 7 | import { ShelterModule } from './shelter/shelter.module'; 8 | import { JwtStrategy } from './auth/strategies/jwt.strategy'; 9 | import { LocalStrategy } from './auth/strategies/local.strategy'; 10 | import { EmailService } from './mail/mail.service'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ 15 | isGlobal: true, 16 | }), 17 | AuthModule, 18 | UserModule, 19 | PetModule, 20 | ShelterModule, 21 | 22 | ], 23 | controllers: [], 24 | providers: [ PrismaService, JwtStrategy, LocalStrategy, EmailService] , 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | HttpException, 5 | HttpStatus, 6 | Post, 7 | } from '@nestjs/common'; 8 | import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; 9 | 10 | import { AuthService } from './auth.service'; 11 | import { LoginDto } from './dto/login.dto'; 12 | import { TokenDto } from './dto/token.dto'; 13 | 14 | @ApiTags('auth') 15 | @Controller('auth') 16 | export class AuthController { 17 | constructor(private authService: AuthService) {} 18 | 19 | @Post('login') 20 | @ApiOperation({ summary: 'Log in a user' }) 21 | @ApiBody({ description: 'User Login', type: LoginDto }) 22 | @ApiResponse({ status: 200, description: 'Success', type: TokenDto }) 23 | async login(@Body() loginDto: LoginDto) { 24 | return this.authService.login(loginDto); 25 | } 26 | 27 | @Post('refresh') 28 | @ApiOperation({ summary: 'Refresh token' }) 29 | async refresh(@Body() body: { refresh_token: string }) { 30 | const refreshToken = await this.authService.findRefreshToken( 31 | body.refresh_token, 32 | ); 33 | if (!refreshToken) { 34 | throw new HttpException('Invalid refresh token', HttpStatus.UNAUTHORIZED); 35 | } 36 | 37 | const user = await this.authService.findUserById(refreshToken.userId); 38 | if (!user) { 39 | throw new HttpException('User not found', HttpStatus.UNAUTHORIZED); 40 | } 41 | 42 | const accessToken = this.authService.generateAccessToken(user); 43 | 44 | const userResponse = { ...user }; 45 | delete userResponse.password; 46 | 47 | return { 48 | access_token: accessToken, 49 | ...userResponse, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { UserValidationService } from 'src/validation/user-validation.service'; 6 | import { PrismaService } from 'prisma/prisma.service'; 7 | import { AuthController } from './auth.controller'; 8 | import { JwtStrategy } from './strategies/jwt.strategy'; 9 | 10 | @Module({ 11 | imports: [ 12 | PassportModule, 13 | JwtModule.register({ 14 | secret: process.env.JWT_SECRET, 15 | signOptions: { expiresIn: '30d' }, 16 | }), 17 | ], 18 | controllers: [AuthController], 19 | providers: [AuthService, UserValidationService, PrismaService, JwtStrategy], 20 | exports: [AuthService, UserValidationService, JwtModule] 21 | }) 22 | 23 | export class AuthModule { } 24 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | 4 | import * as bcrypt from 'bcryptjs'; 5 | 6 | import { UserValidationService } from '../validation/user-validation.service'; 7 | import { PrismaService } from 'prisma/prisma.service'; 8 | import { LoginResponse } from './interface/login.interface'; 9 | import { LoginDto } from './dto/login.dto'; 10 | 11 | @Injectable() 12 | export class AuthService { 13 | constructor( 14 | private userValidationService: UserValidationService, 15 | private jwtService: JwtService, 16 | private prisma: PrismaService, 17 | ) {} 18 | 19 | public async findUserById(userId: number) { 20 | return this.userValidationService.findUserById(userId); 21 | } 22 | 23 | public generateAccessToken(user: any): string { 24 | const payload = { email: user.email, sub: user.id }; 25 | return this.jwtService.sign(payload); 26 | } 27 | 28 | async hashPassword(password: string): Promise { 29 | const salt = await bcrypt.genSalt(); 30 | return bcrypt.hash(password, salt); 31 | } 32 | 33 | async comparePasswords( 34 | password: string, 35 | storedPassword: string, 36 | ): Promise { 37 | return bcrypt.compare(password, storedPassword); 38 | } 39 | 40 | async validateUser(email: string, password: string): Promise { 41 | const user = await this.userValidationService.findUserByEmail(email); 42 | const validPassword = await this.comparePasswords(password, user.password); 43 | 44 | if (!user || !validPassword) return null; 45 | const { password: _, ...result } = user; 46 | return result; 47 | } 48 | 49 | async generateRefreshToken(userId: number) { 50 | const sevenDays = 60 * 60 * 24 * 7; 51 | const refreshToken = this.jwtService.sign( 52 | { userId }, 53 | { expiresIn: sevenDays }, 54 | ); 55 | 56 | await this.prisma.refreshToken.create({ 57 | data: { 58 | userId, 59 | token: refreshToken, 60 | expiryDate: new Date(Date.now() + sevenDays * 1000), 61 | }, 62 | }); 63 | 64 | return refreshToken; 65 | } 66 | 67 | async findRefreshToken(token: string) { 68 | return await this.prisma.refreshToken.findUnique({ 69 | where: { token }, 70 | }); 71 | } 72 | 73 | async login({ email, password }: LoginDto): Promise { 74 | const user = await this.validateUser(email, password); 75 | if (!user) throw new UnauthorizedException('Invalid email or password'); 76 | 77 | const { password: _, ...userResponse } = user; 78 | 79 | const { id: sub } = user; 80 | const [token, refreshToken] = await Promise.all([ 81 | this.createToken(email, sub), 82 | this.generateRefreshToken(sub), 83 | ]); 84 | 85 | return { 86 | access_token: token, 87 | refresh_token: refreshToken, 88 | ...userResponse, 89 | }; 90 | } 91 | 92 | private async createToken(email: string, sub: number): Promise { 93 | return this.jwtService.sign({ email, sub }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/auth/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 3 | 4 | export class LoginDto { 5 | @ApiProperty() 6 | @IsEmail() 7 | @IsNotEmpty() 8 | email: string; 9 | 10 | @ApiProperty() 11 | @IsString() 12 | @IsNotEmpty() 13 | password: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/auth/dto/token.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class TokenDto { 4 | @ApiProperty({ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', description: 'JWT token' }) 5 | accessToken: string; 6 | } -------------------------------------------------------------------------------- /src/auth/guards/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /src/auth/interface/login.interface.ts: -------------------------------------------------------------------------------- 1 | export interface LoginResponse { 2 | access_token: string; 3 | refresh_token: string; 4 | id: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy, ExtractJwt } from 'passport-jwt'; 4 | 5 | @Injectable() 6 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { 7 | constructor() { 8 | super({ 9 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 10 | secretOrKey: process.env.JWT_SECRET, 11 | signOptions: { expiresIn: '30d'}, 12 | ignoreExpiration: false, 13 | }); 14 | } 15 | 16 | async validate(payload: any) { 17 | return { userId: payload.sub, username: payload.username }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/auth/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { AuthService } from '../auth.service'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private authService: AuthService) { 9 | super({ 10 | usernameField: 'username', 11 | passwordField: 'password', 12 | }); 13 | } 14 | 15 | async validate(username: string, password: string): Promise { 16 | const user = await this.authService.validateUser(username, password); 17 | if (!user) { 18 | throw new UnauthorizedException('Invalid username or password'); 19 | } 20 | return user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/decorators/get-user-id.decorator.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 3 | 4 | export const GetUserId = createParamDecorator( 5 | (data: unknown, ctx: ExecutionContext): number => { 6 | const request = ctx.switchToHttp().getRequest(); 7 | return request.user?.userId; 8 | }, 9 | ); 10 | -------------------------------------------------------------------------------- /src/enums/role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | Admin = 'admin', 3 | Volunteer = 'volunteer' 4 | } -------------------------------------------------------------------------------- /src/images/images.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ImagesService } from './images.service'; 3 | import { PrismaService } from 'prisma/prisma.service'; 4 | 5 | @Module({ 6 | providers: [ImagesService, PrismaService] 7 | }) 8 | export class ImagesModule {} 9 | -------------------------------------------------------------------------------- /src/images/images.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { promises as fsPromises } from 'fs'; 3 | import { PrismaService } from 'prisma/prisma.service'; 4 | import * as path from 'path'; 5 | import { ConfigService } from '@nestjs/config'; 6 | 7 | @Injectable() 8 | export class ImagesService { 9 | constructor(private prisma: PrismaService, private configService: ConfigService) {} 10 | 11 | 12 | async saveImage(base64: string): Promise { 13 | const fileName = `${Date.now()}.png`; 14 | const filePath = path.join('/usr/src/app/uploads', fileName); 15 | 16 | await this.saveBase64AsFile(base64, filePath); 17 | 18 | const domain = this.configService.get('DOMAIN'); 19 | const accessibleUrl = `${domain}/uploads/${fileName}`; 20 | 21 | return accessibleUrl; 22 | } 23 | 24 | async ensureDirectoryExistence(filePath: string) { 25 | const dirname = path.dirname(filePath); 26 | try { 27 | await fsPromises.access(dirname); 28 | } catch (e) { 29 | await fsPromises.mkdir(dirname, { recursive: true }); 30 | } 31 | } 32 | 33 | private async saveBase64AsFile(base64: string, filePath: string): Promise { 34 | const base64Data = base64.replace(/^data:image\/\w+;base64,/, ''); 35 | const buffer = Buffer.from(base64Data, 'base64'); 36 | 37 | await this.ensureDirectoryExistence(filePath); 38 | await fsPromises.writeFile(filePath, buffer); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/mail/dto/send-email.dto.ts: -------------------------------------------------------------------------------- 1 | export class SendEmailDto { 2 | to: string; 3 | subject: string; 4 | text: string; 5 | html: string; 6 | } -------------------------------------------------------------------------------- /src/mail/mail.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MailerModule } from '@nestjs-modules/mailer'; 3 | import { EmailService } from './mail.service'; 4 | import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; 5 | 6 | @Module({ 7 | imports: [ 8 | MailerModule.forRoot({ 9 | transport: { 10 | host: process.env.SMTP_HOST, 11 | port: process.env.SMTP_PORT, 12 | secure: false, 13 | auth: { 14 | user: process.env.SMTP_USER, 15 | pass: process.env.SMTP_PASS, 16 | }, 17 | }, 18 | defaults: { 19 | from: '"Projeto Arcanimal" ', 20 | }, 21 | template: { 22 | dir: process.cwd() + '/src/mail/templates', 23 | adapter: new HandlebarsAdapter(), 24 | options: { 25 | strict: true, 26 | }, 27 | }, 28 | }), 29 | ], 30 | providers: [EmailService], 31 | exports: [EmailService] 32 | }) 33 | export class EmailModule {} 34 | -------------------------------------------------------------------------------- /src/mail/mail.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { MailerService } from '@nestjs-modules/mailer'; 3 | import { join } from 'path'; 4 | 5 | @Injectable() 6 | export class EmailService { 7 | constructor(private mailerService: MailerService) {} 8 | 9 | async sendWelcomeEmail(user: { name: string; email: string; password: string }) { 10 | await this.mailerService.sendMail({ 11 | to: user.email, 12 | subject: '[Arcanimal] Bem vindo!', 13 | template: './welcome', 14 | context: { 15 | name: user.name, 16 | email: user.email, 17 | password: user.password 18 | }, 19 | attachments: [ 20 | { 21 | filename: 'logo-arcanimal.png', 22 | path: join(process.cwd(), 'assets/logo-arcanimal.png'), 23 | cid: 'logo' 24 | } 25 | ] 26 | }); 27 | } 28 | 29 | async sendRecoverPasswordEmail(user: { name: string; email: string; password: string }) { 30 | await this.mailerService.sendMail({ 31 | to: user.email, 32 | subject: '[Arcanimal] Recuperar Senha', 33 | template: './recover-password', 34 | context: { 35 | name: user.name, 36 | email: user.email, 37 | password: user.password, 38 | }, 39 | attachments: [ 40 | { 41 | filename: 'logo-arcanimal.png', 42 | path: join(process.cwd(), 'assets/logo-arcanimal.png'), 43 | cid: 'logo' 44 | } 45 | ] 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/mail/templates/recover-password.hbs: -------------------------------------------------------------------------------- 1 |

Olá, {{name}}!

2 |

Você solicitou a recuperação de senha.

3 |

Sua nova senha é: {{password}}

4 |

Equipe Arcanimal.

5 | -------------------------------------------------------------------------------- /src/mail/templates/welcome.hbs: -------------------------------------------------------------------------------- 1 |

Bem-vindo, {{name}}!

2 | Logo 3 |

Seu e-mail de acesso é: {{email}}

4 |

Sua senha inicial é: {{password}}

5 |

Equipe Arcanimal.

-------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 4 | import { Logger, ValidationPipe } from '@nestjs/common'; 5 | import * as express from 'express'; 6 | import * as bodyParser from 'body-parser'; 7 | import * as dotenv from 'dotenv'; 8 | dotenv.config(); 9 | 10 | async function bootstrap() { 11 | const logger: Logger = new Logger(bootstrap.name); 12 | const app = await NestFactory.create(AppModule); 13 | app.use('/uploads', express.static('/usr/src/app/uploads')); 14 | 15 | app.use(bodyParser.json({ limit: '50mb' })); 16 | app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); 17 | 18 | app.useGlobalPipes( 19 | new ValidationPipe({ 20 | whitelist: true, 21 | forbidNonWhitelisted: true, 22 | transform: true, 23 | disableErrorMessages: false, 24 | validationError: { target: false }, 25 | }), 26 | ); 27 | 28 | app.enableCors(); 29 | 30 | const config = new DocumentBuilder() 31 | .setTitle('Arcanimal API') 32 | .setDescription('API para a plataforma Arcanimal') 33 | .setVersion('1.0') 34 | .addBearerAuth( 35 | { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 36 | 'BearerAuth', 37 | ) 38 | .build(); 39 | 40 | const document = SwaggerModule.createDocument(app, config); 41 | 42 | SwaggerModule.setup('api', app, document); 43 | 44 | const port: number = +process.env.PORT || 8000; 45 | 46 | await app.listen(port); 47 | logger.log(`Application is running on port: ${port}`); 48 | } 49 | 50 | bootstrap(); 51 | -------------------------------------------------------------------------------- /src/pet/dto/create-pet.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsNotEmpty, IsString, IsUrl, IsOptional, IsInt } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class CreatePetDto { 5 | @ApiProperty({ description: 'Location where the pet was found or is located' }) 6 | @IsNotEmpty() 7 | @IsString() 8 | location: string; 9 | 10 | @ApiProperty({ description: 'Contact information for the person or shelter associated with the pet' }) 11 | @IsNotEmpty() 12 | @IsString() 13 | contact: string; 14 | 15 | @ApiProperty({ description: 'Gender of the pet' }) 16 | @IsNotEmpty() 17 | @IsString() 18 | gender: string; 19 | 20 | @ApiProperty({ description: 'Breed of the pet' }) 21 | @IsNotEmpty() 22 | @IsString() 23 | breed: string; 24 | 25 | @ApiProperty({ description: 'Size of the pet' }) 26 | @IsNotEmpty() 27 | @IsString() 28 | size: string; 29 | 30 | @ApiProperty({ description: 'Type of animal (e.g., dog, cat)' }) 31 | @IsNotEmpty() 32 | @IsString() 33 | type: string; 34 | 35 | @ApiProperty({ description: 'Detailed description of the pet', required: false }) 36 | @IsString() 37 | @IsOptional() 38 | details: string; 39 | 40 | @ApiProperty({ description: 'Color of the pet' }) 41 | @IsNotEmpty() 42 | @IsString() 43 | color: string; 44 | 45 | @ApiProperty({ description: 'Name of the pet' }) 46 | @IsNotEmpty() 47 | @IsString() 48 | name: string; 49 | 50 | @ApiProperty({ description: 'Photo of the pet', required: false }) 51 | @IsUrl() 52 | @IsOptional() 53 | url: string; 54 | 55 | @ApiProperty({ description: 'Base54 of photo of the pet', required: false }) 56 | @IsString() 57 | @IsNotEmpty() 58 | imageBase64?: string; 59 | 60 | @ApiProperty({ description: 'Whether the pet has been found' }) 61 | @IsBoolean() 62 | found: boolean; 63 | 64 | @ApiProperty({ description: 'ShelderId' }) 65 | @IsNotEmpty() 66 | @IsInt() 67 | shelterId: number; 68 | 69 | @ApiProperty({ description: 'UserId' }) 70 | @IsNotEmpty() 71 | @IsInt() 72 | updatedBy: number; 73 | } 74 | -------------------------------------------------------------------------------- /src/pet/dto/read-pet.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class PetReadDto { 4 | @ApiProperty({ description: 'The unique identifier of the pet' }) 5 | id: number; 6 | 7 | @ApiProperty({ description: 'Location where the pet is found' }) 8 | location: string; 9 | 10 | @ApiProperty({ description: 'Contact number for pet inquiries' }) 11 | contact: string; 12 | 13 | @ApiProperty({ description: 'Gender of the pet' }) 14 | gender: string; 15 | 16 | @ApiProperty({ description: 'Breed of the pet' }) 17 | breed: string; 18 | 19 | @ApiProperty({ description: 'Size of the pet' }) 20 | size: string; 21 | 22 | @ApiProperty({ description: 'Type of the animal' }) 23 | type: string; 24 | 25 | @ApiProperty({ description: 'Color of the pet' }) 26 | color: string; 27 | 28 | @ApiProperty({ description: 'Name of the pet' }) 29 | name: string; 30 | 31 | @ApiProperty({ description: 'URL to pet details' }) 32 | url: string; 33 | 34 | @ApiProperty({ description: 'Found status of the pet' }) 35 | found: boolean; 36 | 37 | @ApiProperty({ description: 'Details about the pet' }) 38 | details: string; 39 | 40 | @ApiProperty({ description: 'Identifier of the shelter this pet belongs to' }) 41 | shelterId: number; 42 | 43 | @ApiProperty({ description: 'Creation date of the record' }) 44 | createdAt: Date; 45 | 46 | @ApiProperty({ description: 'Last update date of the record' }) 47 | updatedAt: Date; 48 | } 49 | -------------------------------------------------------------------------------- /src/pet/dto/update-pet.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsInt, IsNotEmpty, IsOptional, IsString, IsUrl } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | 4 | export class UpdatePetDto { 5 | @ApiPropertyOptional({ description: 'Location where the pet was found or is located' }) 6 | @IsString() 7 | @IsOptional() 8 | location?: string; 9 | 10 | @ApiPropertyOptional({ description: 'Contact information for the person or shelter associated with the pet' }) 11 | @IsString() 12 | @IsOptional() 13 | contact?: string; 14 | 15 | @ApiPropertyOptional({ description: 'Gender of the pet' }) 16 | @IsString() 17 | @IsOptional() 18 | gender?: string; 19 | 20 | @ApiPropertyOptional({ description: 'Breed of the pet' }) 21 | @IsString() 22 | @IsOptional() 23 | breed?: string; 24 | 25 | @ApiPropertyOptional({ description: 'Size of the pet' }) 26 | @IsString() 27 | @IsOptional() 28 | size?: string; 29 | 30 | @ApiPropertyOptional({ description: 'Type of animal (e.g., dog, cat)' }) 31 | @IsString() 32 | @IsOptional() 33 | type?: string; 34 | 35 | @ApiPropertyOptional({ description: 'Detailed description of the pet' }) 36 | @IsString() 37 | @IsOptional() 38 | details?: string; 39 | 40 | @ApiPropertyOptional({ description: 'Color of the pet' }) 41 | @IsString() 42 | @IsOptional() 43 | color?: string; 44 | 45 | @ApiPropertyOptional({ description: 'Name of the pet' }) 46 | @IsString() 47 | @IsOptional() 48 | name?: string; 49 | 50 | @ApiPropertyOptional({ description: 'URL to a photo of the pet' }) 51 | @IsUrl() 52 | @IsOptional() 53 | url?: string; 54 | 55 | @ApiPropertyOptional({ description: 'Whether the pet has been found' }) 56 | @IsBoolean() 57 | @IsOptional() 58 | found?: boolean; 59 | 60 | @ApiProperty({ description: 'UserId' }) 61 | @IsNotEmpty() 62 | @IsInt() 63 | updatedBy: number; 64 | } 65 | -------------------------------------------------------------------------------- /src/pet/pet.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Put, Delete, Param, Body, Query } from '@nestjs/common'; 2 | import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; 3 | import { PetService } from './pet.service'; 4 | import { CreatePetDto } from './dto/create-pet.dto'; 5 | import { UpdatePetDto } from './dto/update-pet.dto'; 6 | 7 | @ApiTags('pets') 8 | @Controller('pets') 9 | export class PetController { 10 | constructor(private readonly petService: PetService) {} 11 | 12 | @Post() 13 | @ApiOperation({ summary: 'Create a new pet' }) 14 | @ApiResponse({ status: 201, description: 'Pet created successfully.' }) 15 | async create(@Body() createPetDto: CreatePetDto) { 16 | return this.petService.create(createPetDto); 17 | } 18 | 19 | @Get() 20 | @ApiOperation({ summary: 'Retrieve all pets with optional pagination' }) 21 | @ApiQuery({ name: 'page', required: false , type: 'number' }) 22 | @ApiResponse({ status: 200, description: 'Returned all pets successfully.' }) 23 | async findAll(@Query('page') page?: number) { 24 | const petsAndTotal = await this.petService.findAll(page, Number(process.env.PAGE_LIMIT)); 25 | return { 26 | data: petsAndTotal.data, 27 | total: petsAndTotal.total 28 | } 29 | } 30 | 31 | @Get(':id') 32 | @ApiOperation({ summary: 'Retrieve a pet by id' }) 33 | @ApiResponse({ status: 200, description: 'Pet found.' }) 34 | async findOne(@Param('id') id: number) { 35 | return this.petService.findOne(id); 36 | } 37 | 38 | @Put(':id') 39 | @ApiOperation({ summary: 'Update a pet by id' }) 40 | @ApiResponse({ status: 200, description: 'Pet updated successfully.' }) 41 | async update(@Param('id') id: number, @Body() updatePetDto: UpdatePetDto) { 42 | return this.petService.update(id, updatePetDto); 43 | } 44 | 45 | @Delete(':id') 46 | @ApiOperation({ summary: 'Delete a pet by id' }) 47 | @ApiResponse({ status: 200, description: 'Pet deleted successfully.' }) 48 | async remove(@Param('id') id: number) { 49 | return this.petService.remove(id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/pet/pet.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PetController } from './pet.controller'; 3 | import { PetService } from './pet.service'; 4 | import { PrismaService } from 'prisma/prisma.service'; 5 | import { ImagesService } from 'src/images/images.service'; 6 | 7 | @Module({ 8 | controllers: [PetController], 9 | providers: [ImagesService, PetService, PrismaService], 10 | exports:[PetService] 11 | }) 12 | 13 | export class PetModule {} 14 | -------------------------------------------------------------------------------- /src/pet/pet.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from 'prisma/prisma.service'; 3 | import { CreatePetDto } from './dto/create-pet.dto'; 4 | import { UpdatePetDto } from './dto/update-pet.dto'; 5 | import { ImagesService } from 'src/images/images.service'; 6 | 7 | @Injectable() 8 | export class PetService { 9 | constructor(private prisma: PrismaService, private imagesService: ImagesService) {} 10 | 11 | async create(createPetDto: CreatePetDto) { 12 | const { shelterId, imageBase64, ...rest } = createPetDto; 13 | 14 | let imagePath = null; 15 | if (imageBase64) { 16 | imagePath = await this.imagesService.saveImage(imageBase64); 17 | } 18 | 19 | return this.prisma.pet.create({ 20 | data: { 21 | ...rest, 22 | Shelter: { 23 | connect: { id: shelterId }, 24 | }, 25 | url: imagePath 26 | }, 27 | }); 28 | } 29 | 30 | async findAll(page: number, limit: number) { 31 | 32 | let queryParams: any = {}; 33 | if (page) { 34 | const pageNumber = Number(page); 35 | queryParams.skip = (pageNumber - 1) * limit; 36 | queryParams.take = limit; 37 | } 38 | 39 | const pets = await this.prisma.pet.findMany(queryParams); 40 | const total = await this.prisma.pet.count(); 41 | return {data: pets, total} 42 | 43 | } 44 | 45 | 46 | async findOne(id: number) { 47 | return this.prisma.pet.findUnique({ 48 | where: { id }, 49 | }); 50 | } 51 | 52 | async update(id: number, updatePetDto: UpdatePetDto) { 53 | return this.prisma.pet.update({ 54 | where: { id }, 55 | data: updatePetDto, 56 | }); 57 | } 58 | 59 | async remove(id: number) { 60 | return this.prisma.pet.delete({ 61 | where: { id }, 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/shelter/dto/create-shelter.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsArray, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class CreateShelterDto { 5 | @ApiProperty({ description: 'Location of the shelter' }) 6 | @IsNotEmpty() 7 | @IsString() 8 | location: string; 9 | 10 | @ApiProperty({ description: 'Address of the shelter' }) 11 | @IsString() 12 | address: string; 13 | 14 | @ApiProperty({ description: 'Name of the shelter' }) 15 | @IsNotEmpty() 16 | @IsString() 17 | name: string; 18 | 19 | @ApiProperty({ description: 'Email address of the shelter' }) 20 | @IsString() 21 | email: string; 22 | 23 | @ApiProperty({ description: 'Phone number of the shelter' }) 24 | @IsString() 25 | phone: string; 26 | 27 | @ApiProperty({ description: 'Available spaces of the shelter in terms of number of pets it can accommodate' }) 28 | @IsInt() 29 | @IsOptional() 30 | occupation?: number; 31 | 32 | @ApiProperty({ description: 'Available spaces of the shelter in terms of number of pets it can accommodate' }) 33 | @IsInt() 34 | @IsOptional() 35 | capacity?: number; 36 | 37 | @ApiProperty({ description: 'Detailed owner of the shelter', example:'' }) 38 | @IsString() 39 | @IsOptional() 40 | owner: string; 41 | 42 | @ApiProperty({ description: 'Current needs of the shelter' }) 43 | @IsArray() 44 | @IsOptional() 45 | needs: string[]; 46 | 47 | @ApiProperty({ description: 'Other needs of the shelter' }) 48 | @IsString() 49 | other_needs: string; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/shelter/dto/read-shelter.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class ShelterReadDto { 4 | @ApiProperty({ description: 'Unique identifier of the shelter' }) 5 | id: number; 6 | 7 | @ApiProperty({ description: 'Location of the shelter' }) 8 | location: string; 9 | 10 | @ApiProperty({ description: 'Address of the shelter' }) 11 | address: string; 12 | 13 | @ApiProperty({ description: 'Name of the shelter' }) 14 | name: string; 15 | 16 | @ApiProperty({ description: 'Email address of the shelter' }) 17 | email: string; 18 | 19 | @ApiProperty({ description: 'Phone number of the shelter' }) 20 | phone: string; 21 | 22 | @ApiProperty({ description: 'Available spaces of the shelter' }) 23 | spaces: number; 24 | 25 | @ApiProperty() 26 | owner: string; 27 | 28 | @ApiProperty({ description: 'Current needs of the shelter' }) 29 | needs: string[]; 30 | 31 | @ApiProperty({ description: 'Current needs of the shelter' }) 32 | other_needs: string[]; 33 | 34 | @ApiProperty({ description: 'Creation date of the shelter record' }) 35 | createdAt: Date; 36 | 37 | @ApiProperty({ description: 'Last update date of the shelter record' }) 38 | updatedAt: Date; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/shelter/dto/update-shelter.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsArray, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | 4 | export class UpdateShelterDto { 5 | @ApiPropertyOptional({ description: 'Location of the shelter' }) 6 | @IsString() 7 | @IsOptional() 8 | location?: string; 9 | 10 | @ApiPropertyOptional({ description: 'Address of the shelter' }) 11 | @IsNotEmpty() 12 | @IsString() 13 | address: string; 14 | 15 | @ApiPropertyOptional({ description: 'Name of the shelter' }) 16 | @IsString() 17 | @IsOptional() 18 | name?: string; 19 | 20 | @ApiPropertyOptional({ description: 'Email address of the shelter' }) 21 | @IsString() 22 | @IsOptional() 23 | email?: string; 24 | 25 | @ApiPropertyOptional({ description: 'Phone number of the shelter' }) 26 | @IsString() 27 | @IsOptional() 28 | phone?: string; 29 | 30 | @ApiPropertyOptional() 31 | @IsInt() 32 | occupation: number; 33 | 34 | @ApiProperty() 35 | @IsInt() 36 | capacity: number; 37 | 38 | @ApiPropertyOptional({ description: 'Detailed owner of the shelter', example:'' }) 39 | @IsString() 40 | @IsOptional() 41 | owner?: string; 42 | 43 | @ApiPropertyOptional({ description: 'Current needs of the shelter' }) 44 | @IsArray() 45 | @IsOptional() 46 | needs?: string[]; 47 | 48 | @ApiPropertyOptional({ description: 'Other needs of the shelter' }) 49 | @IsString() 50 | other_needs: string; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/shelter/shelter-csv-parser.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable } from '@nestjs/common'; 2 | import { plainToInstance } from 'class-transformer'; 3 | import { validate } from 'class-validator'; 4 | import * as Papa from 'papaparse'; 5 | import { CreateShelterDto } from './dto/create-shelter.dto'; 6 | 7 | @Injectable() 8 | export class ShelterCsvParser { 9 | async parse(csv: Express.Multer.File) { 10 | const csvContent = String(csv.buffer); 11 | 12 | const { data: parsedData } = Papa.parse(csvContent, { 13 | skipEmptyLines: true, 14 | }); 15 | 16 | if (parsedData.length <= 1) { 17 | throw new BadRequestException('Invalid CSV'); 18 | } 19 | 20 | parsedData.shift(); // Remove header 21 | 22 | const shelters = (parsedData as string[]) 23 | .map( 24 | ([ 25 | name, 26 | address, 27 | location, 28 | email, 29 | phone, 30 | capacity, 31 | occupation, 32 | owner, 33 | needs, 34 | other_needs, 35 | ]) => ({ 36 | name, 37 | address, 38 | location, 39 | email, 40 | phone, 41 | capacity: Number(capacity), 42 | occupation: Number(occupation), 43 | owner, 44 | needs: needs.split('|'), 45 | other_needs, 46 | }), 47 | ) 48 | .map((rawShelter) => plainToInstance(CreateShelterDto, rawShelter)); 49 | 50 | const errors = ( 51 | await Promise.all( 52 | shelters.map(async (shelter, index) => { 53 | const shelterErrors = await validate(shelter); 54 | 55 | return { 56 | lineNumber: index, 57 | errors: shelterErrors.flatMap(({ constraints }) => 58 | Object.values(constraints), 59 | ), 60 | }; 61 | }), 62 | ) 63 | ).filter((shelterPromise) => shelterPromise.errors.length > 0); 64 | 65 | if (errors.length > 0) { 66 | throw new BadRequestException(errors); 67 | } 68 | 69 | return shelters; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/shelter/shelter.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Put, 6 | Delete, 7 | Param, 8 | Body, 9 | Query, 10 | UseGuards, 11 | UseInterceptors, 12 | FileTypeValidator, 13 | ParseFilePipe, 14 | UploadedFile, 15 | BadRequestException, 16 | } from '@nestjs/common'; 17 | import { 18 | ApiTags, 19 | ApiOperation, 20 | ApiResponse, 21 | ApiQuery, 22 | ApiBearerAuth, 23 | ApiConsumes, 24 | ApiBody, 25 | } from '@nestjs/swagger'; 26 | import { ShelterService } from './shelter.service'; 27 | import { CreateShelterDto } from './dto/create-shelter.dto'; 28 | import { UpdateShelterDto } from './dto/update-shelter.dto'; 29 | import { AuthGuard } from '@nestjs/passport'; 30 | import { GetUserId } from 'src/decorators/get-user-id.decorator'; 31 | import { FileInterceptor } from '@nestjs/platform-express'; 32 | 33 | @ApiTags('shelters') 34 | @Controller('shelters') 35 | export class ShelterController { 36 | constructor(private readonly shelterService: ShelterService) {} 37 | 38 | @ApiBearerAuth('BearerAuth') 39 | @UseGuards(AuthGuard('jwt')) 40 | @Post() 41 | @ApiOperation({ summary: 'Create a new shelter' }) 42 | @ApiResponse({ status: 201, description: 'Shelter created successfully.' }) 43 | async create( 44 | @Body() createShelterDto: CreateShelterDto, 45 | @GetUserId() userId: number, 46 | ) { 47 | return this.shelterService.create(createShelterDto, userId); 48 | } 49 | 50 | 51 | @ApiBearerAuth('BearerAuth') 52 | @UseGuards(AuthGuard('jwt')) 53 | @Post('import-data') 54 | @UseInterceptors(FileInterceptor('file')) 55 | @ApiOperation({ summary: 'Import shelters data from XLSX file' }) 56 | @ApiConsumes('multipart/form-data') 57 | @ApiBody({ 58 | description: 'Upload file', 59 | schema: { 60 | type: 'object', 61 | properties: { 62 | file: { 63 | type: 'string', 64 | format: 'binary', 65 | }, 66 | }, 67 | }, 68 | }) 69 | async importData( 70 | @UploadedFile( 71 | new ParseFilePipe({ 72 | validators: [ 73 | new FileTypeValidator({ fileType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), 74 | ], 75 | }), 76 | ) 77 | file: Express.Multer.File, 78 | @GetUserId() userId: number, 79 | ) { 80 | if (!file) { 81 | throw new BadRequestException('No file uploaded'); 82 | } 83 | 84 | return this.shelterService.importData(file, userId); 85 | } 86 | 87 | @Get() 88 | @ApiOperation({ summary: 'Retrieve all shelters with optional pagination' }) 89 | @ApiQuery({ name: 'page', required: false, type: 'number' }) 90 | @ApiQuery({ name: 'localOccupation', required: false, type: 'number' }) 91 | @ApiQuery({ name: 'location', required: false, type: 'string' }) 92 | @ApiResponse({ 93 | status: 200, 94 | description: 'Returned all shelters successfully.', 95 | }) 96 | async find( 97 | @Query('page') page?: number, 98 | @Query('localOccupation') localOccupation?: number, 99 | @Query('location') location?: string, 100 | ) { 101 | const pagination = page ? { page, limit: Number(process.env.PAGE_LIMIT) } : undefined; 102 | const filters = { localOccupation, location }; 103 | const findOptions = { pagination, filters }; 104 | return this.shelterService.find(findOptions); 105 | } 106 | 107 | @Get(':id') 108 | @ApiOperation({ summary: 'Retrieve a shelter by id' }) 109 | @ApiResponse({ status: 200, description: 'Shelter found.' }) 110 | async findOne(@Param('id') id: number) { 111 | return this.shelterService.findOne(id); 112 | } 113 | 114 | @ApiBearerAuth('BearerAuth') 115 | @UseGuards(AuthGuard('jwt')) 116 | @Put(':id') 117 | @ApiOperation({ summary: 'Update a shelter by id' }) 118 | @ApiResponse({ status: 200, description: 'Shelter updated successfully.' }) 119 | async update( 120 | @Param('id') id: number, 121 | @Body() updateShelterDto: UpdateShelterDto, 122 | @GetUserId() userId: number, 123 | ) { 124 | return this.shelterService.update(id, updateShelterDto, userId); 125 | } 126 | 127 | @ApiBearerAuth('BearerAuth') 128 | @UseGuards(AuthGuard('jwt')) 129 | @Delete(':id') 130 | @ApiOperation({ summary: 'Delete a shelter by id' }) 131 | @ApiResponse({ status: 200, description: 'Shelter deleted successfully.' }) 132 | async remove(@Param('id') id: number) { 133 | return this.shelterService.remove(id); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/shelter/shelter.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from 'prisma/prisma.service'; 3 | import { ShelterCsvParser } from './shelter-csv-parser'; 4 | import { ShelterController } from './shelter.controller'; 5 | import { ShelterService } from './shelter.service'; 6 | 7 | @Module({ 8 | controllers: [ShelterController], 9 | providers: [ShelterService, PrismaService, ShelterCsvParser], 10 | exports: [ShelterService, ShelterCsvParser], 11 | }) 12 | export class ShelterModule {} 13 | -------------------------------------------------------------------------------- /src/shelter/shelter.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; 2 | import { PrismaService } from 'prisma/prisma.service'; 3 | import { CreateShelterDto } from './dto/create-shelter.dto'; 4 | import { UpdateShelterDto } from './dto/update-shelter.dto'; 5 | import { Prisma } from '@prisma/client'; 6 | import { ShelterCsvParser } from './shelter-csv-parser'; 7 | import * as XLSX from 'xlsx'; 8 | 9 | @Injectable() 10 | export class ShelterService { 11 | constructor(private prisma: PrismaService, private csvParser: ShelterCsvParser) {} 12 | 13 | async create(createShelterDto: CreateShelterDto, userId: number) { 14 | 15 | return this.prisma.shelter.create({ 16 | data: { 17 | ...createShelterDto, 18 | updatedBy: userId, 19 | }, 20 | }); 21 | } 22 | 23 | async importData(file: Express.Multer.File, userId: number) { 24 | const shelters = this.parseFile(file); 25 | 26 | return this.prisma.shelter.createMany({ 27 | data: shelters.map((shelter) => ({ ...shelter, updatedBy: userId })), 28 | }); 29 | } 30 | 31 | private parseFile(file: Express.Multer.File): CreateShelterDto[] { 32 | const workbook = XLSX.read(file.buffer, { type: 'buffer' }); 33 | 34 | // Parse Cadastro sheet 35 | const cadastroSheet = workbook.Sheets['Cadastro']; 36 | if (!cadastroSheet) { 37 | throw new BadRequestException('Sheet "cadastro" not found'); 38 | } 39 | const cadastroData = XLSX.utils.sheet_to_json>(cadastroSheet); 40 | 41 | // Parse Monitoramento sheet 42 | const monitoramentoSheet = workbook.Sheets['Monitoramento']; 43 | if (!monitoramentoSheet) { 44 | throw new BadRequestException('Sheet "monitoramento" not found'); 45 | } 46 | const monitoramentoData = XLSX.utils.sheet_to_json>(monitoramentoSheet); 47 | 48 | // Parse Necessidades sheet 49 | const necessidadesSheet = workbook.Sheets['Necessidades dos abrigos']; 50 | if (!necessidadesSheet) { 51 | throw new BadRequestException('Sheet "necessidades" not found'); 52 | } 53 | const necessidadesData = XLSX.utils.sheet_to_json>(necessidadesSheet); 54 | 55 | const headers = Object.keys(necessidadesData[0]); 56 | const needKeys = headers.slice(1, headers.length); // Skip the first column which is the name of the shelter 57 | 58 | return cadastroData.map((data: Record) => { 59 | const monitoramento = monitoramentoData.find((m: any) => m['Nome do abrigo'] === data['Nome do abrigo']); 60 | const necessidades = necessidadesData.find((n: any) => n['Nome do abrigo'] === data['Nome do abrigo']); 61 | 62 | const needs: string[] = []; 63 | if (necessidades) { 64 | needKeys.forEach((key) => { 65 | if (necessidades[key] === true || necessidades[key] === '✔' || necessidades[key] === '1' || necessidades[key] === 'TRUE') { 66 | needs.push(key); 67 | } 68 | }); 69 | } 70 | 71 | const endereco = [data.Endereço, data.Bairro, data.CEP].filter(Boolean).join(' - '); 72 | 73 | const capacidadeTotal = monitoramento ? parseInt(monitoramento['Qual a capacidade total de animais do Abrigo?'], 10) : 0; 74 | const vagasDisponiveis = monitoramento ? parseInt(monitoramento['Número de vagas disponíveis'], 10) : 0; 75 | 76 | return { 77 | location: data.Cidade, 78 | address: endereco, 79 | name: data['Nome do abrigo'], 80 | email: data.Email, 81 | phone: String(data['Contato da pessoa responsável'] || '') , 82 | capacity: capacidadeTotal, 83 | occupation: capacidadeTotal - vagasDisponiveis, 84 | owner: data['Nome da pessoa responsável'], 85 | needs: needs.map((need) => need.replace(/'$/, '')), 86 | other_needs: '', 87 | }; 88 | }); 89 | } 90 | 91 | async find(findOptions: { 92 | pagination?: { page: number; limit: number }; 93 | filters?: { localOccupation?: number; location?: string }; 94 | }) { 95 | const { pagination, filters } = findOptions; 96 | const queryParams: Prisma.ShelterFindManyArgs = { 97 | where: {}, 98 | orderBy: { name: 'asc' } 99 | }; 100 | 101 | if (pagination && pagination.page) { 102 | const { page, limit } = pagination; 103 | queryParams.skip = (page - 1) * limit; 104 | queryParams.take = limit; 105 | } 106 | 107 | if (filters) { 108 | const { localOccupation, location } = filters; 109 | 110 | if (location) { 111 | queryParams.where.location = location; 112 | } 113 | } 114 | 115 | const filteredShelters = await this.prisma.shelter.findMany(queryParams); 116 | 117 | const total = await this.prisma.shelter.count({ where: queryParams.where }); 118 | 119 | return { data: filteredShelters, total }; 120 | 121 | } 122 | 123 | async findOne(id: number) { 124 | return this.prisma.shelter.findUnique({ 125 | where: { id }, 126 | }); 127 | } 128 | 129 | async update(id: number, updateShelterDto: UpdateShelterDto, userId: number) { 130 | 131 | return this.prisma.shelter.update({ 132 | where: { id }, 133 | data: { ...updateShelterDto, updatedBy: userId }, 134 | }); 135 | 136 | } 137 | 138 | async remove(id: number) { 139 | return this.prisma.shelter.delete({ 140 | where: { id }, 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/user/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Role } from 'src/enums/role.enum'; 4 | 5 | 6 | export class CreateUserDto { 7 | @ApiProperty({ description: 'Full name of the user' }) 8 | @IsNotEmpty() 9 | @IsString() 10 | name: string; 11 | 12 | @ApiProperty({ description: 'Phone number of the user' }) 13 | @IsNotEmpty() 14 | @IsString() 15 | phone: string; 16 | 17 | @ApiProperty({ description: 'Email address of the user' }) 18 | @IsEmail() 19 | email: string; 20 | 21 | @ApiProperty({ description: 'Password of the user' }) 22 | @IsString() 23 | @IsOptional() 24 | password: string; 25 | 26 | @ApiProperty({ description: 'Role of the user in the system'}) 27 | @IsEnum(Role) 28 | role: Role; 29 | } 30 | -------------------------------------------------------------------------------- /src/user/dto/read-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class UserReadDto { 4 | @ApiProperty({ description: 'Unique identifier of the user' }) 5 | id: number; 6 | 7 | @ApiProperty({ description: 'Name of the user' }) 8 | name: string; 9 | 10 | @ApiProperty({ description: 'Phone number of the user' }) 11 | phone: string; 12 | 13 | @ApiProperty({ description: 'Email address of the user' }) 14 | email: string; 15 | 16 | @ApiProperty({ description: 'Role of the user in the system' }) 17 | role: string; 18 | 19 | @ApiProperty({ description: 'Creation date of the user account' }) 20 | createdAt: Date; 21 | 22 | @ApiProperty({ description: 'Last update date of the user account' }) 23 | updatedAt: Date; 24 | 25 | @ApiProperty({ description: 'Last update user id' }) 26 | updatedBy: String; 27 | } 28 | -------------------------------------------------------------------------------- /src/user/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | import { Role } from 'src/enums/role.enum'; 4 | 5 | export class UpdateUserDto { 6 | @ApiPropertyOptional({ description: 'Full name of the user' }) 7 | @IsString() 8 | @IsOptional() 9 | name?: string; 10 | 11 | @ApiPropertyOptional({ description: 'Phone number of the user' }) 12 | @IsString() 13 | @IsOptional() 14 | phone?: string; 15 | 16 | @ApiPropertyOptional({ description: 'Email address of the user' }) 17 | @IsEmail() 18 | @IsOptional() 19 | email?: string; 20 | 21 | @ApiPropertyOptional({ description: 'Password of the user' }) 22 | @IsString() 23 | password: string; 24 | 25 | @ApiPropertyOptional({ description: 'Role of the user in the system', enum: ['admin', 'volunteer'] }) 26 | @IsEnum(Role) 27 | role?: Role; 28 | } 29 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; 2 | import { ApiTags, ApiOperation, ApiResponse, ApiQuery, ApiBearerAuth } from '@nestjs/swagger'; 3 | import { UserService } from './user.service'; 4 | import { CreateUserDto } from './dto/create-user.dto'; 5 | import { UpdateUserDto } from './dto/update-user.dto'; 6 | import { Role } from 'src/enums/role.enum'; 7 | import { AuthGuard } from '@nestjs/passport'; 8 | import { GetUserId } from 'src/decorators/get-user-id.decorator'; 9 | 10 | @ApiTags('users') 11 | @Controller('users') 12 | export class UserController { 13 | constructor(private readonly userService: UserService) {} 14 | 15 | @Get() 16 | @ApiOperation({ summary: 'Retrieve all users with optional pagination' }) 17 | @ApiQuery({ name: 'page', required: false, type: 'number' }) 18 | @ApiResponse({ status: 200, description: 'Returned all users successfully along with total count.' }) 19 | async findAll(@Query('page') page?: number) { 20 | const usersAndTotal = await this.userService.findAll(page, Number(process.env.PAGE_LIMIT)); 21 | return { 22 | data: usersAndTotal.data, 23 | total: usersAndTotal.total 24 | }; 25 | } 26 | 27 | @Get('role') 28 | @ApiOperation({ summary: 'Retrieve users by role with optional pagination' }) 29 | @ApiQuery({ name: 'role', required: true }) 30 | @ApiQuery({ name: 'page', required: false, type: 'number' }) 31 | @ApiResponse({ status: 200, description: 'Returned users with specified role along with total count.' }) 32 | async findByRole(@Query('role') role: Role, @Query('page') page?: number) { 33 | const usersAndTotal = await this.userService.findAllWithRole(role, page, Number(process.env.PAGE_LIMIT) ); 34 | return { 35 | data: usersAndTotal.data, 36 | total: usersAndTotal.total 37 | }; 38 | } 39 | 40 | @ApiBearerAuth('BearerAuth') 41 | @UseGuards(AuthGuard('jwt')) 42 | @Put(':id') 43 | @ApiOperation({ summary: 'Update a user by id' }) 44 | @ApiResponse({ status: 200, description: 'User updated successfully.' }) 45 | async update(@Param('id') id: number, @Body() updateUserDto: UpdateUserDto, @GetUserId() userId: number) { 46 | return this.userService.update(id, updateUserDto, userId); 47 | } 48 | 49 | @ApiBearerAuth('BearerAuth') 50 | @UseGuards(AuthGuard('jwt')) 51 | @Delete(':id') 52 | @ApiOperation({ summary: 'Delete a user by id' }) 53 | @ApiResponse({ status: 200, description: 'User deleted successfully.' }) 54 | async remove(@Param('id') id: number) { 55 | return this.userService.remove(id); 56 | } 57 | 58 | @ApiBearerAuth('BearerAuth') 59 | @UseGuards(AuthGuard('jwt')) 60 | @Post('volunteer') 61 | @ApiOperation({ summary: 'Create a new volunteer' }) 62 | @ApiResponse({ status: 201, description: 'Volunteer created successfully.' }) 63 | async createVolunteer(@Body() createUserDto: CreateUserDto, @GetUserId() userId: number) { 64 | return this.userService.createVolunteer(createUserDto, userId); 65 | } 66 | 67 | @ApiBearerAuth('BearerAuth') 68 | @UseGuards(AuthGuard('jwt')) 69 | @Post('admin') 70 | @ApiOperation({ summary: 'Create a new admin' }) 71 | @ApiResponse({ status: 201, description: 'Admin created successfully.' }) 72 | async createAdmin(@Body() createUserDto: CreateUserDto, @GetUserId() userId: number) { 73 | return this.userService.createAdmin(createUserDto, userId); 74 | } 75 | 76 | @Post('reset-password') 77 | @ApiOperation({ summary: 'Reset password' }) 78 | @ApiQuery({ name: 'email', required: true, type: 'string' }) 79 | async resetPassword(@Query('email') email: string) { 80 | return await this.userService.resetUserPassword(email); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | import { PrismaService } from 'prisma/prisma.service'; 5 | import { AuthModule } from '../auth/auth.module'; 6 | import { EmailModule } from '../mail/mail.module'; 7 | 8 | @Module({ 9 | imports: [AuthModule, EmailModule], 10 | controllers: [UserController], 11 | providers: [UserService, PrismaService], 12 | exports: [UserService] 13 | }) 14 | 15 | export class UserModule {} 16 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { ConflictException, Injectable } from '@nestjs/common'; 2 | import { PrismaService } from 'prisma/prisma.service'; 3 | import { CreateUserDto } from './dto/create-user.dto'; 4 | import { UpdateUserDto } from './dto/update-user.dto'; 5 | import { Role } from 'src/enums/role.enum'; 6 | import { AuthService } from 'src/auth/auth.service'; 7 | import { UserReadDto } from './dto/read-user.dto'; 8 | import { EmailService } from 'src/mail/mail.service'; 9 | import { generateRandomPassword } from 'src/utils/generateRandomPassword'; 10 | 11 | @Injectable() 12 | export class UserService { 13 | constructor( 14 | private prisma: PrismaService, 15 | private authService: AuthService, 16 | private mailService: EmailService 17 | ) {} 18 | 19 | private toReadUser(user: any): UserReadDto { 20 | return { 21 | id: user.id, 22 | name: user.name, 23 | email: user.email, 24 | role: user.role, 25 | phone: user.phone, 26 | createdAt: user.createdAt, 27 | updatedAt: user.updatedAt, 28 | updatedBy: user.updatedBy 29 | }; 30 | } 31 | 32 | private toReadUsers(users: any[]): UserReadDto[] { 33 | return users.map(user => ({ 34 | id: user.id, 35 | name: user.name, 36 | email: user.email, 37 | role: user.role, 38 | phone: user.phone, 39 | createdAt: user.createdAt, 40 | updatedAt: user.updatedAt, 41 | updatedBy: user.updatedBy 42 | })); 43 | } 44 | 45 | async createAdmin(createUserDto: CreateUserDto, createdByUserId: number) { 46 | 47 | const existingUser = await this.prisma.user.findUnique({ 48 | where: { 49 | email: createUserDto.email, 50 | }, 51 | }); 52 | 53 | if (existingUser) { 54 | throw new ConflictException('Email already exists'); 55 | } 56 | 57 | const hashedPassword = await this.authService.hashPassword(createUserDto.password); 58 | 59 | const user = await this.prisma.user.create({ 60 | data: { 61 | ...createUserDto, 62 | password: hashedPassword, 63 | role: Role.Admin, 64 | updatedBy: createdByUserId, 65 | }, 66 | }); 67 | 68 | return this.toReadUser(user) 69 | 70 | } 71 | 72 | async createVolunteer(createUserDto: CreateUserDto, createdByUserId: number) { 73 | 74 | const existingUser = await this.prisma.user.findUnique({ 75 | where: { 76 | email: createUserDto.email, 77 | }, 78 | }); 79 | 80 | if (existingUser) { 81 | throw new ConflictException('Email already exists'); 82 | } 83 | 84 | const defaultPassword = generateRandomPassword(8); 85 | const hashedPassword = await this.authService.hashPassword(defaultPassword); 86 | 87 | const user = await this.prisma.user.create({ 88 | data: { 89 | ...createUserDto, 90 | password: hashedPassword, 91 | role: Role.Volunteer, 92 | updatedBy: createdByUserId, 93 | }, 94 | }); 95 | 96 | await this.mailService.sendWelcomeEmail({ 97 | name: user.name, 98 | email: user.email, 99 | password: defaultPassword 100 | }); 101 | 102 | return this.toReadUser(user) 103 | 104 | } 105 | 106 | async resetUserPassword(email: string) { 107 | const user = await this.prisma.user.findUnique({ 108 | where: { email }, 109 | }); 110 | 111 | if (!user) { 112 | throw new ConflictException('Usuário não encontrado.'); 113 | } 114 | 115 | const newPassword = generateRandomPassword(8); 116 | const hashedPassword = await this.authService.hashPassword(newPassword); 117 | 118 | await this.prisma.user.update({ 119 | where: { email }, 120 | data: { password: hashedPassword }, 121 | }); 122 | 123 | await this.mailService.sendRecoverPasswordEmail({ 124 | name: user.name, 125 | email: user.email, 126 | password: newPassword 127 | }); 128 | 129 | return { message: "Senha resetada com sucesso e e-mail enviado." }; 130 | 131 | } 132 | 133 | 134 | async findAll(page?: number, limit?: number): Promise<{ data: UserReadDto[], total: number }> { 135 | 136 | let queryParams: any = {}; 137 | if (page) { 138 | const pageNumber = Number(page); 139 | queryParams.skip = (pageNumber - 1) * limit; 140 | queryParams.take = limit; 141 | } 142 | 143 | const users = await this.prisma.user.findMany(queryParams); 144 | const total = await this.prisma.user.count(); 145 | 146 | return { data: this.toReadUsers(users), total }; 147 | } 148 | 149 | 150 | async findAllWithRole(role: Role, page?: number, limit?: number ): Promise<{ data: UserReadDto[], total: number }> { 151 | 152 | let queryParams: any = { 153 | where: { role: role } 154 | }; 155 | if (page) { 156 | const pageNumber = Number(page); 157 | queryParams.skip = (pageNumber - 1) * limit; 158 | queryParams.take = limit; 159 | } 160 | 161 | const users = await this.prisma.user.findMany(queryParams); 162 | const total = await this.prisma.user.count({ where: { role: role } }); 163 | 164 | return { data: this.toReadUsers(users), total }; 165 | } 166 | 167 | 168 | async update(id: number, updateUserDto: UpdateUserDto, updatedByUserId: number) { 169 | 170 | if(updateUserDto.email) { 171 | const existingUser = await this.prisma.user.findUnique({ 172 | where: { 173 | email: updateUserDto.email, 174 | }, 175 | }); 176 | 177 | if (existingUser && existingUser.id !== id) { 178 | throw new ConflictException('Email already exists'); 179 | } 180 | } 181 | 182 | if(updateUserDto.password){ 183 | const hashedPassword = await this.authService.hashPassword(updateUserDto.password); 184 | updateUserDto.password = hashedPassword 185 | } 186 | 187 | 188 | return this.prisma.user.update({ 189 | where: { id: id }, 190 | data: {...updateUserDto, 191 | updatedBy: updatedByUserId, 192 | }, 193 | }); 194 | } 195 | 196 | async remove(id: number) { 197 | return this.prisma.user.delete({ 198 | where: { id: id }, 199 | }); 200 | } 201 | 202 | } 203 | 204 | -------------------------------------------------------------------------------- /src/utils/generateRandomPassword.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | 3 | export function generateRandomPassword(length: number): string { 4 | const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 5 | const randomValues = randomBytes(length); 6 | let result = ''; 7 | for (let i = 0; i < length; i++) { 8 | result += charset[randomValues[i] % charset.length]; 9 | } 10 | return result; 11 | } 12 | -------------------------------------------------------------------------------- /src/validation/user-validation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as bcrypt from 'bcryptjs'; 3 | import { PrismaService } from 'prisma/prisma.service'; 4 | 5 | @Injectable() 6 | export class UserValidationService { 7 | constructor(private prisma: PrismaService) {} 8 | 9 | async validateUser(email: string, pass: string): Promise { 10 | const user = await this.prisma.user.findUnique({ 11 | where: { email }, 12 | select: { 13 | id: true, 14 | email: true, 15 | password: true, 16 | role: true, 17 | }, 18 | }); 19 | 20 | if (!user) return null; 21 | 22 | const isPasswordValid = await bcrypt.compare(pass, user.password); 23 | if (!isPasswordValid) return null; 24 | 25 | const { password, ...result } = user; 26 | return result; 27 | } 28 | 29 | async findUserById(id: number) { 30 | return this.prisma.user.findUnique({ 31 | where: { id }, 32 | }); 33 | } 34 | 35 | async findUserByEmail(email: string) { 36 | return this.prisma.user.findUnique({ 37 | where: { email }, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------