add auth docs, add eslint import/order

This commit is contained in:
andres
2023-07-16 19:44:58 +02:00
parent 0f3e89900a
commit aa7ece41a9
74 changed files with 1152 additions and 164 deletions

View File

@@ -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,
},
},
}

View File

@@ -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
}
}
]
} }
} }

View File

@@ -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",

View File

@@ -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: [

View File

@@ -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 {

View File

@@ -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 }
} }
} }

View File

@@ -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()

View File

@@ -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,

View File

@@ -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
}) })

View File

@@ -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 {

View File

@@ -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()}`)
} }

View File

@@ -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))
} }
} }

View File

@@ -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,

View File

@@ -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,

View File

@@ -0,0 +1,6 @@
import { IsUUID } from 'class-validator'
export class EmailVerificationDto {
@IsUUID('4')
code: string
}

View 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'

View File

@@ -0,0 +1,9 @@
import { IsEmail, Length } from 'class-validator'
export class LoginDto {
@Length(3, 30)
password: string
@IsEmail()
email: string
}

View File

@@ -0,0 +1,6 @@
import { IsEmail } from 'class-validator'
export class RecoverPasswordDto {
@IsEmail()
email: string
}

View File

@@ -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
} }

View File

@@ -0,0 +1,6 @@
import { IsUUID } from 'class-validator'
export class ResendVerificationEmailDto {
@IsUUID()
userId: string
}

View File

@@ -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) {}

View File

@@ -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']) {}

View File

@@ -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
} }
} }

View File

@@ -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

View File

@@ -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()

View File

@@ -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 = {

View File

@@ -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
} }

View File

@@ -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
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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

View File

@@ -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,

View File

@@ -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'])
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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,

View File

@@ -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,

View File

@@ -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'

View File

@@ -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) {}

View File

@@ -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
} }
} }

View File

@@ -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) {}

View File

@@ -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({

View File

@@ -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'

View File

@@ -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) {}

View File

@@ -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) {

View File

@@ -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)
} }
} }

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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))
} }

View File

@@ -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,

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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 }),

View File

@@ -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()

View File

@@ -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 === '') {

View File

@@ -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
} }

View File

@@ -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)
} }
} }

View File

@@ -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) {

View File

@@ -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) {}

View File

@@ -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 {

View File

@@ -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'])
} }
} }

View File

@@ -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`)

View File

@@ -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 })
} }
} }

View File

@@ -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
} }

View File

@@ -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) {}

View File

@@ -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)

View File

@@ -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 }
} }

View File

@@ -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],

View File

@@ -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()

View File

@@ -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)

View File

@@ -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 глобально

View File

@@ -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)
}, },
}) })

View File

@@ -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)', () => {

747
yarn.lock

File diff suppressed because it is too large Load Diff