initial commit
This commit is contained in:
commit
90904de1a1
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# full-stack-auth
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=O5Qry8cBhG4)
|
||||||
|
|
||||||
|
### Функционал:
|
||||||
|
- Авторизация
|
||||||
|
- Отправка писем
|
||||||
|
- Fullstack
|
||||||
|
- Сесии
|
||||||
|
|
||||||
|
### Инструменты:
|
||||||
|
#### Server
|
||||||
|
- Docker
|
||||||
|
- Nest.js
|
||||||
|
- Prisma
|
||||||
|
- PostgreSQL
|
||||||
|
- Redis
|
||||||
|
#### Client
|
||||||
|
- Next.js
|
||||||
|
- Shad.cn
|
||||||
|
- React Query
|
||||||
50
nestjs-server/.env
Normal file
50
nestjs-server/.env
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Environment settings
|
||||||
|
NODE_ENV='development'
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
APPLICATION_PORT=4000
|
||||||
|
APPLICATION_URL='http://localhost:${APPLICATION_PORT}'
|
||||||
|
ALLOWED_ORIGIN='http://localhost:3000'
|
||||||
|
|
||||||
|
# Session settings
|
||||||
|
COOKIES_SECRET='secret'
|
||||||
|
SESSION_SECRET='secret'
|
||||||
|
SESSION_NAME='session'
|
||||||
|
SESSION_DOMAIN='localhost'
|
||||||
|
SESSION_MAX_AGE='30d'
|
||||||
|
SESSION_HTTP_ONLY=true
|
||||||
|
SESSION_SECURE=false
|
||||||
|
SESSION_FOLDER='sessions'
|
||||||
|
|
||||||
|
# Postgres settings
|
||||||
|
POSTGRES_USER='root'
|
||||||
|
POSTGRES_PASSWORD='123456'
|
||||||
|
POSTGRES_HOST='localhost'
|
||||||
|
POSTGRES_PORT=5433
|
||||||
|
POSTGRES_DB='full-authorization'
|
||||||
|
POSTGRES_URI="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
|
||||||
|
|
||||||
|
# Redis settings
|
||||||
|
REDIS_USER='default'
|
||||||
|
REDIS_PASSWORD='123456'
|
||||||
|
REDIS_HOST='localhost'
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_URI="redis://:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}"
|
||||||
|
|
||||||
|
# Recaptcha
|
||||||
|
GOOGLE_RECAPTCHA_SECRET_KEY='6Lfu0OEqAAAAANOCwQx7cf_QeKiQ6Y0eMd75ix6e'
|
||||||
|
|
||||||
|
# OAuth Google
|
||||||
|
GOOGLE_CLIENT_ID='187382652585-p6kb68mhf1okpmootkldtrh8hbdp9jgi.apps.googleusercontent.com'
|
||||||
|
GOOGLE_CLIENT_SECRET='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'
|
||||||
|
|
||||||
|
# OAuth Yandex
|
||||||
|
YANDEX_CLIENT_ID='e551c3678339417dbc10755459a31639'
|
||||||
|
YANDEX_CLIENT_SECRET='ad441f80e9f44f9bb4c645871ea06f80'
|
||||||
|
|
||||||
|
|
||||||
|
# Email sending
|
||||||
|
MAIL_HOST='smtp.yandex.ru'
|
||||||
|
MAIL_PORT='25'
|
||||||
|
MAIL_LOGIN='svk741@yandex.ru'
|
||||||
|
MAIL_PASSWORD='atwljofoizgeeeys'
|
||||||
25
nestjs-server/.eslintrc.js
Normal file
25
nestjs-server/.eslintrc.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
35
nestjs-server/.gitignore
vendored
Normal file
35
nestjs-server/.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
19
nestjs-server/.prettierrc
Normal file
19
nestjs-server/.prettierrc
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": true,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"importOrderSortSpecifiers": true,
|
||||||
|
"importOrderCaseInsensitive": true,
|
||||||
|
"importOrderParserPlugins": [
|
||||||
|
"classProperties",
|
||||||
|
"decorators-legacy",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"importOrder": ["<THIRD_PARTY_MODULES>", "^@/(.*)$", "^../(.*)", "^./(.*)"],
|
||||||
|
"plugins": ["@trivago/prettier-plugin-sort-imports"]
|
||||||
|
}
|
||||||
73
nestjs-server/README.md
Normal file
73
nestjs-server/README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ yarn run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ yarn run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ yarn run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ yarn run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ yarn run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ yarn run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](LICENSE).
|
||||||
36
nestjs-server/docker-compose.yml
Normal file
36
nestjs-server/docker-compose.yml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: postgres
|
||||||
|
image: postgres:15.2
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB}
|
||||||
|
ports:
|
||||||
|
- 5433:5432
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: redis
|
||||||
|
image: redis:5.0
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
8
nestjs-server/nest-cli.json
Normal file
8
nestjs-server/nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
11127
nestjs-server/package-lock.json
generated
Normal file
11127
nestjs-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
88
nestjs-server/package.json
Normal file
88
nestjs-server/package.json
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"name": "nestjs-server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/config": "^3.2.3",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/mapped-types": "*",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestlab/google-recaptcha": "^3.8.0",
|
||||||
|
"@prisma/client": "^5.19.0",
|
||||||
|
"@react-email/components": "^0.0.23",
|
||||||
|
"@react-email/html": "^0.0.10",
|
||||||
|
"argon2": "^0.41.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
|
"connect-redis": "^7.1.1",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
|
"ioredis": "^5.4.1",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
"@nestjs/schematics": "^10.0.0",
|
||||||
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/cookie-parser": "^1.4.7",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/express-session": "^1.18.0",
|
||||||
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/react": "^18.3.4",
|
||||||
|
"@types/supertest": "^2.0.12",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"eslint": "^8.42.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-loader": "^9.4.3",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
nestjs-server/prisma/__generated__/default.d.ts
generated
vendored
Normal file
1
nestjs-server/prisma/__generated__/default.d.ts
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
1
nestjs-server/prisma/__generated__/default.js
generated
Normal file
1
nestjs-server/prisma/__generated__/default.js
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = { ...require('.') }
|
||||||
1
nestjs-server/prisma/__generated__/edge.d.ts
generated
vendored
Normal file
1
nestjs-server/prisma/__generated__/edge.d.ts
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./default"
|
||||||
231
nestjs-server/prisma/__generated__/edge.js
generated
Normal file
231
nestjs-server/prisma/__generated__/edge.js
generated
Normal file
File diff suppressed because one or more lines are too long
223
nestjs-server/prisma/__generated__/index-browser.js
generated
Normal file
223
nestjs-server/prisma/__generated__/index-browser.js
generated
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
Decimal,
|
||||||
|
objectEnumValues,
|
||||||
|
makeStrictEnum,
|
||||||
|
Public,
|
||||||
|
getRuntime
|
||||||
|
} = require('./runtime/index-browser.js')
|
||||||
|
|
||||||
|
|
||||||
|
const Prisma = {}
|
||||||
|
|
||||||
|
exports.Prisma = Prisma
|
||||||
|
exports.$Enums = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 5.19.0
|
||||||
|
* Query Engine version: a9055b89e58b4b5bfb59600785423b1db3d0e75d
|
||||||
|
*/
|
||||||
|
Prisma.prismaVersion = {
|
||||||
|
client: "5.19.0",
|
||||||
|
engine: "a9055b89e58b4b5bfb59600785423b1db3d0e75d"
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)};
|
||||||
|
Prisma.PrismaClientUnknownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientRustPanicError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientInitializationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientValidationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.NotFoundError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`NotFoundError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.Decimal = Decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
Prisma.sql = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.empty = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.join = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.raw = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.validator = Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
Prisma.getExtensionContext = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.defineExtension = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand utilities for JSON filtering
|
||||||
|
*/
|
||||||
|
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||||
|
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||||
|
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||||
|
|
||||||
|
Prisma.NullTypes = {
|
||||||
|
DbNull: objectEnumValues.classes.DbNull,
|
||||||
|
JsonNull: objectEnumValues.classes.JsonNull,
|
||||||
|
AnyNull: objectEnumValues.classes.AnyNull
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.Prisma.UserScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
email: 'email',
|
||||||
|
password: 'password',
|
||||||
|
displayName: 'displayName',
|
||||||
|
picture: 'picture',
|
||||||
|
role: 'role',
|
||||||
|
isVerified: 'isVerified',
|
||||||
|
isTwoFactorEnabled: 'isTwoFactorEnabled',
|
||||||
|
method: 'method',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.AccountScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
provider: 'provider',
|
||||||
|
refreshToken: 'refreshToken',
|
||||||
|
accessToken: 'accessToken',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
userId: 'userId'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.TokenScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
email: 'email',
|
||||||
|
token: 'token',
|
||||||
|
type: 'type',
|
||||||
|
expiresIn: 'expiresIn',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.SortOrder = {
|
||||||
|
asc: 'asc',
|
||||||
|
desc: 'desc'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.QueryMode = {
|
||||||
|
default: 'default',
|
||||||
|
insensitive: 'insensitive'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
};
|
||||||
|
exports.UserRole = exports.$Enums.UserRole = {
|
||||||
|
REGULAR: 'REGULAR',
|
||||||
|
ADMIN: 'ADMIN'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.AuthMethod = exports.$Enums.AuthMethod = {
|
||||||
|
CREDENTIALS: 'CREDENTIALS',
|
||||||
|
GOOGLE: 'GOOGLE',
|
||||||
|
YANDEX: 'YANDEX'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.TokenType = exports.$Enums.TokenType = {
|
||||||
|
VERIFICATION: 'VERIFICATION',
|
||||||
|
TWO_FACTOR: 'TWO_FACTOR',
|
||||||
|
PASSWORD_RESET: 'PASSWORD_RESET'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.ModelName = {
|
||||||
|
User: 'User',
|
||||||
|
Account: 'Account',
|
||||||
|
Token: 'Token'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a stub Prisma Client that will error at runtime if called.
|
||||||
|
*/
|
||||||
|
class PrismaClient {
|
||||||
|
constructor() {
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, prop) {
|
||||||
|
let message
|
||||||
|
const runtime = getRuntime()
|
||||||
|
if (runtime.isEdge) {
|
||||||
|
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
|
||||||
|
- Use Prisma Accelerate: https://pris.ly/d/accelerate
|
||||||
|
- Use Driver Adapters: https://pris.ly/d/driver-adapters
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `
|
||||||
|
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
|
||||||
|
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.PrismaClient = PrismaClient
|
||||||
|
|
||||||
|
Object.assign(exports, Prisma)
|
||||||
5519
nestjs-server/prisma/__generated__/index.d.ts
generated
vendored
Normal file
5519
nestjs-server/prisma/__generated__/index.d.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
252
nestjs-server/prisma/__generated__/index.js
generated
Normal file
252
nestjs-server/prisma/__generated__/index.js
generated
Normal file
File diff suppressed because one or more lines are too long
BIN
nestjs-server/prisma/__generated__/libquery_engine-darwin-arm64.dylib.node
generated
Executable file
BIN
nestjs-server/prisma/__generated__/libquery_engine-darwin-arm64.dylib.node
generated
Executable file
Binary file not shown.
97
nestjs-server/prisma/__generated__/package.json
generated
Normal file
97
nestjs-server/prisma/__generated__/package.json
generated
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"name": "prisma-client-b1cc2a51e1070bb1bc8adb58400c3941c95098951d93e55cbe80bb0a786403ef",
|
||||||
|
"main": "index.js",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"browser": "index-browser.js",
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"require": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./edge": {
|
||||||
|
"types": "./edge.d.ts",
|
||||||
|
"require": "./edge.js",
|
||||||
|
"import": "./edge.js",
|
||||||
|
"default": "./edge.js"
|
||||||
|
},
|
||||||
|
"./react-native": {
|
||||||
|
"types": "./react-native.d.ts",
|
||||||
|
"require": "./react-native.js",
|
||||||
|
"import": "./react-native.js",
|
||||||
|
"default": "./react-native.js"
|
||||||
|
},
|
||||||
|
"./extension": {
|
||||||
|
"types": "./extension.d.ts",
|
||||||
|
"require": "./extension.js",
|
||||||
|
"import": "./extension.js",
|
||||||
|
"default": "./extension.js"
|
||||||
|
},
|
||||||
|
"./index-browser": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"require": "./index-browser.js",
|
||||||
|
"import": "./index-browser.js",
|
||||||
|
"default": "./index-browser.js"
|
||||||
|
},
|
||||||
|
"./index": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"require": "./index.js",
|
||||||
|
"import": "./index.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./wasm": {
|
||||||
|
"types": "./wasm.d.ts",
|
||||||
|
"require": "./wasm.js",
|
||||||
|
"import": "./wasm.js",
|
||||||
|
"default": "./wasm.js"
|
||||||
|
},
|
||||||
|
"./runtime/library": {
|
||||||
|
"types": "./runtime/library.d.ts",
|
||||||
|
"require": "./runtime/library.js",
|
||||||
|
"import": "./runtime/library.js",
|
||||||
|
"default": "./runtime/library.js"
|
||||||
|
},
|
||||||
|
"./runtime/binary": {
|
||||||
|
"types": "./runtime/binary.d.ts",
|
||||||
|
"require": "./runtime/binary.js",
|
||||||
|
"import": "./runtime/binary.js",
|
||||||
|
"default": "./runtime/binary.js"
|
||||||
|
},
|
||||||
|
"./generator-build": {
|
||||||
|
"require": "./generator-build/index.js",
|
||||||
|
"import": "./generator-build/index.js",
|
||||||
|
"default": "./generator-build/index.js"
|
||||||
|
},
|
||||||
|
"./sql": {
|
||||||
|
"require": {
|
||||||
|
"types": "./sql.d.ts",
|
||||||
|
"node": "./sql.js",
|
||||||
|
"default": "./sql.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"types": "./sql.d.ts",
|
||||||
|
"node": "./sql.mjs",
|
||||||
|
"default": "./sql.mjs"
|
||||||
|
},
|
||||||
|
"default": "./sql.js"
|
||||||
|
},
|
||||||
|
"./*": "./*"
|
||||||
|
},
|
||||||
|
"version": "5.19.0",
|
||||||
|
"sideEffects": false
|
||||||
|
}
|
||||||
31
nestjs-server/prisma/__generated__/runtime/edge-esm.js
generated
Normal file
31
nestjs-server/prisma/__generated__/runtime/edge-esm.js
generated
Normal file
File diff suppressed because one or more lines are too long
31
nestjs-server/prisma/__generated__/runtime/edge.js
generated
Normal file
31
nestjs-server/prisma/__generated__/runtime/edge.js
generated
Normal file
File diff suppressed because one or more lines are too long
365
nestjs-server/prisma/__generated__/runtime/index-browser.d.ts
generated
vendored
Normal file
365
nestjs-server/prisma/__generated__/runtime/index-browser.d.ts
generated
vendored
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
declare class AnyNull extends NullTypesEnumValue {
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type Args<T, F extends Operation> = T extends {
|
||||||
|
[K: symbol]: {
|
||||||
|
types: {
|
||||||
|
operations: {
|
||||||
|
[K in F]: {
|
||||||
|
args: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} ? T[symbol]['types']['operations'][F]['args'] : any;
|
||||||
|
|
||||||
|
declare class DbNull extends NullTypesEnumValue {
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace Decimal {
|
||||||
|
export type Constructor = typeof Decimal;
|
||||||
|
export type Instance = Decimal;
|
||||||
|
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||||
|
export type Modulo = Rounding | 9;
|
||||||
|
export type Value = string | number | Decimal;
|
||||||
|
|
||||||
|
// http://mikemcl.github.io/decimal.js/#constructor-properties
|
||||||
|
export interface Config {
|
||||||
|
precision?: number;
|
||||||
|
rounding?: Rounding;
|
||||||
|
toExpNeg?: number;
|
||||||
|
toExpPos?: number;
|
||||||
|
minE?: number;
|
||||||
|
maxE?: number;
|
||||||
|
crypto?: boolean;
|
||||||
|
modulo?: Modulo;
|
||||||
|
defaults?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class Decimal {
|
||||||
|
readonly d: number[];
|
||||||
|
readonly e: number;
|
||||||
|
readonly s: number;
|
||||||
|
|
||||||
|
constructor(n: Decimal.Value);
|
||||||
|
|
||||||
|
absoluteValue(): Decimal;
|
||||||
|
abs(): Decimal;
|
||||||
|
|
||||||
|
ceil(): Decimal;
|
||||||
|
|
||||||
|
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
comparedTo(n: Decimal.Value): number;
|
||||||
|
cmp(n: Decimal.Value): number;
|
||||||
|
|
||||||
|
cosine(): Decimal;
|
||||||
|
cos(): Decimal;
|
||||||
|
|
||||||
|
cubeRoot(): Decimal;
|
||||||
|
cbrt(): Decimal;
|
||||||
|
|
||||||
|
decimalPlaces(): number;
|
||||||
|
dp(): number;
|
||||||
|
|
||||||
|
dividedBy(n: Decimal.Value): Decimal;
|
||||||
|
div(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
dividedToIntegerBy(n: Decimal.Value): Decimal;
|
||||||
|
divToInt(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
equals(n: Decimal.Value): boolean;
|
||||||
|
eq(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
floor(): Decimal;
|
||||||
|
|
||||||
|
greaterThan(n: Decimal.Value): boolean;
|
||||||
|
gt(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
greaterThanOrEqualTo(n: Decimal.Value): boolean;
|
||||||
|
gte(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
hyperbolicCosine(): Decimal;
|
||||||
|
cosh(): Decimal;
|
||||||
|
|
||||||
|
hyperbolicSine(): Decimal;
|
||||||
|
sinh(): Decimal;
|
||||||
|
|
||||||
|
hyperbolicTangent(): Decimal;
|
||||||
|
tanh(): Decimal;
|
||||||
|
|
||||||
|
inverseCosine(): Decimal;
|
||||||
|
acos(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicCosine(): Decimal;
|
||||||
|
acosh(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicSine(): Decimal;
|
||||||
|
asinh(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicTangent(): Decimal;
|
||||||
|
atanh(): Decimal;
|
||||||
|
|
||||||
|
inverseSine(): Decimal;
|
||||||
|
asin(): Decimal;
|
||||||
|
|
||||||
|
inverseTangent(): Decimal;
|
||||||
|
atan(): Decimal;
|
||||||
|
|
||||||
|
isFinite(): boolean;
|
||||||
|
|
||||||
|
isInteger(): boolean;
|
||||||
|
isInt(): boolean;
|
||||||
|
|
||||||
|
isNaN(): boolean;
|
||||||
|
|
||||||
|
isNegative(): boolean;
|
||||||
|
isNeg(): boolean;
|
||||||
|
|
||||||
|
isPositive(): boolean;
|
||||||
|
isPos(): boolean;
|
||||||
|
|
||||||
|
isZero(): boolean;
|
||||||
|
|
||||||
|
lessThan(n: Decimal.Value): boolean;
|
||||||
|
lt(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
lessThanOrEqualTo(n: Decimal.Value): boolean;
|
||||||
|
lte(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
logarithm(n?: Decimal.Value): Decimal;
|
||||||
|
log(n?: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
minus(n: Decimal.Value): Decimal;
|
||||||
|
sub(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
modulo(n: Decimal.Value): Decimal;
|
||||||
|
mod(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
naturalExponential(): Decimal;
|
||||||
|
exp(): Decimal;
|
||||||
|
|
||||||
|
naturalLogarithm(): Decimal;
|
||||||
|
ln(): Decimal;
|
||||||
|
|
||||||
|
negated(): Decimal;
|
||||||
|
neg(): Decimal;
|
||||||
|
|
||||||
|
plus(n: Decimal.Value): Decimal;
|
||||||
|
add(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
precision(includeZeros?: boolean): number;
|
||||||
|
sd(includeZeros?: boolean): number;
|
||||||
|
|
||||||
|
round(): Decimal;
|
||||||
|
|
||||||
|
sine() : Decimal;
|
||||||
|
sin() : Decimal;
|
||||||
|
|
||||||
|
squareRoot(): Decimal;
|
||||||
|
sqrt(): Decimal;
|
||||||
|
|
||||||
|
tangent() : Decimal;
|
||||||
|
tan() : Decimal;
|
||||||
|
|
||||||
|
times(n: Decimal.Value): Decimal;
|
||||||
|
mul(n: Decimal.Value) : Decimal;
|
||||||
|
|
||||||
|
toBinary(significantDigits?: number): string;
|
||||||
|
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toDecimalPlaces(decimalPlaces?: number): Decimal;
|
||||||
|
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
toDP(decimalPlaces?: number): Decimal;
|
||||||
|
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toExponential(decimalPlaces?: number): string;
|
||||||
|
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toFixed(decimalPlaces?: number): string;
|
||||||
|
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toFraction(max_denominator?: Decimal.Value): Decimal[];
|
||||||
|
|
||||||
|
toHexadecimal(significantDigits?: number): string;
|
||||||
|
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
toHex(significantDigits?: number): string;
|
||||||
|
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toJSON(): string;
|
||||||
|
|
||||||
|
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toNumber(): number;
|
||||||
|
|
||||||
|
toOctal(significantDigits?: number): string;
|
||||||
|
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toPower(n: Decimal.Value): Decimal;
|
||||||
|
pow(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
toPrecision(significantDigits?: number): string;
|
||||||
|
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toSignificantDigits(significantDigits?: number): Decimal;
|
||||||
|
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
toSD(significantDigits?: number): Decimal;
|
||||||
|
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toString(): string;
|
||||||
|
|
||||||
|
truncated(): Decimal;
|
||||||
|
trunc(): Decimal;
|
||||||
|
|
||||||
|
valueOf(): string;
|
||||||
|
|
||||||
|
static abs(n: Decimal.Value): Decimal;
|
||||||
|
static acos(n: Decimal.Value): Decimal;
|
||||||
|
static acosh(n: Decimal.Value): Decimal;
|
||||||
|
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static asin(n: Decimal.Value): Decimal;
|
||||||
|
static asinh(n: Decimal.Value): Decimal;
|
||||||
|
static atan(n: Decimal.Value): Decimal;
|
||||||
|
static atanh(n: Decimal.Value): Decimal;
|
||||||
|
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
|
||||||
|
static cbrt(n: Decimal.Value): Decimal;
|
||||||
|
static ceil(n: Decimal.Value): Decimal;
|
||||||
|
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
static clone(object?: Decimal.Config): Decimal.Constructor;
|
||||||
|
static config(object: Decimal.Config): Decimal.Constructor;
|
||||||
|
static cos(n: Decimal.Value): Decimal;
|
||||||
|
static cosh(n: Decimal.Value): Decimal;
|
||||||
|
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static exp(n: Decimal.Value): Decimal;
|
||||||
|
static floor(n: Decimal.Value): Decimal;
|
||||||
|
static hypot(...n: Decimal.Value[]): Decimal;
|
||||||
|
static isDecimal(object: any): object is Decimal;
|
||||||
|
static ln(n: Decimal.Value): Decimal;
|
||||||
|
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
|
||||||
|
static log2(n: Decimal.Value): Decimal;
|
||||||
|
static log10(n: Decimal.Value): Decimal;
|
||||||
|
static max(...n: Decimal.Value[]): Decimal;
|
||||||
|
static min(...n: Decimal.Value[]): Decimal;
|
||||||
|
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static noConflict(): Decimal.Constructor; // Browser only
|
||||||
|
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
|
||||||
|
static random(significantDigits?: number): Decimal;
|
||||||
|
static round(n: Decimal.Value): Decimal;
|
||||||
|
static set(object: Decimal.Config): Decimal.Constructor;
|
||||||
|
static sign(n: Decimal.Value): number;
|
||||||
|
static sin(n: Decimal.Value): Decimal;
|
||||||
|
static sinh(n: Decimal.Value): Decimal;
|
||||||
|
static sqrt(n: Decimal.Value): Decimal;
|
||||||
|
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static sum(...n: Decimal.Value[]): Decimal;
|
||||||
|
static tan(n: Decimal.Value): Decimal;
|
||||||
|
static tanh(n: Decimal.Value): Decimal;
|
||||||
|
static trunc(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
static readonly default?: Decimal.Constructor;
|
||||||
|
static readonly Decimal?: Decimal.Constructor;
|
||||||
|
|
||||||
|
static readonly precision: number;
|
||||||
|
static readonly rounding: Decimal.Rounding;
|
||||||
|
static readonly toExpNeg: number;
|
||||||
|
static readonly toExpPos: number;
|
||||||
|
static readonly minE: number;
|
||||||
|
static readonly maxE: number;
|
||||||
|
static readonly crypto: boolean;
|
||||||
|
static readonly modulo: Decimal.Modulo;
|
||||||
|
|
||||||
|
static readonly ROUND_UP: 0;
|
||||||
|
static readonly ROUND_DOWN: 1;
|
||||||
|
static readonly ROUND_CEIL: 2;
|
||||||
|
static readonly ROUND_FLOOR: 3;
|
||||||
|
static readonly ROUND_HALF_UP: 4;
|
||||||
|
static readonly ROUND_HALF_DOWN: 5;
|
||||||
|
static readonly ROUND_HALF_EVEN: 6;
|
||||||
|
static readonly ROUND_HALF_CEIL: 7;
|
||||||
|
static readonly ROUND_HALF_FLOOR: 8;
|
||||||
|
static readonly EUCLID: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
|
||||||
|
[K in keyof A]: Exact<A[K], W[K]>;
|
||||||
|
} : W) : never) | (A extends Narrowable ? A : never);
|
||||||
|
|
||||||
|
export declare function getRuntime(): GetRuntimeOutput;
|
||||||
|
|
||||||
|
declare type GetRuntimeOutput = {
|
||||||
|
id: Runtime;
|
||||||
|
prettyName: string;
|
||||||
|
isEdge: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare class JsonNull extends NullTypesEnumValue {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates more strict variant of an enum which, unlike regular enum,
|
||||||
|
* throws on non-existing property access. This can be useful in following situations:
|
||||||
|
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
|
||||||
|
* - enum values are generated dynamically from DMMF.
|
||||||
|
*
|
||||||
|
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
|
||||||
|
* will result in `undefined` value being used, which will be accepted. Using strict enum
|
||||||
|
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
|
||||||
|
*
|
||||||
|
* Note: if you need to check for existence of a value in the enum you can still use either
|
||||||
|
* `in` operator or `hasOwnProperty` function.
|
||||||
|
*
|
||||||
|
* @param definition
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
|
||||||
|
|
||||||
|
declare type Narrowable = string | number | bigint | boolean | [];
|
||||||
|
|
||||||
|
declare class NullTypesEnumValue extends ObjectEnumValue {
|
||||||
|
_getNamespace(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for unique values of object-valued enums.
|
||||||
|
*/
|
||||||
|
declare abstract class ObjectEnumValue {
|
||||||
|
constructor(arg?: symbol);
|
||||||
|
abstract _getNamespace(): string;
|
||||||
|
_getName(): string;
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare const objectEnumValues: {
|
||||||
|
classes: {
|
||||||
|
DbNull: typeof DbNull;
|
||||||
|
JsonNull: typeof JsonNull;
|
||||||
|
AnyNull: typeof AnyNull;
|
||||||
|
};
|
||||||
|
instances: {
|
||||||
|
DbNull: DbNull;
|
||||||
|
JsonNull: JsonNull;
|
||||||
|
AnyNull: AnyNull;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
|
||||||
|
|
||||||
|
declare namespace Public {
|
||||||
|
export {
|
||||||
|
validator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { Public }
|
||||||
|
|
||||||
|
declare type Runtime = "edge-routine" | "workerd" | "deno" | "lagon" | "react-native" | "netlify" | "electron" | "node" | "bun" | "edge-light" | "fastly" | "unknown";
|
||||||
|
|
||||||
|
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
|
||||||
|
|
||||||
|
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
|
||||||
|
|
||||||
|
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
|
||||||
|
|
||||||
|
export { }
|
||||||
13
nestjs-server/prisma/__generated__/runtime/index-browser.js
generated
Normal file
13
nestjs-server/prisma/__generated__/runtime/index-browser.js
generated
Normal file
File diff suppressed because one or more lines are too long
3353
nestjs-server/prisma/__generated__/runtime/library.d.ts
generated
vendored
Normal file
3353
nestjs-server/prisma/__generated__/runtime/library.d.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
143
nestjs-server/prisma/__generated__/runtime/library.js
generated
Normal file
143
nestjs-server/prisma/__generated__/runtime/library.js
generated
Normal file
File diff suppressed because one or more lines are too long
80
nestjs-server/prisma/__generated__/runtime/react-native.js
generated
vendored
Normal file
80
nestjs-server/prisma/__generated__/runtime/react-native.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
32
nestjs-server/prisma/__generated__/runtime/wasm.js
generated
Normal file
32
nestjs-server/prisma/__generated__/runtime/wasm.js
generated
Normal file
File diff suppressed because one or more lines are too long
82
nestjs-server/prisma/__generated__/schema.prisma
generated
Normal file
82
nestjs-server/prisma/__generated__/schema.prisma
generated
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
output = "./__generated__"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("POSTGRES_URI")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
|
||||||
|
displayName String
|
||||||
|
picture String?
|
||||||
|
|
||||||
|
role UserRole @default(REGULAR)
|
||||||
|
|
||||||
|
isVerified Boolean @default(false) @map("is_verified")
|
||||||
|
isTwoFactorEnabled Boolean @default(false) @map("is_two_factor_enabled")
|
||||||
|
|
||||||
|
method AuthMethod
|
||||||
|
|
||||||
|
accounts Account[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
type String
|
||||||
|
provider String
|
||||||
|
|
||||||
|
refreshToken String? @map("refresh_token")
|
||||||
|
accessToken String? @map("access_token")
|
||||||
|
expiresAt Int @map("expires_at")
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
userId String? @map("user_id")
|
||||||
|
|
||||||
|
@@map("accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Token {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
email String
|
||||||
|
token String @unique
|
||||||
|
type TokenType
|
||||||
|
expiresIn DateTime @map("expires_in")
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@map("tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserRole {
|
||||||
|
REGULAR
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthMethod {
|
||||||
|
CREDENTIALS
|
||||||
|
GOOGLE
|
||||||
|
YANDEX
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TokenType {
|
||||||
|
VERIFICATION
|
||||||
|
TWO_FACTOR
|
||||||
|
PASSWORD_RESET
|
||||||
|
}
|
||||||
1
nestjs-server/prisma/__generated__/wasm.d.ts
generated
vendored
Normal file
1
nestjs-server/prisma/__generated__/wasm.d.ts
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
223
nestjs-server/prisma/__generated__/wasm.js
generated
Normal file
223
nestjs-server/prisma/__generated__/wasm.js
generated
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
Decimal,
|
||||||
|
objectEnumValues,
|
||||||
|
makeStrictEnum,
|
||||||
|
Public,
|
||||||
|
getRuntime
|
||||||
|
} = require('./runtime/index-browser.js')
|
||||||
|
|
||||||
|
|
||||||
|
const Prisma = {}
|
||||||
|
|
||||||
|
exports.Prisma = Prisma
|
||||||
|
exports.$Enums = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 5.19.0
|
||||||
|
* Query Engine version: a9055b89e58b4b5bfb59600785423b1db3d0e75d
|
||||||
|
*/
|
||||||
|
Prisma.prismaVersion = {
|
||||||
|
client: "5.19.0",
|
||||||
|
engine: "a9055b89e58b4b5bfb59600785423b1db3d0e75d"
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)};
|
||||||
|
Prisma.PrismaClientUnknownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientRustPanicError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientInitializationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientValidationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.NotFoundError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`NotFoundError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.Decimal = Decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
Prisma.sql = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.empty = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.join = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.raw = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.validator = Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
Prisma.getExtensionContext = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.defineExtension = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand utilities for JSON filtering
|
||||||
|
*/
|
||||||
|
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||||
|
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||||
|
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||||
|
|
||||||
|
Prisma.NullTypes = {
|
||||||
|
DbNull: objectEnumValues.classes.DbNull,
|
||||||
|
JsonNull: objectEnumValues.classes.JsonNull,
|
||||||
|
AnyNull: objectEnumValues.classes.AnyNull
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.Prisma.UserScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
email: 'email',
|
||||||
|
password: 'password',
|
||||||
|
displayName: 'displayName',
|
||||||
|
picture: 'picture',
|
||||||
|
role: 'role',
|
||||||
|
isVerified: 'isVerified',
|
||||||
|
isTwoFactorEnabled: 'isTwoFactorEnabled',
|
||||||
|
method: 'method',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.AccountScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
provider: 'provider',
|
||||||
|
refreshToken: 'refreshToken',
|
||||||
|
accessToken: 'accessToken',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
userId: 'userId'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.TokenScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
email: 'email',
|
||||||
|
token: 'token',
|
||||||
|
type: 'type',
|
||||||
|
expiresIn: 'expiresIn',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.SortOrder = {
|
||||||
|
asc: 'asc',
|
||||||
|
desc: 'desc'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.QueryMode = {
|
||||||
|
default: 'default',
|
||||||
|
insensitive: 'insensitive'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
};
|
||||||
|
exports.UserRole = exports.$Enums.UserRole = {
|
||||||
|
REGULAR: 'REGULAR',
|
||||||
|
ADMIN: 'ADMIN'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.AuthMethod = exports.$Enums.AuthMethod = {
|
||||||
|
CREDENTIALS: 'CREDENTIALS',
|
||||||
|
GOOGLE: 'GOOGLE',
|
||||||
|
YANDEX: 'YANDEX'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.TokenType = exports.$Enums.TokenType = {
|
||||||
|
VERIFICATION: 'VERIFICATION',
|
||||||
|
TWO_FACTOR: 'TWO_FACTOR',
|
||||||
|
PASSWORD_RESET: 'PASSWORD_RESET'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.ModelName = {
|
||||||
|
User: 'User',
|
||||||
|
Account: 'Account',
|
||||||
|
Token: 'Token'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a stub Prisma Client that will error at runtime if called.
|
||||||
|
*/
|
||||||
|
class PrismaClient {
|
||||||
|
constructor() {
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, prop) {
|
||||||
|
let message
|
||||||
|
const runtime = getRuntime()
|
||||||
|
if (runtime.isEdge) {
|
||||||
|
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
|
||||||
|
- Use Prisma Accelerate: https://pris.ly/d/accelerate
|
||||||
|
- Use Driver Adapters: https://pris.ly/d/driver-adapters
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `
|
||||||
|
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
|
||||||
|
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.PrismaClient = PrismaClient
|
||||||
|
|
||||||
|
Object.assign(exports, Prisma)
|
||||||
82
nestjs-server/prisma/schema.prisma
Normal file
82
nestjs-server/prisma/schema.prisma
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
output = "./__generated__"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("POSTGRES_URI")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
|
||||||
|
displayName String
|
||||||
|
picture String?
|
||||||
|
|
||||||
|
role UserRole @default(REGULAR)
|
||||||
|
|
||||||
|
isVerified Boolean @default(false) @map("is_verified")
|
||||||
|
isTwoFactorEnabled Boolean @default(false) @map("is_two_factor_enabled")
|
||||||
|
|
||||||
|
method AuthMethod
|
||||||
|
|
||||||
|
accounts Account[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
type String
|
||||||
|
provider String
|
||||||
|
|
||||||
|
refreshToken String? @map("refresh_token")
|
||||||
|
accessToken String? @map("access_token")
|
||||||
|
expiresAt Int @map("expires_at")
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
userId String? @map("user_id")
|
||||||
|
|
||||||
|
@@map("accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Token {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
email String
|
||||||
|
token String @unique
|
||||||
|
type TokenType
|
||||||
|
expiresIn DateTime @map("expires_in")
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@map("tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserRole {
|
||||||
|
REGULAR
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthMethod {
|
||||||
|
CREDENTIALS
|
||||||
|
GOOGLE
|
||||||
|
YANDEX
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TokenType {
|
||||||
|
VERIFICATION
|
||||||
|
TWO_FACTOR
|
||||||
|
PASSWORD_RESET
|
||||||
|
}
|
||||||
30
nestjs-server/src/app.module.ts
Normal file
30
nestjs-server/src/app.module.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ConfigModule } from '@nestjs/config'
|
||||||
|
|
||||||
|
import { AuthModule } from './auth/auth.module'
|
||||||
|
import { EmailConfirmationModule } from './auth/email-confirmation/email-confirmation.module'
|
||||||
|
import { PasswordRecoveryModule } from './auth/password-recovery/password-recovery.module'
|
||||||
|
import { ProviderModule } from './auth/provider/provider.module'
|
||||||
|
import { TwoFactorAuthModule } from './auth/two-factor-auth/two-factor-auth.module'
|
||||||
|
import { IS_DEV_ENV } from './libs/common/utils/is-dev.util'
|
||||||
|
import { MailModule } from './libs/mail/mail.module'
|
||||||
|
import { PrismaModule } from './prisma/prisma.module'
|
||||||
|
import { UserModule } from './user/user.module'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
ignoreEnvFile: !IS_DEV_ENV,
|
||||||
|
isGlobal: true
|
||||||
|
}),
|
||||||
|
PrismaModule,
|
||||||
|
AuthModule,
|
||||||
|
UserModule,
|
||||||
|
ProviderModule,
|
||||||
|
MailModule,
|
||||||
|
EmailConfirmationModule,
|
||||||
|
PasswordRecoveryModule,
|
||||||
|
TwoFactorAuthModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
86
nestjs-server/src/auth/auth.controller.ts
Normal file
86
nestjs-server/src/auth/auth.controller.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
UseGuards
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { Recaptcha } from '@nestlab/google-recaptcha'
|
||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { AuthService } from './auth.service'
|
||||||
|
import { LoginDto } from './dto/login.dto'
|
||||||
|
import { RegisterDto } from './dto/register.dto'
|
||||||
|
import { AuthProviderGuard } from './guards/provider.guard'
|
||||||
|
import { ProviderService } from './provider/provider.service'
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
public constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly providerService: ProviderService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Recaptcha()
|
||||||
|
@Post('register')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
public async register(@Body() dto: RegisterDto) {
|
||||||
|
return this.authService.register(dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Recaptcha()
|
||||||
|
@Post('login')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
public async login(@Req() req: Request, @Body() dto: LoginDto) {
|
||||||
|
return this.authService.login(req, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthProviderGuard)
|
||||||
|
@Get('/oauth/callback/:provider')
|
||||||
|
public async callback(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
@Query('code') code: string,
|
||||||
|
@Param('provider') provider: string
|
||||||
|
) {
|
||||||
|
if (!code) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Не был предоставлен код авторизации.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.authService.extractProfileFromCode(req, provider, code)
|
||||||
|
|
||||||
|
return res.redirect(
|
||||||
|
`${this.configService.getOrThrow<string>('ALLOWED_ORIGIN')}/dashboard/settings`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthProviderGuard)
|
||||||
|
@Get('/oauth/connect/:provider')
|
||||||
|
public async connect(@Param('provider') provider: string) {
|
||||||
|
const providerInstance = this.providerService.findByService(provider)
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: providerInstance.getAuthUrl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('logout')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
public async logout(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Res({ passthrough: true }) res: Response
|
||||||
|
) {
|
||||||
|
return this.authService.logout(req, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
34
nestjs-server/src/auth/auth.module.ts
Normal file
34
nestjs-server/src/auth/auth.module.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { forwardRef, Module } from '@nestjs/common'
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config'
|
||||||
|
import { GoogleRecaptchaModule } from '@nestlab/google-recaptcha'
|
||||||
|
|
||||||
|
import { getProvidersConfig } from '@/config/providers.config'
|
||||||
|
import { getRecaptchaConfig } from '@/config/recaptcha.config'
|
||||||
|
import { MailService } from '@/libs/mail/mail.service'
|
||||||
|
import { UserService } from '@/user/user.service'
|
||||||
|
|
||||||
|
import { AuthController } from './auth.controller'
|
||||||
|
import { AuthService } from './auth.service'
|
||||||
|
import { EmailConfirmationModule } from './email-confirmation/email-confirmation.module'
|
||||||
|
import { ProviderModule } from './provider/provider.module'
|
||||||
|
import { TwoFactorAuthService } from './two-factor-auth/two-factor-auth.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ProviderModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: getProvidersConfig,
|
||||||
|
inject: [ConfigService]
|
||||||
|
}),
|
||||||
|
GoogleRecaptchaModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: getRecaptchaConfig,
|
||||||
|
inject: [ConfigService]
|
||||||
|
}),
|
||||||
|
forwardRef(() => EmailConfirmationModule)
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, UserService, MailService, TwoFactorAuthService],
|
||||||
|
exports: [AuthService]
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
189
nestjs-server/src/auth/auth.service.ts
Normal file
189
nestjs-server/src/auth/auth.service.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import {
|
||||||
|
ConflictException,
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
NotFoundException,
|
||||||
|
UnauthorizedException
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { AuthMethod, User } from '@prisma/__generated__'
|
||||||
|
import { verify } from 'argon2'
|
||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { PrismaService } from '@/prisma/prisma.service'
|
||||||
|
import { UserService } from '@/user/user.service'
|
||||||
|
|
||||||
|
import { LoginDto } from './dto/login.dto'
|
||||||
|
import { RegisterDto } from './dto/register.dto'
|
||||||
|
import { EmailConfirmationService } from './email-confirmation/email-confirmation.service'
|
||||||
|
import { ProviderService } from './provider/provider.service'
|
||||||
|
import { TwoFactorAuthService } from './two-factor-auth/two-factor-auth.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
public constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly providerService: ProviderService,
|
||||||
|
private readonly emailConfirmationService: EmailConfirmationService,
|
||||||
|
private readonly twoFactorAuthService: TwoFactorAuthService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async register(dto: RegisterDto) {
|
||||||
|
const isExists = await this.userService.findByEmail(dto.email)
|
||||||
|
|
||||||
|
if (isExists) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Регистрация не удалась. Пользователь с таким email уже существует. Пожалуйста, используйте другой email или войдите в систему.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = await this.userService.create(
|
||||||
|
dto.email,
|
||||||
|
dto.password,
|
||||||
|
dto.name,
|
||||||
|
'',
|
||||||
|
AuthMethod.CREDENTIALS,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.emailConfirmationService.sendVerificationToken(newUser.email)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
'Вы успешно зарегистрировались. Пожалуйста, подтвердите ваш email. Сообщение было отправлено на ваш почтовый адрес.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async login(req: Request, dto: LoginDto) {
|
||||||
|
const user = await this.userService.findByEmail(dto.email)
|
||||||
|
|
||||||
|
if (!user || !user.password) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Пользователь не найден. Пожалуйста, проверьте введенные данные'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidPassword = await verify(user.password, dto.password)
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
'Неверный пароль. Пожалуйста, попробуйте еще раз или восстановите пароль, если забыли его.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isVerified) {
|
||||||
|
await this.emailConfirmationService.sendVerificationToken(
|
||||||
|
user.email
|
||||||
|
)
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
'Ваш email не подтвержден. Пожалуйста, проверьте вашу почту и подтвердите адрес.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isTwoFactorEnabled) {
|
||||||
|
if (!dto.code) {
|
||||||
|
await this.twoFactorAuthService.sendTwoFactorToken(user.email)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
'Проверьте вашу почту. Требуется код двухфакторной аутентификации.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.twoFactorAuthService.validateTwoFactorToken(
|
||||||
|
user.email,
|
||||||
|
dto.code
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.saveSession(req, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async extractProfileFromCode(
|
||||||
|
req: Request,
|
||||||
|
provider: string,
|
||||||
|
code: string
|
||||||
|
) {
|
||||||
|
const providerInstance = this.providerService.findByService(provider)
|
||||||
|
const profile = await providerInstance.findUserByCode(code)
|
||||||
|
|
||||||
|
const account = await this.prismaService.account.findFirst({
|
||||||
|
where: {
|
||||||
|
id: profile.id,
|
||||||
|
provider: profile.provider
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let user = account?.userId
|
||||||
|
? await this.userService.findById(account.userId)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return this.saveSession(req, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await this.userService.create(
|
||||||
|
profile.email,
|
||||||
|
'',
|
||||||
|
profile.name,
|
||||||
|
profile.picture,
|
||||||
|
AuthMethod[profile.provider.toUpperCase()],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
await this.prismaService.account.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
type: 'oauth',
|
||||||
|
provider: profile.provider,
|
||||||
|
accessToken: profile.access_token,
|
||||||
|
refreshToken: profile.refresh_token,
|
||||||
|
expiresAt: profile.expires_at
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.saveSession(req, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(req: Request, res: Response): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
req.session.destroy(err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(
|
||||||
|
new InternalServerErrorException(
|
||||||
|
'Не удалось завершить сессию. Возможно, возникла проблема с сервером или сессия уже была завершена.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
res.clearCookie(
|
||||||
|
this.configService.getOrThrow<string>('SESSION_NAME')
|
||||||
|
)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async saveSession(req: Request, user: User) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
req.session.userId = user.id
|
||||||
|
|
||||||
|
req.session.save(err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(
|
||||||
|
new InternalServerErrorException(
|
||||||
|
'Не удалось сохранить сессию. Проверьте, правильно ли настроены параметры сессии.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
user
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
18
nestjs-server/src/auth/decorators/auth.decorator.ts
Normal file
18
nestjs-server/src/auth/decorators/auth.decorator.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { applyDecorators, UseGuards } from '@nestjs/common'
|
||||||
|
import { UserRole } from '@prisma/__generated__'
|
||||||
|
|
||||||
|
import { AuthGuard } from '../guards/auth.guard'
|
||||||
|
import { RolesGuard } from '../guards/roles.guard'
|
||||||
|
|
||||||
|
import { Roles } from './roles.decorator'
|
||||||
|
|
||||||
|
export function Authorization(...roles: UserRole[]) {
|
||||||
|
if (roles.length > 0) {
|
||||||
|
return applyDecorators(
|
||||||
|
Roles(...roles),
|
||||||
|
UseGuards(AuthGuard, RolesGuard)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyDecorators(UseGuards(AuthGuard))
|
||||||
|
}
|
||||||
11
nestjs-server/src/auth/decorators/authorized.decorator.ts
Normal file
11
nestjs-server/src/auth/decorators/authorized.decorator.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
|
||||||
|
import { User } from '@prisma/__generated__'
|
||||||
|
|
||||||
|
export const Authorized = createParamDecorator(
|
||||||
|
(data: keyof User, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest()
|
||||||
|
const user = request.user
|
||||||
|
|
||||||
|
return data ? user[data] : user
|
||||||
|
}
|
||||||
|
)
|
||||||
6
nestjs-server/src/auth/decorators/roles.decorator.ts
Normal file
6
nestjs-server/src/auth/decorators/roles.decorator.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common'
|
||||||
|
import { UserRole } from '@prisma/__generated__'
|
||||||
|
|
||||||
|
export const ROLES_KEY = 'roles'
|
||||||
|
|
||||||
|
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles)
|
||||||
23
nestjs-server/src/auth/dto/login.dto.ts
Normal file
23
nestjs-server/src/auth/dto/login.dto.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MinLength
|
||||||
|
} from 'class-validator'
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@IsString({ message: 'Email должен быть строкой.' })
|
||||||
|
@IsEmail({}, { message: 'Некорректный формат email.' })
|
||||||
|
@IsNotEmpty({ message: 'Email обязателен для заполнения.' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@IsString({ message: 'Пароль должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Поле пароль не может быть пустым.' })
|
||||||
|
@MinLength(6, { message: 'Пароль должен содержать не менее 6 символов.' })
|
||||||
|
password: string
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
code: string
|
||||||
|
}
|
||||||
37
nestjs-server/src/auth/dto/register.dto.ts
Normal file
37
nestjs-server/src/auth/dto/register.dto.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsString,
|
||||||
|
MinLength,
|
||||||
|
Validate
|
||||||
|
} from 'class-validator'
|
||||||
|
|
||||||
|
import { IsPasswordsMatchingConstraint } from '@/libs/common/decorators/is-passwords-matching-constraint.decorator'
|
||||||
|
|
||||||
|
export class RegisterDto {
|
||||||
|
@IsString({ message: 'Имя должно быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Имя обязательно для заполнения.' })
|
||||||
|
name: string
|
||||||
|
|
||||||
|
@IsString({ message: 'Email должен быть строкой.' })
|
||||||
|
@IsEmail({}, { message: 'Некорректный формат email.' })
|
||||||
|
@IsNotEmpty({ message: 'Email обязателен для заполнения.' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@IsString({ message: 'Пароль должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Пароль обязателен для заполнения.' })
|
||||||
|
@MinLength(6, {
|
||||||
|
message: 'Пароль должен содержать минимум 6 символов.'
|
||||||
|
})
|
||||||
|
password: string
|
||||||
|
|
||||||
|
@IsString({ message: 'Пароль подтверждения должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Поле подтверждения пароля не может быть пустым.' })
|
||||||
|
@MinLength(6, {
|
||||||
|
message: 'Пароль подтверждения должен содержать не менее 6 символов.'
|
||||||
|
})
|
||||||
|
@Validate(IsPasswordsMatchingConstraint, {
|
||||||
|
message: 'Пароли не совпадают.'
|
||||||
|
})
|
||||||
|
passwordRepeat: string
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { IsNotEmpty, IsString } from 'class-validator'
|
||||||
|
|
||||||
|
export class ConfirmationDto {
|
||||||
|
@IsString({ message: 'Токен должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Поле токен не может быть пустым.' })
|
||||||
|
token: string
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Post,
|
||||||
|
Req
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
import { ConfirmationDto } from './dto/confirmation.dto'
|
||||||
|
import { EmailConfirmationService } from './email-confirmation.service'
|
||||||
|
|
||||||
|
@Controller('auth/email-confirmation')
|
||||||
|
export class EmailConfirmationController {
|
||||||
|
constructor(
|
||||||
|
private readonly emailConfirmationService: EmailConfirmationService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
public async newVerification(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Body() dto: ConfirmationDto
|
||||||
|
) {
|
||||||
|
return this.emailConfirmationService.newVerification(req, dto)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { forwardRef, Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { MailModule } from '@/libs/mail/mail.module'
|
||||||
|
import { MailService } from '@/libs/mail/mail.service'
|
||||||
|
import { UserService } from '@/user/user.service'
|
||||||
|
|
||||||
|
import { AuthModule } from '../auth.module'
|
||||||
|
|
||||||
|
import { EmailConfirmationController } from './email-confirmation.controller'
|
||||||
|
import { EmailConfirmationService } from './email-confirmation.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MailModule, forwardRef(() => AuthModule)],
|
||||||
|
controllers: [EmailConfirmationController],
|
||||||
|
providers: [EmailConfirmationService, UserService, MailService],
|
||||||
|
exports: [EmailConfirmationService]
|
||||||
|
})
|
||||||
|
export class EmailConfirmationModule {}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
forwardRef,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { TokenType } from '@prisma/__generated__'
|
||||||
|
import { Request } from 'express'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { MailService } from '@/libs/mail/mail.service'
|
||||||
|
import { PrismaService } from '@/prisma/prisma.service'
|
||||||
|
import { UserService } from '@/user/user.service'
|
||||||
|
|
||||||
|
import { AuthService } from '../auth.service'
|
||||||
|
|
||||||
|
import { ConfirmationDto } from './dto/confirmation.dto'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmailConfirmationService {
|
||||||
|
public constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly mailService: MailService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
@Inject(forwardRef(() => AuthService))
|
||||||
|
private readonly authService: AuthService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async newVerification(req: Request, dto: ConfirmationDto) {
|
||||||
|
const existingToken = await this.prismaService.token.findUnique({
|
||||||
|
where: {
|
||||||
|
token: dto.token,
|
||||||
|
type: TokenType.VERIFICATION
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existingToken) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Токен подтверждения не найден. Пожалуйста, убедитесь, что у вас правильный токен.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExpired = new Date(existingToken.expiresIn) < new Date()
|
||||||
|
|
||||||
|
if (hasExpired) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Токен подтверждения истек. Пожалуйста, запросите новый токен для подтверждения.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await this.userService.findByEmail(
|
||||||
|
existingToken.email
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Пользователь не найден. Пожалуйста, проверьте введенный адрес электронной почты и попробуйте снова.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prismaService.user.update({
|
||||||
|
where: {
|
||||||
|
id: existingUser.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isVerified: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.prismaService.token.delete({
|
||||||
|
where: {
|
||||||
|
id: existingToken.id,
|
||||||
|
type: TokenType.VERIFICATION
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.authService.saveSession(req, existingUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendVerificationToken(email: string) {
|
||||||
|
const verificationToken = await this.generateVerificationToken(email)
|
||||||
|
|
||||||
|
await this.mailService.sendConfirmationEmail(
|
||||||
|
verificationToken.email,
|
||||||
|
verificationToken.token
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateVerificationToken(email: string) {
|
||||||
|
const token = uuidv4()
|
||||||
|
const expiresIn = new Date(new Date().getTime() + 3600 * 1000)
|
||||||
|
|
||||||
|
const existingToken = await this.prismaService.token.findFirst({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
type: TokenType.VERIFICATION
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingToken) {
|
||||||
|
await this.prismaService.token.delete({
|
||||||
|
where: {
|
||||||
|
id: existingToken.id,
|
||||||
|
type: TokenType.VERIFICATION
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationToken = await this.prismaService.token.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
expiresIn,
|
||||||
|
type: TokenType.VERIFICATION
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return verificationToken
|
||||||
|
}
|
||||||
|
}
|
||||||
29
nestjs-server/src/auth/guards/auth.guard.ts
Normal file
29
nestjs-server/src/auth/guards/auth.guard.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException
|
||||||
|
} from '@nestjs/common'
|
||||||
|
|
||||||
|
import { UserService } from '@/user/user.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
public constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
|
public async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest()
|
||||||
|
|
||||||
|
if (typeof request.session.userId === 'undefined') {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
'Пользователь не авторизован. Пожалуйста, войдите в систему, чтобы получить доступ.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userService.findById(request.session.userId)
|
||||||
|
|
||||||
|
request.user = user
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
30
nestjs-server/src/auth/guards/provider.guard.ts
Normal file
30
nestjs-server/src/auth/guards/provider.guard.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
import { ProviderService } from '../provider/provider.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthProviderGuard implements CanActivate {
|
||||||
|
public constructor(private readonly providerService: ProviderService) {}
|
||||||
|
|
||||||
|
public canActivate(context: ExecutionContext) {
|
||||||
|
const request = context.switchToHttp().getRequest() as Request
|
||||||
|
|
||||||
|
const provider = request.params.provider
|
||||||
|
|
||||||
|
const providerInstance = this.providerService.findByService(provider)
|
||||||
|
|
||||||
|
if (!providerInstance) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Провайдер "${provider}" не найден. Пожалуйста, проверьте правильность введенных данных.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
33
nestjs-server/src/auth/guards/roles.guard.ts
Normal file
33
nestjs-server/src/auth/guards/roles.guard.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { Reflector } from '@nestjs/core'
|
||||||
|
import { UserRole } from '@prisma/__generated__'
|
||||||
|
|
||||||
|
import { ROLES_KEY } from '../decorators/roles.decorator'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
public constructor(private readonly reflector: Reflector) {}
|
||||||
|
|
||||||
|
public async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const roles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass()
|
||||||
|
])
|
||||||
|
const request = context.switchToHttp().getRequest()
|
||||||
|
|
||||||
|
if (!roles) return true
|
||||||
|
|
||||||
|
if (!roles.includes(request.user.role)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Недостаточно прав. У вас нет прав доступа к этому ресурсу.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { IsNotEmpty, IsString, MinLength } from 'class-validator'
|
||||||
|
|
||||||
|
export class NewPasswordDto {
|
||||||
|
@IsString({ message: 'Пароль должен быть строкой.' })
|
||||||
|
@MinLength(6, { message: 'Пароль должен содержать не менее 6 символов.' })
|
||||||
|
@IsNotEmpty({ message: 'Поле новый пароль не может быть пустым.' })
|
||||||
|
password: string
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { IsEmail, IsNotEmpty } from 'class-validator'
|
||||||
|
|
||||||
|
export class ResetPasswordDto {
|
||||||
|
@IsEmail({}, { message: 'Введите корректный адрес электронной почты.' })
|
||||||
|
@IsNotEmpty({ message: 'Поле email не может быть пустым.' })
|
||||||
|
email: string
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Post
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { Recaptcha } from '@nestlab/google-recaptcha'
|
||||||
|
|
||||||
|
import { NewPasswordDto } from './dto/new-password.dto'
|
||||||
|
import { ResetPasswordDto } from './dto/reset-password.dto'
|
||||||
|
import { PasswordRecoveryService } from './password-recovery.service'
|
||||||
|
|
||||||
|
@Controller('auth/password-recovery')
|
||||||
|
export class PasswordRecoveryController {
|
||||||
|
constructor(
|
||||||
|
private readonly passwordRecoveryService: PasswordRecoveryService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Recaptcha()
|
||||||
|
@Post('reset')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
public async resetPassword(@Body() dto: ResetPasswordDto) {
|
||||||
|
return this.passwordRecoveryService.resetPassword(dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Recaptcha()
|
||||||
|
@Post('new/:token')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
public async newPassword(
|
||||||
|
@Body() dto: NewPasswordDto,
|
||||||
|
@Param('token') token: string
|
||||||
|
) {
|
||||||
|
return this.passwordRecoveryService.newPassword(dto, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { MailService } from '@/libs/mail/mail.service'
|
||||||
|
import { UserService } from '@/user/user.service'
|
||||||
|
|
||||||
|
import { PasswordRecoveryController } from './password-recovery.controller'
|
||||||
|
import { PasswordRecoveryService } from './password-recovery.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [PasswordRecoveryController],
|
||||||
|
providers: [PasswordRecoveryService, UserService, MailService]
|
||||||
|
})
|
||||||
|
export class PasswordRecoveryModule {}
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { TokenType } from '@prisma/__generated__'
|
||||||
|
import { hash } from 'argon2'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { MailService } from '@/libs/mail/mail.service'
|
||||||
|
import { PrismaService } from '@/prisma/prisma.service'
|
||||||
|
import { UserService } from '@/user/user.service'
|
||||||
|
|
||||||
|
import { NewPasswordDto } from './dto/new-password.dto'
|
||||||
|
import { ResetPasswordDto } from './dto/reset-password.dto'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PasswordRecoveryService {
|
||||||
|
public constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly mailService: MailService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async resetPassword(dto: ResetPasswordDto) {
|
||||||
|
const existingUser = await this.userService.findByEmail(dto.email)
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Пользователь не найден. Пожалуйста, проверьте введенный адрес электронной почты и попробуйте снова.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordResetToken = await this.generatePasswordResetToken(
|
||||||
|
existingUser.email
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.mailService.sendPasswordResetEmail(
|
||||||
|
passwordResetToken.email,
|
||||||
|
passwordResetToken.token
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public async newPassword(dto: NewPasswordDto, token: string) {
|
||||||
|
const existingToken = await this.prismaService.token.findFirst({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
type: TokenType.PASSWORD_RESET
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existingToken) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Токен не найден. Пожалуйста, проверьте правильность введенного токена или запросите новый.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExpired = new Date(existingToken.expiresIn) < new Date()
|
||||||
|
|
||||||
|
if (hasExpired) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Токен истек. Пожалуйста, запросите новый токен для подтверждения сброса пароля.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await this.userService.findByEmail(
|
||||||
|
existingToken.email
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Пользователь не найден. Пожалуйста, проверьте введенный адрес электронной почты и попробуйте снова.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prismaService.user.update({
|
||||||
|
where: {
|
||||||
|
id: existingUser.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
password: await hash(dto.password)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.prismaService.token.delete({
|
||||||
|
where: {
|
||||||
|
id: existingToken.id,
|
||||||
|
type: TokenType.PASSWORD_RESET
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generatePasswordResetToken(email: string) {
|
||||||
|
const token = uuidv4()
|
||||||
|
const expiresIn = new Date(new Date().getTime() + 3600 * 1000)
|
||||||
|
|
||||||
|
const existingToken = await this.prismaService.token.findFirst({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
type: TokenType.PASSWORD_RESET
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingToken) {
|
||||||
|
await this.prismaService.token.delete({
|
||||||
|
where: {
|
||||||
|
id: existingToken.id,
|
||||||
|
type: TokenType.PASSWORD_RESET
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordResetToken = await this.prismaService.token.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
expiresIn,
|
||||||
|
type: TokenType.PASSWORD_RESET
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return passwordResetToken
|
||||||
|
}
|
||||||
|
}
|
||||||
13
nestjs-server/src/auth/provider/provider.constants.ts
Normal file
13
nestjs-server/src/auth/provider/provider.constants.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { FactoryProvider, ModuleMetadata } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { BaseOAuthService } from './services/base-oauth.service'
|
||||||
|
|
||||||
|
export const ProviderOptionsSymbol = Symbol()
|
||||||
|
|
||||||
|
export type TypeOptions = {
|
||||||
|
baseUrl: string
|
||||||
|
services: BaseOAuthService[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TypeAsyncOptions = Pick<ModuleMetadata, 'imports'> &
|
||||||
|
Pick<FactoryProvider<TypeOptions>, 'useFactory' | 'inject'>
|
||||||
41
nestjs-server/src/auth/provider/provider.module.ts
Normal file
41
nestjs-server/src/auth/provider/provider.module.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { DynamicModule, Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ProviderOptionsSymbol,
|
||||||
|
TypeAsyncOptions,
|
||||||
|
TypeOptions
|
||||||
|
} from './provider.constants'
|
||||||
|
import { ProviderService } from './provider.service'
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class ProviderModule {
|
||||||
|
public static register(options: TypeOptions): DynamicModule {
|
||||||
|
return {
|
||||||
|
module: ProviderModule,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
useValue: options.services,
|
||||||
|
provide: ProviderOptionsSymbol
|
||||||
|
},
|
||||||
|
ProviderService
|
||||||
|
],
|
||||||
|
exports: [ProviderService]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static registerAsync(options: TypeAsyncOptions): DynamicModule {
|
||||||
|
return {
|
||||||
|
module: ProviderModule,
|
||||||
|
imports: options.imports,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
useFactory: options.useFactory,
|
||||||
|
provide: ProviderOptionsSymbol,
|
||||||
|
inject: options.inject
|
||||||
|
},
|
||||||
|
ProviderService
|
||||||
|
],
|
||||||
|
exports: [ProviderService]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
nestjs-server/src/auth/provider/provider.service.ts
Normal file
21
nestjs-server/src/auth/provider/provider.service.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { ProviderOptionsSymbol, TypeOptions } from './provider.constants'
|
||||||
|
import { BaseOAuthService } from './services/base-oauth.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProviderService implements OnModuleInit {
|
||||||
|
public constructor(
|
||||||
|
@Inject(ProviderOptionsSymbol) private readonly options: TypeOptions
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public onModuleInit() {
|
||||||
|
for (const provider of this.options.services) {
|
||||||
|
provider.baseUrl = this.options.baseUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public findByService(service: string): BaseOAuthService | null {
|
||||||
|
return this.options.services.find(s => s.name === service) ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
118
nestjs-server/src/auth/provider/services/base-oauth.service.ts
Normal file
118
nestjs-server/src/auth/provider/services/base-oauth.service.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException
|
||||||
|
} from '@nestjs/common'
|
||||||
|
|
||||||
|
import { TypeBaseProviderOptions } from './types/base-provider-options.types'
|
||||||
|
import { TypeUserInfo } from './types/user-info.types'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BaseOAuthService {
|
||||||
|
private BASE_URL: string
|
||||||
|
|
||||||
|
public constructor(private readonly options: TypeBaseProviderOptions) {}
|
||||||
|
|
||||||
|
protected async extractUserInfo(data: any): Promise<TypeUserInfo> {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
provider: this.options.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAuthUrl() {
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
response_type: 'code',
|
||||||
|
client_id: this.options.client_id,
|
||||||
|
redirect_uri: this.getRedirectUrl(),
|
||||||
|
scope: (this.options.scopes ?? []).join(' '),
|
||||||
|
access_type: 'offline',
|
||||||
|
prompt: 'select_account'
|
||||||
|
})
|
||||||
|
|
||||||
|
return `${this.options.authorize_url}?${query}`
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findUserByCode(code: string): Promise<TypeUserInfo> {
|
||||||
|
const client_id = this.options.client_id
|
||||||
|
const client_secret = this.options.client_secret
|
||||||
|
|
||||||
|
const tokenQuery = new URLSearchParams({
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
code,
|
||||||
|
redirect_uri: this.getRedirectUrl(),
|
||||||
|
grant_type: 'authorization_code'
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokensRequest = await fetch(this.options.access_url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: tokenQuery,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!tokensRequest.ok) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Не удалось получить пользователя с ${this.options.profile_url}. Проверьте правильность токена доступа.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await tokensRequest.json()
|
||||||
|
|
||||||
|
if (!tokens.access_token) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Нет токенов с ${this.options.access_url}. Убедитесь, что код авторизации действителен.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRequest = await fetch(this.options.profile_url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens.access_token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userRequest.ok) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
`Не удалось получить пользователя с ${this.options.profile_url}. Проверьте правильность токена доступа.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await userRequest.json()
|
||||||
|
const userData = await this.extractUserInfo(user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...userData,
|
||||||
|
access_token: tokens.access_token,
|
||||||
|
refresh_token: tokens.refresh_token,
|
||||||
|
expires_at: tokens.expires_at || tokens.expires_in,
|
||||||
|
provider: this.options.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRedirectUrl() {
|
||||||
|
return `${this.BASE_URL}/auth/oauth/callback/${this.options.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
set baseUrl(value: string) {
|
||||||
|
this.BASE_URL = value
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.options.name
|
||||||
|
}
|
||||||
|
|
||||||
|
get access_url() {
|
||||||
|
return this.options.access_url
|
||||||
|
}
|
||||||
|
|
||||||
|
get profile_url() {
|
||||||
|
return this.options.profile_url
|
||||||
|
}
|
||||||
|
|
||||||
|
get scopes() {
|
||||||
|
return this.options.scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
46
nestjs-server/src/auth/provider/services/google.provider.ts
Normal file
46
nestjs-server/src/auth/provider/services/google.provider.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { BaseOAuthService } from './base-oauth.service'
|
||||||
|
import { TypeProviderOptions } from './types/provider-options.types'
|
||||||
|
import { TypeUserInfo } from './types/user-info.types'
|
||||||
|
|
||||||
|
export class GoogleProvider extends BaseOAuthService {
|
||||||
|
public constructor(options: TypeProviderOptions) {
|
||||||
|
super({
|
||||||
|
name: 'google',
|
||||||
|
authorize_url: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||||
|
access_url: 'https://oauth2.googleapis.com/token',
|
||||||
|
profile_url: 'https://www.googleapis.com/oauth2/v3/userinfo',
|
||||||
|
scopes: options.scopes,
|
||||||
|
client_id: options.client_id,
|
||||||
|
client_secret: options.client_secret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async extractUserInfo(data: GoogleProfile): Promise<TypeUserInfo> {
|
||||||
|
return super.extractUserInfo({
|
||||||
|
email: data.email,
|
||||||
|
name: data.name,
|
||||||
|
picture: data.picture
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoogleProfile extends Record<string, any> {
|
||||||
|
aud: string
|
||||||
|
azp: string
|
||||||
|
email: string
|
||||||
|
email_verified: boolean
|
||||||
|
exp: number
|
||||||
|
family_name?: string
|
||||||
|
given_name: string
|
||||||
|
hd?: string
|
||||||
|
iat: number
|
||||||
|
iss: string
|
||||||
|
jti?: string
|
||||||
|
locale?: string
|
||||||
|
name: string
|
||||||
|
nbf?: number
|
||||||
|
picture: string
|
||||||
|
sub: string
|
||||||
|
access_token: string
|
||||||
|
refresh_token?: string
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
export type TypeBaseProviderOptions = {
|
||||||
|
name: string
|
||||||
|
authorize_url: string
|
||||||
|
access_url: string
|
||||||
|
profile_url: string
|
||||||
|
scopes: string[]
|
||||||
|
client_id: string
|
||||||
|
client_secret: string
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export type TypeProviderOptions = {
|
||||||
|
scopes: string[]
|
||||||
|
client_id: string
|
||||||
|
client_secret: string
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
export type TypeUserInfo = {
|
||||||
|
id: string
|
||||||
|
picture: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
access_token?: string | null
|
||||||
|
refresh_token?: string
|
||||||
|
expires_at?: number
|
||||||
|
provider: string
|
||||||
|
}
|
||||||
47
nestjs-server/src/auth/provider/services/yandex.provider.ts
Normal file
47
nestjs-server/src/auth/provider/services/yandex.provider.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { BaseOAuthService } from './base-oauth.service'
|
||||||
|
import { TypeProviderOptions } from './types/provider-options.types'
|
||||||
|
import { TypeUserInfo } from './types/user-info.types'
|
||||||
|
|
||||||
|
export class YandexProvider extends BaseOAuthService {
|
||||||
|
public constructor(options: TypeProviderOptions) {
|
||||||
|
super({
|
||||||
|
name: 'yandex',
|
||||||
|
authorize_url: 'https://oauth.yandex.ru/authorize',
|
||||||
|
access_url: 'https://oauth.yandex.ru/token',
|
||||||
|
profile_url: 'https://login.yandex.ru/info?format=json',
|
||||||
|
scopes: options.scopes,
|
||||||
|
client_id: options.client_id,
|
||||||
|
client_secret: options.client_secret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async extractUserInfo(data: YandexProfile): Promise<TypeUserInfo> {
|
||||||
|
return super.extractUserInfo({
|
||||||
|
email: data.emails[0],
|
||||||
|
name: data.display_name,
|
||||||
|
picture: data.default_avatar_id
|
||||||
|
? `https://avatars.yandex.net/get-yapic/${data.default_avatar_id}/islands-200`
|
||||||
|
: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface YandexProfile {
|
||||||
|
login: string
|
||||||
|
id: string
|
||||||
|
client_id: string
|
||||||
|
psuid: string
|
||||||
|
emails?: string[]
|
||||||
|
default_email?: string
|
||||||
|
is_avatar_empty?: boolean
|
||||||
|
default_avatar_id?: string
|
||||||
|
birthday?: string | null
|
||||||
|
first_name?: string
|
||||||
|
last_name?: string
|
||||||
|
display_name?: string
|
||||||
|
real_name?: string
|
||||||
|
sex?: 'male' | 'female' | null
|
||||||
|
default_phone?: { id: number; number: string }
|
||||||
|
access_token: string
|
||||||
|
refresh_token?: string
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { MailService } from '@/libs/mail/mail.service'
|
||||||
|
|
||||||
|
import { TwoFactorAuthService } from './two-factor-auth.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [TwoFactorAuthService, MailService]
|
||||||
|
})
|
||||||
|
export class TwoFactorAuthModule {}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { TokenType } from '@prisma/__generated__'
|
||||||
|
|
||||||
|
import { MailService } from '@/libs/mail/mail.service'
|
||||||
|
import { PrismaService } from '@/prisma/prisma.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TwoFactorAuthService {
|
||||||
|
public constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly mailService: MailService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async validateTwoFactorToken(email: string, code: string) {
|
||||||
|
const existingToken = await this.prismaService.token.findFirst({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
type: TokenType.TWO_FACTOR
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existingToken) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Токен двухфакторной аутентификации не найден. Убедитесь, что вы запрашивали токен для данного адреса электронной почты.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingToken.token !== code) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Неверный код двухфакторной аутентификации. Пожалуйста, проверьте введенный код и попробуйте снова.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExpired = new Date(existingToken.expiresIn) < new Date()
|
||||||
|
|
||||||
|
if (hasExpired) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Срок действия токена двухфакторной аутентификации истек. Пожалуйста, запросите новый токен.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prismaService.token.delete({
|
||||||
|
where: {
|
||||||
|
id: existingToken.id,
|
||||||
|
type: TokenType.TWO_FACTOR
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendTwoFactorToken(email: string) {
|
||||||
|
const twoFactorToken = await this.generateTwoFactorToken(email)
|
||||||
|
|
||||||
|
await this.mailService.sendTwoFactorTokenEmail(
|
||||||
|
twoFactorToken.email,
|
||||||
|
twoFactorToken.token
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateTwoFactorToken(email: string) {
|
||||||
|
const token = Math.floor(
|
||||||
|
Math.random() * (1000000 - 100000) + 100000
|
||||||
|
).toString()
|
||||||
|
const expiresIn = new Date(new Date().getTime() + 300000)
|
||||||
|
|
||||||
|
const existingToken = await this.prismaService.token.findFirst({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
type: TokenType.TWO_FACTOR
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingToken) {
|
||||||
|
await this.prismaService.token.delete({
|
||||||
|
where: {
|
||||||
|
id: existingToken.id,
|
||||||
|
type: TokenType.TWO_FACTOR
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const twoFactorToken = await this.prismaService.token.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
expiresIn,
|
||||||
|
type: TokenType.TWO_FACTOR
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return twoFactorToken
|
||||||
|
}
|
||||||
|
}
|
||||||
21
nestjs-server/src/config/mailer.config.ts
Normal file
21
nestjs-server/src/config/mailer.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { MailerOptions } from '@nestjs-modules/mailer'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
|
||||||
|
import { isDev } from '@/libs/common/utils/is-dev.util'
|
||||||
|
|
||||||
|
export const getMailerConfig = async (
|
||||||
|
configService: ConfigService
|
||||||
|
): Promise<MailerOptions> => ({
|
||||||
|
transport: {
|
||||||
|
host: configService.getOrThrow<string>('MAIL_HOST'),
|
||||||
|
port: configService.getOrThrow<number>('MAIL_PORT'),
|
||||||
|
secure: !isDev(configService),
|
||||||
|
auth: {
|
||||||
|
user: configService.getOrThrow<string>('MAIL_LOGIN'),
|
||||||
|
pass: configService.getOrThrow<string>('MAIL_PASSWORD')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
from: `"TeaCoder Team" ${configService.getOrThrow<string>('MAIL_LOGIN')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
27
nestjs-server/src/config/providers.config.ts
Normal file
27
nestjs-server/src/config/providers.config.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
|
||||||
|
import { TypeOptions } from '@/auth/provider/provider.constants'
|
||||||
|
import { GoogleProvider } from '@/auth/provider/services/google.provider'
|
||||||
|
import { YandexProvider } from '@/auth/provider/services/yandex.provider'
|
||||||
|
|
||||||
|
export const getProvidersConfig = async (
|
||||||
|
configService: ConfigService
|
||||||
|
): Promise<TypeOptions> => ({
|
||||||
|
baseUrl: configService.getOrThrow<string>('APPLICATION_URL'),
|
||||||
|
services: [
|
||||||
|
new GoogleProvider({
|
||||||
|
client_id: configService.getOrThrow<string>('GOOGLE_CLIENT_ID'),
|
||||||
|
client_secret: configService.getOrThrow<string>(
|
||||||
|
'GOOGLE_CLIENT_SECRET'
|
||||||
|
),
|
||||||
|
scopes: ['email', 'profile']
|
||||||
|
}),
|
||||||
|
new YandexProvider({
|
||||||
|
client_id: configService.getOrThrow<string>('YANDEX_CLIENT_ID'),
|
||||||
|
client_secret: configService.getOrThrow<string>(
|
||||||
|
'YANDEX_CLIENT_SECRET'
|
||||||
|
),
|
||||||
|
scopes: ['login:email', 'login:avatar', 'login:info']
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
12
nestjs-server/src/config/recaptcha.config.ts
Normal file
12
nestjs-server/src/config/recaptcha.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { GoogleRecaptchaModuleOptions } from '@nestlab/google-recaptcha'
|
||||||
|
|
||||||
|
import { isDev } from '@/libs/common/utils/is-dev.util'
|
||||||
|
|
||||||
|
export const getRecaptchaConfig = async (
|
||||||
|
configService: ConfigService
|
||||||
|
): Promise<GoogleRecaptchaModuleOptions> => ({
|
||||||
|
secretKey: configService.getOrThrow<string>('GOOGLE_RECAPTCHA_SECRET_KEY'),
|
||||||
|
response: req => req.headers.recaptcha,
|
||||||
|
skipIf: isDev(configService)
|
||||||
|
})
|
||||||
7
nestjs-server/src/express-session.d.ts
vendored
Normal file
7
nestjs-server/src/express-session.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'express-session'
|
||||||
|
|
||||||
|
declare module 'express-session' {
|
||||||
|
interface SessionData {
|
||||||
|
userId?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
ValidationArguments,
|
||||||
|
ValidatorConstraint,
|
||||||
|
ValidatorConstraintInterface
|
||||||
|
} from 'class-validator'
|
||||||
|
|
||||||
|
import { RegisterDto } from '@/auth/dto/register.dto'
|
||||||
|
|
||||||
|
@ValidatorConstraint({ name: 'IsPasswordsMatching', async: false })
|
||||||
|
export class IsPasswordsMatchingConstraint
|
||||||
|
implements ValidatorConstraintInterface
|
||||||
|
{
|
||||||
|
public validate(passwordRepeat: string, args: ValidationArguments) {
|
||||||
|
const obj = args.object as RegisterDto
|
||||||
|
return obj.password === passwordRepeat
|
||||||
|
}
|
||||||
|
|
||||||
|
public defaultMessage(validationArguments?: ValidationArguments) {
|
||||||
|
return 'Пароли не совпадают'
|
||||||
|
}
|
||||||
|
}
|
||||||
9
nestjs-server/src/libs/common/utils/is-dev.util.ts
Normal file
9
nestjs-server/src/libs/common/utils/is-dev.util.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
export const isDev = (configService: ConfigService) =>
|
||||||
|
configService.getOrThrow('NODE_ENV') === 'development'
|
||||||
|
|
||||||
|
export const IS_DEV_ENV = process.env.NODE_ENV === 'development'
|
||||||
114
nestjs-server/src/libs/common/utils/ms.util.ts
Normal file
114
nestjs-server/src/libs/common/utils/ms.util.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
const s = 1000
|
||||||
|
const m = s * 60
|
||||||
|
const h = m * 60
|
||||||
|
const d = h * 24
|
||||||
|
const w = d * 7
|
||||||
|
const y = d * 365.25
|
||||||
|
|
||||||
|
type Unit =
|
||||||
|
| 'Years'
|
||||||
|
| 'Year'
|
||||||
|
| 'Yrs'
|
||||||
|
| 'Yr'
|
||||||
|
| 'Y'
|
||||||
|
| 'Weeks'
|
||||||
|
| 'Week'
|
||||||
|
| 'W'
|
||||||
|
| 'Days'
|
||||||
|
| 'Day'
|
||||||
|
| 'D'
|
||||||
|
| 'Hours'
|
||||||
|
| 'Hour'
|
||||||
|
| 'Hrs'
|
||||||
|
| 'Hr'
|
||||||
|
| 'H'
|
||||||
|
| 'Minutes'
|
||||||
|
| 'Minute'
|
||||||
|
| 'Mins'
|
||||||
|
| 'Min'
|
||||||
|
| 'M'
|
||||||
|
| 'Seconds'
|
||||||
|
| 'Second'
|
||||||
|
| 'Secs'
|
||||||
|
| 'Sec'
|
||||||
|
| 's'
|
||||||
|
| 'Milliseconds'
|
||||||
|
| 'Millisecond'
|
||||||
|
| 'Msecs'
|
||||||
|
| 'Msec'
|
||||||
|
| 'Ms'
|
||||||
|
|
||||||
|
type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>
|
||||||
|
|
||||||
|
export type StringValue =
|
||||||
|
| `${number}`
|
||||||
|
| `${number}${UnitAnyCase}`
|
||||||
|
| `${number} ${UnitAnyCase}`
|
||||||
|
|
||||||
|
// ms('1 minute'); // вернет 60000
|
||||||
|
// ms('2 hours'); // вернет 7200000
|
||||||
|
// ms('500 ms'); // вернет 500
|
||||||
|
export function ms(str: StringValue): number {
|
||||||
|
if (typeof str !== 'string' || str.length === 0 || str.length > 100) {
|
||||||
|
throw new Error(
|
||||||
|
'Value provided to ms() must be a string with length between 1 and 99.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const match =
|
||||||
|
/^(?<value>-?(?:\d+)?\.?\d+) *(?<type>milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
|
||||||
|
str
|
||||||
|
)
|
||||||
|
|
||||||
|
const groups = match?.groups as { value: string; type?: string } | undefined
|
||||||
|
if (!groups) {
|
||||||
|
return NaN
|
||||||
|
}
|
||||||
|
const n = parseFloat(groups.value)
|
||||||
|
const type = (groups.type || 'ms').toLowerCase() as Lowercase<Unit>
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'years':
|
||||||
|
case 'year':
|
||||||
|
case 'yrs':
|
||||||
|
case 'yr':
|
||||||
|
case 'y':
|
||||||
|
return n * y
|
||||||
|
case 'weeks':
|
||||||
|
case 'week':
|
||||||
|
case 'w':
|
||||||
|
return n * w
|
||||||
|
case 'days':
|
||||||
|
case 'day':
|
||||||
|
case 'd':
|
||||||
|
return n * d
|
||||||
|
case 'hours':
|
||||||
|
case 'hour':
|
||||||
|
case 'hrs':
|
||||||
|
case 'hr':
|
||||||
|
case 'h':
|
||||||
|
return n * h
|
||||||
|
case 'minutes':
|
||||||
|
case 'minute':
|
||||||
|
case 'mins':
|
||||||
|
case 'min':
|
||||||
|
case 'm':
|
||||||
|
return n * m
|
||||||
|
case 'seconds':
|
||||||
|
case 'second':
|
||||||
|
case 'secs':
|
||||||
|
case 'sec':
|
||||||
|
case 's':
|
||||||
|
return n * s
|
||||||
|
case 'milliseconds':
|
||||||
|
case 'millisecond':
|
||||||
|
case 'msecs':
|
||||||
|
case 'msec':
|
||||||
|
case 'ms':
|
||||||
|
return n
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Ошибка: единица времени ${type} была распознана, но не существует соответствующего случая. Пожалуйста, проверьте введенные данные.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
nestjs-server/src/libs/common/utils/parse-boolean.util.ts
Normal file
19
nestjs-server/src/libs/common/utils/parse-boolean.util.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export function parseBoolean(value: string): boolean {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const lowerValue = value.trim().toLowerCase()
|
||||||
|
if (lowerValue === 'true') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (lowerValue === 'false') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Не удалось преобразовать значение "${value}" в логическое значение.`
|
||||||
|
)
|
||||||
|
}
|
||||||
19
nestjs-server/src/libs/mail/mail.module.ts
Normal file
19
nestjs-server/src/libs/mail/mail.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { MailerModule } from '@nestjs-modules/mailer'
|
||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config'
|
||||||
|
|
||||||
|
import { getMailerConfig } from '@/config/mailer.config'
|
||||||
|
|
||||||
|
import { MailService } from './mail.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MailerModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: getMailerConfig,
|
||||||
|
inject: [ConfigService]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [MailService]
|
||||||
|
})
|
||||||
|
export class MailModule {}
|
||||||
44
nestjs-server/src/libs/mail/mail.service.ts
Normal file
44
nestjs-server/src/libs/mail/mail.service.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { MailerService } from '@nestjs-modules/mailer'
|
||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { render } from '@react-email/components'
|
||||||
|
|
||||||
|
import { ConfirmationTemplate } from './templates/confirmation.template'
|
||||||
|
import { ResetPasswordTemplate } from './templates/reset-password.template'
|
||||||
|
import { TwoFactorAuthTemplate } from './templates/two-factor-auth.template'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService {
|
||||||
|
public constructor(
|
||||||
|
private readonly mailerService: MailerService,
|
||||||
|
private readonly configService: ConfigService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async sendConfirmationEmail(email: string, token: string) {
|
||||||
|
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN')
|
||||||
|
const html = await render(ConfirmationTemplate({ domain, token }))
|
||||||
|
|
||||||
|
return this.sendMail(email, 'Подтверждение почты', html)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendPasswordResetEmail(email: string, token: string) {
|
||||||
|
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN')
|
||||||
|
const html = await render(ResetPasswordTemplate({ domain, token }))
|
||||||
|
|
||||||
|
return this.sendMail(email, 'Сброс пароля', html)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendTwoFactorTokenEmail(email: string, token: string) {
|
||||||
|
const html = await render(TwoFactorAuthTemplate({ token }))
|
||||||
|
|
||||||
|
return this.sendMail(email, 'Подтверждение вашей личности', html)
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendMail(email: string, subject: string, html: string) {
|
||||||
|
return this.mailerService.sendMail({
|
||||||
|
to: email,
|
||||||
|
subject,
|
||||||
|
html
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { Body, Heading, Link, Tailwind, Text } from "@react-email/components"
|
||||||
|
import { Html } from "@react-email/html"
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
interface ConfirmationTemplateProps {
|
||||||
|
domain: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmationTemplate({
|
||||||
|
domain,
|
||||||
|
token
|
||||||
|
}: ConfirmationTemplateProps) {
|
||||||
|
const confirmLink = `${domain}/auth/new-verification?token=${token}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tailwind>
|
||||||
|
<Html>
|
||||||
|
<Body className='text-black'>
|
||||||
|
<Heading>Подтверждение почты</Heading>
|
||||||
|
<Text>
|
||||||
|
Привет! Чтобы подтвердить свой адрес электронной почты, пожалуйста, перейдите по следующей ссылке:
|
||||||
|
</Text>
|
||||||
|
<Link href={confirmLink}>Подтвердить почту</Link>
|
||||||
|
<Text>
|
||||||
|
Эта ссылка действительна в течение 1 часа. Если вы не запрашивали подтверждение, просто проигнорируйте это сообщение.
|
||||||
|
</Text>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
</Tailwind>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Heading,
|
||||||
|
Link,
|
||||||
|
Tailwind,
|
||||||
|
Text
|
||||||
|
} from '@react-email/components';
|
||||||
|
import { Html } from '@react-email/html';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface ResetPasswordTemplateProps {
|
||||||
|
domain: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResetPasswordTemplate({ domain, token }: ResetPasswordTemplateProps) {
|
||||||
|
const resetLink = `${domain}/auth/new-password?token=${token}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tailwind>
|
||||||
|
<Html>
|
||||||
|
<Body className='text-black'>
|
||||||
|
<Heading>Сброс пароля</Heading>
|
||||||
|
<Text>
|
||||||
|
Привет! Вы запросили сброс пароля. Пожалуйста, перейдите по следующей ссылке, чтобы создать новый пароль:
|
||||||
|
</Text>
|
||||||
|
<Link href={resetLink}>Подтвердить сброс пароля</Link>
|
||||||
|
<Text>
|
||||||
|
Эта ссылка действительна в течение 1 часа. Если вы не запрашивали сброс пароля, просто проигнорируйте это сообщение.
|
||||||
|
</Text>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Heading,
|
||||||
|
Tailwind,
|
||||||
|
Text
|
||||||
|
} from '@react-email/components';
|
||||||
|
import { Html } from '@react-email/html';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface TwoFactorAuthTemplateProps {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TwoFactorAuthTemplate({ token }: TwoFactorAuthTemplateProps) {
|
||||||
|
return (
|
||||||
|
<Tailwind>
|
||||||
|
<Html>
|
||||||
|
<Body className='text-black'>
|
||||||
|
<Heading>Двухфакторная аутентификация</Heading>
|
||||||
|
<Text>Ваш код двухфакторной аутентификации: <strong>{token}</strong></Text>
|
||||||
|
<Text>
|
||||||
|
Пожалуйста, введите этот код в приложении для завершения процесса аутентификации.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Если вы не запрашивали этот код, просто проигнорируйте это сообщение.
|
||||||
|
</Text>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
nestjs-server/src/main.ts
Normal file
59
nestjs-server/src/main.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { ValidationPipe } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { NestFactory } from '@nestjs/core'
|
||||||
|
import RedisStore from 'connect-redis'
|
||||||
|
import * as cookieParser from 'cookie-parser'
|
||||||
|
import * as session from 'express-session'
|
||||||
|
import IORedis from 'ioredis'
|
||||||
|
|
||||||
|
import { AppModule } from './app.module'
|
||||||
|
import { ms, StringValue } from './libs/common/utils/ms.util'
|
||||||
|
import { parseBoolean } from './libs/common/utils/parse-boolean.util'
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule)
|
||||||
|
|
||||||
|
const config = app.get(ConfigService)
|
||||||
|
const redis = new IORedis(config.getOrThrow('REDIS_URI'))
|
||||||
|
|
||||||
|
app.use(cookieParser(config.getOrThrow<string>('COOKIES_SECRET')))
|
||||||
|
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
transform: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: config.getOrThrow<string>('SESSION_SECRET'),
|
||||||
|
name: config.getOrThrow<string>('SESSION_NAME'),
|
||||||
|
resave: true,
|
||||||
|
saveUninitialized: false,
|
||||||
|
cookie: {
|
||||||
|
domain: config.getOrThrow<string>('SESSION_DOMAIN'),
|
||||||
|
maxAge: ms(config.getOrThrow<StringValue>('SESSION_MAX_AGE')),
|
||||||
|
httpOnly: parseBoolean(
|
||||||
|
config.getOrThrow<string>('SESSION_HTTP_ONLY')
|
||||||
|
),
|
||||||
|
secure: parseBoolean(
|
||||||
|
config.getOrThrow<string>('SESSION_SECURE')
|
||||||
|
),
|
||||||
|
sameSite: 'lax'
|
||||||
|
},
|
||||||
|
store: new RedisStore({
|
||||||
|
client: redis,
|
||||||
|
prefix: config.getOrThrow<string>('SESSION_FOLDER')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
app.enableCors({
|
||||||
|
origin: config.getOrThrow<string>('ALLOWED_ORIGIN'),
|
||||||
|
credentials: true,
|
||||||
|
exposedHeaders: ['set-cookie']
|
||||||
|
})
|
||||||
|
|
||||||
|
await app.listen(config.getOrThrow<number>('APPLICATION_PORT'))
|
||||||
|
}
|
||||||
|
bootstrap()
|
||||||
10
nestjs-server/src/prisma/prisma.module.ts
Normal file
10
nestjs-server/src/prisma/prisma.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { PrismaService } from './prisma.service'
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService]
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
16
nestjs-server/src/prisma/prisma.service.ts
Normal file
16
nestjs-server/src/prisma/prisma.service.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
|
||||||
|
import { PrismaClient } from '@prisma/__generated__'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService
|
||||||
|
extends PrismaClient
|
||||||
|
implements OnModuleInit, OnModuleDestroy
|
||||||
|
{
|
||||||
|
public async onModuleInit(): Promise<void> {
|
||||||
|
await this.$connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onModuleDestroy(): Promise<void> {
|
||||||
|
await this.$disconnect
|
||||||
|
}
|
||||||
|
}
|
||||||
15
nestjs-server/src/user/dto/update-user.dto.ts
Normal file
15
nestjs-server/src/user/dto/update-user.dto.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator'
|
||||||
|
|
||||||
|
export class UpdateUserDto {
|
||||||
|
@IsString({ message: 'Имя должно быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Имя обязательно для заполнения.' })
|
||||||
|
name: string
|
||||||
|
|
||||||
|
@IsString({ message: 'Email должен быть строкой.' })
|
||||||
|
@IsEmail({}, { message: 'Некорректный формат email.' })
|
||||||
|
@IsNotEmpty({ message: 'Email обязателен для заполнения.' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@IsBoolean({ message: 'isTwoFactorEnabled должно быть булевым значением.' })
|
||||||
|
isTwoFactorEnabled: boolean
|
||||||
|
}
|
||||||
45
nestjs-server/src/user/user.controller.ts
Normal file
45
nestjs-server/src/user/user.controller.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Patch
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { UserRole } from '@prisma/__generated__'
|
||||||
|
|
||||||
|
import { Authorization } from '@/auth/decorators/auth.decorator'
|
||||||
|
import { Authorized } from '@/auth/decorators/authorized.decorator'
|
||||||
|
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto'
|
||||||
|
import { UserService } from './user.service'
|
||||||
|
|
||||||
|
@Controller('users')
|
||||||
|
export class UserController {
|
||||||
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
|
@Authorization()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Get('profile')
|
||||||
|
public async findProfile(@Authorized('id') userId: string) {
|
||||||
|
return this.userService.findById(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorization(UserRole.ADMIN)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Get('by-id/:id')
|
||||||
|
public async findById(@Param('id') id: string) {
|
||||||
|
return this.userService.findById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorization()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Patch('profile')
|
||||||
|
public async updateProfile(
|
||||||
|
@Authorized('id') userId: string,
|
||||||
|
@Body() dto: UpdateUserDto
|
||||||
|
) {
|
||||||
|
return this.userService.update(userId, dto)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
nestjs-server/src/user/user.module.ts
Normal file
10
nestjs-server/src/user/user.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { UserController } from './user.controller'
|
||||||
|
import { UserService } from './user.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [UserController],
|
||||||
|
providers: [UserService]
|
||||||
|
})
|
||||||
|
export class UserModule {}
|
||||||
86
nestjs-server/src/user/user.service.ts
Normal file
86
nestjs-server/src/user/user.service.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common'
|
||||||
|
import { AuthMethod } from '@prisma/__generated__'
|
||||||
|
import { hash } from 'argon2'
|
||||||
|
|
||||||
|
import { PrismaService } from '@/prisma/prisma.service'
|
||||||
|
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async findById(id: string) {
|
||||||
|
const user = await this.prismaService.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
accounts: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Пользователь не найден. Пожалуйста, проверьте введенные данные.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findByEmail(email: string) {
|
||||||
|
const user = await this.prismaService.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
accounts: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
displayName: string,
|
||||||
|
picture: string,
|
||||||
|
method: AuthMethod,
|
||||||
|
isVerified: boolean
|
||||||
|
) {
|
||||||
|
const user = await this.prismaService.user.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
password: password ? await hash(password) : '',
|
||||||
|
displayName,
|
||||||
|
picture,
|
||||||
|
method,
|
||||||
|
isVerified
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
accounts: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(userId: string, dto: UpdateUserDto) {
|
||||||
|
const user = await this.findById(userId)
|
||||||
|
|
||||||
|
const updatedUser = await this.prismaService.user.update({
|
||||||
|
where: {
|
||||||
|
id: user.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
email: dto.email,
|
||||||
|
displayName: dto.name,
|
||||||
|
isTwoFactorEnabled: dto.isTwoFactorEnabled
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return updatedUser
|
||||||
|
}
|
||||||
|
}
|
||||||
4
nestjs-server/tsconfig.build.json
Normal file
4
nestjs-server/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
27
nestjs-server/tsconfig.json
Normal file
27
nestjs-server/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@prisma/__generated__": ["prisma/__generated__"],
|
||||||
|
"@prisma/__generated__/*": ["prisma/__generated__/*"]
|
||||||
|
},
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"jsx": "react"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
nextjs-client/.env
Normal file
2
nextjs-client/.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
SERVER_URL='http://localhost:4000'
|
||||||
|
GOOGLE_RECAPTCHA_SITE_KEY='6Lfu0OEqAAAAAEzPtIHYhNikyXWcEwhyECcKD6n-'
|
||||||
36
nextjs-client/.gitignore
vendored
Normal file
36
nextjs-client/.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
23
nextjs-client/.prettierrc
Normal file
23
nextjs-client/.prettierrc
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": true,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"importOrderSortSpecifiers": true,
|
||||||
|
"importOrder": [
|
||||||
|
"<THIRD_PARTY_MODULES>",
|
||||||
|
"^@/app/(.*)$",
|
||||||
|
"^@/features/(.*)$",
|
||||||
|
"^@/shared/(.*)$",
|
||||||
|
"^../(.*)$",
|
||||||
|
"^./(.*)$"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@trivago/prettier-plugin-sort-imports",
|
||||||
|
"prettier-plugin-tailwindcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
36
nextjs-client/README.md
Normal file
36
nextjs-client/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||||
18
nextjs-client/components.json
Normal file
18
nextjs-client/components.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/shared/styles/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/shared/components",
|
||||||
|
"ui": "@/shared/components/ui",
|
||||||
|
"utils": "@/shared/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
nextjs-client/next.config.mjs
Normal file
24
nextjs-client/next.config.mjs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
missingSuspenseWithCSRBailout: false
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
SERVER_URL: process.env.SERVER_URL,
|
||||||
|
GOOGLE_RECAPTCHA_SITE_KEY: process.env.GOOGLE_RECAPTCHA_SITE_KEY
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'lh3.googleusercontent.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'avatars.yandex.net'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
3360
nextjs-client/package-lock.json
generated
Normal file
3360
nextjs-client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
nextjs-client/package.json
Normal file
47
nextjs-client/package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "nextjs-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
|
"@tanstack/react-query": "^5.55.0",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"geist": "^1.3.1",
|
||||||
|
"lucide-react": "^0.439.0",
|
||||||
|
"next": "14.2.3",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-google-recaptcha": "^3.1.0",
|
||||||
|
"react-hook-form": "^7.53.0",
|
||||||
|
"react-icons": "^5.3.0",
|
||||||
|
"sonner": "^1.5.0",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"@types/react-google-recaptcha": "^2.1.9",
|
||||||
|
"postcss": "^8",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
nextjs-client/postcss.config.mjs
Normal file
8
nextjs-client/postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
11
nextjs-client/src/app/auth/login/page.tsx
Normal file
11
nextjs-client/src/app/auth/login/page.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import { LoginForm } from '@/features/auth/components'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Войти в аккаунт'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return <LoginForm />
|
||||||
|
}
|
||||||
11
nextjs-client/src/app/auth/new-password/page.tsx
Normal file
11
nextjs-client/src/app/auth/new-password/page.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import { NewPasswordForm } from '@/features/auth/components'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Новый пароль'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewPasswordPage() {
|
||||||
|
return <NewPasswordForm />
|
||||||
|
}
|
||||||
11
nextjs-client/src/app/auth/new-verification/page.tsx
Normal file
11
nextjs-client/src/app/auth/new-verification/page.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import { NewVerificationForm } from '@/features/auth/components'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Подтверждение почты'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewVerificationPage() {
|
||||||
|
return <NewVerificationForm />
|
||||||
|
}
|
||||||
11
nextjs-client/src/app/auth/register/page.tsx
Normal file
11
nextjs-client/src/app/auth/register/page.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import { RegisterForm } from '@/features/auth/components'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Создать аккаунт'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
return <RegisterForm />
|
||||||
|
}
|
||||||
11
nextjs-client/src/app/auth/reset-password/page.tsx
Normal file
11
nextjs-client/src/app/auth/reset-password/page.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import { ResetPasswordForm } from '@/features/auth/components'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Сброс пароля'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ResetPasswordPage() {
|
||||||
|
return <ResetPasswordForm />
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user