From 612b2326f94067f3c21f79fc0b7941165c97740d Mon Sep 17 00:00:00 2001 From: Andres Date: Wed, 14 Jun 2023 16:13:22 +0200 Subject: [PATCH] resend email in progress --- package.json | 1 + pnpm-lock.yaml | 7 +++ prisma/schema.prisma | 9 +-- src/app.module.ts | 5 +- .../common/jwt-payload-extractor.guard.ts | 20 ------- .../common/jwt-payload-extractor.strategy.ts | 27 --------- .../decorators/cookie.decorator.ts | 6 ++ src/modules/auth/auth.controller.ts | 54 ++++++++++------- src/modules/auth/auth.service.ts | 19 +++--- src/modules/auth/guards/jwt-refresh.guard.ts | 5 ++ .../auth/strategies/jwt-refresh.strategy.ts | 34 +++++++++++ src/modules/auth/strategies/jwt.strategy.ts | 38 +++++------- src/modules/auth/strategies/local.strategy.ts | 6 +- .../auth/strategies/refresh-token.strategy.ts | 27 --------- src/modules/users/entities/user.entity.ts | 8 +++ .../users/infrastructure/users.repository.ts | 21 ++++--- src/modules/users/services/users.service.ts | 58 ++++++++++++++----- 17 files changed, 181 insertions(+), 164 deletions(-) delete mode 100644 src/guards/common/jwt-payload-extractor.guard.ts delete mode 100644 src/guards/common/jwt-payload-extractor.strategy.ts create mode 100644 src/infrastructure/decorators/cookie.decorator.ts create mode 100644 src/modules/auth/guards/jwt-refresh.guard.ts create mode 100644 src/modules/auth/strategies/jwt-refresh.strategy.ts delete mode 100644 src/modules/auth/strategies/refresh-token.strategy.ts create mode 100644 src/modules/users/entities/user.entity.ts diff --git a/package.json b/package.json index 3b444b1..1adcb3d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "reflect-metadata": "^0.1.13", + "remeda": "^1.19.0", "rxjs": "^7.8.1", "uuid": "^9.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 626e345..6604f40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ dependencies: reflect-metadata: specifier: ^0.1.13 version: 0.1.13 + remeda: + specifier: ^1.19.0 + version: 1.19.0 rxjs: specifier: ^7.8.1 version: 7.8.1 @@ -6951,6 +6954,10 @@ packages: engines: {node: '>= 0.10'} dev: false + /remeda@1.19.0: + resolution: {integrity: sha512-iwZohiXDhC1K+adRI6OB+tYxOfXyX7DaPXQDZrR5s1k7umrkG3Yd2+QDfSrYFlC7oc0IqeUns6RqSjNkERXeLw==} + dev: false + /remote-content@3.0.1: resolution: {integrity: sha512-zEMsvb4GgxVKBBTHgy2tte67RYBZx2Kyg9mTYpg+JfATHDqYJqhuC3zG1VoiYhDVP5JaB5+mPKcAvdnT0n3jxA==} dependencies: diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e4c2f34..f5e4cf4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,6 +26,7 @@ model User { email String @unique password String isAdmin Boolean @default(false) + isEmailVerified Boolean @default(false) name String @db.VarChar(40) avatar String? deckCount Int @default(0) @@ -49,7 +50,7 @@ model RevokedToken { userId String token String @unique revokedAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) } @@ -60,7 +61,7 @@ model RefreshToken { token String @unique @db.VarChar(255) expiresAt DateTime isRevoked Boolean @default(false) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) } @@ -83,8 +84,8 @@ model Card { moreId String? created DateTime @default(now()) updated DateTime @updatedAt - user User @relation(fields: [userId], references: [id]) - decks Deck @relation(fields: [deckId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + decks Deck @relation(fields: [deckId], references: [id], onDelete: Cascade) grades Grade[] // One-to-many relation @@index([userId]) diff --git a/src/app.module.ts b/src/app.module.ts index 08c0464..9b90ac3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,13 +1,12 @@ import { Module } from '@nestjs/common' import { JwtStrategy } from './modules/auth/strategies/jwt.strategy' -import { JwtPayloadExtractorStrategy } from './guards/common/jwt-payload-extractor.strategy' -import { JwtPayloadExtractorGuard } from './guards/common/jwt-payload-extractor.guard' import { ConfigModule } from './settings/config.module' import { AuthModule } from './modules/auth/auth.module' import { UsersModule } from './modules/users/users.module' import { PrismaModule } from './prisma.module' import { MailerModule } from '@nestjs-modules/mailer' import * as process from 'process' +import { JwtRefreshStrategy } from './modules/auth/strategies/jwt-refresh.strategy' @Module({ imports: [ @@ -29,7 +28,7 @@ import * as process from 'process' }), ], controllers: [], - providers: [JwtStrategy, JwtPayloadExtractorStrategy, JwtPayloadExtractorGuard], + providers: [JwtStrategy, JwtRefreshStrategy], exports: [], }) export class AppModule {} diff --git a/src/guards/common/jwt-payload-extractor.guard.ts b/src/guards/common/jwt-payload-extractor.guard.ts deleted file mode 100644 index 4229c57..0000000 --- a/src/guards/common/jwt-payload-extractor.guard.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common' -import { AuthGuard } from '@nestjs/passport' - -@Injectable() -export class JwtPayloadExtractorGuard extends AuthGuard('payloadExtractor') { - canActivate(context: ExecutionContext) { - // Add your custom authentication logic here - // for example, call super.logIn(request) to establish a session. - console.log('JwtPayloadExtractorGuard') - return super.canActivate(context) - } - - handleRequest(err, user, info) { - // You can throw an exception based on either "info" or "err" arguments - if (err) { - throw err || new UnauthorizedException() - } - return user - } -} diff --git a/src/guards/common/jwt-payload-extractor.strategy.ts b/src/guards/common/jwt-payload-extractor.strategy.ts deleted file mode 100644 index ad93eac..0000000 --- a/src/guards/common/jwt-payload-extractor.strategy.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common' -import { PassportStrategy } from '@nestjs/passport' -import { ExtractJwt, Strategy } from 'passport-jwt' -import { AppSettings } from '../../settings/app-settings' -import { AuthService } from '../../modules/auth/auth.service' - -@Injectable() -export class JwtPayloadExtractorStrategy extends PassportStrategy(Strategy, 'payloadExtractor') { - constructor( - @Inject(AppSettings.name) private readonly appSettings: AppSettings, - private readonly authService: AuthService - ) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY, - }) - } - - async validate(payload: any) { - console.log(payload) - const userId = payload.userId - const name = payload.name - if (payload) return { userId, name } - return null - } -} diff --git a/src/infrastructure/decorators/cookie.decorator.ts b/src/infrastructure/decorators/cookie.decorator.ts new file mode 100644 index 0000000..49afced --- /dev/null +++ b/src/infrastructure/decorators/cookie.decorator.ts @@ -0,0 +1,6 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common' + +export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest() + return data ? request.cookies?.[data] : request.cookies +}) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 3afbe87..286bb22 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -11,13 +11,16 @@ import { BadRequestException, Res, HttpCode, + HttpStatus, } from '@nestjs/common' import { AuthService } from './auth.service' import { RegistrationDto } from './dto/registration.dto' import { LocalAuthGuard } from './guards/local-auth.guard' import { UsersService } from '../users/services/users.service' import { JwtAuthGuard } from './guards/jwt-auth.guard' - +import { Response as ExpressResponse } from 'express' +import { JwtRefreshGuard } from './guards/jwt-refresh.guard' +import { Cookies } from '../../infrastructure/decorators/cookie.decorator' @Controller('auth') export class AuthController { constructor( @@ -28,7 +31,7 @@ export class AuthController { @UseGuards(JwtAuthGuard) @Get('me') async getUserData(@Request() req) { - const userId = req.user.userId + const userId = req.user.id const user = await this.usersService.getUserById(userId) if (!user) throw new UnauthorizedException() @@ -42,12 +45,13 @@ export class AuthController { @HttpCode(200) @UseGuards(LocalAuthGuard) - @Post('sign-in') - async login(@Request() req, @Res({ passthrough: true }) res) { + @Post('login') + async login(@Request() req, @Res({ passthrough: true }) res: ExpressResponse) { const userData = req.user.data res.cookie('refreshToken', userData.refreshToken, { httpOnly: true, - secure: true, + // secure: true, + path: '/v1/auth/refresh-token', }) return { accessToken: req.user.data.accessToken } } @@ -62,7 +66,8 @@ export class AuthController { ) } - @Post('registration-confirmation') + @HttpCode(HttpStatus.OK) + @Post('verify-email') async confirmRegistration(@Body('code') confirmationCode) { const result = await this.authService.confirmEmail(confirmationCode) if (!result) { @@ -71,12 +76,12 @@ export class AuthController { return null } - @Post('registration-email-resending') - async resendRegistrationEmail(@Body('email') email: string) { - const isResented = await this.authService.resendCode(email) - if (!isResented) + @Post('resend-verification-email') + async resendRegistrationEmail(@Body('userId') userId: string) { + const isResent = await this.authService.resendCode(userId) + if (!isResent) throw new BadRequestException({ - message: 'email already confirmed or such email not found', + message: 'Email already confirmed or such email was not found', field: 'email', }) return null @@ -84,24 +89,31 @@ export class AuthController { @UseGuards(JwtAuthGuard) @Post('logout') - async logout(@Request() req) { - if (!req.cookie?.refreshToken) throw new UnauthorizedException() - await this.usersService.addRevokedToken(req.cookie.refreshToken) + async logout( + @Cookies('refreshToken') refreshToken: string, + @Res({ passthrough: true }) res: ExpressResponse + ) { + if (!refreshToken) throw new UnauthorizedException() + await this.usersService.addRevokedToken(refreshToken) + res.clearCookie('refreshToken') return null } - @UseGuards(JwtAuthGuard) - @Post('refresh-token') - async refreshToken(@Request() req, @Response() res) { - if (!req.cookie?.refreshToken) throw new UnauthorizedException() + @HttpCode(HttpStatus.OK) + @UseGuards(JwtRefreshGuard) + @Get('refresh-token') + async refreshToken(@Request() req, @Response({ passthrough: true }) res: ExpressResponse) { + if (!req.cookies?.refreshToken) throw new UnauthorizedException() const userId = req.user.id const newTokens = await this.authService.createJwtTokensPair(userId) res.cookie('refreshToken', newTokens.refreshToken, { httpOnly: true, - secure: true, - path: '/refresh', + // secure: true, + path: '/v1/auth/refresh-token', expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), }) - return { accessToken: newTokens.accessToken } + return { + accessToken: newTokens.accessToken, + } } } diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 3f1807e..1d9bc4e 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,5 +1,5 @@ import { Injectable, UnauthorizedException } from '@nestjs/common' -import { addDays, isAfter } from 'date-fns' +import { addDays, isBefore } from 'date-fns' import * as jwt from 'jsonwebtoken' import * as bcrypt from 'bcrypt' import { UsersRepository } from '../users/infrastructure/users.repository' @@ -56,7 +56,6 @@ export class AuthService { isRevoked: false, }, }) - return { accessToken, refreshToken, @@ -133,19 +132,21 @@ export class AuthService { } async confirmEmail(token: string): Promise { - const user = await this.usersRepository.findUserByVerificationToken(token) - if (!user || user.isEmailVerified) return false - const dbToken = user.verificationToken - const isTokenExpired = isAfter(user.verificationTokenExpiry, new Date()) + const verificationWithUser = await this.usersRepository.findUserByVerificationToken(token) + console.log(verificationWithUser) + if (!verificationWithUser || verificationWithUser.isEmailVerified) return false + const dbToken = verificationWithUser.verificationToken + const isTokenExpired = isBefore(verificationWithUser.verificationTokenExpiry, new Date()) + console.log({ isTokenExpired }) if (dbToken !== token || isTokenExpired) { return false } - return await this.usersRepository.updateConfirmation(user.id) + return await this.usersRepository.updateConfirmation(verificationWithUser.userId) } - async resendCode(email: string) { - const user = await this.usersRepository.findUserByEmail(email) + async resendCode(userId: string) { + const user = await this.usersRepository.findUserById(userId) if (!user || user?.verification.isEmailVerified) return null const updatedUser = await this.usersRepository.updateVerificationToken(user.id) if (!updatedUser) return null diff --git a/src/modules/auth/guards/jwt-refresh.guard.ts b/src/modules/auth/guards/jwt-refresh.guard.ts new file mode 100644 index 0000000..6d1075d --- /dev/null +++ b/src/modules/auth/guards/jwt-refresh.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common' +import { AuthGuard } from '@nestjs/passport' + +@Injectable() +export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {} diff --git a/src/modules/auth/strategies/jwt-refresh.strategy.ts b/src/modules/auth/strategies/jwt-refresh.strategy.ts new file mode 100644 index 0000000..dc0c960 --- /dev/null +++ b/src/modules/auth/strategies/jwt-refresh.strategy.ts @@ -0,0 +1,34 @@ +import { Inject, Injectable } from '@nestjs/common' +import { PassportStrategy } from '@nestjs/passport' +import { Strategy } from 'passport-jwt' +import { UsersService } from '../../users/services/users.service' +import { AppSettings } from '../../../settings/app-settings' +import { Request } from 'express' + +const cookieExtractor = function (req: Request) { + console.log(req.cookies) + let token = null + if (req && req.cookies) { + token = req.cookies['refreshToken'] + } + console.log(token) + return token +} +// ... +@Injectable() +export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { + constructor( + @Inject(AppSettings.name) private readonly appSettings: AppSettings, + private userService: UsersService + ) { + super({ + jwtFromRequest: cookieExtractor, + ignoreExpiration: true, + secretOrKey: appSettings.auth.REFRESH_JWT_SECRET_KEY, + }) + } + + async validate(payload: any) { + return this.userService.getUserById(payload.userId) + } +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index eda3c78..3c23545 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -1,12 +1,17 @@ import { Inject, Injectable, UnauthorizedException } from '@nestjs/common' import { PassportStrategy } from '@nestjs/passport' import { ExtractJwt, Strategy } from 'passport-jwt' +import { AuthService } from '../auth.service' import { AppSettings } from '../../../settings/app-settings' -import { Request } from 'express' +import { UsersService } from '../../users/services/users.service' @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) { + constructor( + @Inject(AppSettings.name) private readonly appSettings: AppSettings, + private authService: AuthService, + private userService: UsersService + ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, @@ -14,29 +19,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }) } - async validate(request: Request, payload: any) { - const accessToken = request.headers.authorization?.split(' ')[1] - const refreshToken = request.cookies.refreshToken // Extract refresh token from cookies - - // If there's no refresh token, simply validate the user based on payload - if (!refreshToken) { - return { userId: payload.userId } - } - - try { - const newAccessToken = await this.authService.checkToken(accessToken, refreshToken) - - // If new access token were issued, attach it to the response headers - if (newAccessToken) { - request.res.setHeader('Authorization', `Bearer ${newAccessToken.accessToken}`) - } - request.res.cookie('refreshToken', newAccessToken.refreshToken, { - httpOnly: true, - path: '/auth/refresh-token', - }) - return { userId: payload.userId } - } catch (error) { - throw new UnauthorizedException('Invalid tokens') + async validate(payload: any) { + console.log(payload) + const user = await this.userService.getUserById(payload.userId) + if (!user) { + throw new UnauthorizedException() } + return user } } diff --git a/src/modules/auth/strategies/local.strategy.ts b/src/modules/auth/strategies/local.strategy.ts index a0deaf6..893fa6c 100644 --- a/src/modules/auth/strategies/local.strategy.ts +++ b/src/modules/auth/strategies/local.strategy.ts @@ -12,10 +12,10 @@ export class LocalStrategy extends PassportStrategy(Strategy) { } async validate(email: string, password: string): Promise { - const credentials = await this.authService.checkCredentials(email, password) - if (credentials.resultCode === 1) { + const newCredentials = await this.authService.checkCredentials(email, password) + if (newCredentials.resultCode === 1) { throw new UnauthorizedException() } - return credentials + return newCredentials } } diff --git a/src/modules/auth/strategies/refresh-token.strategy.ts b/src/modules/auth/strategies/refresh-token.strategy.ts deleted file mode 100644 index 47ee822..0000000 --- a/src/modules/auth/strategies/refresh-token.strategy.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common' -import { PassportStrategy } from '@nestjs/passport' -import { ExtractJwt, Strategy } from 'passport-jwt' -import { AppSettings } from '../../../settings/app-settings' -import { Request } from 'express' - -type JwtPayload = { - userId: string - username: string -} - -@Injectable() -export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { - constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY, - passReqToCallback: true, - }) - } - - validate(req: Request, payload: any) { - const refreshToken = req.get('Authorization').replace('Bearer', '').trim() - return { ...payload, refreshToken } - } -} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts new file mode 100644 index 0000000..9cef036 --- /dev/null +++ b/src/modules/users/entities/user.entity.ts @@ -0,0 +1,8 @@ +import { Prisma } from '@prisma/client' + +export class User implements Prisma.UserUncheckedCreateInput { + id: string + email: string + password: string + name: string +} diff --git a/src/modules/users/infrastructure/users.repository.ts b/src/modules/users/infrastructure/users.repository.ts index 3d0920a..0e67bec 100644 --- a/src/modules/users/infrastructure/users.repository.ts +++ b/src/modules/users/infrastructure/users.repository.ts @@ -10,7 +10,8 @@ import { addHours } from 'date-fns' import { IUsersRepository } from '../services/users.service' import { v4 as uuidv4 } from 'uuid' import { PrismaService } from '../../../prisma.service' - +import { pick } from 'remeda' +import { Prisma } from '@prisma/client' @Injectable() export class UsersRepository implements IUsersRepository { constructor(private prisma: PrismaService) {} @@ -38,14 +39,8 @@ export class UsersRepository implements IUsersRepository { }), ]) - console.log(users, 'usersFromBase') const totalPages = Math.ceil(totalItems / itemsPerPage) - const usersView = users.map(u => ({ - id: u.id, - name: u.name, - email: u.email, - })) - console.log(usersView, 'users---') + const usersView = users.map(u => pick(u, ['id', 'name', 'email', 'isEmailVerified'])) return { totalPages, currentPage, @@ -56,6 +51,7 @@ export class UsersRepository implements IUsersRepository { } async createUser(newUser: CreateUserInput): Promise { + console.log(newUser) return await this.prisma.user.create({ data: { email: newUser.email, @@ -88,13 +84,16 @@ export class UsersRepository implements IUsersRepository { return result.count > 0 } - async findUserById(id: string): Promise { - const user = await this.prisma.user.findUnique({ where: { id } }) + async findUserById(id: string, include?: Prisma.UserInclude) { + const user = await this.prisma.user.findUnique({ + where: { id }, + include, + }) if (!user) { return null } - return user + return user as Prisma.UserGetPayload<{ include: typeof include }> } async findUserByEmail(email: string): Promise { diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index a658cdd..96a7a8a 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -21,11 +21,12 @@ export class UsersService { async createUser(name: string, password: string, email: string): Promise { const passwordHash = await this._generateHash(password) + const verificationToken = uuidv4() const newUser: CreateUserInput = { name: name || email.split('@')[0], email: email, password: passwordHash, - verificationToken: uuidv4(), + verificationToken, verificationTokenExpiry: addHours(new Date(), 24), isEmailVerified: false, } @@ -33,23 +34,32 @@ export class UsersService { if (!createdUser) { return null } - try { - await this.emailService.sendMail({ - from: 'andrii ', - to: createdUser.email, - text: 'hello and welcome', - subject: 'E-mail confirmation ', - }) - } catch (e) { - console.log(e) - } + await this.sendConfirmationEmail({ + email: createdUser.email, + name: createdUser.name, + verificationToken: verificationToken, + }) return { id: createdUser.id, name: createdUser.name, email: createdUser.email, } } - + async resendConfirmationEmail(userId: string) { + const user = await this.usersRepository.findUserById(userId, { verification: true }) + if (!user) { + return null + } + if (user.isEmailVerified) { + return null + } + await this.sendConfirmationEmail({ + email: user.email, + name: user.name, + verificationToken: user.verification.verificationToken, + }) + return true + } async deleteUserById(id: string): Promise { return await this.usersRepository.deleteUserById(id) } @@ -70,7 +80,27 @@ export class UsersService { return null } } - + private async sendConfirmationEmail({ + email, + name, + verificationToken, + }: { + email: string + name: string + verificationToken: string + }) { + try { + await this.emailService.sendMail({ + from: 'andrii ', + to: email, + text: 'hello and welcome, token is: ' + verificationToken, + html: `Hello ${name}!
Please confirm your email by clicking on the link below:
Confirm email`, + subject: 'E-mail confirmation ', + }) + } catch (e) { + console.log(e) + } + } private async _generateHash(password: string) { return await bcrypt.hash(password, 10) } @@ -88,7 +118,7 @@ export interface IUsersRepository { deleteUserById(id: string): Promise - findUserById(id: string): Promise + // findUserById(id: string): Promise revokeToken(id: string, token: string): Promise }