auth services implmented

This commit is contained in:
Berkay 2025-07-25 22:46:37 +03:00
parent 8ca2d34dc6
commit b4b752ca3a
92 changed files with 5282 additions and 296 deletions

View File

@ -25,3 +25,26 @@ docker builder prune --all
npm install @prisma/client
npm install -D prisma
npm install class-validator class-transformer --legacy-peer-deps
npx prisma generate # generate client
npx prisma db pull # update local schema
npx prisma db seed # seed database
nest generate module
nest generate service
nest generate controller
or / alias
nest g module
nest g service
nest g controller
npm install @liaoliaots/nestjs-redis ioredis --legacy-peer-deps
npx prisma migrate dev --name "comment" # good for production creates step of migration
npx prisma db push # update remote schema # not good for production creates no step of migration
npx prisma validate
npx prisma format

View File

@ -3,12 +3,12 @@ FROM node:20-alpine AS builder
WORKDIR /usr/src/app
COPY backend/package*.json ./
COPY ServicesApi/package*.json ./
RUN npm install -g npm@latest
RUN npm ci
RUN npm ci --legacy-peer-deps
COPY backend .
COPY ServicesApi .
RUN npm run build

View File

@ -9,12 +9,18 @@
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@liaoliaots/nestjs-redis": "^10.0.0",
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@prisma/client": "^6.12.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"ioredis": "^5.6.1",
"redis": "^5.6.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@ -1292,6 +1298,12 @@
}
}
},
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"license": "MIT"
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@ -1916,6 +1928,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@liaoliaots/nestjs-redis": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@liaoliaots/nestjs-redis/-/nestjs-redis-10.0.0.tgz",
"integrity": "sha512-uCTmlzM4q+UYADwsJEQph0mbf4u0MrktFhByi50M5fNy/+fJoWlhSqrgvjtVKjHnqydxy1gyuU6vHJEOBp9cjg==",
"license": "MIT",
"dependencies": {
"tslib": "2.7.0"
},
"engines": {
"node": ">=16.13.0"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"ioredis": "^5.0.0"
}
},
"node_modules/@liaoliaots/nestjs-redis/node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"license": "0BSD"
},
"node_modules/@lukeed/csprng": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
@ -2780,7 +2815,7 @@
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.12.0.tgz",
"integrity": "sha512-HovZWzhWEMedHxmjefQBRZa40P81N7/+74khKFz9e1AFjakcIQdXgMWKgt20HaACzY+d1LRBC+L4tiz71t9fkg==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"jiti": "2.4.2"
@ -2790,14 +2825,14 @@
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.12.0.tgz",
"integrity": "sha512-plbz6z72orcqr0eeio7zgUrZj5EudZUpAeWkFTA/DDdXEj28YHDXuiakvR6S7sD6tZi+jiwQEJAPeV6J6m/tEQ==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.12.0.tgz",
"integrity": "sha512-4BRZZUaAuB4p0XhTauxelvFs7IllhPmNLvmla0bO1nkECs8n/o1pUvAVbQ/VOrZR5DnF4HED0PrGai+rIOVePA==",
"devOptional": true,
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@ -2811,14 +2846,14 @@
"version": "6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc.tgz",
"integrity": "sha512-70vhecxBJlRr06VfahDzk9ow4k1HIaSfVUT3X0/kZoHCMl9zbabut4gEXAyzJZxaCGi5igAA7SyyfBI//mmkbQ==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.12.0.tgz",
"integrity": "sha512-EamoiwrK46rpWaEbLX9aqKDPOd8IyLnZAkiYXFNuq0YsU0Z8K09/rH8S7feOWAVJ3xzeSgcEJtBlVDrajM9Sag==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.12.0",
@ -2830,12 +2865,72 @@
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.12.0.tgz",
"integrity": "sha512-nRerTGhTlgyvcBlyWgt8OLNIV7QgJS2XYXMJD1hysorMCuLAjuDDuoxmVt7C2nLxbuxbWPp7OuFRHC23HqD9dA==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.12.0"
}
},
"node_modules/@redis/bloom": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.1.tgz",
"integrity": "sha512-5/22U76IMEfn6TeZ+uvjXspHw+ykBF0kpBa8xouzeHaQMXs/auqBUOEYzU2VKYDvnd2RSpPTyIg82oB7PpUgLg==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.6.1"
}
},
"node_modules/@redis/client": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.1.tgz",
"integrity": "sha512-bWHmSFIJ5w1Y4aHsYs46XMDHKQsBHFRhNcllYaBxz2Zl+lu+gbm5yI9BqxvKh48bLTs/Wx1Kns0gN2WIasE8MA==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@redis/json": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.1.tgz",
"integrity": "sha512-cTggVzPIVuiFeXcEcnTRiUzV7rmUvM9KUYxWiHyjsAVACTEUe4ifKkvzrij0H/z3ammU5tfGACffDB3olBwtVA==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.6.1"
}
},
"node_modules/@redis/search": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.1.tgz",
"integrity": "sha512-+eOjx8O2YoKygjqkLpTHqcAq0zKLjior+ee2tRBx/3RSf1+OHxiC9Y6NstshQpvB1XHqTw9n7+f0+MsRJZrp0g==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.6.1"
}
},
"node_modules/@redis/time-series": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.1.tgz",
"integrity": "sha512-sd3q4jMJdoSO2akw1L9NrdFI1JJ6zeMgMUoTh4a34p9sY3AnOI4aDLCecy8L2IcPAP1oNR3TbLFJiCJDQ35QTA==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.6.1"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -3526,6 +3621,12 @@
"@types/superagent": "^8.1.0"
}
},
"node_modules/@types/validator": {
"version": "13.15.2",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz",
"integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==",
"license": "MIT"
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@ -4274,20 +4375,6 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-import-phases": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.13.0"
},
"peerDependencies": {
"acorn": "^8.14.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@ -5100,6 +5187,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/class-transformer": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
"license": "MIT"
},
"node_modules/class-validator": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
"integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
"license": "MIT",
"dependencies": {
"@types/validator": "^13.11.8",
"libphonenumber-js": "^1.11.1",
"validator": "^13.9.0"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@ -5218,6 +5322,15 @@
"node": ">=0.8"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -5593,6 +5706,15 @@
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -7149,6 +7271,30 @@
"kind-of": "^6.0.2"
}
},
"node_modules/ioredis": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz",
"integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -8083,7 +8229,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@ -8237,6 +8383,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/libphonenumber-js": {
"version": "1.12.10",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.10.tgz",
"integrity": "sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==",
"license": "MIT"
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -8296,6 +8448,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -9271,7 +9435,7 @@
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.12.0.tgz",
"integrity": "sha512-pmV7NEqQej9WjizN6RSNIwf7Y+jeh9mY1JEX2WjGxJi4YZWexClhde1yz/FuvAM+cTwzchcMytu2m4I6wPkIzg==",
"devOptional": true,
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@ -9465,6 +9629,43 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/redis": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/redis/-/redis-5.6.1.tgz",
"integrity": "sha512-O9DwAvcBm/lrlkGE0A6gNBtUdA8J9oD9njeLYlLzmm+MGTR7nd7VkpspfXqeXFg3gm89zldDqckyaHhXfhY80g==",
"license": "MIT",
"dependencies": {
"@redis/bloom": "5.6.1",
"@redis/client": "5.6.1",
"@redis/json": "5.6.1",
"@redis/search": "5.6.1",
"@redis/time-series": "5.6.1"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
@ -10045,6 +10246,12 @@
"node": ">=8"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@ -10949,7 +11156,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@ -11091,6 +11298,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@ -11113,6 +11333,15 @@
"node": ">=10.12.0"
}
},
"node_modules/validator": {
"version": "13.15.15",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -11169,56 +11398,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/webpack": {
"version": "5.100.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.2.tgz",
"integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.24.0",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.2",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.11",
"json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.2",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1",
"webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependenciesMeta": {
"webpack-cli": {
"optional": true
}
}
},
"node_modules/webpack-node-externals": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz",
@ -11239,137 +11418,6 @@
"node": ">=10.13.0"
}
},
"node_modules/webpack/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/webpack/node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/webpack/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/webpack/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/webpack/node_modules/estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/webpack/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/webpack/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/webpack/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/webpack/node_modules/schema-utils": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -20,12 +20,18 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@liaoliaots/nestjs-redis": "^10.0.0",
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@prisma/client": "^6.12.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"ioredis": "^5.6.1",
"redis": "^5.6.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",

View File

@ -1,11 +0,0 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { PrismaService } from '@/prisma.service';
import { PrismaService } from '@/src/prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService], // Dışarııyoruz ki başka modüller kullanabilsin
exports: [PrismaService],
})
export class PrismaModule {}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AccountsController } from './accounts.controller';
describe('AccountsController', () => {
let controller: AccountsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AccountsController],
}).compile();
controller = module.get<AccountsController>(AccountsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,39 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
HttpCode,
} from '@nestjs/common';
import { AccountsService } from './accounts.service';
@Controller('accounts')
export class AccountsController {
constructor(private accountsService: AccountsService) {}
@Post('filter')
@HttpCode(200)
async filterAccounts(@Body() query: any) {
const result = await this.accountsService.findWithPagination(query);
const { pagination, data } = result;
if (data.length === 0) {
return { pagination, data: [] };
}
const resultRefined = data.map((rec: any) => ({
...rec,
build_decision_book_payments: rec.build_decision_book_payments?.map(
(pmt: any) => ({
...pmt,
ratePercent:
((pmt.payment_amount / rec.currency_value) * 100).toFixed(2) + '%',
}),
),
}));
return { pagination, data: resultRefined };
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { AccountsService } from './accounts.service';
import { AccountsController } from './accounts.controller';
import { PrismaModule } from '@/prisma/prisma.module';
import { CacheService } from '../cache.service';
import { UtilsModule } from '../utils/utils.module';
@Module({
imports: [PrismaModule, UtilsModule],
providers: [AccountsService, CacheService],
controllers: [AccountsController],
})
export class AccountsModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AccountsService } from './accounts.service';
describe('AccountsService', () => {
let service: AccountsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AccountsService],
}).compile();
service = module.get<AccountsService>(AccountsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/src/prisma.service';
import { Prisma, account_records } from '@prisma/client';
import { CacheService } from '../cache.service';
import { PaginationHelper, PaginationInfo } from '../utils/pagination-helper';
@Injectable()
export class AccountsService {
constructor(
private prisma: PrismaService,
private cacheService: CacheService,
private paginationHelper: PaginationHelper,
) {}
async findAll(filter: any): Promise<Partial<account_records>[]> {
return this.prisma.account_records.findMany({
where: { ...filter },
});
}
async findDynamic(
query: Prisma.account_recordsFindManyArgs,
): Promise<{ totalCount: number; result: Partial<account_records>[] }> {
const totalCount = await this.prisma.account_records.count({
where: query.where,
});
const result = await this.prisma.account_records.findMany(query);
return { totalCount, result };
}
async findWithPagination(
query: any & { page?: number; pageSize?: number },
): Promise<{ data: any[]; pagination: PaginationInfo }> {
return this.paginationHelper.paginate(this.prisma.account_records, query);
}
async findOne(uuid: string): Promise<Partial<account_records> | null> {
return this.prisma.account_records.findUnique({
where: { uu_id: uuid },
});
}
}

View File

@ -0,0 +1,36 @@
http://localhost:3000/accounts/filter
{
"where": {
"build_parts": {
"part_code": {
"contains": "10",
"mode": "insensitive"
}
}
},
"select": {
"process_comment": true,
"bank_date": true,
"currency_value": true,
"build_parts": {
"select": {
"part_code": true
}
},
"build_decision_book_payments": {
"select": {
"payment_amount": true,
"process_date": true,
"build_decision_book_items": {
"select": {
"item_order": true,
"item_comment": true
}
}
}
}
},
"page": 2,
"pageSize": 5
}

View File

@ -1,12 +1,50 @@
import { Module } from '@nestjs/common';
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { PrismaModule } from '@/prisma/prisma.module';
import { AccountsModule } from './accounts/accounts.module';
import { AuthModule } from './auth/auth.module';
import { RedisModule } from '@liaoliaots/nestjs-redis';
import { CacheService } from './cache.service';
import { LoggerMiddleware } from '@/src/middleware/logger.middleware';
const redisConfig = {
host: '10.10.2.15',
port: 6379,
password: 'your_strong_password_here',
};
const modulesList = [UsersModule, AccountsModule, AuthModule];
const serviceModuleList = [
PrismaModule,
RedisModule.forRoot({
config: redisConfig,
}),
];
const controllersList = [AppController];
const providersList = [AppService, CacheService];
const exportsList = [CacheService];
@Module({
imports: [PrismaModule, UsersModule],
controllers: [AppController],
providers: [AppService],
imports: [...modulesList, ...serviceModuleList],
controllers: controllersList,
providers: providersList,
exports: exportsList,
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'accounts', method: RequestMethod.ALL },
{ path: 'users/*path', method: RequestMethod.ALL },
)
.forRoutes('*');
}
}

View File

@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { LoginModule } from '@/src/auth/login/login.module';
import { SelectModule } from '@/src/auth/select/select.module';
import { PasswordService } from '@/src/auth/password/password.service';
import { PasswordModule } from '@/src/auth/password/password.module';
import { LogoutModule } from '@/src/auth/logout/logout.module';
import { DisconnectModule } from '@/src/auth/disconnect/disconnect.module';
import { TokenModule } from '@/src/auth/token/token.module';
@Module({
imports: [
LoginModule,
LogoutModule,
SelectModule,
PasswordModule,
DisconnectModule,
TokenModule,
],
providers: [PasswordService],
})
export class AuthModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DisconnectController } from './disconnect.controller';
describe('DisconnectController', () => {
let controller: DisconnectController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DisconnectController],
}).compile();
controller = module.get<DisconnectController>(DisconnectController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('disconnect')
export class DisconnectController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { DisconnectService } from './disconnect.service';
import { DisconnectController } from './disconnect.controller';
@Module({
providers: [DisconnectService],
controllers: [DisconnectController]
})
export class DisconnectModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DisconnectService } from './disconnect.service';
describe('DisconnectService', () => {
let service: DisconnectService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DisconnectService],
}).compile();
service = module.get<DisconnectService>(DisconnectService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class DisconnectService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LoginController } from './login.controller';
describe('LoginController', () => {
let controller: LoginController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [LoginController],
}).compile();
controller = module.get<LoginController>(LoginController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('login')
export class LoginController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { LoginController } from './login.controller';
import { LoginService } from './login.service';
@Module({
controllers: [LoginController],
providers: [LoginService]
})
export class LoginModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LoginService } from './login.service';
describe('LoginService', () => {
let service: LoginService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LoginService],
}).compile();
service = module.get<LoginService>(LoginService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class LoginService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LogoutController } from './logout.controller';
describe('LogoutController', () => {
let controller: LogoutController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [LogoutController],
}).compile();
controller = module.get<LogoutController>(LogoutController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('logout')
export class LogoutController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { LogoutService } from './logout.service';
import { LogoutController } from './logout.controller';
@Module({
providers: [LogoutService],
controllers: [LogoutController]
})
export class LogoutModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LogoutService } from './logout.service';
describe('LogoutService', () => {
let service: LogoutService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LogoutService],
}).compile();
service = module.get<LogoutService>(LogoutService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class LogoutService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ChangeController } from './change.controller';
describe('ChangeController', () => {
let controller: ChangeController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ChangeController],
}).compile();
controller = module.get<ChangeController>(ChangeController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('change')
export class ChangeController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ChangeService } from './change.service';
import { ChangeController } from './change.controller';
@Module({
providers: [ChangeService],
controllers: [ChangeController]
})
export class ChangeModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ChangeService } from './change.service';
describe('ChangeService', () => {
let service: ChangeService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ChangeService],
}).compile();
service = module.get<ChangeService>(ChangeService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class ChangeService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CreateController } from './create.controller';
describe('CreateController', () => {
let controller: CreateController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CreateController],
}).compile();
controller = module.get<CreateController>(CreateController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('create')
export class CreateController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { CreateService } from './create.service';
import { CreateController } from './create.controller';
@Module({
providers: [CreateService],
controllers: [CreateController]
})
export class CreateModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CreateService } from './create.service';
describe('CreateService', () => {
let service: CreateService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CreateService],
}).compile();
service = module.get<CreateService>(CreateService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class CreateService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PasswordController } from './password.controller';
describe('PasswordController', () => {
let controller: PasswordController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PasswordController],
}).compile();
controller = module.get<PasswordController>(PasswordController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('password')
export class PasswordController {}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PasswordController } from './password.controller';
import { CreateModule } from './create/create.module';
import { ChangeModule } from './change/change.module';
import { ResetModule } from './reset/reset.module';
import { VerifyOtpModule } from './verify-otp/verify-otp.module';
@Module({
controllers: [PasswordController],
imports: [CreateModule, ChangeModule, ResetModule, VerifyOtpModule]
})
export class PasswordModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PasswordService } from './password.service';
describe('PasswordService', () => {
let service: PasswordService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PasswordService],
}).compile();
service = module.get<PasswordService>(PasswordService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class PasswordService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ResetController } from './reset.controller';
describe('ResetController', () => {
let controller: ResetController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ResetController],
}).compile();
controller = module.get<ResetController>(ResetController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('reset')
export class ResetController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ResetService } from './reset.service';
import { ResetController } from './reset.controller';
@Module({
providers: [ResetService],
controllers: [ResetController]
})
export class ResetModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ResetService } from './reset.service';
describe('ResetService', () => {
let service: ResetService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ResetService],
}).compile();
service = module.get<ResetService>(ResetService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class ResetService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { VerifyOtpController } from './verify-otp.controller';
describe('VerifyOtpController', () => {
let controller: VerifyOtpController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [VerifyOtpController],
}).compile();
controller = module.get<VerifyOtpController>(VerifyOtpController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('verify-otp')
export class VerifyOtpController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { VerifyOtpService } from './verify-otp.service';
import { VerifyOtpController } from './verify-otp.controller';
@Module({
providers: [VerifyOtpService],
controllers: [VerifyOtpController]
})
export class VerifyOtpModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { VerifyOtpService } from './verify-otp.service';
describe('VerifyOtpService', () => {
let service: VerifyOtpService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [VerifyOtpService],
}).compile();
service = module.get<VerifyOtpService>(VerifyOtpService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class VerifyOtpService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SelectController } from './select.controller';
describe('SelectController', () => {
let controller: SelectController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SelectController],
}).compile();
controller = module.get<SelectController>(SelectController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('select')
export class SelectController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { SelectController } from './select.controller';
import { SelectService } from './select.service';
@Module({
controllers: [SelectController],
providers: [SelectService]
})
export class SelectModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SelectService } from './select.service';
describe('SelectService', () => {
let service: SelectService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SelectService],
}).compile();
service = module.get<SelectService>(SelectService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class SelectService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CheckController } from './check.controller';
describe('CheckController', () => {
let controller: CheckController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CheckController],
}).compile();
controller = module.get<CheckController>(CheckController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('check')
export class CheckController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { CheckService } from './check.service';
import { CheckController } from './check.controller';
@Module({
providers: [CheckService],
controllers: [CheckController]
})
export class CheckModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CheckService } from './check.service';
describe('CheckService', () => {
let service: CheckService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CheckService],
}).compile();
service = module.get<CheckService>(CheckService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class CheckService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RefreshController } from './refresh.controller';
describe('RefreshController', () => {
let controller: RefreshController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RefreshController],
}).compile();
controller = module.get<RefreshController>(RefreshController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('refresh')
export class RefreshController {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { RefreshService } from './refresh.service';
import { RefreshController } from './refresh.controller';
@Module({
providers: [RefreshService],
controllers: [RefreshController]
})
export class RefreshModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RefreshService } from './refresh.service';
describe('RefreshService', () => {
let service: RefreshService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RefreshService],
}).compile();
service = module.get<RefreshService>(RefreshService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class RefreshService {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TokenController } from './token.controller';
describe('TokenController', () => {
let controller: TokenController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TokenController],
}).compile();
controller = module.get<TokenController>(TokenController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('token')
export class TokenController {}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TokenService } from './token.service';
import { TokenController } from './token.controller';
import { CheckModule } from './check/check.module';
import { RefreshModule } from './refresh/refresh.module';
@Module({
providers: [TokenService],
controllers: [TokenController],
imports: [CheckModule, RefreshModule],
})
export class TokenModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TokenService } from './token.service';
describe('TokenService', () => {
let service: TokenService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TokenService],
}).compile();
service = module.get<TokenService>(TokenService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class TokenService {}

View File

@ -0,0 +1,102 @@
import { Injectable } from '@nestjs/common';
import { RedisService } from '@liaoliaots/nestjs-redis';
import Redis from 'ioredis';
@Injectable()
export class CacheService {
private client: Redis;
constructor(private readonly redisService: RedisService) {
this.client = this.redisService.getOrThrow();
}
async set(key: string, value: any) {
await this.client.set(key, JSON.stringify(value));
}
async get(key: string): Promise<any | null> {
const value = await this.client.get(key);
if (!value) {
return null;
}
return JSON.parse(value);
}
async get_with_keys(listKeys: (string | null)[]): Promise<any | null> {
const joinKeys = this.createRegexPattern(listKeys);
const value = await this.client.get(joinKeys);
if (!value) {
return null;
}
return JSON.parse(value);
}
async set_with_ttl(key: string, value: any, ttl: number) {
await this.client.set(key, JSON.stringify(value), 'EX', ttl);
}
async check_ttl(key: string): Promise<number> {
return this.client.ttl(key);
}
async delete(key: string): Promise<boolean> {
const deleted = await this.client.del(key);
if (deleted === 0) {
return false;
}
return true;
}
/**
* Delete multiple keys matching a pattern.
* Simplified version that just returns the count of deleted keys.
*
* @param listKeys - List of key components to form pattern for deletion.
* @returns Number of deleted keys
*/
async deleteMultiple(listKeys: (string | null)[]): Promise<number> {
const regex = this.createRegexPattern(listKeys);
const keys: string[] = [];
let cursor = '0';
do {
const [nextCursor, matchedKeys] = await this.client.scan(
cursor,
'MATCH',
regex,
);
cursor = nextCursor;
keys.push(...matchedKeys);
} while (cursor !== '0');
if (keys.length === 0) {
return 0;
}
let deletedCount = 0;
for (const key of keys) {
const result = await this.client.del(key);
deletedCount += result;
}
return deletedCount;
}
/**
* Create a regex pattern from list of keys
* This is a simplified implementation - adjust according to your needs
*/
/**
* Create a regex pattern from list of keys
* Replaces null/undefined values with '*' wildcards
* @param listKeys Array of key components, can contain null/undefined values
* @returns Redis pattern string with wildcards
*/
createRegexPattern(listKeys: (string | null)[]): string {
return listKeys.map((key) => (key === null ? '*' : key)).join(':') + '*';
}
async check(key: string): Promise<boolean> {
const exists = await this.client.exists(key);
return exists === 1;
}
}

View File

@ -0,0 +1,10 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[LoggerMiddleware] ${req.method} ${req.originalUrl}`);
next();
}
}

View File

@ -0,0 +1,90 @@
// Token type redis base TOKEN object
enum UserType {
employee = 1,
occupant = 2,
}
interface Credentials {
person_id: number;
person_name: string;
}
interface ApplicationToken {
// Application Token Object -> is the main object for the user
user_type: number;
credential_token: string;
user_uu_id: string;
user_id: number;
person_id: number;
person_uu_id: string;
request?: Record<string, any>; // Request Info of Client
expires_at?: number; // Expiry timestamp
reachable_event_codes?: Record<string, any>; // ID list of reachable event codes as "endpoint_code": ["UUID", "UUID"]
reachable_app_codes?: Record<string, any>; // ID list of reachable applications as "page_url": ["UUID", "UUID"]
}
interface OccupantToken {
// Selection of the occupant type for a build part is made by the user
living_space_id: number; // Internal use
living_space_uu_id: string; // Outer use
occupant_type_id: number;
occupant_type_uu_id: string;
occupant_type: string;
build_id: number;
build_uuid: string;
build_part_id: number;
build_part_uuid: string;
responsible_company_id?: number;
responsible_company_uuid?: string;
responsible_employee_id?: number;
responsible_employee_uuid?: string;
}
interface CompanyToken {
// Selection of the company for an employee is made by the user
company_id: number;
company_uu_id: string;
department_id: number; // ID list of departments
department_uu_id: string; // UUID list of departments
duty_id: number;
duty_uu_id: string;
staff_id: number;
staff_uu_id: string;
employee_id: number;
employee_uu_id: string;
bulk_duties_id: number;
}
interface OccupantTokenObject extends ApplicationToken {
// Occupant Token Object -> Requires selection of the occupant type for a specific build part
available_occupants: Record<string, any> | null;
selected?: Record<string, any>; // Selected Occupant Type
is_employee: boolean; // Always false
is_occupant: boolean; // Always true
}
interface EmployeeTokenObject extends ApplicationToken {
// Full hierarchy Employee[staff_id] -> Staff -> Duty -> Department -> Company
companies_id_list: number[]; // List of company objects
companies_uu_id_list: string[]; // UUID list of company objects
duty_id_list: number[]; // List of duty objects
duty_uu_id_list: string[]; // UUID list of duty objects
selected?: Record<string, any>; // Selected Company Object
is_employee: boolean; // Always true
is_occupant: boolean; // Always false
}
// Union type for token objects
type TokenDictType = EmployeeTokenObject | OccupantTokenObject;
export {
UserType,
Credentials,
ApplicationToken,
OccupantToken,
CompanyToken,
OccupantTokenObject,
EmployeeTokenObject,
TokenDictType,
};

View File

@ -0,0 +1,3 @@
export function processUsers(): void {
console.log('Processing users New...');
}

View File

@ -6,39 +6,17 @@ import {
Delete,
Param,
Body,
HttpCode,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from '@prisma/client';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
async findAll(): Promise<User[]> {
return this.usersService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<User | null> {
return this.usersService.findOne(Number(id));
}
@Post()
async create(@Body() data: { name: string; email: string }): Promise<User> {
return this.usersService.create(data);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() data: Partial<{ name: string; email: string }>,
): Promise<User> {
return this.usersService.update(Number(id), data);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<User> {
return this.usersService.remove(Number(id));
@Post('filter')
@HttpCode(200)
async filterUsers(@Body() query: any) {
return this.usersService.findWithPagination(query);
}
}

View File

@ -2,10 +2,12 @@ import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from '@/prisma/prisma.module';
import { CacheService } from '../cache.service';
import { UtilsModule } from '../utils/utils.module';
@Module({
imports: [PrismaModule],
providers: [UsersService],
imports: [PrismaModule, UtilsModule],
providers: [UsersService, CacheService],
controllers: [UsersController],
exports: [UsersService],
})

View File

@ -1,31 +1,28 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma.service';
import { User } from '@prisma/client';
import { PrismaService } from '@/src/prisma.service';
import { users } from '@prisma/client';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findAll(): Promise<User[]> {
return this.prisma.user.findMany();
async findAll(): Promise<users[]> {
return this.prisma.users.findMany();
}
async findOne(id: number): Promise<User | null> {
return this.prisma.user.findUnique({ where: { id } });
async findOne(id: number): Promise<users | null> {
return this.prisma.users.findUnique({ where: { id } });
}
async create(data: { name: string; email: string }): Promise<User> {
return this.prisma.user.create({ data });
async create(data: any): Promise<users> {
return this.prisma.users.create({ data });
}
async update(
id: number,
data: Partial<{ name: string; email: string }>,
): Promise<User> {
return this.prisma.user.update({ where: { id }, data });
async update(id: number, data: any): Promise<users> {
return this.prisma.users.update({ where: { id }, data });
}
async remove(id: number): Promise<User> {
return this.prisma.user.delete({ where: { id } });
async remove(id: number): Promise<users> {
return this.prisma.users.delete({ where: { id } });
}
}

View File

@ -1,31 +1,42 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma.service';
import { User } from '@prisma/client';
import { PrismaService } from '@/src/prisma.service';
import { Prisma, users } from '@prisma/client';
import { CacheService } from '../cache.service';
import { PaginationHelper, PaginationInfo } from '../utils/pagination-helper';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
private cacheService: CacheService,
private paginationHelper: PaginationHelper,
) {}
async findAll(): Promise<User[]> {
return this.prisma.user.findMany();
async findAll(filter: any): Promise<Partial<users>[]> {
return this.prisma.users.findMany({
where: { ...filter },
});
}
async findOne(id: number): Promise<User | null> {
return this.prisma.user.findUnique({ where: { id } });
async findDynamic(
query: Prisma.usersFindManyArgs,
): Promise<{ totalCount: number; result: Partial<users>[] }> {
const totalCount = await this.prisma.users.count({
where: query.where,
});
const result = await this.prisma.users.findMany(query);
return { totalCount, result };
}
async create(data: { name: string; email: string }): Promise<User> {
return this.prisma.user.create({ data });
async findWithPagination(
query: any & { page?: number; pageSize?: number },
): Promise<{ data: any[]; pagination: PaginationInfo }> {
return this.paginationHelper.paginate(this.prisma.users, query);
}
async update(
id: number,
data: Partial<{ name: string; email: string }>,
): Promise<User> {
return this.prisma.user.update({ where: { id }, data });
}
async remove(id: number): Promise<User> {
return this.prisma.user.delete({ where: { id } });
async findOne(uuid: string): Promise<Partial<users> | null> {
return this.prisma.users.findUnique({
where: { uu_id: uuid },
});
}
}

View File

@ -0,0 +1,65 @@
import crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid';
interface TokenConfig {
ACCESS_TOKEN_LENGTH: number;
REFRESHER_TOKEN_LENGTH: number;
}
const tokenConfig: TokenConfig = {
ACCESS_TOKEN_LENGTH: 64,
REFRESHER_TOKEN_LENGTH: 128,
};
class PasswordHandlers {
generate_random_uu_id(is_string: boolean = true): string {
return is_string ? uuidv4().toString() : uuidv4();
}
create_hashed_password(
domain: string,
uuid: string,
password: string,
): string {
const data = `${domain}:${uuid}:${password}`;
return crypto.createHash('sha256').update(data).digest('hex');
}
check_password(
domain: string,
uuid: string,
password: string,
hashed_password: string,
): boolean {
return (
this.create_hashed_password(domain, uuid, password) === hashed_password
);
}
generateAccessToken(): string {
return this.generateToken(tokenConfig.ACCESS_TOKEN_LENGTH);
}
generateRefreshToken(): string {
return this.generateToken(tokenConfig.REFRESHER_TOKEN_LENGTH);
}
generateToken(length: number): string {
const letters = 'abcdefghijklmnopqrstuvwxyz';
const mergedLetters = [...letters, ...letters.toUpperCase().split('')];
let token = crypto.randomBytes(length).toString('base64url');
token = token
.split('')
.map((char) =>
mergedLetters.includes(char)
? char
: mergedLetters[Math.floor(Math.random() * mergedLetters.length)],
)
.join('');
return token;
}
}
export { PasswordHandlers };

View File

@ -0,0 +1,124 @@
import {
TokenDictType,
OccupantTokenObject,
EmployeeTokenObject,
UserType,
} from '@/src/types/auth/token';
import { CacheService } from '@/src/cache.service';
import { users } from '@prisma/client';
import { PasswordHandlers } from './login_handler';
import { Injectable } from '@nestjs/common';
@Injectable()
export class RedisHandlers {
AUTH_TOKEN = 'AUTH_TOKEN';
constructor(
private readonly cacheService: CacheService,
private readonly passwordService: PasswordHandlers,
) {
this.cacheService = cacheService;
this.passwordService = passwordService;
}
async process_redis_object(redis_object: any): Promise<TokenDictType> {
if (!redis_object) {
throw new Error('Invalid Redis object: Object is null or undefined');
}
if (redis_object.user_type === UserType.employee) {
const validateEmployeeToken = (obj: any): obj is EmployeeTokenObject => {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.user_type === 'number' &&
typeof obj.user_uu_id === 'string' &&
typeof obj.user_id === 'number' &&
typeof obj.person_id === 'number' &&
typeof obj.person_uu_id === 'string' &&
Array.isArray(obj.companies_id_list) &&
Array.isArray(obj.companies_uu_id_list) &&
Array.isArray(obj.duty_id_list) &&
Array.isArray(obj.duty_uu_id_list)
);
};
const empToken: EmployeeTokenObject = {
...redis_object,
is_employee: true,
is_occupant: false,
user_type: UserType.employee,
credential_token: redis_object.credential_token || '',
};
if (!validateEmployeeToken(empToken)) {
throw new Error(
'Invalid Redis object: Does not match EmployeeTokenObject interface',
);
}
return empToken;
}
if (redis_object.user_type === UserType.occupant) {
const validateOccupantToken = (obj: any): obj is OccupantTokenObject => {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.user_type === 'number' &&
typeof obj.user_uu_id === 'string' &&
typeof obj.user_id === 'number' &&
typeof obj.person_id === 'number' &&
typeof obj.person_uu_id === 'string'
);
};
const occToken: OccupantTokenObject = {
...redis_object,
is_employee: false,
is_occupant: true,
user_type: UserType.occupant,
credential_token: redis_object.credential_token || '',
available_occupants: redis_object.available_occupants || null,
};
if (!validateOccupantToken(occToken)) {
throw new Error(
'Invalid Redis object: Does not match OccupantTokenObject interface',
);
}
return occToken;
}
throw new Error(`Invalid user_type: ${redis_object.user_type}`);
}
async get_object_from_redis(access_token: string): Promise<TokenDictType> {
const token = await this.cacheService.get(access_token);
return this.process_redis_object(token);
}
async set_login_to_redis(user: users, token: TokenDictType): Promise<any> {
const generated_token = this.passwordService.generateAccessToken();
const listKeys = [this.AUTH_TOKEN, generated_token, user.uu_id];
await this.cacheService.set_with_ttl(
this.cacheService.createRegexPattern(listKeys),
token,
60 * 60 * 24,
);
return generated_token;
}
async update_token_via_token(token: string, additional: any): Promise<any> {
const listKeys = [this.AUTH_TOKEN, token, '*'];
const accessObject = await this.cacheService.get_with_keys(listKeys);
if (!accessObject) throw new Error('Token not found');
const processedObject: TokenDictType =
await this.process_redis_object(accessObject);
if (processedObject.is_employee) {
processedObject.selected = additional;
}
if (processedObject.is_occupant) {
processedObject.selected = additional;
}
const listKeysNew = [this.AUTH_TOKEN, token, processedObject.user_uu_id];
await this.cacheService.set_with_ttl(
this.cacheService.createRegexPattern(listKeysNew),
processedObject,
60 * 60 * 24,
);
return token;
}
}

View File

@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/src/prisma.service';
export interface PaginationInfo {
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
pageCount: number;
}
type ModelDelegate = {
count: (args: any) => Promise<number>;
findMany: (args: any) => Promise<any[]>;
};
@Injectable()
export class PaginationHelper {
constructor(private prisma: PrismaService) {}
/**
* Sayfalama destekli sorgu yapar
*
* @param modelDelegate Prisma model delegesi (ör. prisma.users)
* @param query Prisma findMany argümanları + opsiyonel page, pageSize
* @returns { data, pagination } sonuç ve sayfalama bilgisi
*/
async paginate(
modelDelegate: ModelDelegate,
query: any & { page?: number; pageSize?: number },
): Promise<{ data: any[]; pagination: PaginationInfo }> {
const { page = 1, pageSize = 10, ...prismaQuery } = query;
const totalCount = await modelDelegate.count({ where: prismaQuery.where });
const totalPages = Math.max(Math.ceil(totalCount / pageSize), 1);
const pageNumber = page < 1 ? 1 : page > totalPages ? totalPages : page;
const pageSizeNumber = pageSize > 0 ? pageSize : 10;
const data = await modelDelegate.findMany({
...prismaQuery,
skip: (pageNumber - 1) * pageSizeNumber,
take: pageSizeNumber,
});
const pageCount = data.length;
return {
data,
pagination: {
totalCount,
page: pageNumber,
pageSize: pageSizeNumber,
totalPages,
hasNextPage: pageNumber < totalPages,
hasPreviousPage: pageNumber > 1,
pageCount,
},
};
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { PaginationHelper } from './pagination-helper';
import { PrismaService } from '@/src/prisma.service';
@Module({
providers: [PaginationHelper, PrismaService],
exports: [PaginationHelper],
})
export class UtilsModule {}

View File

@ -1,6 +1,7 @@
{
"compilerOptions": {
"types": ["node"],
"typeRoots": ["./node_modules/@types"],
"module": "commonjs",
"declaration": true,
"removeComments": true,

View File

@ -3,7 +3,7 @@ services:
container_name: backend_service
build:
context: .
dockerfile: backend/Dockerfile
dockerfile: ServicesApi/Dockerfile
target: builder
ports:
- "3000:3000"
@ -14,7 +14,7 @@ services:
command: npm run start:dev
restart: unless-stopped
volumes:
- ./backend:/usr/src/app
- ./ServicesApi:/usr/src/app
- node_modules:/usr/src/app/node_modules
volumes: