add email account validation

This commit is contained in:
andres
2023-06-13 01:39:33 +02:00
parent 59b4eb582e
commit 6c62d87ee6
40 changed files with 578 additions and 6453 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
DATABASE_URL='mysql://root@127.0.0.1:3306/flashcards'
ACCESS_JWT_SECRET_KEY=123
REFRESH_JWT_SECRET_KEY=123
AWS_ACCESS_KEY=123
AWS_SECRET_ACCESS_KEY=123
AWS_REGION=123
AWS_SES_SMTP_HOST=123
AWS_SES_SMTP_PORT=123
AWS_SES_SMTP_USER=123
AWS_SES_SMTP_PASS=123

View File

@@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}

4
.prettierrc.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
...require('@it-incubator/prettier-config'),
//override settings here
}

View File

@@ -20,6 +20,9 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-ses": "^3.350.0",
"@aws-sdk/credential-provider-node": "^3.350.0",
"@nestjs-modules/mailer": "^1.8.1",
"@nestjs/common": "9.4.1", "@nestjs/common": "9.4.1",
"@nestjs/config": "^2.3.3", "@nestjs/config": "^2.3.3",
"@nestjs/core": "9.4.1", "@nestjs/core": "9.4.1",
@@ -46,6 +49,7 @@
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@it-incubator/prettier-config": "^0.1.1",
"@nestjs/cli": "^9.5.0", "@nestjs/cli": "^9.5.0",
"@nestjs/schematics": "^9.2.0", "@nestjs/schematics": "^9.2.0",
"@nestjs/testing": "^9.4.1", "@nestjs/testing": "^9.4.1",

5852
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,13 +16,13 @@ model Verification {
verificationToken String? @unique @default(uuid()) verificationToken String? @unique @default(uuid())
verificationTokenExpiry DateTime? verificationTokenExpiry DateTime?
verificationEmailsSent Int @default(0) verificationEmailsSent Int @default(0)
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId]) @@index([userId])
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(uuid())
email String @unique email String @unique
password String password String
isAdmin Boolean @default(false) isAdmin Boolean @default(false)
@@ -40,6 +40,8 @@ model User {
verification Verification? verification Verification?
AccessToken AccessToken[] AccessToken AccessToken[]
RefreshToken RefreshToken[] RefreshToken RefreshToken[]
@@fulltext([name, email])
} }
model AccessToken { model AccessToken {

View File

@@ -1,20 +1,35 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common'
import { JwtStrategy } from './modules/auth/strategies/jwt.strategy'; import { JwtStrategy } from './modules/auth/strategies/jwt.strategy'
import { JwtPayloadExtractorStrategy } from './guards/common/jwt-payload-extractor.strategy'; import { JwtPayloadExtractorStrategy } from './guards/common/jwt-payload-extractor.strategy'
import { JwtPayloadExtractorGuard } from './guards/common/jwt-payload-extractor.guard'; import { JwtPayloadExtractorGuard } from './guards/common/jwt-payload-extractor.guard'
import { ConfigModule } from './settings/config.module'; import { ConfigModule } from './settings/config.module'
import { AuthModule } from './modules/auth/auth.module'; import { AuthModule } from './modules/auth/auth.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 * as process from 'process'
@Module({ @Module({
imports: [ConfigModule, AuthModule, UsersModule, PrismaModule], imports: [
controllers: [], ConfigModule,
providers: [ AuthModule,
JwtStrategy, UsersModule,
JwtPayloadExtractorStrategy, PrismaModule,
JwtPayloadExtractorGuard,
MailerModule.forRoot({
transport: {
host: process.env.AWS_SES_SMTP_HOST,
port: +process.env.AWS_SES_SMTP_PORT,
secure: false,
auth: {
user: process.env.AWS_SES_SMTP_USER,
pass: process.env.AWS_SES_SMTP_PASS,
},
},
}),
], ],
controllers: [],
providers: [JwtStrategy, JwtPayloadExtractorStrategy, JwtPayloadExtractorGuard],
exports: [], exports: [],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common'
@Injectable() @Injectable()
export class AppService { export class AppService {
getHello(): string { getHello(): string {
return 'Hello World!'; return 'Hello World!'
} }
} }

View File

@@ -1,37 +1,30 @@
import { import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'
ExceptionFilter, import { Request, Response } from 'express'
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException) @Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter { export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) { catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); const ctx = host.switchToHttp()
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 = {
errorsMessages: [], errorsMessages: [],
};
const responseBody: any = exception.getResponse();
if (typeof responseBody.message === 'object') {
responseBody.message.forEach((e) =>
errorsResponse.errorsMessages.push(e),
);
} else {
errorsResponse.errorsMessages.push(responseBody.message);
} }
response.status(status).json(errorsResponse); const responseBody: any = exception.getResponse()
if (typeof responseBody.message === 'object') {
responseBody.message.forEach(e => errorsResponse.errorsMessages.push(e))
} else {
errorsResponse.errorsMessages.push(responseBody.message)
}
response.status(status).json(errorsResponse)
} else { } else {
response.status(status).json({ response.status(status).json({
statusCode: status, statusCode: status,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
path: request.url, path: request.url,
}); })
} }
} }
} }

View File

@@ -1,24 +1,18 @@
import { import { BadRequestException, CanActivate, ExecutionContext } from '@nestjs/common'
BadRequestException, import { UsersRepository } from '../../modules/users/infrastructure/users.repository'
CanActivate,
ExecutionContext,
} from '@nestjs/common';
import { UsersRepository } from '../../modules/users/infrastructure/users.repository';
export class LimitsControlGuard implements CanActivate { export class LimitsControlGuard implements CanActivate {
constructor(private usersRepository: UsersRepository) {} constructor(private usersRepository: UsersRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> | null { async canActivate(context: ExecutionContext): Promise<boolean> | null {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest()
const email = request.body.email; const email = request.body.email
const userWithExistingEmail = await this.usersRepository.findUserByEmail( const userWithExistingEmail = await this.usersRepository.findUserByEmail(email)
email,
);
if (userWithExistingEmail) if (userWithExistingEmail)
throw new BadRequestException({ throw new BadRequestException({
message: 'email already exist', message: 'email already exist',
field: 'email', field: 'email',
}); })
return true; return true
} }
} }

View File

@@ -1,23 +1,19 @@
import { import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
ExecutionContext, import { AuthGuard } from '@nestjs/passport'
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable() @Injectable()
export class JwtPayloadExtractorGuard extends AuthGuard('payloadExtractor') { export class JwtPayloadExtractorGuard extends AuthGuard('payloadExtractor') {
canActivate(context: ExecutionContext) { canActivate(context: ExecutionContext) {
// Add your custom authentication logic here // Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session. // for example, call super.logIn(request) to establish a session.
return super.canActivate(context); return super.canActivate(context)
} }
handleRequest(err, user, info) { handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments // You can throw an exception based on either "info" or "err" arguments
if (err) { if (err) {
throw err || new UnauthorizedException(); throw err || new UnauthorizedException()
} }
return user; return user
} }
} }

View File

@@ -1,27 +1,22 @@
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'
@Injectable() @Injectable()
export class JwtPayloadExtractorStrategy extends PassportStrategy( export class JwtPayloadExtractorStrategy extends PassportStrategy(Strategy, 'payloadExtractor') {
Strategy, constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
'payloadExtractor',
) {
constructor(
@Inject(AppSettings.name) private readonly appSettings: AppSettings,
) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, ignoreExpiration: false,
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY, secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
}); })
} }
async validate(payload: any) { async validate(payload: any) {
const userId = payload.userId; const userId = payload.userId
const login = payload.login; const name = payload.name
if (payload) return { userId, login }; if (payload) return { userId, name }
return null; return null
} }
} }

View File

@@ -1,9 +1,9 @@
export class Pagination { export class Pagination {
static getPaginationData(query) { static getPaginationData(query) {
const page = typeof query.PageNumber === 'string' ? +query.PageNumber : 1; const page = typeof query.PageNumber === 'string' ? +query.PageNumber : 1
const pageSize = typeof query.PageSize === 'string' ? +query.PageSize : 10; const pageSize = typeof query.PageSize === 'string' ? +query.PageSize : 10
const searchNameTerm = const searchNameTerm = typeof query.SearchNameTerm === 'string' ? query.SearchNameTerm : ''
typeof query.SearchNameTerm === 'string' ? query.SearchNameTerm : ''; const searchEmailTerm = typeof query.SearchEmailTerm === 'string' ? query.SearchEmailTerm : ''
return { page, pageSize, searchNameTerm }; return { page, pageSize, searchNameTerm, searchEmailTerm }
} }
} }

View File

@@ -1,38 +1,41 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'; import { AppModule } from './app.module'
import { BadRequestException, ValidationPipe } from '@nestjs/common'; import { BadRequestException, ValidationPipe } from '@nestjs/common'
import { HttpExceptionFilter } from './exception.filter'; import { HttpExceptionFilter } from './exception.filter'
import * as cookieParser from 'cookie-parser'; import * as cookieParser from 'cookie-parser'
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule)
app.setGlobalPrefix('v1')
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Flashcards') .setTitle('Flashcards')
.setDescription('The config API description') .setDescription('The config API description')
.setVersion('1.0') .setVersion('1.0')
.build(); .build()
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config)
SwaggerModule.setup('docs', app, document); SwaggerModule.setup('docs', app, document)
app.useGlobalPipes( app.useGlobalPipes(
new ValidationPipe({ new ValidationPipe({
stopAtFirstError: false, stopAtFirstError: false,
exceptionFactory: (errors) => { exceptionFactory: errors => {
const customErrors = errors.map((e) => { const customErrors = errors.map(e => {
const firstError = JSON.stringify(e.constraints); const firstError = JSON.stringify(e.constraints)
return { field: e.property, message: firstError }; return { field: e.property, message: firstError }
}); })
throw new BadRequestException(customErrors); throw new BadRequestException(customErrors)
}, },
}), })
); )
app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalFilters(new HttpExceptionFilter())
app.use(cookieParser()); app.use(cookieParser())
await app.listen(process.env.PORT || 3000); await app.listen(process.env.PORT || 3000)
} }
try { try {
bootstrap(); bootstrap()
} catch (e) { } catch (e) {
console.log('BOOTSTRAP CALL FAILED'); console.log('BOOTSTRAP CALL FAILED')
console.log('ERROR: '); console.log('ERROR: ')
console.log(e); console.log(e)
} }

View File

@@ -11,93 +11,96 @@ import {
BadRequestException, BadRequestException,
Res, Res,
HttpCode, HttpCode,
} from '@nestjs/common'; } from '@nestjs/common'
import { AuthService } from './auth.service'; import { AuthService } from './auth.service'
import { RegistrationDto } from './dto/registration.dto'; import { RegistrationDto } from './dto/registration.dto'
import { LocalAuthGuard } from './guards/local-auth.guard'; import { LocalAuthGuard } from './guards/local-auth.guard'
import { UsersService } from '../users/services/users.service'; import { UsersService } from '../users/services/users.service'
import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { JwtAuthGuard } from './guards/jwt-auth.guard'
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly usersService: UsersService, private readonly usersService: UsersService
) {} ) {}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('me') @Get('me')
async getUserData(@Request() req) { async getUserData(@Request() req) {
const userId = req.user.userId; const userId = req.user.userId
const user = await this.usersService.getUserById(userId); const user = await this.usersService.getUserById(userId)
if (!user) throw new UnauthorizedException(); if (!user) throw new UnauthorizedException()
return { return {
email: user.email, email: user.email,
name: user.name, name: user.name,
is: user.id, id: user.id,
};
} }
}
@HttpCode(200) @HttpCode(200)
@UseGuards(LocalAuthGuard) @UseGuards(LocalAuthGuard)
@Post('login') @Post('sign-in')
async login(@Request() req, @Res({ passthrough: true }) res) { async login(@Request() req, @Res({ passthrough: true }) res) {
const userData = req.user.data; console.log(req)
const userData = req.user.data
res.cookie('refreshToken', userData.refreshToken, { res.cookie('refreshToken', userData.refreshToken, {
httpOnly: true, httpOnly: true,
secure: true, secure: true,
}); })
return { accessToken: req.user.data.accessToken }; return { accessToken: req.user.data.accessToken }
} }
@HttpCode(201) @HttpCode(201)
@Post('registration') @Post('sign-up')
async registration(@Body() registrationData: RegistrationDto) { async registration(@Body() registrationData: RegistrationDto) {
return await this.usersService.createUser( return await this.usersService.createUser(
registrationData.name, registrationData.name,
registrationData.password, registrationData.password,
registrationData.email, registrationData.email
); )
} }
@Post('registration-confirmation') @Post('registration-confirmation')
async confirmRegistration(@Body('code') confirmationCode) { async confirmRegistration(@Body('code') confirmationCode) {
const result = await this.authService.confirmEmail(confirmationCode); const result = await this.authService.confirmEmail(confirmationCode)
if (!result) { if (!result) {
throw new NotFoundException(); throw new NotFoundException()
} }
return null; return null
} }
@Post('registration-email-resending') @Post('registration-email-resending')
async resendRegistrationEmail(@Body('email') email: string) { async resendRegistrationEmail(@Body('email') email: string) {
const isResented = await this.authService.resendCode(email); const isResented = await this.authService.resendCode(email)
if (!isResented) if (!isResented)
throw new BadRequestException({ throw new BadRequestException({
message: 'email already confirmed or such email not found', message: 'email already confirmed or such email not found',
field: 'email', field: 'email',
}); })
return null; return null
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Post('logout') @Post('logout')
async logout(@Request() req) { async logout(@Request() req) {
if (!req.cookie?.refreshToken) throw new UnauthorizedException(); if (!req.cookie?.refreshToken) throw new UnauthorizedException()
await this.usersService.addRevokedToken(req.cookie.refreshToken); await this.usersService.addRevokedToken(req.cookie.refreshToken)
return null; return null
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Post('refresh-token') @Post('refresh-token')
async refreshToken(@Request() req, @Response() res) { async refreshToken(@Request() req, @Response() res) {
if (!req.cookie?.refreshToken) throw new UnauthorizedException(); if (!req.cookie?.refreshToken) throw new UnauthorizedException()
const userId = req.user.id; const userId = req.user.id
const newTokens = this.authService.createJwtTokensPair(userId, null); const newTokens = this.authService.createJwtTokensPair(userId, null)
res.cookie('refreshToken', newTokens.refreshToken, { res.cookie('refreshToken', newTokens.refreshToken, {
httpOnly: true, httpOnly: true,
secure: true, secure: true,
}); })
return { accessToken: newTokens.accessToken }; return { accessToken: newTokens.accessToken }
} }
} }

View File

@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'; import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller'
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module'
import { LocalStrategy } from './strategies/local.strategy'; import { LocalStrategy } from './strategies/local.strategy'
@Module({ @Module({
imports: [UsersModule], imports: [UsersModule],

View File

@@ -1,34 +1,34 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common'
import { isAfter } from 'date-fns'; import { isAfter } from 'date-fns'
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken'
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt'
import { UsersRepository } from '../users/infrastructure/users.repository'; import { UsersRepository } from '../users/infrastructure/users.repository'
import * as process from 'process'; import * as process from 'process'
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor(private usersRepository: UsersRepository) {} constructor(private usersRepository: UsersRepository) {}
createJwtTokensPair(userId: string, email: string | null) { createJwtTokensPair(userId: string, email: string | null) {
const accessSecretKey = process.env.ACCESS_JWT_SECRET_KEY; const accessSecretKey = process.env.ACCESS_JWT_SECRET_KEY
const refreshSecretKey = process.env.REFRESH_JWT_SECRET_KEY; const refreshSecretKey = process.env.REFRESH_JWT_SECRET_KEY
const payload: { userId: string; date: Date; email: string | null } = { const payload: { userId: string; date: Date; email: string | null } = {
userId, userId,
date: new Date(), date: new Date(),
email, email,
}; }
const accessToken = jwt.sign(payload, accessSecretKey, { expiresIn: '1d' }); const accessToken = jwt.sign(payload, accessSecretKey, { expiresIn: '1d' })
const refreshToken = jwt.sign(payload, refreshSecretKey, { const refreshToken = jwt.sign(payload, refreshSecretKey, {
expiresIn: '30d', expiresIn: '30d',
}); })
return { return {
accessToken, accessToken,
refreshToken, refreshToken,
}; }
} }
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,
@@ -36,11 +36,8 @@ export class AuthService {
accessToken: null, accessToken: null,
refreshToken: null, refreshToken: null,
}, },
}; }
const isPasswordValid = await this.isPasswordCorrect( const isPasswordValid = await this.isPasswordCorrect(password, user.password)
password,
user.password,
);
if (!isPasswordValid) { if (!isPasswordValid) {
return { return {
resultCode: 1, resultCode: 1,
@@ -50,39 +47,37 @@ export class AuthService {
refreshToken: null, refreshToken: null,
}, },
}, },
};
} }
const tokensPair = this.createJwtTokensPair(user.id, user.email); }
const tokensPair = this.createJwtTokensPair(user.id, user.email)
return { return {
resultCode: 0, resultCode: 0,
data: tokensPair, data: tokensPair,
}; }
} }
private async isPasswordCorrect(password: string, hash: string) { private async isPasswordCorrect(password: string, hash: string) {
return bcrypt.compare(password, hash); return bcrypt.compare(password, hash)
} }
async confirmEmail(token: string): Promise<boolean> { async confirmEmail(token: string): Promise<boolean> {
const user = await this.usersRepository.findUserByVerificationToken(token); const user = await this.usersRepository.findUserByVerificationToken(token)
if (!user || user.isEmailVerified) return false; if (!user || user.isEmailVerified) return false
const dbToken = user.verificationToken; const dbToken = user.verificationToken
const isTokenExpired = isAfter(user.verificationTokenExpiry, new Date()); const isTokenExpired = isAfter(user.verificationTokenExpiry, new Date())
if (dbToken !== token || isTokenExpired) { if (dbToken !== token || isTokenExpired) {
return false; return false
} }
return await this.usersRepository.updateConfirmation(user.id); return await this.usersRepository.updateConfirmation(user.id)
} }
async resendCode(email: string) { async resendCode(email: string) {
const user = await this.usersRepository.findUserByEmail(email); const user = await this.usersRepository.findUserByEmail(email)
if (!user || user?.verification.isEmailVerified) return null; if (!user || user?.verification.isEmailVerified) return null
const updatedUser = await this.usersRepository.updateVerificationToken( const updatedUser = await this.usersRepository.updateVerificationToken(user.id)
user.id, if (!updatedUser) return null
);
if (!updatedUser) return null;
return true; return true
} }
} }

View File

@@ -1,10 +1,10 @@
import { IsEmail, Length } from 'class-validator'; import { IsEmail, Length } from 'class-validator'
export class RegistrationDto { export class RegistrationDto {
@Length(3, 30) @Length(3, 30)
name: string; name: string
@Length(3, 30) @Length(3, 30)
password: string; password: string
@IsEmail() @IsEmail()
email: string; email: string
} }

View File

@@ -1,4 +1,4 @@
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

@@ -5,48 +5,48 @@ import {
Injectable, Injectable,
NotFoundException, NotFoundException,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common'
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken'
import { UsersRepository } from '../../users/infrastructure/users.repository'; import { UsersRepository } from '../../users/infrastructure/users.repository'
@Injectable() @Injectable()
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor(private readonly usersRepository: UsersRepository) {} constructor(private readonly usersRepository: UsersRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest()
if (!request.headers || !request.headers.authorization) { if (!request.headers || !request.headers.authorization) {
throw new BadRequestException([{ message: 'No any auth headers' }]); throw new BadRequestException([{ message: 'No any auth headers' }])
} }
const authorizationData = request.headers.authorization.split(' '); const authorizationData = request.headers.authorization.split(' ')
const token = authorizationData[1]; const token = authorizationData[1]
const tokenName = authorizationData[0]; const tokenName = authorizationData[0]
if (tokenName != 'Bearer') { if (tokenName != 'Bearer') {
throw new UnauthorizedException([ throw new UnauthorizedException([
{ {
message: 'login or password invalid', message: 'login or password invalid',
}, },
]); ])
} }
try { try {
const secretKey = process.env.JWT_SECRET_KEY; const secretKey = process.env.JWT_SECRET_KEY
const decoded: any = jwt.verify(token, secretKey!); const decoded: any = jwt.verify(token, secretKey!)
const user = await this.usersRepository.findUserById(decoded.userId); const user = await this.usersRepository.findUserById(decoded.userId)
if (!user) { if (!user) {
throw new NotFoundException([ throw new NotFoundException([
{ {
field: 'token', field: 'token',
message: 'user not found', message: 'user not found',
}, },
]); ])
} }
} catch (e) { } catch (e) {
console.log(e); console.log(e)
throw new UnauthorizedException([ throw new UnauthorizedException([
{ {
message: 'login or password invalid', message: 'login or password invalid',
}, },
]); ])
} }
return true; return true
} }
} }

View File

@@ -1,29 +1,22 @@
import { import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
CanActivate, import { Observable } from 'rxjs'
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable() @Injectable()
export class BaseAuthGuard implements CanActivate { export class BaseAuthGuard implements CanActivate {
canActivate( canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
context: ExecutionContext, const request = context.switchToHttp().getRequest()
): boolean | Promise<boolean> | Observable<boolean> { const exceptedAuthInput = 'Basic YWRtaW46cXdlcnR5'
const request = context.switchToHttp().getRequest();
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 {
if (request.headers.authorization != exceptedAuthInput) { if (request.headers.authorization != exceptedAuthInput) {
throw new UnauthorizedException([ throw new UnauthorizedException([
{ {
message: 'login or password invalid', message: 'login or password invalid',
}, },
]); ])
} }
} }
return true; return true
} }
} }

View File

@@ -1,25 +1,22 @@
import { import { ExecutionContext, Injectable, UsePipes, ValidationPipe } from '@nestjs/common'
ExecutionContext, import { AuthGuard } from '@nestjs/passport'
Injectable,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable() @Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') { export class JwtAuthGuard extends AuthGuard('jwt') {
constructor() { constructor() {
super(); super()
} }
@UsePipes(new ValidationPipe()) @UsePipes(new ValidationPipe())
validateLoginDto(): void {} validateLoginDto(): void {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const res: boolean = await (super.canActivate(context) as Promise<boolean>); async canActivate(context: ExecutionContext): Promise<boolean> {
if (!res) return false; const req = context.switchToHttp().getRequest()
const res: boolean = await (super.canActivate(context) as Promise<boolean>)
if (!res) return false
// check DTO // check DTO
return res; return res
} }
} }

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport'
@Injectable() @Injectable()
export class LocalAuthGuard extends AuthGuard('local') {} export class LocalAuthGuard extends AuthGuard('local') {}

View File

@@ -1,21 +1,19 @@
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'
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor( constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
@Inject(AppSettings.name) private readonly appSettings: AppSettings,
) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: true, ignoreExpiration: true,
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY, secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
}); })
} }
async validate(payload: any) { async validate(payload: any) {
return { userId: payload.userId }; return { userId: payload.userId }
} }
} }

View File

@@ -1,21 +1,21 @@
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()
export class LocalStrategy extends PassportStrategy(Strategy) { export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) { constructor(private readonly authService: AuthService) {
super({ super({
usernameField: 'login', usernameField: 'email',
}); })
} }
async validate(login: string, password: string): Promise<any> { async validate(email: string, password: string): Promise<any> {
const user = await this.authService.checkCredentials(login, password); const user = await this.authService.checkCredentials(email, password)
if (user.resultCode === 1) { if (user.resultCode === 1) {
throw new UnauthorizedException(); throw new UnauthorizedException()
} }
return user; return user
} }
} }

View File

@@ -1,29 +1,25 @@
import { IEvent } from '@nestjs/cqrs'; import { IEvent } from '@nestjs/cqrs'
export class ResultNotification<T = null> { export class ResultNotification<T = null> {
constructor(data: T | null = null) { constructor(data: T | null = null) {
this.data = data; this.data = data
} }
extensions: NotificationExtension[] = []; extensions: NotificationExtension[] = []
code = 0; code = 0
data: T | null = null; data: T | null = null
hasError() { hasError() {
return this.code !== 0; return this.code !== 0
} }
addError( addError(message: string, key: string | null = null, code: number | null = null) {
message: string, this.code = code ?? 1
key: string | null = null, this.extensions.push(new NotificationExtension(message, key))
code: number | null = null,
) {
this.code = code ?? 1;
this.extensions.push(new NotificationExtension(message, key));
} }
addData(data: T) { addData(data: T) {
this.data = data; this.data = data
} }
} }
@@ -31,39 +27,34 @@ export class NotificationExtension {
constructor(public message: string, public key: string | null) {} constructor(public message: string, public key: string | null) {}
} }
export class DomainResultNotification< export class DomainResultNotification<TData = null> extends ResultNotification<TData> {
TData = null, public events: IEvent[] = []
> extends ResultNotification<TData> {
public events: IEvent[] = [];
addEvents(...events: IEvent[]) { addEvents(...events: IEvent[]) {
this.events = [...this.events, ...events]; this.events = [...this.events, ...events]
} }
static create<T>( static create<T>(
mainNotification: DomainResultNotification<T>, mainNotification: DomainResultNotification<T>,
...otherNotifications: DomainResultNotification[] ...otherNotifications: DomainResultNotification[]
) { ) {
const domainResultNotification = new DomainResultNotification<T>(); const domainResultNotification = new DomainResultNotification<T>()
if (!!mainNotification.data) { if (!!mainNotification.data) {
domainResultNotification.addData(mainNotification.data); domainResultNotification.addData(mainNotification.data)
} }
domainResultNotification.events = mainNotification.events; domainResultNotification.events = mainNotification.events
mainNotification.extensions.forEach((e) => { mainNotification.extensions.forEach(e => {
domainResultNotification.addError(e.message, e.key); domainResultNotification.addError(e.message, e.key)
}); })
otherNotifications.forEach((n) => { otherNotifications.forEach(n => {
domainResultNotification.events = [ domainResultNotification.events = [...domainResultNotification.events, ...n.events]
...domainResultNotification.events, n.extensions.forEach(e => {
...n.events, domainResultNotification.addError(e.message, e.key)
]; })
n.extensions.forEach((e) => { })
domainResultNotification.addError(e.message, e.key);
});
});
return domainResultNotification; return domainResultNotification
} }
} }

View File

@@ -1,58 +1,50 @@
import { DomainResultNotification, ResultNotification } from './notification'; import { DomainResultNotification, ResultNotification } from './notification'
import { validateOrReject } from 'class-validator'; import { validateOrReject } from 'class-validator'
import { IEvent } from '@nestjs/cqrs'; import { IEvent } from '@nestjs/cqrs'
import { import { validationErrorsMapper, ValidationPipeErrorType } from '../../../settings/pipes-setup'
validationErrorsMapper,
ValidationPipeErrorType,
} from '../../../settings/pipes-setup';
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)
} }
} }
export const validateEntityOrThrow = async (entity: any) => { export const validateEntityOrThrow = async (entity: any) => {
try { try {
await validateOrReject(entity); await validateOrReject(entity)
} catch (errors) { } catch (errors) {
const resultNotification: ResultNotification = mapErrorsToNotification( const resultNotification: ResultNotification = mapErrorsToNotification(
validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray( validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray(errors)
errors, )
),
);
throw new DomainError('domain entity validation error', resultNotification); throw new DomainError('domain entity validation error', resultNotification)
} }
}; }
export const validateEntity = async <T extends object>( export const validateEntity = async <T extends object>(
entity: T, entity: T,
events: IEvent[], events: IEvent[]
): Promise<DomainResultNotification<T>> => { ): Promise<DomainResultNotification<T>> => {
try { try {
await validateOrReject(entity); await validateOrReject(entity)
} catch (errors) { } catch (errors) {
const resultNotification: DomainResultNotification<T> = const resultNotification: DomainResultNotification<T> = mapErrorsToNotification<T>(
mapErrorsToNotification<T>( validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray(errors)
validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray( )
errors, resultNotification.addData(entity)
), resultNotification.addEvents(...events)
); return resultNotification
resultNotification.addData(entity);
resultNotification.addEvents(...events);
return resultNotification;
} }
const domainResultNotification = new DomainResultNotification<T>(entity); const domainResultNotification = new DomainResultNotification<T>(entity)
domainResultNotification.addEvents(...events); domainResultNotification.addEvents(...events)
return domainResultNotification; return domainResultNotification
}; }
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

@@ -8,11 +8,11 @@ import {
Post, Post,
Query, Query,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common'
import { UsersService } from '../services/users.service'; import { UsersService } from '../services/users.service'
import { CreateUserDto } from '../dto/create-user.dto'; import { CreateUserDto } from '../dto/create-user.dto'
import { Pagination } from '../../../infrastructure/common/pagination.service'; import { Pagination } from '../../../infrastructure/common/pagination.service'
import { BaseAuthGuard } from '../../auth/guards/base-auth.guard'; import { BaseAuthGuard } from '../../auth/guards/base-auth.guard'
@Controller('users') @Controller('users')
export class UsersController { export class UsersController {
@@ -20,28 +20,31 @@ export class UsersController {
@Get() @Get()
async findAll(@Query() query) { async findAll(@Query() query) {
const { page, pageSize, searchNameTerm } = const { page, pageSize, searchNameTerm, searchEmailTerm } = Pagination.getPaginationData(query)
Pagination.getPaginationData(query); const users = await this.usersService.getUsers(page, pageSize, searchNameTerm, searchEmailTerm)
const users = await this.usersService.getUsers( if (!users) throw new NotFoundException('Users not found')
page, return users
pageSize,
searchNameTerm,
);
if (!users) throw new NotFoundException('Users not found');
return users;
} }
//@UseGuards(BaseAuthGuard) //@UseGuards(BaseAuthGuard)
@Post() @Post()
async create(@Body() createUserDto: CreateUserDto) { async create(@Body() createUserDto: CreateUserDto) {
return await this.usersService.createUser( return await this.usersService.createUser(
createUserDto.login, createUserDto.login,
createUserDto.password, createUserDto.password,
createUserDto.email, createUserDto.email
); )
} }
@UseGuards(BaseAuthGuard) @UseGuards(BaseAuthGuard)
@Delete(':id') @Delete(':id')
async remove(@Param('id') id: string) { async remove(@Param('id') id: string) {
return await this.usersService.deleteUserById(id); return await this.usersService.deleteUserById(id)
}
@UseGuards(BaseAuthGuard)
@Delete()
async removeAll() {
return await this.usersService.deleteAllUsers()
} }
} }

View File

@@ -1,10 +1,10 @@
import { Length, Matches } from 'class-validator'; import { Length, Matches } from 'class-validator'
export class CreateUserDto { export class CreateUserDto {
@Length(3, 10) @Length(3, 10)
login: string; login: string
@Length(6, 20) @Length(6, 20)
password: string; password: string
@Matches(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/) @Matches(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/)
email: string; email: string
} }

View File

@@ -1,4 +1,4 @@
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

@@ -4,12 +4,12 @@ import {
User, User,
UserViewType, UserViewType,
VerificationWithUser, VerificationWithUser,
} from '../../../types/types'; } from '../../../types/types'
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common'
import { addHours } from 'date-fns'; import { addHours } from 'date-fns'
import { IUsersRepository } from '../services/users.service'; import { IUsersRepository } from '../services/users.service'
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid'
import { PrismaService } from '../../../prisma.service'; import { PrismaService } from '../../../prisma.service'
@Injectable() @Injectable()
export class UsersRepository implements IUsersRepository { export class UsersRepository implements IUsersRepository {
@@ -19,12 +19,16 @@ export class UsersRepository implements IUsersRepository {
currentPage: number, currentPage: number,
itemsPerPage: number, itemsPerPage: number,
searchNameTerm: string, searchNameTerm: string,
searchEmailTerm: string
): Promise<EntityWithPaginationType<UserViewType>> { ): Promise<EntityWithPaginationType<UserViewType>> {
const where = { const where = {
name: { name: {
search: searchNameTerm, search: searchNameTerm || undefined,
}, },
}; email: {
search: searchEmailTerm || undefined,
},
}
const [totalItems, users] = await this.prisma.$transaction([ const [totalItems, users] = await this.prisma.$transaction([
this.prisma.user.count({ where }), this.prisma.user.count({ where }),
this.prisma.user.findMany({ this.prisma.user.findMany({
@@ -32,23 +36,23 @@ export class UsersRepository implements IUsersRepository {
skip: (currentPage - 1) * itemsPerPage, skip: (currentPage - 1) * itemsPerPage,
take: itemsPerPage, take: itemsPerPage,
}), }),
]); ])
console.log(users, 'usersFromBase'); console.log(users, 'usersFromBase')
const totalPages = Math.ceil(totalItems / itemsPerPage); const totalPages = Math.ceil(totalItems / itemsPerPage)
const usersView = users.map((u) => ({ const usersView = users.map(u => ({
id: u.id, id: u.id,
name: u.name, name: u.name,
email: u.email, email: u.email,
})); }))
console.log(usersView, 'users---'); console.log(usersView, 'users---')
return { return {
totalPages, totalPages,
currentPage, currentPage,
itemsPerPage, itemsPerPage,
totalItems, totalItems,
items: usersView, items: usersView,
}; }
} }
async createUser(newUser: CreateUserInput): Promise<User | null> { async createUser(newUser: CreateUserInput): Promise<User | null> {
@@ -67,7 +71,7 @@ export class UsersRepository implements IUsersRepository {
include: { include: {
verification: true, verification: true,
}, },
}); })
} }
async deleteUserById(id: string): Promise<boolean> { async deleteUserById(id: string): Promise<boolean> {
@@ -75,34 +79,37 @@ export class UsersRepository implements IUsersRepository {
where: { where: {
id, id,
}, },
}); })
return result.isDeleted; return result.isDeleted
}
async deleteAllUsers(): Promise<boolean> {
const result = await this.prisma.user.deleteMany()
return result.count > 0
} }
async findUserById(id: string): Promise<User | null> { async findUserById(id: string): Promise<User | null> {
const user = await this.prisma.user.findUnique({ where: { id } }); const user = await this.prisma.user.findUnique({ where: { id } })
if (!user) { if (!user) {
return null; return null
} }
return user; return user
} }
async findUserByEmail(email: string): Promise<User | null> { async findUserByEmail(email: string): Promise<User | null> {
const user = await this.prisma.user.findUnique({ const user = await this.prisma.user.findUnique({
where: { email }, where: { email },
include: { verification: true }, include: { verification: true },
}); })
if (!user) { if (!user) {
return null; return null
} }
return user; return user
} }
async findUserByVerificationToken( async findUserByVerificationToken(token: string): Promise<VerificationWithUser | null> {
token: string,
): Promise<VerificationWithUser | null> {
const verification = await this.prisma.verification.findUnique({ const verification = await this.prisma.verification.findUnique({
where: { where: {
verificationToken: token, verificationToken: token,
@@ -110,11 +117,11 @@ export class UsersRepository implements IUsersRepository {
include: { include: {
user: true, user: true,
}, },
}); })
if (!verification) { if (!verification) {
return null; return null
} }
return verification; return verification
} }
async updateConfirmation(id: string) { async updateConfirmation(id: string) {
@@ -125,8 +132,8 @@ export class UsersRepository implements IUsersRepository {
data: { data: {
isEmailVerified: true, isEmailVerified: true,
}, },
}); })
return result.isEmailVerified; return result.isEmailVerified
} }
async updateVerificationToken(id: string) { async updateVerificationToken(id: string) {
@@ -141,7 +148,7 @@ export class UsersRepository implements IUsersRepository {
include: { include: {
user: true, user: true,
}, },
}); })
} }
async revokeToken(id: string, token: string): Promise<User | null> { async revokeToken(id: string, token: string): Promise<User | null> {
@@ -155,10 +162,10 @@ export class UsersRepository implements IUsersRepository {
include: { include: {
user: true, user: true,
}, },
}); })
if (!revokedToken.user) { if (!revokedToken.user) {
return null; return null
} }
return revokedToken.user; return revokedToken.user
} }
} }

View File

@@ -1,34 +1,26 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid'
import { import { CreateUserInput, EntityWithPaginationType, User, UserViewType } from '../../../types/types'
CreateUserInput, import { addHours } from 'date-fns'
EntityWithPaginationType, import { Injectable } from '@nestjs/common'
User, import jwt from 'jsonwebtoken'
UserViewType, import { UsersRepository } from '../infrastructure/users.repository'
} from '../../../types/types'; import * as bcrypt from 'bcrypt'
import { addHours } from 'date-fns'; import { MailerService } from '@nestjs-modules/mailer'
import { Injectable } from '@nestjs/common';
import jwt from 'jsonwebtoken';
import { UsersRepository } from '../infrastructure/users.repository';
import * as bcrypt from 'bcrypt';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor(private usersRepository: UsersRepository) {} constructor(private usersRepository: UsersRepository, private emailService: MailerService) {}
async getUsers(page: number, pageSize: number, searchNameTerm: string) { async getUsers(page: number, pageSize: number, searchNameTerm: string, searchEmailTerm: string) {
return await this.usersRepository.getUsers(page, pageSize, searchNameTerm); return await this.usersRepository.getUsers(page, pageSize, searchNameTerm, searchEmailTerm)
} }
async getUserById(id: string) { async getUserById(id: string) {
return await this.usersRepository.findUserById(id); return await this.usersRepository.findUserById(id)
} }
async createUser( async createUser(name: string, password: string, email: string): Promise<UserViewType | null> {
name: string, const passwordHash = await this._generateHash(password)
password: string,
email: string,
): Promise<UserViewType | null> {
const passwordHash = await this._generateHash(password);
const newUser: CreateUserInput = { const newUser: CreateUserInput = {
name: name || email.split('@')[0], name: name || email.split('@')[0],
email: email, email: email,
@@ -36,37 +28,51 @@ export class UsersService {
verificationToken: uuidv4(), verificationToken: uuidv4(),
verificationTokenExpiry: addHours(new Date(), 24), verificationTokenExpiry: addHours(new Date(), 24),
isEmailVerified: false, isEmailVerified: false,
};
const createdUser = await this.usersRepository.createUser(newUser);
if (!createdUser) {
return null;
} }
const createdUser = await this.usersRepository.createUser(newUser)
if (!createdUser) {
return null
}
try {
await this.emailService.sendMail({
from: 'andrii <andrii@andrii.es>',
to: createdUser.email,
text: 'hello and welcome',
subject: 'E-mail confirmation ',
})
} catch (e) {
console.log(e)
}
return { return {
id: createdUser.id, id: createdUser.id,
name: createdUser.name, name: createdUser.name,
email: createdUser.email, email: createdUser.email,
}; }
} }
async deleteUserById(id: string): Promise<boolean> { async deleteUserById(id: string): Promise<boolean> {
return await this.usersRepository.deleteUserById(id); return await this.usersRepository.deleteUserById(id)
}
async deleteAllUsers(): Promise<boolean> {
return await this.usersRepository.deleteAllUsers()
} }
async addRevokedToken(token: string) { async addRevokedToken(token: string) {
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); return this.usersRepository.revokeToken(decoded.userId, token)
} catch (e) { } catch (e) {
console.log('Decoding error: e'); console.log(`Decoding error: ${e}`)
return null; return null
} }
} }
private async _generateHash(password: string) { private async _generateHash(password: string) {
return await bcrypt.hash(password, 10); return await bcrypt.hash(password, 10)
} }
} }
@@ -75,13 +81,14 @@ export interface IUsersRepository {
page: number, page: number,
pageSize: number, pageSize: number,
searchNameTerm: string, searchNameTerm: string,
): Promise<EntityWithPaginationType<UserViewType>>; searchEmailTerm: string
): Promise<EntityWithPaginationType<UserViewType>>
createUser(newUser: CreateUserInput): Promise<User | null>; createUser(newUser: CreateUserInput): Promise<User | null>
deleteUserById(id: string): Promise<boolean>; deleteUserById(id: string): Promise<boolean>
findUserById(id: string): Promise<User | null>; findUserById(id: string): Promise<User | null>
revokeToken(id: string, token: string): Promise<User | null>; revokeToken(id: string, token: string): Promise<User | null>
} }

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common'
import { UsersService } from './services/users.service'; import { UsersService } from './services/users.service'
import { UsersController } from './api/users.controller'; import { UsersController } from './api/users.controller'
import { UsersRepository } from './infrastructure/users.repository'; import { UsersRepository } from './infrastructure/users.repository'
@Module({ @Module({
controllers: [UsersController], controllers: [UsersController],

View File

@@ -1,5 +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()
@Module({ @Module({

View File

@@ -1,15 +1,15 @@
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client'
@Injectable() @Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit { export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() { async onModuleInit() {
await this.$connect(); await this.$connect()
} }
async enableShutdownHooks(app: INestApplication) { async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => { this.$on('beforeExit', async () => {
await app.close(); await app.close()
}); })
} }
} }

View File

@@ -2,53 +2,44 @@
//по умолчанию переменные беруться сначала из ENV илм смотрят всегда на staging //по умолчанию переменные беруться сначала из ENV илм смотрят всегда на staging
//для подстановки локальных значений переменных использовать исключительно локальные env файлы env.development.local //для подстановки локальных значений переменных использовать исключительно локальные env файлы env.development.local
//при необзодимости добавляем сюда нужные приложению переменные //при необзодимости добавляем сюда нужные приложению переменные
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv'
dotenv.config(); dotenv.config()
export type EnvironmentVariable = { [key: string]: string | undefined }; export type EnvironmentVariable = { [key: string]: string | undefined }
export type EnvironmentsTypes = export type EnvironmentsTypes = 'DEVELOPMENT' | 'STAGING' | 'PRODUCTION' | 'TEST'
| 'DEVELOPMENT'
| 'STAGING'
| 'PRODUCTION'
| 'TEST';
export class EnvironmentSettings { export class EnvironmentSettings {
constructor(private env: EnvironmentsTypes) {} constructor(private env: EnvironmentsTypes) {}
getEnv() { getEnv() {
return this.env; return this.env
} }
isProduction() { isProduction() {
return this.env === 'PRODUCTION'; return this.env === 'PRODUCTION'
} }
isStaging() { isStaging() {
return this.env === 'STAGING'; return this.env === 'STAGING'
} }
isDevelopment() { isDevelopment() {
return this.env === 'DEVELOPMENT'; return this.env === 'DEVELOPMENT'
} }
isTesting() { isTesting() {
return this.env === 'TEST'; return this.env === 'TEST'
} }
} }
class AuthSettings { class AuthSettings {
public readonly BASE_AUTH_HEADER: string; public readonly BASE_AUTH_HEADER: string
public readonly ACCESS_JWT_SECRET_KEY: string; public readonly ACCESS_JWT_SECRET_KEY: string
public readonly REFRESH_JWT_SECRET_KEY: string; public readonly REFRESH_JWT_SECRET_KEY: string
constructor(private envVariables: EnvironmentVariable) { constructor(private envVariables: EnvironmentVariable) {
this.BASE_AUTH_HEADER = this.BASE_AUTH_HEADER = envVariables.BASE_AUTH_HEADER || 'Basic YWRtaW46cXdlcnR5'
envVariables.BASE_AUTH_HEADER || 'Basic YWRtaW46cXdlcnR5'; this.ACCESS_JWT_SECRET_KEY = envVariables.ACCESS_JWT_SECRET_KEY || 'accessJwtSecret'
this.ACCESS_JWT_SECRET_KEY = this.REFRESH_JWT_SECRET_KEY = envVariables.REFRESH_JWT_SECRET_KEY || 'refreshJwtSecret'
envVariables.ACCESS_JWT_SECRET_KEY || 'accessJwtSecret';
this.REFRESH_JWT_SECRET_KEY =
envVariables.REFRESH_JWT_SECRET_KEY || 'refreshJwtSecret';
} }
} }
export class AppSettings { export class AppSettings {
constructor(public env: EnvironmentSettings, public auth: AuthSettings) {} constructor(public env: EnvironmentSettings, public auth: AuthSettings) {}
} }
const env = new EnvironmentSettings( const env = new EnvironmentSettings((process.env.NODE_ENV || 'DEVELOPMENT') as EnvironmentsTypes)
(process.env.NODE_ENV || 'DEVELOPMENT') as EnvironmentsTypes, const auth = new AuthSettings(process.env)
); export const appSettings = new AppSettings(env, auth)
const auth = new AuthSettings(process.env);
export const appSettings = new AppSettings(env, auth);

View File

@@ -1,5 +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

@@ -1,23 +1,19 @@
import { import { BadRequestException, INestApplication, ValidationPipe } from '@nestjs/common'
BadRequestException, import { ValidationError } from 'class-validator'
INestApplication,
ValidationPipe,
} from '@nestjs/common';
import { ValidationError } from 'class-validator';
export const validationErrorsMapper = { export const validationErrorsMapper = {
mapValidationErrorArrayToValidationPipeErrorTypeArray( mapValidationErrorArrayToValidationPipeErrorTypeArray(
errors: ValidationError[], errors: ValidationError[]
): 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,
})); }))
}); })
}, },
}; }
export function pipesSetup(app: INestApplication) { export function pipesSetup(app: INestApplication) {
app.useGlobalPipes( app.useGlobalPipes(
@@ -28,16 +24,14 @@ export function pipesSetup(app: INestApplication) {
stopAtFirstError: true, stopAtFirstError: true,
exceptionFactory: (errors: ValidationError[]) => { exceptionFactory: (errors: ValidationError[]) => {
const err = const err =
validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray( validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray(errors)
errors, throw new BadRequestException(err)
);
throw new BadRequestException(err);
}, },
}), })
); )
} }
export type ValidationPipeErrorType = { export type ValidationPipeErrorType = {
field: string; field: string
message: string; message: string
}; }

View File

@@ -1,145 +1,142 @@
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client'
export type NewestLikesType = { export type NewestLikesType = {
id: string; id: string
login: string; login: string
addedAt: Date; addedAt: Date
}; }
export type ExtendedLikesInfoType = { export type ExtendedLikesInfoType = {
dislikesCount: number; dislikesCount: number
likesCount: number; likesCount: number
myStatus: string; myStatus: string
newestLikes: Array<NewestLikesType>; newestLikes: Array<NewestLikesType>
}; }
export type PostType = { export type PostType = {
addedAt: Date; addedAt: Date
id?: string; id?: string
title: string | null; title: string | null
shortDescription: string | null; shortDescription: string | null
content: string | null; content: string | null
blogId: string; blogId: string
blogName?: string | null; blogName?: string | null
extendedLikesInfo: ExtendedLikesInfoType; extendedLikesInfo: ExtendedLikesInfoType
}; }
export type BlogType = { export type BlogType = {
id: string; id: string
name: string | null; name: string | null
youtubeUrl: string | null; youtubeUrl: string | null
}; }
export type LikeType = { export type LikeType = {
userId: string; userId: string
login: string; login: string
action: string; action: string
addedAt: Date; addedAt: Date
}; }
export type CommentType = { export type CommentType = {
id: string; id: string
content: string; //20<len<300 content: string //20<len<300
postId: string; postId: string
userId: string; userId: string
userLogin: string; userLogin: string
addedAt: Date; addedAt: Date
likesInfo?: { likesInfo?: {
likesCount: number; likesCount: number
dislikesCount: number; dislikesCount: number
myStatus: string; myStatus: string
}; }
}; }
export type EntityWithPaginationType<T> = { export type EntityWithPaginationType<T> = {
totalPages: number; totalPages: number
currentPage: number; currentPage: number
itemsPerPage: number; itemsPerPage: number
totalItems: number; totalItems: number
items: T[]; items: T[]
}; }
export type QueryDataType = { export type QueryDataType = {
page: number; page: number
pageSize: number; pageSize: number
searchNameTerm: string; searchNameTerm: string
}; }
export type ErrorMessageType = { export type ErrorMessageType = {
message: string; message: string
field: string; field: string
}; }
const userInclude: Prisma.UserInclude = { const userInclude: Prisma.UserInclude = {
verification: true, verification: true,
}; }
export type VerificationWithUser = Prisma.VerificationGetPayload<{ export type VerificationWithUser = Prisma.VerificationGetPayload<{
include: { user: true }; include: { user: true }
}>; }>
export type User = Prisma.UserGetPayload<{ export type User = Prisma.UserGetPayload<{
include: typeof userInclude; include: typeof userInclude
}>; }>
export type CreateUserInput = Omit< export type CreateUserInput = Omit<Prisma.UserCreateInput & Prisma.VerificationCreateInput, 'user'>
Prisma.UserCreateInput & Prisma.VerificationCreateInput,
'user'
>;
export type UserType = { export type UserType = {
accountData: Prisma.UserCreateInput; accountData: Prisma.UserCreateInput
emailConfirmation: EmailConfirmationType; emailConfirmation: EmailConfirmationType
}; }
export type UserViewType = { export type UserViewType = {
id: string; id: string
name: string; name: string
email: string; email: string
}; }
export type UserAccountType = { export type UserAccountType = {
id: string; id: string
email: string; email: string
login: string; login: string
passwordHash: string; passwordHash: string
createdAt: Date; createdAt: Date
revokedTokens?: string[] | null; revokedTokens?: string[] | null
}; }
export type SentConfirmationEmailType = { export type SentConfirmationEmailType = {
sentDate: Date; sentDate: Date
}; }
export type LoginAttemptType = { export type LoginAttemptType = {
attemptDate: Date; attemptDate: Date
ip: string; ip: string
}; }
export type EmailConfirmationType = { export type EmailConfirmationType = {
isConfirmed: boolean; isConfirmed: boolean
confirmationCode: string; confirmationCode: string
expirationDate: Date; expirationDate: Date
sentEmails?: SentConfirmationEmailType[]; sentEmails?: SentConfirmationEmailType[]
}; }
export type LimitsControlType = { export type LimitsControlType = {
userIp: string; userIp: string
url: string; url: string
time: Date; time: Date
}; }
export type CheckLimitsType = { export type CheckLimitsType = {
login: string | null; login: string | null
userIp: string; userIp: string
url: string; url: string
time: Date; time: Date
}; }
export type EmailConfirmationMessageType = { export type EmailConfirmationMessageType = {
email: string; email: string
message: string; message: string
subject: string; subject: string
isSent: boolean; isSent: boolean
createdAt: Date; createdAt: Date
}; }
export enum LikeAction { export enum LikeAction {
Like = 'Like', Like = 'Like',
Dislike = 'Dislike', Dislike = 'Dislike',
None = 'None', None = 'None',
} }
export type LikeActionType = 'Like' | 'Dislike' | 'None'; export type LikeActionType = 'Like' | 'Dislike' | 'None'

View File

@@ -1,24 +1,21 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common'
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)', () => {
let app: INestApplication; let app: INestApplication
beforeEach(async () => { beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({ const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule], imports: [AppModule],
}).compile(); }).compile()
app = moduleFixture.createNestApplication(); app = moduleFixture.createNestApplication()
await app.init(); await app.init()
}); })
it('/ (GET)', () => { it('/ (GET)', () => {
return request(app.getHttpServer()) return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')
.get('/') })
.expect(200) })
.expect('Hello World!');
});
});