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