mirror of
https://github.com/ershisan99/flashcards-api.git
synced 2025-12-16 20:59:26 +00:00
add auth docs, add eslint import/order
This commit is contained in:
30
.eslintrc.js
30
.eslintrc.js
@@ -9,6 +9,7 @@ module.exports = {
|
|||||||
extends: [
|
extends: [
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:prettier/recommended',
|
'plugin:prettier/recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
],
|
],
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
@@ -21,5 +22,32 @@ module.exports = {
|
|||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'newlines-between': 'always',
|
||||||
|
alphabetize: {
|
||||||
|
order: 'asc',
|
||||||
|
caseInsensitive: true,
|
||||||
},
|
},
|
||||||
};
|
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'padding-line-between-statements': [
|
||||||
|
'error',
|
||||||
|
{ blankLine: 'always', prev: '*', next: 'return' },
|
||||||
|
{ blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
|
||||||
|
{
|
||||||
|
blankLine: 'any',
|
||||||
|
prev: ['const', 'let', 'var'],
|
||||||
|
next: ['const', 'let', 'var'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
"collection": "@nestjs/schematics",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true
|
"deleteOutDir": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "@nestjs/swagger",
|
||||||
|
"options": {
|
||||||
|
"classValidatorShim": true,
|
||||||
|
"introspectComments": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,11 @@
|
|||||||
"@types/node": "18.16.12",
|
"@types/node": "18.16.12",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||||
"@typescript-eslint/parser": "^5.59.6",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.41.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.5.5",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
import * as process from 'process'
|
||||||
|
|
||||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
|
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
|
||||||
import { JwtStrategy } from './modules/auth/strategies/jwt.strategy'
|
import { CqrsModule } from '@nestjs/cqrs'
|
||||||
import { ConfigModule } from './settings/config.module'
|
import { MailerModule } from '@nestjs-modules/mailer'
|
||||||
|
|
||||||
|
import { FileUploadService } from './infrastructure/file-upload-service/file-upload.service'
|
||||||
import { AuthModule } from './modules/auth/auth.module'
|
import { AuthModule } from './modules/auth/auth.module'
|
||||||
|
import { JwtRefreshStrategy } from './modules/auth/strategies/jwt-refresh.strategy'
|
||||||
|
import { JwtStrategy } from './modules/auth/strategies/jwt.strategy'
|
||||||
|
import { CardsModule } from './modules/cards/cards.module'
|
||||||
|
import { DecksModule } from './modules/decks/decks.module'
|
||||||
import { UsersModule } from './modules/users/users.module'
|
import { UsersModule } from './modules/users/users.module'
|
||||||
import { PrismaModule } from './prisma.module'
|
import { PrismaModule } from './prisma.module'
|
||||||
import { MailerModule } from '@nestjs-modules/mailer'
|
import { ConfigModule } from './settings/config.module'
|
||||||
import * as process from 'process'
|
|
||||||
import { JwtRefreshStrategy } from './modules/auth/strategies/jwt-refresh.strategy'
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs'
|
|
||||||
import { DecksModule } from './modules/decks/decks.module'
|
|
||||||
import { CardsModule } from './modules/cards/cards.module'
|
|
||||||
import { FileUploadService } from './infrastructure/file-upload-service/file-upload.service'
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|||||||
const response = ctx.getResponse<Response>()
|
const response = ctx.getResponse<Response>()
|
||||||
const request = ctx.getRequest<Request>()
|
const request = ctx.getRequest<Request>()
|
||||||
const status = exception.getStatus()
|
const status = exception.getStatus()
|
||||||
|
|
||||||
if (status === 400) {
|
if (status === 400) {
|
||||||
const errorsResponse = {
|
const errorsResponse = {
|
||||||
errorMessages: [],
|
errorMessages: [],
|
||||||
}
|
}
|
||||||
const responseBody: any = exception.getResponse()
|
const responseBody: any = exception.getResponse()
|
||||||
|
|
||||||
if (typeof responseBody.message === 'object') {
|
if (typeof responseBody.message === 'object') {
|
||||||
responseBody.message.forEach(e => errorsResponse.errorMessages.push(e))
|
responseBody.message.forEach(e => errorsResponse.errorMessages.push(e))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { omit } from 'remeda'
|
|||||||
export const setCountKey = <K extends string, L extends string>(key: K, newKey: L) => {
|
export const setCountKey = <K extends string, L extends string>(key: K, newKey: L) => {
|
||||||
return <T extends Record<string, any>>(obj: T) => {
|
return <T extends Record<string, any>>(obj: T) => {
|
||||||
obj[newKey] = obj['_count'][key]
|
obj[newKey] = obj['_count'][key]
|
||||||
|
|
||||||
return omit(obj, ['_count']) as Omit<T, '_count'> & { [P in L]: number }
|
return omit(obj, ['_count']) as Omit<T, '_count'> & { [P in L]: number }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsNumber, IsOptional } from 'class-validator'
|
|
||||||
import { Type } from 'class-transformer'
|
import { Type } from 'class-transformer'
|
||||||
|
import { IsNumber, IsOptional } from 'class-validator'
|
||||||
|
|
||||||
export class PaginationDto {
|
export class PaginationDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isObject } from 'remeda'
|
import { isObject } from 'remeda'
|
||||||
|
|
||||||
import { DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE } from './pagination.constants'
|
import { DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE } from './pagination.constants'
|
||||||
|
|
||||||
export class Pagination {
|
export class Pagination {
|
||||||
@@ -17,6 +18,7 @@ export class Pagination {
|
|||||||
!isNaN(Number(query.itemsPerPage))
|
!isNaN(Number(query.itemsPerPage))
|
||||||
? +query.itemsPerPage
|
? +query.itemsPerPage
|
||||||
: DEFAULT_PAGE_SIZE
|
: DEFAULT_PAGE_SIZE
|
||||||
|
|
||||||
return { currentPage, itemsPerPage, ...query }
|
return { currentPage, itemsPerPage, ...query }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ export class Pagination {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const totalPages = Math.ceil(count / itemsPerPage)
|
const totalPages = Math.ceil(count / itemsPerPage)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pagination: {
|
pagination: {
|
||||||
totalPages,
|
totalPages,
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'
|
|||||||
|
|
||||||
export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
|
export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
|
||||||
const request = ctx.switchToHttp().getRequest()
|
const request = ctx.switchToHttp().getRequest()
|
||||||
|
|
||||||
return data ? request.cookies?.[data] : request.cookies
|
return data ? request.cookies?.[data] : request.cookies
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
import { PrismaService } from '../../prisma.service'
|
import { PrismaService } from '../../prisma.service'
|
||||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileUploadService {
|
export class FileUploadService {
|
||||||
|
|||||||
16
src/main.ts
16
src/main.ts
@@ -1,13 +1,15 @@
|
|||||||
import { NestFactory } from '@nestjs/core'
|
|
||||||
import { AppModule } from './app.module'
|
|
||||||
import { Logger } from '@nestjs/common'
|
import { Logger } from '@nestjs/common'
|
||||||
import { HttpExceptionFilter } from './exception.filter'
|
import { NestFactory } from '@nestjs/core'
|
||||||
import * as cookieParser from 'cookie-parser'
|
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
||||||
|
import * as cookieParser from 'cookie-parser'
|
||||||
|
|
||||||
|
import { AppModule } from './app.module'
|
||||||
|
import { HttpExceptionFilter } from './exception.filter'
|
||||||
import { pipesSetup } from './settings/pipes-setup'
|
import { pipesSetup } from './settings/pipes-setup'
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule)
|
const app = await NestFactory.create(AppModule)
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: true,
|
origin: true,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
@@ -19,13 +21,19 @@ async function bootstrap() {
|
|||||||
.setTitle('Flashcards')
|
.setTitle('Flashcards')
|
||||||
.setDescription('The config API description')
|
.setDescription('The config API description')
|
||||||
.setVersion('1.0')
|
.setVersion('1.0')
|
||||||
|
.addTag('Auth')
|
||||||
|
.addTag('Decks')
|
||||||
|
.addTag('Cards')
|
||||||
|
.addTag('Admin')
|
||||||
.build()
|
.build()
|
||||||
const document = SwaggerModule.createDocument(app, config)
|
const document = SwaggerModule.createDocument(app, config)
|
||||||
|
|
||||||
SwaggerModule.setup('docs', app, document)
|
SwaggerModule.setup('docs', app, document)
|
||||||
pipesSetup(app)
|
pipesSetup(app)
|
||||||
app.useGlobalFilters(new HttpExceptionFilter())
|
app.useGlobalFilters(new HttpExceptionFilter())
|
||||||
await app.listen(process.env.PORT || 3000)
|
await app.listen(process.env.PORT || 3000)
|
||||||
const logger = new Logger('NestApplication')
|
const logger = new Logger('NestApplication')
|
||||||
|
|
||||||
logger.log(`Application is running on: ${await app.getUrl()}`)
|
logger.log(`Application is running on: ${await app.getUrl()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,21 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common'
|
||||||
import { RegistrationDto } from './dto/registration.dto'
|
|
||||||
import { LocalAuthGuard, JwtAuthGuard, JwtRefreshGuard } from './guards'
|
|
||||||
import { Response as ExpressResponse } from 'express'
|
|
||||||
import { Cookies } from '../../infrastructure/decorators'
|
|
||||||
import { CommandBus } from '@nestjs/cqrs'
|
import { CommandBus } from '@nestjs/cqrs'
|
||||||
|
import { ApiBody, ApiTags } from '@nestjs/swagger'
|
||||||
|
import { Response as ExpressResponse } from 'express'
|
||||||
|
|
||||||
|
import { Cookies } from '../../infrastructure/decorators'
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmailVerificationDto,
|
||||||
|
LoginDto,
|
||||||
|
RecoverPasswordDto,
|
||||||
|
RegistrationDto,
|
||||||
|
ResendVerificationEmailDto,
|
||||||
|
} from './dto'
|
||||||
|
import { LoginResponse, UserEntity } from './entities/auth.entity'
|
||||||
|
import { JwtAuthGuard, JwtRefreshGuard, LocalAuthGuard } from './guards'
|
||||||
import {
|
import {
|
||||||
CreateUserCommand,
|
CreateUserCommand,
|
||||||
GetCurrentUserDataCommand,
|
GetCurrentUserDataCommand,
|
||||||
@@ -28,22 +38,29 @@ import {
|
|||||||
VerifyEmailCommand,
|
VerifyEmailCommand,
|
||||||
} from './use-cases'
|
} from './use-cases'
|
||||||
|
|
||||||
|
@ApiTags('Auth')
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private commandBus: CommandBus) {}
|
constructor(private commandBus: CommandBus) {}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get('me')
|
@Get('me')
|
||||||
async getUserData(@Request() req) {
|
async getUserData(@Request() req): Promise<UserEntity> {
|
||||||
const userId = req.user.id
|
const userId = req.user.id
|
||||||
|
|
||||||
return await this.commandBus.execute(new GetCurrentUserDataCommand(userId))
|
return await this.commandBus.execute(new GetCurrentUserDataCommand(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(200)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiBody({ type: LoginDto })
|
||||||
@UseGuards(LocalAuthGuard)
|
@UseGuards(LocalAuthGuard)
|
||||||
@Post('login')
|
@Post('login')
|
||||||
async login(@Request() req, @Res({ passthrough: true }) res: ExpressResponse) {
|
async login(
|
||||||
|
@Request() req,
|
||||||
|
@Res({ passthrough: true }) res: ExpressResponse
|
||||||
|
): Promise<LoginResponse> {
|
||||||
const userData = req.user.data
|
const userData = req.user.data
|
||||||
|
|
||||||
res.cookie('refreshToken', userData.refreshToken, {
|
res.cookie('refreshToken', userData.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'none',
|
sameSite: 'none',
|
||||||
@@ -55,63 +72,77 @@ export class AuthController {
|
|||||||
sameSite: 'none',
|
sameSite: 'none',
|
||||||
secure: true,
|
secure: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { accessToken: req.user.data.accessToken }
|
return { accessToken: req.user.data.accessToken }
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(201)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@Post('sign-up')
|
@Post('sign-up')
|
||||||
async registration(@Body() registrationData: RegistrationDto) {
|
async registration(@Body() registrationData: RegistrationDto): Promise<UserEntity> {
|
||||||
return await this.commandBus.execute(new CreateUserCommand(registrationData))
|
return await this.commandBus.execute(new CreateUserCommand(registrationData))
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@Post('verify-email')
|
@Post('verify-email')
|
||||||
async confirmRegistration(@Body('code') confirmationCode) {
|
async confirmRegistration(@Body() body: EmailVerificationDto): Promise<void> {
|
||||||
return await this.commandBus.execute(new VerifyEmailCommand(confirmationCode))
|
return await this.commandBus.execute(new VerifyEmailCommand(body.code))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@Post('resend-verification-email')
|
@Post('resend-verification-email')
|
||||||
async resendVerificationEmail(@Body('userId') userId: string) {
|
async resendVerificationEmail(@Body() body: ResendVerificationEmailDto): Promise<void> {
|
||||||
return await this.commandBus.execute(new ResendVerificationEmailCommand(userId))
|
return await this.commandBus.execute(new ResendVerificationEmailCommand(body.userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Post('logout')
|
@Post('logout')
|
||||||
async logout(
|
async logout(
|
||||||
@Cookies('refreshToken') refreshToken: string,
|
@Cookies('refreshToken') refreshToken: string,
|
||||||
@Res({ passthrough: true }) res: ExpressResponse
|
@Res({ passthrough: true }) res: ExpressResponse
|
||||||
) {
|
): Promise<void> {
|
||||||
if (!refreshToken) throw new UnauthorizedException()
|
if (!refreshToken) throw new UnauthorizedException()
|
||||||
await this.commandBus.execute(new LogoutCommand(refreshToken))
|
await this.commandBus.execute(new LogoutCommand(refreshToken))
|
||||||
res.clearCookie('refreshToken')
|
res.clearCookie('refreshToken')
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(JwtRefreshGuard)
|
@UseGuards(JwtRefreshGuard)
|
||||||
@Get('refresh-token')
|
@Post('refresh-token')
|
||||||
async refreshToken(@Request() req, @Response({ passthrough: true }) res: ExpressResponse) {
|
async refreshToken(
|
||||||
|
@Request() req,
|
||||||
|
@Response({ passthrough: true }) res: ExpressResponse
|
||||||
|
): Promise<LoginResponse> {
|
||||||
if (!req.cookies?.refreshToken) throw new UnauthorizedException()
|
if (!req.cookies?.refreshToken) throw new UnauthorizedException()
|
||||||
const userId = req.user.id
|
const userId = req.user.id
|
||||||
const newTokens = await this.commandBus.execute(new RefreshTokenCommand(userId))
|
const newTokens = await this.commandBus.execute(new RefreshTokenCommand(userId))
|
||||||
|
|
||||||
res.cookie('refreshToken', newTokens.refreshToken, {
|
res.cookie('refreshToken', newTokens.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
// secure: true,
|
// secure: true,
|
||||||
path: '/v1/auth/refresh-token',
|
path: '/v1/auth/refresh-token',
|
||||||
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: newTokens.accessToken,
|
accessToken: newTokens.accessToken,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@Post('recover-password')
|
@Post('recover-password')
|
||||||
async recoverPassword(@Body('email') email: string) {
|
async recoverPassword(@Body() body: RecoverPasswordDto): Promise<void> {
|
||||||
return await this.commandBus.execute(new SendPasswordRecoveryEmailCommand(email))
|
return await this.commandBus.execute(new SendPasswordRecoveryEmailCommand(body.email))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@Post('reset-password/:token')
|
@Post('reset-password/:token')
|
||||||
async resetPassword(@Body('password') password: string, @Param('token') token: string) {
|
async resetPassword(
|
||||||
|
@Body('password') password: string,
|
||||||
|
@Param('token') token: string
|
||||||
|
): Promise<void> {
|
||||||
return await this.commandBus.execute(new ResetPasswordCommand(token, password))
|
return await this.commandBus.execute(new ResetPasswordCommand(token, password))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
import { AuthService } from './auth.service'
|
|
||||||
import { AuthController } from './auth.controller'
|
|
||||||
import { UsersModule } from '../users/users.module'
|
|
||||||
import { LocalStrategy } from './strategies/local.strategy'
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs'
|
import { CqrsModule } from '@nestjs/cqrs'
|
||||||
|
|
||||||
|
import { UsersModule } from '../users/users.module'
|
||||||
|
|
||||||
|
import { AuthController } from './auth.controller'
|
||||||
|
import { AuthService } from './auth.service'
|
||||||
|
import { AuthRepository } from './infrastructure/auth.repository'
|
||||||
|
import { LocalStrategy } from './strategies/local.strategy'
|
||||||
import {
|
import {
|
||||||
CreateUserHandler,
|
CreateUserHandler,
|
||||||
GetCurrentUserDataHandler,
|
GetCurrentUserDataHandler,
|
||||||
@@ -14,7 +17,6 @@ import {
|
|||||||
SendPasswordRecoveryEmailHandler,
|
SendPasswordRecoveryEmailHandler,
|
||||||
VerifyEmailHandler,
|
VerifyEmailHandler,
|
||||||
} from './use-cases'
|
} from './use-cases'
|
||||||
import { AuthRepository } from './infrastructure/auth.repository'
|
|
||||||
|
|
||||||
const commandHandlers = [
|
const commandHandlers = [
|
||||||
CreateUserHandler,
|
CreateUserHandler,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import * as process from 'process'
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import * as bcrypt from 'bcrypt'
|
||||||
import { addDays } from 'date-fns'
|
import { addDays } from 'date-fns'
|
||||||
import * as jwt from 'jsonwebtoken'
|
import * as jwt from 'jsonwebtoken'
|
||||||
import * as bcrypt from 'bcrypt'
|
|
||||||
import { UsersRepository } from '../users/infrastructure/users.repository'
|
|
||||||
import * as process from 'process'
|
|
||||||
import { PrismaService } from '../../prisma.service'
|
import { PrismaService } from '../../prisma.service'
|
||||||
|
import { UsersRepository } from '../users/infrastructure/users.repository'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -21,6 +23,7 @@ export class AuthService {
|
|||||||
const refreshToken = jwt.sign(payload, refreshSecretKey, {
|
const refreshToken = jwt.sign(payload, refreshSecretKey, {
|
||||||
expiresIn: '30d',
|
expiresIn: '30d',
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.prisma.refreshToken.create({
|
await this.prisma.refreshToken.create({
|
||||||
data: {
|
data: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
@@ -29,6 +32,7 @@ export class AuthService {
|
|||||||
isRevoked: false,
|
isRevoked: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
@@ -37,6 +41,7 @@ export class AuthService {
|
|||||||
|
|
||||||
async checkCredentials(email: string, password: string) {
|
async checkCredentials(email: string, password: string) {
|
||||||
const user = await this.usersRepository.findUserByEmail(email)
|
const user = await this.usersRepository.findUserByEmail(email)
|
||||||
|
|
||||||
if (!user /*|| !user.emailConfirmation.isConfirmed*/)
|
if (!user /*|| !user.emailConfirmation.isConfirmed*/)
|
||||||
return {
|
return {
|
||||||
resultCode: 1,
|
resultCode: 1,
|
||||||
@@ -46,6 +51,7 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const isPasswordValid = await this.isPasswordCorrect(password, user.password)
|
const isPasswordValid = await this.isPasswordCorrect(password, user.password)
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
return {
|
return {
|
||||||
resultCode: 1,
|
resultCode: 1,
|
||||||
@@ -58,6 +64,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tokensPair = await this.createJwtTokensPair(user.id)
|
const tokensPair = await this.createJwtTokensPair(user.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resultCode: 0,
|
resultCode: 0,
|
||||||
data: tokensPair,
|
data: tokensPair,
|
||||||
|
|||||||
6
src/modules/auth/dto/email-verification.dto.ts
Normal file
6
src/modules/auth/dto/email-verification.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsUUID } from 'class-validator'
|
||||||
|
|
||||||
|
export class EmailVerificationDto {
|
||||||
|
@IsUUID('4')
|
||||||
|
code: string
|
||||||
|
}
|
||||||
6
src/modules/auth/dto/index.ts
Normal file
6
src/modules/auth/dto/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './email-verification.dto'
|
||||||
|
export * from './login.dto'
|
||||||
|
export * from './recover-password.dto'
|
||||||
|
export * from './registration.dto'
|
||||||
|
export * from './resend-verification-email.dto'
|
||||||
|
export * from './update-auth.dto'
|
||||||
9
src/modules/auth/dto/login.dto.ts
Normal file
9
src/modules/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { IsEmail, Length } from 'class-validator'
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@Length(3, 30)
|
||||||
|
password: string
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email: string
|
||||||
|
}
|
||||||
6
src/modules/auth/dto/recover-password.dto.ts
Normal file
6
src/modules/auth/dto/recover-password.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsEmail } from 'class-validator'
|
||||||
|
|
||||||
|
export class RecoverPasswordDto {
|
||||||
|
@IsEmail()
|
||||||
|
email: string
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { IsEmail, Length, IsOptional } from 'class-validator'
|
import { IsEmail, IsOptional, Length } from 'class-validator'
|
||||||
|
|
||||||
export class RegistrationDto {
|
export class RegistrationDto {
|
||||||
@Length(3, 30)
|
@Length(3, 30)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
name: string
|
name?: string
|
||||||
|
|
||||||
@Length(3, 30)
|
@Length(3, 30)
|
||||||
password: string
|
password: string
|
||||||
|
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/modules/auth/dto/resend-verification-email.dto.ts
Normal file
6
src/modules/auth/dto/resend-verification-email.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsUUID } from 'class-validator'
|
||||||
|
|
||||||
|
export class ResendVerificationEmailDto {
|
||||||
|
@IsUUID()
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types'
|
import { PartialType } from '@nestjs/mapped-types'
|
||||||
|
|
||||||
import { RegistrationDto } from './registration.dto'
|
import { RegistrationDto } from './registration.dto'
|
||||||
|
|
||||||
export class UpdateAuthDto extends PartialType(RegistrationDto) {}
|
export class UpdateAuthDto extends PartialType(RegistrationDto) {}
|
||||||
|
|||||||
@@ -1 +1,18 @@
|
|||||||
export class Auth {}
|
import { OmitType } from '@nestjs/swagger'
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
isEmailVerified: boolean
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
created: string
|
||||||
|
updated: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginResponse {
|
||||||
|
accessToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserEntity extends OmitType(User, ['password']) {}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export class BaseAuthGuard implements CanActivate {
|
|||||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
const request = context.switchToHttp().getRequest()
|
const request = context.switchToHttp().getRequest()
|
||||||
const exceptedAuthInput = 'Basic YWRtaW46cXdlcnR5'
|
const exceptedAuthInput = 'Basic YWRtaW46cXdlcnR5'
|
||||||
|
|
||||||
if (!request.headers || !request.headers.authorization) {
|
if (!request.headers || !request.headers.authorization) {
|
||||||
throw new UnauthorizedException([{ message: 'No any auth headers' }])
|
throw new UnauthorizedException([{ message: 'No any auth headers' }])
|
||||||
} else {
|
} else {
|
||||||
@@ -17,6 +18,7 @@ export class BaseAuthGuard implements CanActivate {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
|||||||
const req = context.switchToHttp().getRequest()
|
const req = context.switchToHttp().getRequest()
|
||||||
|
|
||||||
const res: boolean = await (super.canActivate(context) as Promise<boolean>)
|
const res: boolean = await (super.canActivate(context) as Promise<boolean>)
|
||||||
|
|
||||||
if (!res) return false
|
if (!res) return false
|
||||||
|
|
||||||
// check DTO
|
// check DTO
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
|
|
||||||
import { PrismaService } from '../../../prisma.service'
|
import { PrismaService } from '../../../prisma.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common'
|
import { Inject, Injectable } from '@nestjs/common'
|
||||||
import { PassportStrategy } from '@nestjs/passport'
|
import { PassportStrategy } from '@nestjs/passport'
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt'
|
import { ExtractJwt, Strategy } from 'passport-jwt'
|
||||||
|
|
||||||
import { AppSettings } from '../../../settings/app-settings'
|
import { AppSettings } from '../../../settings/app-settings'
|
||||||
|
|
||||||
type JwtPayload = {
|
type JwtPayload = {
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common'
|
import { Inject, Injectable } from '@nestjs/common'
|
||||||
import { PassportStrategy } from '@nestjs/passport'
|
import { PassportStrategy } from '@nestjs/passport'
|
||||||
import { Strategy } from 'passport-jwt'
|
|
||||||
import { UsersService } from '../../users/services/users.service'
|
|
||||||
import { AppSettings } from '../../../settings/app-settings'
|
|
||||||
import { Request } from 'express'
|
import { Request } from 'express'
|
||||||
|
import { Strategy } from 'passport-jwt'
|
||||||
|
|
||||||
|
import { AppSettings } from '../../../settings/app-settings'
|
||||||
|
import { UsersService } from '../../users/services/users.service'
|
||||||
|
|
||||||
const cookieExtractor = function (req: Request) {
|
const cookieExtractor = function (req: Request) {
|
||||||
let token = null
|
let token = null
|
||||||
|
|
||||||
if (req && req.cookies) {
|
if (req && req.cookies) {
|
||||||
token = req.cookies['refreshToken']
|
token = req.cookies['refreshToken']
|
||||||
}
|
}
|
||||||
|
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'
|
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'
|
||||||
import { PassportStrategy } from '@nestjs/passport'
|
import { PassportStrategy } from '@nestjs/passport'
|
||||||
|
import { Request as RequestType } from 'express'
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt'
|
import { ExtractJwt, Strategy } from 'passport-jwt'
|
||||||
import { AuthService } from '../auth.service'
|
|
||||||
import { AppSettings } from '../../../settings/app-settings'
|
import { AppSettings } from '../../../settings/app-settings'
|
||||||
import { UsersService } from '../../users/services/users.service'
|
import { UsersService } from '../../users/services/users.service'
|
||||||
import { Request as RequestType } from 'express'
|
import { AuthService } from '../auth.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
@@ -25,9 +26,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
|
|
||||||
async validate(payload: any) {
|
async validate(payload: any) {
|
||||||
const user = await this.userService.getUserById(payload.userId)
|
const user = await this.userService.getUserById(payload.userId)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException()
|
throw new UnauthorizedException()
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +38,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
if (req.cookies && 'accessToken' in req.cookies && req.cookies.accessToken.length > 0) {
|
if (req.cookies && 'accessToken' in req.cookies && req.cookies.accessToken.length > 0) {
|
||||||
return req.cookies.accessToken
|
return req.cookies.accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common'
|
import { Injectable, UnauthorizedException } from '@nestjs/common'
|
||||||
import { PassportStrategy } from '@nestjs/passport'
|
import { PassportStrategy } from '@nestjs/passport'
|
||||||
import { Strategy } from 'passport-local'
|
import { Strategy } from 'passport-local'
|
||||||
|
|
||||||
import { AuthService } from '../auth.service'
|
import { AuthService } from '../auth.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -13,9 +14,11 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
|
|||||||
|
|
||||||
async validate(email: string, password: string): Promise<any> {
|
async validate(email: string, password: string): Promise<any> {
|
||||||
const newCredentials = await this.authService.checkCredentials(email, password)
|
const newCredentials = await this.authService.checkCredentials(email, password)
|
||||||
|
|
||||||
if (newCredentials.resultCode === 1) {
|
if (newCredentials.resultCode === 1) {
|
||||||
throw new UnauthorizedException('Invalid credentials')
|
throw new UnauthorizedException('Invalid credentials')
|
||||||
}
|
}
|
||||||
|
|
||||||
return newCredentials
|
return newCredentials
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { HttpStatus } from '@nestjs/common'
|
||||||
import { Test, TestingModule } from '@nestjs/testing'
|
import { Test, TestingModule } from '@nestjs/testing'
|
||||||
import * as request from 'supertest'
|
import * as request from 'supertest'
|
||||||
import { HttpStatus } from '@nestjs/common'
|
|
||||||
import { AppModule } from '../../../../app.module'
|
import { AppModule } from '../../../../app.module'
|
||||||
import { RegistrationDto } from '../../dto/registration.dto'
|
import { RegistrationDto } from '../../dto'
|
||||||
|
|
||||||
describe('AuthController (e2e)', () => {
|
describe('AuthController (e2e)', () => {
|
||||||
let app
|
let app
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
|
||||||
import { CreateUserInput, UserViewType } from '../../../types/types'
|
|
||||||
import { addHours } from 'date-fns'
|
import { addHours } from 'date-fns'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { CreateUserInput, UserViewType } from '../../../types/types'
|
||||||
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
import { UsersService } from '../../users/services/users.service'
|
import { UsersService } from '../../users/services/users.service'
|
||||||
|
import { RegistrationDto } from '../dto'
|
||||||
|
|
||||||
export class CreateUserCommand {
|
export class CreateUserCommand {
|
||||||
constructor(public readonly user: { name: string; password: string; email: string }) {}
|
constructor(public readonly user: RegistrationDto) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CommandHandler(CreateUserCommand)
|
@CommandHandler(CreateUserCommand)
|
||||||
@@ -29,6 +31,7 @@ export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
|
|||||||
isEmailVerified: false,
|
isEmailVerified: false,
|
||||||
}
|
}
|
||||||
const createdUser = await this.usersRepository.createUser(newUser)
|
const createdUser = await this.usersRepository.createUser(newUser)
|
||||||
|
|
||||||
if (!createdUser) {
|
if (!createdUser) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,7 @@ export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
|
|||||||
name: createdUser.name,
|
name: createdUser.name,
|
||||||
verificationToken: verificationToken,
|
verificationToken: verificationToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: createdUser.id,
|
id: createdUser.id,
|
||||||
name: createdUser.name,
|
name: createdUser.name,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { UserViewType } from '../../../types/types'
|
|
||||||
import { UnauthorizedException } from '@nestjs/common'
|
import { UnauthorizedException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
import { pick } from 'remeda'
|
import { pick } from 'remeda'
|
||||||
|
|
||||||
|
import { UserViewType } from '../../../types/types'
|
||||||
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
|
|
||||||
export class GetCurrentUserDataCommand {
|
export class GetCurrentUserDataCommand {
|
||||||
@@ -17,6 +18,6 @@ export class GetCurrentUserDataHandler implements ICommandHandler<GetCurrentUser
|
|||||||
|
|
||||||
if (!user) throw new UnauthorizedException()
|
if (!user) throw new UnauthorizedException()
|
||||||
|
|
||||||
return pick(user, ['email', 'name', 'id', 'isEmailVerified', 'avatar'])
|
return pick(user, ['email', 'name', 'id', 'isEmailVerified', 'avatar', 'created', 'updated'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { Logger } from '@nestjs/common'
|
import { Logger } from '@nestjs/common'
|
||||||
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
|
|
||||||
export class LogoutCommand {
|
export class LogoutCommand {
|
||||||
constructor(public readonly refreshToken: string) {}
|
constructor(public readonly refreshToken: string) {}
|
||||||
}
|
}
|
||||||
@@ -17,13 +18,18 @@ export class LogoutHandler implements ICommandHandler<LogoutCommand> {
|
|||||||
const token = command.refreshToken
|
const token = command.refreshToken
|
||||||
|
|
||||||
const secretKey = process.env.JWT_SECRET_KEY
|
const secretKey = process.env.JWT_SECRET_KEY
|
||||||
|
|
||||||
if (!secretKey) throw new Error('JWT_SECRET_KEY is not defined')
|
if (!secretKey) throw new Error('JWT_SECRET_KEY is not defined')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded: any = jwt.verify(token, secretKey)
|
const decoded: any = jwt.verify(token, secretKey)
|
||||||
return this.usersRepository.revokeToken(decoded.userId, token)
|
|
||||||
|
await this.usersRepository.revokeToken(decoded.userId, token)
|
||||||
|
|
||||||
|
return null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.log(`Decoding error: ${e}`)
|
this.logger.log(`Decoding error: ${e}`)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
import { AuthRepository } from '../infrastructure/auth.repository'
|
|
||||||
import * as jwt from 'jsonwebtoken'
|
|
||||||
import { addDays } from 'date-fns'
|
import { addDays } from 'date-fns'
|
||||||
|
import * as jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
|
import { AuthRepository } from '../infrastructure/auth.repository'
|
||||||
|
|
||||||
export class RefreshTokenCommand {
|
export class RefreshTokenCommand {
|
||||||
constructor(public readonly userId: string) {}
|
constructor(public readonly userId: string) {}
|
||||||
@@ -26,7 +27,9 @@ export class RefreshTokenHandler implements ICommandHandler<RefreshTokenCommand>
|
|||||||
expiresIn: '30d',
|
expiresIn: '30d',
|
||||||
})
|
})
|
||||||
const expiresIn = addDays(new Date(), 30)
|
const expiresIn = addDays(new Date(), 30)
|
||||||
|
|
||||||
await this.authRepository.createRefreshToken(userId, refreshToken, expiresIn)
|
await this.authRepository.createRefreshToken(userId, refreshToken, expiresIn)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
import { UsersService } from '../../users/services/users.service'
|
import { UsersService } from '../../users/services/users.service'
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export class ResendVerificationEmailHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await this.usersRepository.updateVerificationToken(user.id)
|
const updatedUser = await this.usersRepository.updateVerificationToken(user.id)
|
||||||
|
|
||||||
await this.usersService.sendConfirmationEmail({
|
await this.usersService.sendConfirmationEmail({
|
||||||
email: updatedUser.user.email,
|
email: updatedUser.user.email,
|
||||||
name: updatedUser.user.name,
|
name: updatedUser.user.name,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
import { UsersService } from '../../users/services/users.service'
|
import { UsersService } from '../../users/services/users.service'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { NotFoundException } from '@nestjs/common'
|
import { NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
import { UsersService } from '../../users/services/users.service'
|
import { UsersService } from '../../users/services/users.service'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
|
|
||||||
export class SendPasswordRecoveryEmailCommand {
|
export class SendPasswordRecoveryEmailCommand {
|
||||||
constructor(public readonly email: string) {}
|
constructor(public readonly email: string) {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
import { isBefore } from 'date-fns'
|
import { isBefore } from 'date-fns'
|
||||||
|
|
||||||
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
|
|
||||||
export class VerifyEmailCommand {
|
export class VerifyEmailCommand {
|
||||||
@@ -15,6 +16,7 @@ export class VerifyEmailHandler implements ICommandHandler<VerifyEmailCommand> {
|
|||||||
const token = command.token
|
const token = command.token
|
||||||
|
|
||||||
const verificationWithUser = await this.usersRepository.findUserByVerificationToken(token)
|
const verificationWithUser = await this.usersRepository.findUserByVerificationToken(token)
|
||||||
|
|
||||||
if (!verificationWithUser) throw new NotFoundException('User not found')
|
if (!verificationWithUser) throw new NotFoundException('User not found')
|
||||||
|
|
||||||
if (verificationWithUser.isEmailVerified)
|
if (verificationWithUser.isEmailVerified)
|
||||||
@@ -22,14 +24,17 @@ export class VerifyEmailHandler implements ICommandHandler<VerifyEmailCommand> {
|
|||||||
|
|
||||||
const dbToken = verificationWithUser.verificationToken
|
const dbToken = verificationWithUser.verificationToken
|
||||||
const isTokenExpired = isBefore(verificationWithUser.verificationTokenExpiry, new Date())
|
const isTokenExpired = isBefore(verificationWithUser.verificationTokenExpiry, new Date())
|
||||||
|
|
||||||
if (dbToken !== token || isTokenExpired) {
|
if (dbToken !== token || isTokenExpired) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.usersRepository.updateEmailVerification(verificationWithUser.userId)
|
const result = await this.usersRepository.updateEmailVerification(verificationWithUser.userId)
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new NotFoundException('User not found')
|
throw new NotFoundException('User not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common'
|
||||||
|
import { CommandBus } from '@nestjs/cqrs'
|
||||||
|
import { FileFieldsInterceptor } from '@nestjs/platform-express'
|
||||||
|
import { ApiTags } from '@nestjs/swagger'
|
||||||
|
|
||||||
|
import { JwtAuthGuard } from '../auth/guards'
|
||||||
|
|
||||||
import { CardsService } from './cards.service'
|
import { CardsService } from './cards.service'
|
||||||
import { UpdateCardDto } from './dto'
|
import { UpdateCardDto } from './dto'
|
||||||
import { CommandBus } from '@nestjs/cqrs'
|
|
||||||
import { DeleteCardByIdCommand, GetDeckByIdCommand, UpdateCardCommand } from './use-cases'
|
import { DeleteCardByIdCommand, GetDeckByIdCommand, UpdateCardCommand } from './use-cases'
|
||||||
import { JwtAuthGuard } from '../auth/guards'
|
|
||||||
import { FileFieldsInterceptor } from '@nestjs/platform-express'
|
|
||||||
|
|
||||||
|
@ApiTags('Cards')
|
||||||
@Controller('cards')
|
@Controller('cards')
|
||||||
export class CardsController {
|
export class CardsController {
|
||||||
constructor(private readonly decksService: CardsService, private commandBus: CommandBus) {}
|
constructor(private readonly decksService: CardsService, private commandBus: CommandBus) {}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
import { CardsService } from './cards.service'
|
|
||||||
import { CardsController } from './cards.controller'
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs'
|
import { CqrsModule } from '@nestjs/cqrs'
|
||||||
import { DeleteCardByIdHandler, GetDeckByIdHandler, UpdateCardHandler } from './use-cases'
|
|
||||||
import { CardsRepository } from './infrastructure/cards.repository'
|
|
||||||
import { FileUploadService } from '../../infrastructure/file-upload-service/file-upload.service'
|
import { FileUploadService } from '../../infrastructure/file-upload-service/file-upload.service'
|
||||||
|
|
||||||
|
import { CardsController } from './cards.controller'
|
||||||
|
import { CardsService } from './cards.service'
|
||||||
|
import { CardsRepository } from './infrastructure/cards.repository'
|
||||||
|
import { DeleteCardByIdHandler, GetDeckByIdHandler, UpdateCardHandler } from './use-cases'
|
||||||
|
|
||||||
const commandHandlers = [GetDeckByIdHandler, DeleteCardByIdHandler, UpdateCardHandler]
|
const commandHandlers = [GetDeckByIdHandler, DeleteCardByIdHandler, UpdateCardHandler]
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Length } from 'class-validator'
|
import { Length } from 'class-validator'
|
||||||
|
|
||||||
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
|
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
|
||||||
import { IsOptionalOrEmptyString, IsOrderBy } from '../../../infrastructure/decorators'
|
import { IsOptionalOrEmptyString, IsOrderBy } from '../../../infrastructure/decorators'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types'
|
import { PartialType } from '@nestjs/mapped-types'
|
||||||
|
|
||||||
import { CreateCardDto } from './create-card.dto'
|
import { CreateCardDto } from './create-card.dto'
|
||||||
|
|
||||||
export class UpdateCardDto extends PartialType(CreateCardDto) {}
|
export class UpdateCardDto extends PartialType(CreateCardDto) {}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
|
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
|
||||||
import { PrismaService } from '../../../prisma.service'
|
|
||||||
import { GetAllCardsInDeckDto } from '../dto/get-all-cards.dto'
|
|
||||||
import { CreateCardDto } from '../dto/create-card.dto'
|
|
||||||
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
|
||||||
import { createPrismaOrderBy } from '../../../infrastructure/common/helpers/get-order-by-object'
|
import { createPrismaOrderBy } from '../../../infrastructure/common/helpers/get-order-by-object'
|
||||||
import { UpdateCardDto } from '../dto/update-card.dto'
|
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
||||||
|
import { PrismaService } from '../../../prisma.service'
|
||||||
|
import { CreateCardDto, GetAllCardsInDeckDto, UpdateCardDto } from '../dto'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CardsRepository {
|
export class CardsRepository {
|
||||||
@@ -31,6 +30,7 @@ export class CardsRepository {
|
|||||||
...card,
|
...card,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await tx.deck.update({
|
await tx.deck.update({
|
||||||
where: {
|
where: {
|
||||||
id: deckId,
|
id: deckId,
|
||||||
@@ -41,6 +41,7 @@ export class CardsRepository {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return created
|
return created
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -80,6 +81,7 @@ export class CardsRepository {
|
|||||||
take: itemsPerPage,
|
take: itemsPerPage,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
return Pagination.transformPaginationData(result, { currentPage, itemsPerPage })
|
return Pagination.transformPaginationData(result, { currentPage, itemsPerPage })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(e?.message)
|
this.logger.error(e?.message)
|
||||||
@@ -129,6 +131,7 @@ export class CardsRepository {
|
|||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await tx.deck.update({
|
await tx.deck.update({
|
||||||
where: {
|
where: {
|
||||||
id: deleted.deckId,
|
id: deleted.deckId,
|
||||||
@@ -139,6 +142,7 @@ export class CardsRepository {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return deleted
|
return deleted
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { CardsRepository } from '../infrastructure/cards.repository'
|
|
||||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
|
import { CardsRepository } from '../infrastructure/cards.repository'
|
||||||
|
|
||||||
export class DeleteCardByIdCommand {
|
export class DeleteCardByIdCommand {
|
||||||
constructor(public readonly id: string, public readonly userId: string) {}
|
constructor(public readonly id: string, public readonly userId: string) {}
|
||||||
@@ -12,10 +13,12 @@ export class DeleteCardByIdHandler implements ICommandHandler<DeleteCardByIdComm
|
|||||||
|
|
||||||
async execute(command: DeleteCardByIdCommand) {
|
async execute(command: DeleteCardByIdCommand) {
|
||||||
const card = await this.cardsRepository.findCardById(command.id)
|
const card = await this.cardsRepository.findCardById(command.id)
|
||||||
|
|
||||||
if (!card) throw new NotFoundException(`Card with id ${command.id} not found`)
|
if (!card) throw new NotFoundException(`Card with id ${command.id} not found`)
|
||||||
if (card.userId !== command.userId) {
|
if (card.userId !== command.userId) {
|
||||||
throw new BadRequestException(`You can't delete a card that you don't own`)
|
throw new BadRequestException(`You can't delete a card that you don't own`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.cardsRepository.deleteCardById(command.id)
|
return await this.cardsRepository.deleteCardById(command.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
import { CardsRepository } from '../infrastructure/cards.repository'
|
import { CardsRepository } from '../infrastructure/cards.repository'
|
||||||
|
|
||||||
export class GetDeckByIdCommand {
|
export class GetDeckByIdCommand {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { CardsRepository } from '../infrastructure/cards.repository'
|
|
||||||
import { UpdateCardDto } from '../dto/update-card.dto'
|
|
||||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service'
|
import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service'
|
||||||
|
import { UpdateCardDto } from '../dto'
|
||||||
|
import { CardsRepository } from '../infrastructure/cards.repository'
|
||||||
|
|
||||||
export class UpdateCardCommand {
|
export class UpdateCardCommand {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -43,6 +44,7 @@ export class UpdateCardHandler implements ICommandHandler<UpdateCardCommand> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const result = await Promise.all([addQuestionImagePromise, addAnswerImagePromise])
|
const result = await Promise.all([addQuestionImagePromise, addAnswerImagePromise])
|
||||||
|
|
||||||
questionImg = result[0].fileUrl
|
questionImg = result[0].fileUrl
|
||||||
answerImg = result[1].fileUrl
|
answerImg = result[1].fileUrl
|
||||||
} else if (command.answerImg) {
|
} else if (command.answerImg) {
|
||||||
@@ -51,6 +53,7 @@ export class UpdateCardHandler implements ICommandHandler<UpdateCardCommand> {
|
|||||||
command.answerImg?.originalname
|
command.answerImg?.originalname
|
||||||
)
|
)
|
||||||
const result = await addAnswerImagePromise
|
const result = await addAnswerImagePromise
|
||||||
|
|
||||||
answerImg = result.fileUrl
|
answerImg = result.fileUrl
|
||||||
} else if (command.questionImg) {
|
} else if (command.questionImg) {
|
||||||
const addQuestionImagePromise = this.fileUploadService.uploadFile(
|
const addQuestionImagePromise = this.fileUploadService.uploadFile(
|
||||||
@@ -58,6 +61,7 @@ export class UpdateCardHandler implements ICommandHandler<UpdateCardCommand> {
|
|||||||
command.questionImg?.originalname
|
command.questionImg?.originalname
|
||||||
)
|
)
|
||||||
const result = await addQuestionImagePromise
|
const result = await addQuestionImagePromise
|
||||||
|
|
||||||
questionImg = result.fileUrl
|
questionImg = result.fileUrl
|
||||||
}
|
}
|
||||||
if (command.card.questionImg === '') {
|
if (command.card.questionImg === '') {
|
||||||
@@ -66,6 +70,7 @@ export class UpdateCardHandler implements ICommandHandler<UpdateCardCommand> {
|
|||||||
if (command.card.answerImg === '') {
|
if (command.card.answerImg === '') {
|
||||||
answerImg = null
|
answerImg = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.cardsRepository.updateCardById(command.cardId, {
|
return await this.cardsRepository.updateCardById(command.cardId, {
|
||||||
...command.card,
|
...command.card,
|
||||||
answerImg,
|
answerImg,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { DomainResultNotification, ResultNotification } from './notification'
|
|
||||||
import { validateOrReject } from 'class-validator'
|
|
||||||
import { IEvent } from '@nestjs/cqrs'
|
import { IEvent } from '@nestjs/cqrs'
|
||||||
|
import { validateOrReject } from 'class-validator'
|
||||||
|
|
||||||
import { validationErrorsMapper, ValidationPipeErrorType } from '../../../settings/pipes-setup'
|
import { validationErrorsMapper, ValidationPipeErrorType } from '../../../settings/pipes-setup'
|
||||||
|
|
||||||
|
import { DomainResultNotification, ResultNotification } from './notification'
|
||||||
|
|
||||||
export class DomainError extends Error {
|
export class DomainError extends Error {
|
||||||
constructor(message: string, public resultNotification: ResultNotification) {
|
constructor(message: string, public resultNotification: ResultNotification) {
|
||||||
super(message)
|
super(message)
|
||||||
@@ -31,11 +33,14 @@ export const validateEntity = async <T extends object>(
|
|||||||
const resultNotification: DomainResultNotification<T> = mapErrorsToNotification<T>(
|
const resultNotification: DomainResultNotification<T> = mapErrorsToNotification<T>(
|
||||||
validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray(errors)
|
validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray(errors)
|
||||||
)
|
)
|
||||||
|
|
||||||
resultNotification.addData(entity)
|
resultNotification.addData(entity)
|
||||||
resultNotification.addEvents(...events)
|
resultNotification.addEvents(...events)
|
||||||
|
|
||||||
return resultNotification
|
return resultNotification
|
||||||
}
|
}
|
||||||
const domainResultNotification = new DomainResultNotification<T>(entity)
|
const domainResultNotification = new DomainResultNotification<T>(entity)
|
||||||
|
|
||||||
domainResultNotification.addEvents(...events)
|
domainResultNotification.addEvents(...events)
|
||||||
|
|
||||||
return domainResultNotification
|
return domainResultNotification
|
||||||
@@ -43,8 +48,10 @@ export const validateEntity = async <T extends object>(
|
|||||||
|
|
||||||
export function mapErrorsToNotification<T>(errors: ValidationPipeErrorType[]) {
|
export function mapErrorsToNotification<T>(errors: ValidationPipeErrorType[]) {
|
||||||
const resultNotification = new DomainResultNotification<T>()
|
const resultNotification = new DomainResultNotification<T>()
|
||||||
|
|
||||||
errors.forEach((item: ValidationPipeErrorType) =>
|
errors.forEach((item: ValidationPipeErrorType) =>
|
||||||
resultNotification.addError(item.message, item.field, 1)
|
resultNotification.addError(item.message, item.field, 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
return resultNotification
|
return resultNotification
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,16 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common'
|
||||||
|
import { CommandBus } from '@nestjs/cqrs'
|
||||||
|
import { FileFieldsInterceptor } from '@nestjs/platform-express'
|
||||||
|
import { ApiTags } from '@nestjs/swagger'
|
||||||
|
|
||||||
|
import { Pagination } from '../../infrastructure/common/pagination/pagination.service'
|
||||||
|
import { JwtAuthGuard } from '../auth/guards'
|
||||||
|
import { CreateCardDto, GetAllCardsInDeckDto } from '../cards/dto'
|
||||||
|
|
||||||
import { DecksService } from './decks.service'
|
import { DecksService } from './decks.service'
|
||||||
import { UpdateDeckDto, CreateDeckDto, GetAllDecksDto } from './dto'
|
import { UpdateDeckDto, CreateDeckDto, GetAllDecksDto } from './dto'
|
||||||
import { CommandBus } from '@nestjs/cqrs'
|
|
||||||
import {
|
import {
|
||||||
CreateDeckCommand,
|
CreateDeckCommand,
|
||||||
DeleteDeckByIdCommand,
|
DeleteDeckByIdCommand,
|
||||||
@@ -27,11 +34,8 @@ import {
|
|||||||
SaveGradeCommand,
|
SaveGradeCommand,
|
||||||
CreateCardCommand,
|
CreateCardCommand,
|
||||||
} from './use-cases'
|
} from './use-cases'
|
||||||
import { JwtAuthGuard } from '../auth/guards'
|
|
||||||
import { CreateCardDto, GetAllCardsInDeckDto } from '../cards/dto'
|
|
||||||
import { Pagination } from '../../infrastructure/common/pagination/pagination.service'
|
|
||||||
import { FileFieldsInterceptor } from '@nestjs/platform-express'
|
|
||||||
|
|
||||||
|
@ApiTags('Decks')
|
||||||
@Controller('decks')
|
@Controller('decks')
|
||||||
export class DecksController {
|
export class DecksController {
|
||||||
constructor(private readonly decksService: DecksService, private commandBus: CommandBus) {}
|
constructor(private readonly decksService: DecksService, private commandBus: CommandBus) {}
|
||||||
@@ -48,6 +52,7 @@ export class DecksController {
|
|||||||
@Body() createDeckDto: CreateDeckDto
|
@Body() createDeckDto: CreateDeckDto
|
||||||
) {
|
) {
|
||||||
const userId = req.user.id
|
const userId = req.user.id
|
||||||
|
|
||||||
return this.commandBus.execute(
|
return this.commandBus.execute(
|
||||||
new CreateDeckCommand({ ...createDeckDto, userId: userId }, files?.cover?.[0])
|
new CreateDeckCommand({ ...createDeckDto, userId: userId }, files?.cover?.[0])
|
||||||
)
|
)
|
||||||
@@ -57,6 +62,7 @@ export class DecksController {
|
|||||||
@Get()
|
@Get()
|
||||||
findAll(@Query() query: GetAllDecksDto, @Req() req) {
|
findAll(@Query() query: GetAllDecksDto, @Req() req) {
|
||||||
const finalQuery = Pagination.getPaginationData(query)
|
const finalQuery = Pagination.getPaginationData(query)
|
||||||
|
|
||||||
return this.commandBus.execute(new GetAllDecksCommand({ ...finalQuery, userId: req.user.id }))
|
return this.commandBus.execute(new GetAllDecksCommand({ ...finalQuery, userId: req.user.id }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +76,7 @@ export class DecksController {
|
|||||||
@Get(':id/cards')
|
@Get(':id/cards')
|
||||||
findCardsInDeck(@Param('id') id: string, @Req() req, @Query() query: GetAllCardsInDeckDto) {
|
findCardsInDeck(@Param('id') id: string, @Req() req, @Query() query: GetAllCardsInDeckDto) {
|
||||||
const finalQuery = Pagination.getPaginationData(query)
|
const finalQuery = Pagination.getPaginationData(query)
|
||||||
|
|
||||||
return this.commandBus.execute(new GetAllCardsInDeckCommand(req.user.id, id, finalQuery))
|
return this.commandBus.execute(new GetAllCardsInDeckCommand(req.user.id, id, finalQuery))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
import { DecksService } from './decks.service'
|
|
||||||
import { DecksController } from './decks.controller'
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs'
|
import { CqrsModule } from '@nestjs/cqrs'
|
||||||
|
|
||||||
|
import { FileUploadService } from '../../infrastructure/file-upload-service/file-upload.service'
|
||||||
|
import { CardsRepository } from '../cards/infrastructure/cards.repository'
|
||||||
|
|
||||||
|
import { DecksController } from './decks.controller'
|
||||||
|
import { DecksService } from './decks.service'
|
||||||
|
import { DecksRepository } from './infrastructure/decks.repository'
|
||||||
|
import { GradesRepository } from './infrastructure/grades.repository'
|
||||||
import {
|
import {
|
||||||
CreateDeckHandler,
|
CreateDeckHandler,
|
||||||
DeleteDeckByIdHandler,
|
DeleteDeckByIdHandler,
|
||||||
@@ -13,10 +19,6 @@ import {
|
|||||||
SaveGradeHandler,
|
SaveGradeHandler,
|
||||||
GetRandomCardInDeckHandler,
|
GetRandomCardInDeckHandler,
|
||||||
} from './use-cases'
|
} from './use-cases'
|
||||||
import { DecksRepository } from './infrastructure/decks.repository'
|
|
||||||
import { CardsRepository } from '../cards/infrastructure/cards.repository'
|
|
||||||
import { GradesRepository } from './infrastructure/grades.repository'
|
|
||||||
import { FileUploadService } from '../../infrastructure/file-upload-service/file-upload.service'
|
|
||||||
|
|
||||||
const commandHandlers = [
|
const commandHandlers = [
|
||||||
CreateDeckHandler,
|
CreateDeckHandler,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsBoolean, IsOptional, Length } from 'class-validator'
|
|
||||||
import { Transform } from 'class-transformer'
|
import { Transform } from 'class-transformer'
|
||||||
|
import { IsBoolean, IsOptional, Length } from 'class-validator'
|
||||||
|
|
||||||
export class CreateDeckDto {
|
export class CreateDeckDto {
|
||||||
@Length(3, 30)
|
@Length(3, 30)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IsUUID } from 'class-validator'
|
import { IsUUID } from 'class-validator'
|
||||||
import { IsOptionalOrEmptyString, IsOrderBy } from '../../../infrastructure/decorators'
|
|
||||||
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
|
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
|
||||||
|
import { IsOptionalOrEmptyString, IsOrderBy } from '../../../infrastructure/decorators'
|
||||||
|
|
||||||
export class GetAllDecksDto extends PaginationDto {
|
export class GetAllDecksDto extends PaginationDto {
|
||||||
@IsOptionalOrEmptyString()
|
@IsOptionalOrEmptyString()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types'
|
import { PartialType } from '@nestjs/mapped-types'
|
||||||
import { CreateDeckDto } from './create-deck.dto'
|
|
||||||
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators'
|
|
||||||
import { IsBoolean } from 'class-validator'
|
import { IsBoolean } from 'class-validator'
|
||||||
|
|
||||||
|
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators'
|
||||||
|
|
||||||
|
import { CreateDeckDto } from './create-deck.dto'
|
||||||
|
|
||||||
export class UpdateDeckDto extends PartialType(CreateDeckDto) {
|
export class UpdateDeckDto extends PartialType(CreateDeckDto) {
|
||||||
@IsOptionalOrEmptyString()
|
@IsOptionalOrEmptyString()
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
|
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { createPrismaOrderBy } from '../../../infrastructure/common/helpers/get-order-by-object'
|
||||||
|
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
||||||
import { PrismaService } from '../../../prisma.service'
|
import { PrismaService } from '../../../prisma.service'
|
||||||
import { GetAllDecksDto } from '../dto'
|
import { GetAllDecksDto } from '../dto'
|
||||||
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
|
||||||
import { createPrismaOrderBy } from '../../../infrastructure/common/helpers/get-order-by-object'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DecksRepository {
|
export class DecksRepository {
|
||||||
@@ -103,6 +104,7 @@ export class DecksRepository {
|
|||||||
this.prisma
|
this.prisma
|
||||||
.$queryRaw`SELECT MAX(card_count) as maxCardsCount FROM (SELECT COUNT(*) as card_count FROM card GROUP BY deckId) AS card_counts;`,
|
.$queryRaw`SELECT MAX(card_count) as maxCardsCount FROM (SELECT COUNT(*) as card_count FROM card GROUP BY deckId) AS card_counts;`,
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
maxCardsCount: Number(max[0].maxCardsCount),
|
maxCardsCount: Number(max[0].maxCardsCount),
|
||||||
...Pagination.transformPaginationData([count, items], { currentPage, itemsPerPage }),
|
...Pagination.transformPaginationData([count, items], { currentPage, itemsPerPage }),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
|
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
|
||||||
|
|
||||||
import { PrismaService } from '../../../prisma.service'
|
import { PrismaService } from '../../../prisma.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
|
import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service'
|
||||||
import { CreateCardDto } from '../../cards/dto'
|
import { CreateCardDto } from '../../cards/dto'
|
||||||
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
||||||
import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service'
|
|
||||||
|
|
||||||
export class CreateCardCommand {
|
export class CreateCardCommand {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -34,6 +35,7 @@ export class CreateCardHandler implements ICommandHandler<CreateCardCommand> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const result = await Promise.all([addQuestionImagePromise, addAnswerImagePromise])
|
const result = await Promise.all([addQuestionImagePromise, addAnswerImagePromise])
|
||||||
|
|
||||||
questionImg = result[0].fileUrl
|
questionImg = result[0].fileUrl
|
||||||
answerImg = result[1].fileUrl
|
answerImg = result[1].fileUrl
|
||||||
} else if (command.answerImg) {
|
} else if (command.answerImg) {
|
||||||
@@ -42,6 +44,7 @@ export class CreateCardHandler implements ICommandHandler<CreateCardCommand> {
|
|||||||
command.answerImg?.originalname
|
command.answerImg?.originalname
|
||||||
)
|
)
|
||||||
const result = await addAnswerImagePromise
|
const result = await addAnswerImagePromise
|
||||||
|
|
||||||
answerImg = result.fileUrl
|
answerImg = result.fileUrl
|
||||||
} else if (command.questionImg) {
|
} else if (command.questionImg) {
|
||||||
const addQuestionImagePromise = this.fileUploadService.uploadFile(
|
const addQuestionImagePromise = this.fileUploadService.uploadFile(
|
||||||
@@ -49,6 +52,7 @@ export class CreateCardHandler implements ICommandHandler<CreateCardCommand> {
|
|||||||
command.questionImg?.originalname
|
command.questionImg?.originalname
|
||||||
)
|
)
|
||||||
const result = await addQuestionImagePromise
|
const result = await addQuestionImagePromise
|
||||||
|
|
||||||
questionImg = result.fileUrl
|
questionImg = result.fileUrl
|
||||||
}
|
}
|
||||||
if (command.card.questionImg === '') {
|
if (command.card.questionImg === '') {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
|
import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service'
|
||||||
import { CreateDeckDto } from '../dto'
|
import { CreateDeckDto } from '../dto'
|
||||||
import { DecksRepository } from '../infrastructure/decks.repository'
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service'
|
|
||||||
|
|
||||||
export class CreateDeckCommand {
|
export class CreateDeckCommand {
|
||||||
constructor(public readonly deck: CreateDeckDto, public readonly cover: Express.Multer.File) {}
|
constructor(public readonly deck: CreateDeckDto, public readonly cover: Express.Multer.File) {}
|
||||||
@@ -22,6 +23,7 @@ export class CreateDeckHandler implements ICommandHandler<CreateDeckCommand> {
|
|||||||
command.cover.buffer,
|
command.cover.buffer,
|
||||||
command.cover.originalname
|
command.cover.originalname
|
||||||
)
|
)
|
||||||
|
|
||||||
cover = result.fileUrl
|
cover = result.fileUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { DecksRepository } from '../infrastructure/decks.repository'
|
|
||||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
|
||||||
export class DeleteDeckByIdCommand {
|
export class DeleteDeckByIdCommand {
|
||||||
constructor(public readonly id: string, public readonly userId: string) {}
|
constructor(public readonly id: string, public readonly userId: string) {}
|
||||||
@@ -12,10 +13,12 @@ export class DeleteDeckByIdHandler implements ICommandHandler<DeleteDeckByIdComm
|
|||||||
|
|
||||||
async execute(command: DeleteDeckByIdCommand) {
|
async execute(command: DeleteDeckByIdCommand) {
|
||||||
const deck = await this.deckRepository.findDeckById(command.id)
|
const deck = await this.deckRepository.findDeckById(command.id)
|
||||||
|
|
||||||
if (!deck) throw new NotFoundException(`Deck with id ${command.id} not found`)
|
if (!deck) throw new NotFoundException(`Deck with id ${command.id} not found`)
|
||||||
if (deck.userId !== command.userId) {
|
if (deck.userId !== command.userId) {
|
||||||
throw new BadRequestException(`You can't delete a deck that you don't own`)
|
throw new BadRequestException(`You can't delete a deck that you don't own`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.deckRepository.deleteDeckById(command.id)
|
return await this.deckRepository.deleteDeckById(command.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
|
||||||
import { GetAllCardsInDeckDto } from '../../cards/dto'
|
|
||||||
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
|
import { GetAllCardsInDeckDto } from '../../cards/dto'
|
||||||
|
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
||||||
import { DecksRepository } from '../infrastructure/decks.repository'
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
|
||||||
export class GetAllCardsInDeckCommand {
|
export class GetAllCardsInDeckCommand {
|
||||||
@@ -21,6 +22,7 @@ export class GetAllCardsInDeckHandler implements ICommandHandler<GetAllCardsInDe
|
|||||||
|
|
||||||
async execute(command: GetAllCardsInDeckCommand) {
|
async execute(command: GetAllCardsInDeckCommand) {
|
||||||
const deck = await this.decksRepository.findDeckById(command.deckId)
|
const deck = await this.decksRepository.findDeckById(command.deckId)
|
||||||
|
|
||||||
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
||||||
|
|
||||||
if (deck.userId !== command.userId && deck.isPrivate) {
|
if (deck.userId !== command.userId && deck.isPrivate) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
import { DecksRepository } from '../infrastructure/decks.repository'
|
|
||||||
import { GetAllDecksDto } from '../dto'
|
import { GetAllDecksDto } from '../dto'
|
||||||
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
|
||||||
export class GetAllDecksCommand {
|
export class GetAllDecksCommand {
|
||||||
constructor(public readonly params: GetAllDecksDto) {}
|
constructor(public readonly params: GetAllDecksDto) {}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
import { DecksRepository } from '../infrastructure/decks.repository'
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
|
||||||
export class GetDeckByIdCommand {
|
export class GetDeckByIdCommand {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
|
||||||
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
||||||
import { DecksRepository } from '../infrastructure/decks.repository'
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
import { pick } from 'remeda'
|
import { pick } from 'remeda'
|
||||||
|
|
||||||
|
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
||||||
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
|
||||||
export class GetRandomCardInDeckCommand {
|
export class GetRandomCardInDeckCommand {
|
||||||
constructor(public readonly userId: string, public readonly deckId: string) {}
|
constructor(public readonly userId: string, public readonly deckId: string) {}
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,7 @@ export class GetRandomCardInDeckHandler implements ICommandHandler<GetRandomCard
|
|||||||
|
|
||||||
private async getSmartRandomCard(cards: Array<CardWithGrade>) {
|
private async getSmartRandomCard(cards: Array<CardWithGrade>) {
|
||||||
const selectionPool: Array<CardWithGrade> = []
|
const selectionPool: Array<CardWithGrade> = []
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
// Calculate the average grade for the card
|
// Calculate the average grade for the card
|
||||||
const averageGrade =
|
const averageGrade =
|
||||||
@@ -40,6 +42,7 @@ export class GetRandomCardInDeckHandler implements ICommandHandler<GetRandomCard
|
|||||||
|
|
||||||
async execute(command: GetRandomCardInDeckCommand) {
|
async execute(command: GetRandomCardInDeckCommand) {
|
||||||
const deck = await this.decksRepository.findDeckById(command.deckId)
|
const deck = await this.decksRepository.findDeckById(command.deckId)
|
||||||
|
|
||||||
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
||||||
|
|
||||||
if (deck.userId !== command.userId && deck.isPrivate) {
|
if (deck.userId !== command.userId && deck.isPrivate) {
|
||||||
@@ -51,6 +54,7 @@ export class GetRandomCardInDeckHandler implements ICommandHandler<GetRandomCard
|
|||||||
command.deckId
|
command.deckId
|
||||||
)
|
)
|
||||||
const smartRandomCard = await this.getSmartRandomCard(cards)
|
const smartRandomCard = await this.getSmartRandomCard(cards)
|
||||||
|
|
||||||
return pick(smartRandomCard, ['id', 'question', 'answer', 'deckId'])
|
return pick(smartRandomCard, ['id', 'question', 'answer', 'deckId'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
|
||||||
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
|
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
||||||
import { DecksRepository } from '../infrastructure/decks.repository'
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
import { GradesRepository } from '../infrastructure/grades.repository'
|
import { GradesRepository } from '../infrastructure/grades.repository'
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export class SaveGradeHandler implements ICommandHandler<SaveGradeCommand> {
|
|||||||
|
|
||||||
async execute(command: SaveGradeCommand) {
|
async execute(command: SaveGradeCommand) {
|
||||||
const deck = await this.decksRepository.findDeckByCardId(command.args.cardId)
|
const deck = await this.decksRepository.findDeckByCardId(command.args.cardId)
|
||||||
|
|
||||||
if (!deck)
|
if (!deck)
|
||||||
throw new NotFoundException(`Deck containing card with id ${command.args.cardId} not found`)
|
throw new NotFoundException(`Deck containing card with id ${command.args.cardId} not found`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { DecksRepository } from '../infrastructure/decks.repository'
|
|
||||||
import { UpdateDeckDto } from '../dto'
|
|
||||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
|
||||||
import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service'
|
import { FileUploadService } from '../../../infrastructure/file-upload-service/file-upload.service'
|
||||||
|
import { UpdateDeckDto } from '../dto'
|
||||||
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
|
||||||
export class UpdateDeckCommand {
|
export class UpdateDeckCommand {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -22,6 +23,7 @@ export class UpdateDeckHandler implements ICommandHandler<UpdateDeckCommand> {
|
|||||||
|
|
||||||
async execute(command: UpdateDeckCommand) {
|
async execute(command: UpdateDeckCommand) {
|
||||||
const deck = await this.deckRepository.findDeckById(command.deckId)
|
const deck = await this.deckRepository.findDeckById(command.deckId)
|
||||||
|
|
||||||
if (!deck) {
|
if (!deck) {
|
||||||
throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
||||||
}
|
}
|
||||||
@@ -36,10 +38,12 @@ export class UpdateDeckHandler implements ICommandHandler<UpdateDeckCommand> {
|
|||||||
command.cover.buffer,
|
command.cover.buffer,
|
||||||
command.cover.originalname
|
command.cover.originalname
|
||||||
)
|
)
|
||||||
|
|
||||||
cover = result.fileUrl
|
cover = result.fileUrl
|
||||||
} else if (command.deck.cover === '') {
|
} else if (command.deck.cover === '') {
|
||||||
cover = null
|
cover = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.deckRepository.updateDeckById(command.deckId, { ...command.deck, cover })
|
return await this.deckRepository.updateDeckById(command.deckId, { ...command.deck, cover })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common'
|
||||||
import { UsersService } from '../services/users.service'
|
import { CommandBus } from '@nestjs/cqrs'
|
||||||
import { CreateUserDto } from '../dto/create-user.dto'
|
import { ApiTags } from '@nestjs/swagger'
|
||||||
|
|
||||||
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
||||||
import { BaseAuthGuard } from '../../auth/guards'
|
import { BaseAuthGuard } from '../../auth/guards'
|
||||||
import { CommandBus } from '@nestjs/cqrs'
|
|
||||||
import { CreateUserCommand } from '../../auth/use-cases'
|
import { CreateUserCommand } from '../../auth/use-cases'
|
||||||
|
import { CreateUserDto } from '../dto/create-user.dto'
|
||||||
|
import { UsersService } from '../services/users.service'
|
||||||
|
|
||||||
|
@ApiTags('Admin')
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(private usersService: UsersService, private commandBus: CommandBus) {}
|
constructor(private usersService: UsersService, private commandBus: CommandBus) {}
|
||||||
@@ -25,7 +28,9 @@ export class UsersController {
|
|||||||
const { page, pageSize } = Pagination.getPaginationData(query)
|
const { page, pageSize } = Pagination.getPaginationData(query)
|
||||||
|
|
||||||
const users = await this.usersService.getUsers(page, pageSize, query.name, query.email)
|
const users = await this.usersService.getUsers(page, pageSize, query.name, query.email)
|
||||||
|
|
||||||
if (!users) throw new NotFoundException('Users not found')
|
if (!users) throw new NotFoundException('Users not found')
|
||||||
|
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types'
|
import { PartialType } from '@nestjs/mapped-types'
|
||||||
|
|
||||||
import { CreateUserDto } from './create-user.dto'
|
import { CreateUserDto } from './create-user.dto'
|
||||||
|
|
||||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
import { addHours } from 'date-fns'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
||||||
|
import { PrismaService } from '../../../prisma.service'
|
||||||
import {
|
import {
|
||||||
CreateUserInput,
|
CreateUserInput,
|
||||||
EntityWithPaginationType,
|
EntityWithPaginationType,
|
||||||
@@ -5,17 +17,6 @@ import {
|
|||||||
UserViewType,
|
UserViewType,
|
||||||
VerificationWithUser,
|
VerificationWithUser,
|
||||||
} from '../../../types/types'
|
} from '../../../types/types'
|
||||||
import {
|
|
||||||
BadRequestException,
|
|
||||||
Injectable,
|
|
||||||
InternalServerErrorException,
|
|
||||||
Logger,
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import { addHours } from 'date-fns'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { PrismaService } from '../../../prisma.service'
|
|
||||||
import { Prisma } from '@prisma/client'
|
|
||||||
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersRepository {
|
export class UsersRepository {
|
||||||
@@ -52,6 +53,7 @@ export class UsersRepository {
|
|||||||
take: itemsPerPage,
|
take: itemsPerPage,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
return Pagination.transformPaginationData(res, { currentPage, itemsPerPage })
|
return Pagination.transformPaginationData(res, { currentPage, itemsPerPage })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
@@ -100,6 +102,7 @@ export class UsersRepository {
|
|||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return result.isDeleted
|
return result.isDeleted
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
@@ -115,6 +118,7 @@ export class UsersRepository {
|
|||||||
async deleteAllUsers(): Promise<number> {
|
async deleteAllUsers(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const result = await this.prisma.user.deleteMany()
|
const result = await this.prisma.user.deleteMany()
|
||||||
|
|
||||||
return result.count
|
return result.count
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(e?.message || e)
|
this.logger.error(e?.message || e)
|
||||||
@@ -128,6 +132,7 @@ export class UsersRepository {
|
|||||||
where: { id },
|
where: { id },
|
||||||
include,
|
include,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -154,6 +159,7 @@ export class UsersRepository {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return user
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
@@ -176,9 +182,11 @@ export class UsersRepository {
|
|||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!verification) {
|
if (!verification) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return verification
|
return verification
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
@@ -206,6 +214,7 @@ export class UsersRepository {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return result.isEmailVerified
|
return result.isEmailVerified
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(e?.message || e)
|
this.logger.error(e?.message || e)
|
||||||
@@ -276,9 +285,11 @@ export class UsersRepository {
|
|||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!resetPassword) {
|
if (!resetPassword) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return resetPassword.user
|
return resetPassword.user
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
@@ -335,9 +346,11 @@ export class UsersRepository {
|
|||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!revokedToken.user) {
|
if (!revokedToken.user) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return revokedToken.user
|
return revokedToken.user
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(e?.message || e)
|
this.logger.error(e?.message || e)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common'
|
import { Injectable, Logger } from '@nestjs/common'
|
||||||
import { UsersRepository } from '../infrastructure/users.repository'
|
|
||||||
import * as bcrypt from 'bcrypt'
|
|
||||||
import { MailerService } from '@nestjs-modules/mailer'
|
import { MailerService } from '@nestjs-modules/mailer'
|
||||||
|
import * as bcrypt from 'bcrypt'
|
||||||
|
|
||||||
|
import { UsersRepository } from '../infrastructure/users.repository'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
@@ -23,6 +24,7 @@ export class UsersService {
|
|||||||
|
|
||||||
async deleteAllUsers(): Promise<{ deleted: number }> {
|
async deleteAllUsers(): Promise<{ deleted: number }> {
|
||||||
const deleted = await this.usersRepository.deleteAllUsers()
|
const deleted = await this.usersRepository.deleteAllUsers()
|
||||||
|
|
||||||
return { deleted }
|
return { deleted }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
import { UsersService } from './services/users.service'
|
import { CqrsModule } from '@nestjs/cqrs'
|
||||||
|
|
||||||
import { UsersController } from './api/users.controller'
|
import { UsersController } from './api/users.controller'
|
||||||
import { UsersRepository } from './infrastructure/users.repository'
|
import { UsersRepository } from './infrastructure/users.repository'
|
||||||
import { CqrsModule } from '@nestjs/cqrs'
|
import { UsersService } from './services/users.service'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [CqrsModule],
|
imports: [CqrsModule],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Global, Module } from '@nestjs/common'
|
import { Global, Module } from '@nestjs/common'
|
||||||
|
|
||||||
import { PrismaService } from './prisma.service'
|
import { PrismaService } from './prisma.service'
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
|
|||||||
@@ -42,4 +42,5 @@ export class AppSettings {
|
|||||||
}
|
}
|
||||||
const env = new EnvironmentSettings((process.env.NODE_ENV || 'DEVELOPMENT') as EnvironmentsTypes)
|
const env = new EnvironmentSettings((process.env.NODE_ENV || 'DEVELOPMENT') as EnvironmentsTypes)
|
||||||
const auth = new AuthSettings(process.env)
|
const auth = new AuthSettings(process.env)
|
||||||
|
|
||||||
export const appSettings = new AppSettings(env, auth)
|
export const appSettings = new AppSettings(env, auth)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Global, Module } from '@nestjs/common'
|
import { Global, Module } from '@nestjs/common'
|
||||||
|
|
||||||
import { appSettings, AppSettings } from './app-settings'
|
import { appSettings, AppSettings } from './app-settings'
|
||||||
|
|
||||||
//главный config модуль для управления env переменными импортируется в app.module.ts глобально
|
//главный config модуль для управления env переменными импортируется в app.module.ts глобально
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const validationErrorsMapper = {
|
|||||||
): ValidationPipeErrorType[] {
|
): ValidationPipeErrorType[] {
|
||||||
return errors.flatMap(error => {
|
return errors.flatMap(error => {
|
||||||
const constraints = error.constraints ?? []
|
const constraints = error.constraints ?? []
|
||||||
|
|
||||||
return Object.entries(constraints).map(([_, value]) => ({
|
return Object.entries(constraints).map(([_, value]) => ({
|
||||||
field: error.property,
|
field: error.property,
|
||||||
message: value,
|
message: value,
|
||||||
@@ -26,6 +27,7 @@ export function pipesSetup(app: INestApplication) {
|
|||||||
exceptionFactory: (errors: ValidationError[]) => {
|
exceptionFactory: (errors: ValidationError[]) => {
|
||||||
const err =
|
const err =
|
||||||
validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray(errors)
|
validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray(errors)
|
||||||
|
|
||||||
throw new BadRequestException(err)
|
throw new BadRequestException(err)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing'
|
|
||||||
import { INestApplication } from '@nestjs/common'
|
import { INestApplication } from '@nestjs/common'
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing'
|
||||||
import * as request from 'supertest'
|
import * as request from 'supertest'
|
||||||
|
|
||||||
import { AppModule } from '../src/app.module'
|
import { AppModule } from '../src/app.module'
|
||||||
|
|
||||||
describe('AppController (e2e)', () => {
|
describe('AppController (e2e)', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user