mirror of
https://github.com/ershisan99/flashcards-api.git
synced 2025-12-17 05:09:26 +00:00
add error handling for db calls
This commit is contained in:
@@ -41,6 +41,7 @@ model User {
|
|||||||
verification Verification?
|
verification Verification?
|
||||||
RevokedToken RevokedToken[]
|
RevokedToken RevokedToken[]
|
||||||
RefreshToken RefreshToken[]
|
RefreshToken RefreshToken[]
|
||||||
|
ResetPassword ResetPassword?
|
||||||
|
|
||||||
@@fulltext([name, email])
|
@@fulltext([name, email])
|
||||||
}
|
}
|
||||||
@@ -66,6 +67,17 @@ model RefreshToken {
|
|||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ResetPassword {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String @unique
|
||||||
|
resetPasswordToken String? @unique @default(uuid())
|
||||||
|
resetPasswordTokenExpiry DateTime?
|
||||||
|
resetPasswordEmailsSent Int @default(0)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
model Card {
|
model Card {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
deckId String
|
deckId String
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Request,
|
Request,
|
||||||
Res,
|
Res,
|
||||||
@@ -24,6 +25,8 @@ import {
|
|||||||
LogoutCommand,
|
LogoutCommand,
|
||||||
RefreshTokenCommand,
|
RefreshTokenCommand,
|
||||||
ResendVerificationEmailCommand,
|
ResendVerificationEmailCommand,
|
||||||
|
ResetPasswordCommand,
|
||||||
|
SendPasswordRecoveryEmailCommand,
|
||||||
VerifyEmailCommand,
|
VerifyEmailCommand,
|
||||||
} from './use-cases'
|
} from './use-cases'
|
||||||
|
|
||||||
@@ -97,4 +100,14 @@ export class AuthController {
|
|||||||
accessToken: newTokens.accessToken,
|
accessToken: newTokens.accessToken,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('recover-password')
|
||||||
|
async recoverPassword(@Body('email') email: string) {
|
||||||
|
return await this.commandBus.execute(new SendPasswordRecoveryEmailCommand(email))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('reset-password/:token')
|
||||||
|
async resetPassword(@Body('password') password: string, @Param('token') token: string) {
|
||||||
|
return await this.commandBus.execute(new ResetPasswordCommand(token, password))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
LogoutHandler,
|
LogoutHandler,
|
||||||
RefreshTokenHandler,
|
RefreshTokenHandler,
|
||||||
ResendVerificationEmailHandler,
|
ResendVerificationEmailHandler,
|
||||||
|
ResetPasswordHandler,
|
||||||
|
SendPasswordRecoveryEmailHandler,
|
||||||
VerifyEmailHandler,
|
VerifyEmailHandler,
|
||||||
} from './use-cases'
|
} from './use-cases'
|
||||||
import { AuthRepository } from './infrastructure/auth.repository'
|
import { AuthRepository } from './infrastructure/auth.repository'
|
||||||
@@ -20,6 +22,8 @@ const commandHandlers = [
|
|||||||
LogoutHandler,
|
LogoutHandler,
|
||||||
RefreshTokenHandler,
|
RefreshTokenHandler,
|
||||||
ResendVerificationEmailHandler,
|
ResendVerificationEmailHandler,
|
||||||
|
ResetPasswordHandler,
|
||||||
|
SendPasswordRecoveryEmailHandler,
|
||||||
VerifyEmailHandler,
|
VerifyEmailHandler,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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()
|
throw new UnauthorizedException('Invalid credentials')
|
||||||
}
|
}
|
||||||
return newCredentials
|
return newCredentials
|
||||||
}
|
}
|
||||||
|
|||||||
143
src/modules/auth/tests/integration/auth.e2e.spec.ts
Normal file
143
src/modules/auth/tests/integration/auth.e2e.spec.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'
|
||||||
|
import * as request from 'supertest'
|
||||||
|
import { HttpStatus } from '@nestjs/common'
|
||||||
|
import { AppModule } from '../../../../app.module'
|
||||||
|
import { RegistrationDto } from '../../dto/registration.dto'
|
||||||
|
|
||||||
|
describe('AuthController (e2e)', () => {
|
||||||
|
let app
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile()
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication()
|
||||||
|
await app.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST sign-up', () => {
|
||||||
|
const registrationData: RegistrationDto = {
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@gmail.com',
|
||||||
|
password: 'secret',
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/sign-up')
|
||||||
|
.send(registrationData)
|
||||||
|
.expect(HttpStatus.CREATED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST sign-up (duplicate)', () => {
|
||||||
|
const registrationData: RegistrationDto = {
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@gmail.com',
|
||||||
|
password: 'secret',
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/sign-up')
|
||||||
|
.send(registrationData)
|
||||||
|
.expect(HttpStatus.BAD_REQUEST)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST login', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ email: 'john@gmail.com', password: 'secret' })
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
.then(response => {
|
||||||
|
expect(response.body.accessToken).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST login (invalid)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ email: 'john@gmail.com', password: 'wrong_password' })
|
||||||
|
.expect(HttpStatus.UNAUTHORIZED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/GET me', async () => {
|
||||||
|
const loginResponse = await request(app.getHttpServer())
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ email: 'john@gmail.com', password: 'secret' })
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/auth/me')
|
||||||
|
.set('Authorization', `Bearer ${loginResponse.body.accessToken}`)
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
})
|
||||||
|
it('/POST verify-email', async () => {
|
||||||
|
// Assuming "john@gmail.com" has a verification code "123456"
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/verify-email')
|
||||||
|
.send({ code: '123456' })
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST verify-email (invalid)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/verify-email')
|
||||||
|
.send({ code: 'wrong_code' })
|
||||||
|
.expect(HttpStatus.UNAUTHORIZED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST resend-verification-email', async () => {
|
||||||
|
// Assuming "john@gmail.com" has a user ID "1"
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/resend-verification-email')
|
||||||
|
.send({ userId: '1' })
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST logout', async () => {
|
||||||
|
const loginResponse = await request(app.getHttpServer())
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ email: 'john@gmail.com', password: 'secret' })
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/logout')
|
||||||
|
.set('Cookie', [`refreshToken=${loginResponse.body.refreshToken}`])
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/GET refresh-token', async () => {
|
||||||
|
const loginResponse = await request(app.getHttpServer())
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ email: 'john@gmail.com', password: 'secret' })
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/auth/refresh-token')
|
||||||
|
.set('Cookie', [`refreshToken=${loginResponse.body.refreshToken}`])
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
.then(response => {
|
||||||
|
expect(response.body.accessToken).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST recover-password', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/recover-password')
|
||||||
|
.send({ email: 'john@gmail.com' })
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/POST reset-password/:token', () => {
|
||||||
|
// Assuming "john@gmail.com" has a password reset token "abcdef"
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/auth/reset-password/abcdef')
|
||||||
|
.send({ password: 'new_password' })
|
||||||
|
.expect(HttpStatus.OK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add more tests for the other endpoints in a similar way
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,3 +4,5 @@ export * from './logout-use-case'
|
|||||||
export * from './resend-verification-email-use-case'
|
export * from './resend-verification-email-use-case'
|
||||||
export * from './refresh-token-use-case'
|
export * from './refresh-token-use-case'
|
||||||
export * from './verify-email-use-case'
|
export * from './verify-email-use-case'
|
||||||
|
export * from './send-password-recovery-email-use-case'
|
||||||
|
export * from './reset-password-use-case'
|
||||||
|
|||||||
40
src/modules/auth/use-cases/reset-password-use-case.ts
Normal file
40
src/modules/auth/use-cases/reset-password-use-case.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
|
import { UsersService } from '../../users/services/users.service'
|
||||||
|
|
||||||
|
export class ResetPasswordCommand {
|
||||||
|
constructor(public readonly resetPasswordToken: string, public readonly newPassword: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandHandler(ResetPasswordCommand)
|
||||||
|
export class ResetPasswordHandler implements ICommandHandler<ResetPasswordCommand> {
|
||||||
|
constructor(
|
||||||
|
private readonly usersRepository: UsersRepository,
|
||||||
|
private readonly usersService: UsersService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: ResetPasswordCommand) {
|
||||||
|
const user = await this.usersRepository.findUserByPasswordResetToken(command.resetPasswordToken)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('Incorrect or expired password reset token')
|
||||||
|
}
|
||||||
|
if (!command.newPassword) {
|
||||||
|
throw new BadRequestException('Password is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPasswordHash = await this.usersService.generateHash(command.newPassword)
|
||||||
|
|
||||||
|
const updatedUser = await this.usersRepository.resetPasswordAndDeleteToken(
|
||||||
|
user.id,
|
||||||
|
newPasswordHash
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!updatedUser) {
|
||||||
|
throw new NotFoundException('Incorrect or expired password reset token')
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
import { NotFoundException } from '@nestjs/common'
|
||||||
|
import { UsersRepository } from '../../users/infrastructure/users.repository'
|
||||||
|
import { UsersService } from '../../users/services/users.service'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export class SendPasswordRecoveryEmailCommand {
|
||||||
|
constructor(public readonly email: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandHandler(SendPasswordRecoveryEmailCommand)
|
||||||
|
export class SendPasswordRecoveryEmailHandler
|
||||||
|
implements ICommandHandler<SendPasswordRecoveryEmailCommand>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly usersRepository: UsersRepository,
|
||||||
|
private readonly usersService: UsersService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: SendPasswordRecoveryEmailCommand) {
|
||||||
|
const user = await this.usersRepository.findUserByEmail(command.email)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found')
|
||||||
|
}
|
||||||
|
const token = uuidv4()
|
||||||
|
const updatedUser = await this.usersRepository.createPasswordResetToken(user.id, token)
|
||||||
|
|
||||||
|
await this.usersService.sendPasswordRecoveryEmail({
|
||||||
|
email: updatedUser.user.email,
|
||||||
|
name: updatedUser.user.name,
|
||||||
|
passwordRecoveryToken: updatedUser.resetPasswordToken,
|
||||||
|
})
|
||||||
|
if (!updatedUser) {
|
||||||
|
throw new NotFoundException('User not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,12 @@ import {
|
|||||||
UserViewType,
|
UserViewType,
|
||||||
VerificationWithUser,
|
VerificationWithUser,
|
||||||
} from '../../../types/types'
|
} from '../../../types/types'
|
||||||
import { Injectable } from '@nestjs/common'
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common'
|
||||||
import { addHours } from 'date-fns'
|
import { addHours } from 'date-fns'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { PrismaService } from '../../../prisma.service'
|
import { PrismaService } from '../../../prisma.service'
|
||||||
@@ -16,12 +21,15 @@ import { Prisma } from '@prisma/client'
|
|||||||
export class UsersRepository {
|
export class UsersRepository {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
private readonly logger = new Logger(UsersRepository.name)
|
||||||
|
|
||||||
async getUsers(
|
async getUsers(
|
||||||
currentPage: number,
|
currentPage: number,
|
||||||
itemsPerPage: number,
|
itemsPerPage: number,
|
||||||
searchNameTerm: string,
|
searchNameTerm: string,
|
||||||
searchEmailTerm: string
|
searchEmailTerm: string
|
||||||
): Promise<EntityWithPaginationType<UserViewType>> {
|
): Promise<EntityWithPaginationType<UserViewType>> {
|
||||||
|
try {
|
||||||
const where = {
|
const where = {
|
||||||
name: {
|
name: {
|
||||||
search: searchNameTerm || undefined,
|
search: searchNameTerm || undefined,
|
||||||
@@ -38,7 +46,6 @@ export class UsersRepository {
|
|||||||
take: itemsPerPage,
|
take: itemsPerPage,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const totalPages = Math.ceil(totalItems / itemsPerPage)
|
const totalPages = Math.ceil(totalItems / itemsPerPage)
|
||||||
const usersView = users.map(u => pick(u, ['id', 'name', 'email', 'isEmailVerified']))
|
const usersView = users.map(u => pick(u, ['id', 'name', 'email', 'isEmailVerified']))
|
||||||
return {
|
return {
|
||||||
@@ -48,9 +55,19 @@ export class UsersRepository {
|
|||||||
totalItems,
|
totalItems,
|
||||||
items: usersView,
|
items: usersView,
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === 'P2025') {
|
||||||
|
throw new BadRequestException({ message: 'Invalid page number', field: 'page' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(newUser: CreateUserInput): Promise<User | null> {
|
async createUser(newUser: CreateUserInput): Promise<User | null> {
|
||||||
|
try {
|
||||||
return await this.prisma.user.create({
|
return await this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: newUser.email,
|
email: newUser.email,
|
||||||
@@ -67,23 +84,48 @@ export class UsersRepository {
|
|||||||
verification: true,
|
verification: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === 'P2002') {
|
||||||
|
throw new BadRequestException({ message: 'Email already exists', field: 'email' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUserById(id: string): Promise<boolean> {
|
async deleteUserById(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
const result = await this.prisma.user.delete({
|
const result = await this.prisma.user.delete({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return result.isDeleted
|
return result.isDeleted
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === 'P2015') {
|
||||||
|
throw new BadRequestException({ message: 'User not found', field: 'id' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllUsers(): Promise<boolean> {
|
async deleteAllUsers(): Promise<number> {
|
||||||
|
try {
|
||||||
const result = await this.prisma.user.deleteMany()
|
const result = await this.prisma.user.deleteMany()
|
||||||
return result.count > 0
|
return result.count
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findUserById(id: string, include?: Prisma.UserInclude) {
|
async findUserById(id: string, include?: Prisma.UserInclude) {
|
||||||
|
try {
|
||||||
const user = await this.prisma.user.findUnique({
|
const user = await this.prisma.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include,
|
include,
|
||||||
@@ -93,9 +135,19 @@ export class UsersRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return user as Prisma.UserGetPayload<{ include: typeof include }>
|
return user as Prisma.UserGetPayload<{ include: typeof include }>
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === 'P2015') {
|
||||||
|
throw new BadRequestException({ message: 'User not found', field: 'id' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findUserByEmail(email: string): Promise<User | null> {
|
async findUserByEmail(email: string): Promise<User | null> {
|
||||||
|
try {
|
||||||
const user = await this.prisma.user.findUnique({
|
const user = await this.prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
include: { verification: true },
|
include: { verification: true },
|
||||||
@@ -105,9 +157,19 @@ export class UsersRepository {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === 'P2015') {
|
||||||
|
throw new BadRequestException({ message: 'User not found', field: 'email' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findUserByVerificationToken(token: string): Promise<VerificationWithUser | null> {
|
async findUserByVerificationToken(token: string): Promise<VerificationWithUser | null> {
|
||||||
|
try {
|
||||||
const verification = await this.prisma.verification.findUnique({
|
const verification = await this.prisma.verification.findUnique({
|
||||||
where: {
|
where: {
|
||||||
verificationToken: token,
|
verificationToken: token,
|
||||||
@@ -120,9 +182,19 @@ export class UsersRepository {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return verification
|
return verification
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === 'P2015') {
|
||||||
|
throw new BadRequestException({ message: 'Verification not found', field: 'token' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEmailVerification(id: string) {
|
async updateEmailVerification(id: string) {
|
||||||
|
try {
|
||||||
const result = await this.prisma.verification.update({
|
const result = await this.prisma.verification.update({
|
||||||
where: {
|
where: {
|
||||||
userId: id,
|
userId: id,
|
||||||
@@ -137,9 +209,14 @@ export class UsersRepository {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
return result.isEmailVerified
|
return result.isEmailVerified
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateVerificationToken(id: string) {
|
async updateVerificationToken(id: string) {
|
||||||
|
try {
|
||||||
return await this.prisma.verification.update({
|
return await this.prisma.verification.update({
|
||||||
where: {
|
where: {
|
||||||
userId: id,
|
userId: id,
|
||||||
@@ -152,9 +229,105 @@ export class UsersRepository {
|
|||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPasswordResetToken(id: string, token: string) {
|
||||||
|
try {
|
||||||
|
return await this.prisma.resetPassword.upsert({
|
||||||
|
where: {
|
||||||
|
userId: id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
resetPasswordToken: token,
|
||||||
|
resetPasswordTokenExpiry: addHours(new Date(), 1),
|
||||||
|
resetPasswordEmailsSent: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
resetPasswordToken: token,
|
||||||
|
resetPasswordTokenExpiry: addHours(new Date(), 1),
|
||||||
|
resetPasswordEmailsSent: 1,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUserByPasswordResetToken(token: string): Promise<User | null> {
|
||||||
|
try {
|
||||||
|
const resetPassword = await this.prisma.resetPassword.findUnique({
|
||||||
|
where: {
|
||||||
|
resetPasswordToken: token,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!resetPassword) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return resetPassword.user
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === 'P2015') {
|
||||||
|
throw new BadRequestException({ message: 'Invalid token', field: 'token' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetPasswordAndDeleteToken(userId: string, password: string) {
|
||||||
|
try {
|
||||||
|
return await this.prisma.$transaction([
|
||||||
|
this.prisma.resetPassword.update({
|
||||||
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
resetPasswordToken: null,
|
||||||
|
resetPasswordTokenExpiry: null,
|
||||||
|
resetPasswordEmailsSent: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokeToken(id: string, token: string): Promise<User | null> {
|
async revokeToken(id: string, token: string): Promise<User | null> {
|
||||||
|
try {
|
||||||
const revokedToken = await this.prisma.revokedToken.create({
|
const revokedToken = await this.prisma.revokedToken.create({
|
||||||
data: {
|
data: {
|
||||||
token: token,
|
token: token,
|
||||||
@@ -168,5 +341,9 @@ export class UsersRepository {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return revokedToken.user
|
return revokedToken.user
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
throw new InternalServerErrorException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ export class UsersService {
|
|||||||
return await this.usersRepository.deleteUserById(id)
|
return await this.usersRepository.deleteUserById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllUsers(): Promise<boolean> {
|
async deleteAllUsers(): Promise<{ deleted: number }> {
|
||||||
return await this.usersRepository.deleteAllUsers()
|
const deleted = await this.usersRepository.deleteAllUsers()
|
||||||
|
return { deleted }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendConfirmationEmail({
|
public async sendConfirmationEmail({
|
||||||
@@ -42,6 +43,27 @@ export class UsersService {
|
|||||||
html: `<b>Hello ${name}!</b><br/>Please confirm your email by clicking on the link below:<br/><a href="http://localhost:3000/confirm-email/${verificationToken}">Confirm email</a>`,
|
html: `<b>Hello ${name}!</b><br/>Please confirm your email by clicking on the link below:<br/><a href="http://localhost:3000/confirm-email/${verificationToken}">Confirm email</a>`,
|
||||||
subject: 'E-mail confirmation',
|
subject: 'E-mail confirmation',
|
||||||
})
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message || e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendPasswordRecoveryEmail({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
passwordRecoveryToken,
|
||||||
|
}: {
|
||||||
|
email: string
|
||||||
|
name: string
|
||||||
|
passwordRecoveryToken: string
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
await this.emailService.sendMail({
|
||||||
|
from: 'Andrii <andrii@andrii.es>',
|
||||||
|
to: email,
|
||||||
|
html: `<b>Hello ${name}!</b><br/>To recover your password follow this link:<br/><a href="http://localhost:3000/confirm-email/${passwordRecoveryToken}">Confirm email</a>. If it doesn't work, copy and paste the following link in your browser:<br/>http://localhost:3000/confirm-email/${passwordRecoveryToken} `,
|
||||||
|
subject: 'Password recovery',
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(e)
|
this.logger.error(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,29 @@ import { PrismaClient } from '@prisma/client'
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
// constructor() {
|
||||||
|
// super({
|
||||||
|
// log: [
|
||||||
|
// {
|
||||||
|
// emit: 'stdout',
|
||||||
|
// level: 'query',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// emit: 'stdout',
|
||||||
|
// level: 'error',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// emit: 'stdout',
|
||||||
|
// level: 'info',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// emit: 'stdout',
|
||||||
|
// level: 'warn',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.$connect()
|
await this.$connect()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user