├── .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 |
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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
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 |
--------------------------------------------------------------------------------