From 4278196096b3e915479edb7b34629c14ee9e014f Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 28 May 2024 23:18:44 +0200 Subject: [PATCH 1/4] change number to string --- backend/src/app.module.ts | 2 ++ backend/src/entities/user-credentials.entity.ts | 2 +- .../auth-module/controller/auth.controller.ts | 4 ++-- .../repositories/user-credentials.repository.ts | 4 ++-- .../src/modules/auth-module/services/auth.service.ts | 12 ++++++++---- .../auth-module/services/token-management.service.ts | 6 +++--- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 95be8a7..e6167c8 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { AccessTokenGuard } from './modules/auth-module/common/guards'; import { DatabaseModule } from './modules/database-module/database.module'; import { SendgridModule } from './modules/sendgrid-module/sendgrid.module'; import { UserModule } from './modules/user-module/user.module'; +import { VerifyModule } from './modules/verify-module/verify.module'; @Module({ imports: [ @@ -22,6 +23,7 @@ import { UserModule } from './modules/user-module/user.module'; AuthModule, UserModule, SendgridModule, + VerifyModule, ], controllers: [AppController], providers: [AppService, { provide: 'APP_GUARD', useClass: AccessTokenGuard }], diff --git a/backend/src/entities/user-credentials.entity.ts b/backend/src/entities/user-credentials.entity.ts index accd20c..fad91b1 100644 --- a/backend/src/entities/user-credentials.entity.ts +++ b/backend/src/entities/user-credentials.entity.ts @@ -9,7 +9,7 @@ import { @Entity() export class UserCredentials { @PrimaryGeneratedColumn('uuid') - public id: number; + public id: string; @Column({ unique: true }) public email: string; diff --git a/backend/src/modules/auth-module/controller/auth.controller.ts b/backend/src/modules/auth-module/controller/auth.controller.ts index 721aa52..b52e273 100644 --- a/backend/src/modules/auth-module/controller/auth.controller.ts +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -50,7 +50,7 @@ export class AuthController { }) @Post('logout') @HttpCode(HttpStatus.OK) - public async logout(@GetCurrentUserId() userId: number): Promise { + public async logout(@GetCurrentUserId() userId: string): Promise { return this.authService.logout(userId); } @@ -70,7 +70,7 @@ export class AuthController { @Post('refresh') @HttpCode(HttpStatus.OK) public async refresh( - @GetCurrentUserId() userId: number, + @GetCurrentUserId() userId: string, @GetCurrentUser('refresh_token') refresh_token: string ): Promise { return this.authService.refresh(userId, refresh_token); diff --git a/backend/src/modules/auth-module/repositories/user-credentials.repository.ts b/backend/src/modules/auth-module/repositories/user-credentials.repository.ts index 0ecab36..86e8cae 100644 --- a/backend/src/modules/auth-module/repositories/user-credentials.repository.ts +++ b/backend/src/modules/auth-module/repositories/user-credentials.repository.ts @@ -26,13 +26,13 @@ export class UserCredentialsRepository { } public async findUserById( - userId: number + userId: string ): Promise { return this.repository.findOne({ where: { id: userId } }); } public async updateUserTokenHash( - userId: number, + userId: string, hashedRt: string | null ): Promise { const result = await this.repository.update(userId, { hashedRt }); diff --git a/backend/src/modules/auth-module/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index fec3d69..53f6d24 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -2,6 +2,7 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service'; import { UserDataRepository } from '../../user-module/repositories/user-data.repository'; +import { EmailVerificationService } from '../../verify-module/services/email-verification.service'; import { TokensDto, UserCredentialsDto } from '../models/dto'; import { UserCredentialsRepository } from '../repositories/user-credentials.repository'; @@ -15,7 +16,8 @@ export class AuthService { private readonly userDataRepository: UserDataRepository, private readonly tokenManagementService: TokenManagementService, private readonly encryptionService: EncryptionService, - private readonly passwordConfirmationMailService: PasswordConfirmationMailService + private readonly passwordConfirmationMailService: PasswordConfirmationMailService, + private readonly emailVerificationService: EmailVerificationService ) {} public async signup(userCredentials: UserCredentialsDto): Promise { @@ -34,6 +36,8 @@ export class AuthService { // user.email // ); + // await this.emailVerificationService.generateEmailVerificationToken(user.id); + return this.generateAndPersistTokens(user.id, user.email); } @@ -59,7 +63,7 @@ export class AuthService { } public async refresh( - userId: number, + userId: string, refreshToken: string ): Promise { const user = await this.userCredentialsRepository.findUserById(userId); @@ -80,7 +84,7 @@ export class AuthService { return this.generateAndPersistTokens(user.id, user.email); } - public async logout(userId: number): Promise { + public async logout(userId: string): Promise { const affected = await this.userCredentialsRepository.updateUserTokenHash( userId, null @@ -90,7 +94,7 @@ export class AuthService { } private async generateAndPersistTokens( - userId: number, + userId: string, email: string ): Promise { const tokens = await this.tokenManagementService.generateTokens( diff --git a/backend/src/modules/auth-module/services/token-management.service.ts b/backend/src/modules/auth-module/services/token-management.service.ts index 0055406..fd91669 100644 --- a/backend/src/modules/auth-module/services/token-management.service.ts +++ b/backend/src/modules/auth-module/services/token-management.service.ts @@ -26,7 +26,7 @@ export class TokenManagementService { } public async generateTokens( - userId: number, + userId: string, email: string ): Promise { const access_token: string = await this.createAccessToken(userId, email); @@ -36,7 +36,7 @@ export class TokenManagementService { } private async createAccessToken( - userId: number, + userId: string, email: string ): Promise { return this.jwt.signAsync( @@ -49,7 +49,7 @@ export class TokenManagementService { } private async createRefreshToken( - userId: number, + userId: string, email: string ): Promise { return this.jwt.signAsync( From 2f418ab14e90f4787d2752ac5ec516f21be9b53f Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 28 May 2024 23:34:08 +0200 Subject: [PATCH 2/4] generate and save token to db + send via sendgrid api --- .../src/entities/email-verification.entity.ts | 33 +++++++++++++++++++ backend/src/entities/index.ts | 1 + .../src/modules/auth-module/auth.module.ts | 2 ++ .../auth-module/services/auth.service.ts | 13 +++++--- .../database-module/database-config.ts | 4 +-- .../password-confirmation.mail.service.ts | 9 +++-- .../repositories/email-verify.repository.ts | 24 ++++++++++++++ .../verify-module/repositories/index.ts | 1 + .../services/email-verification.service.ts | 27 +++++++++++++++ .../modules/verify-module/services/index.ts | 0 .../modules/verify-module/verify.module.ts | 15 +++++++++ 11 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 backend/src/entities/email-verification.entity.ts create mode 100644 backend/src/modules/verify-module/repositories/email-verify.repository.ts create mode 100644 backend/src/modules/verify-module/repositories/index.ts create mode 100644 backend/src/modules/verify-module/services/email-verification.service.ts create mode 100644 backend/src/modules/verify-module/services/index.ts create mode 100644 backend/src/modules/verify-module/verify.module.ts diff --git a/backend/src/entities/email-verification.entity.ts b/backend/src/entities/email-verification.entity.ts new file mode 100644 index 0000000..f49655f --- /dev/null +++ b/backend/src/entities/email-verification.entity.ts @@ -0,0 +1,33 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +import { UserCredentials } from './user-credentials.entity'; + +@Entity() +export class EmailVerification { + @PrimaryGeneratedColumn('uuid') + public id: string; + + @Column() + public token: string; + + @Column() + public expiresAt: Date; + + @OneToOne(() => UserCredentials) + @JoinColumn({ name: 'userCredentialsId' }) + public user: UserCredentials; + + @CreateDateColumn() + public createdAt: Date; + + @UpdateDateColumn() + public updatedAt: Date; +} diff --git a/backend/src/entities/index.ts b/backend/src/entities/index.ts index 6bca565..6d8bde7 100644 --- a/backend/src/entities/index.ts +++ b/backend/src/entities/index.ts @@ -1,2 +1,3 @@ export * from './user-credentials.entity'; export * from './user-data.entity'; +export * from './email-verification.entity'; diff --git a/backend/src/modules/auth-module/auth.module.ts b/backend/src/modules/auth-module/auth.module.ts index 2d14cd7..5688f34 100644 --- a/backend/src/modules/auth-module/auth.module.ts +++ b/backend/src/modules/auth-module/auth.module.ts @@ -5,6 +5,7 @@ import { UserCredentials } from 'src/entities'; import { SendgridModule } from '../sendgrid-module/sendgrid.module'; import { UserModule } from '../user-module/user.module'; +import { VerifyModule } from '../verify-module/verify.module'; import { AuthController } from './controller/auth.controller'; import { UserCredentialsRepository } from './repositories/user-credentials.repository'; @@ -17,6 +18,7 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies'; imports: [ UserModule, SendgridModule, + VerifyModule, JwtModule.register({}), TypeOrmModule.forFeature([UserCredentials]), ], diff --git a/backend/src/modules/auth-module/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index 53f6d24..5bc70eb 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -31,12 +31,15 @@ export class AuthService { await this.userDataRepository.createInitialUserData(user); - // TODO Send email confirmation - // await this.passwordConfirmationMailService.sendPasswordConfirmationMail( - // user.email - // ); + const token = + await this.emailVerificationService.generateEmailVerificationToken( + user.id + ); - // await this.emailVerificationService.generateEmailVerificationToken(user.id); + await this.passwordConfirmationMailService.sendPasswordConfirmationMail( + user.email, + token + ); return this.generateAndPersistTokens(user.id, user.email); } diff --git a/backend/src/modules/database-module/database-config.ts b/backend/src/modules/database-module/database-config.ts index e355798..066f829 100644 --- a/backend/src/modules/database-module/database-config.ts +++ b/backend/src/modules/database-module/database-config.ts @@ -1,6 +1,6 @@ import { ConfigService } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { UserCredentials, UserData } from 'src/entities'; +import { EmailVerification, UserCredentials, UserData } from 'src/entities'; export const databaseConfigFactory = ( configService: ConfigService @@ -13,5 +13,5 @@ export const databaseConfigFactory = ( database: configService.get('DB_NAME'), synchronize: true, logging: true, - entities: [UserCredentials, UserData], + entities: [UserCredentials, UserData, EmailVerification], }); diff --git a/backend/src/modules/sendgrid-module/services/password-confirmation.mail.service.ts b/backend/src/modules/sendgrid-module/services/password-confirmation.mail.service.ts index 53af5aa..de78836 100644 --- a/backend/src/modules/sendgrid-module/services/password-confirmation.mail.service.ts +++ b/backend/src/modules/sendgrid-module/services/password-confirmation.mail.service.ts @@ -16,18 +16,23 @@ export class PasswordConfirmationMailService extends BaseMailService { super(sendGridApiKey); } - public async sendPasswordConfirmationMail(to: string): Promise { + public async sendPasswordConfirmationMail( + to: string, + token: string + ): Promise { const templateId: string = this.templateConfigService.getTemplateId( this.PASSWORD_CONFIRMATION_EMAIL ); + const encodedToken = encodeURIComponent(token); + const mailoptions: SendGridMailApi.MailDataRequired = { to, from: { email: 'info@igor-propisnov.com', name: 'Ticket App' }, templateId: templateId, dynamicTemplateData: { name: 'Mara', - buttonUrl: 'https://igor-propisnov.com', + buttonUrl: `http://localhost:4200/?token=${encodedToken}`, }, }; diff --git a/backend/src/modules/verify-module/repositories/email-verify.repository.ts b/backend/src/modules/verify-module/repositories/email-verify.repository.ts new file mode 100644 index 0000000..5fd075e --- /dev/null +++ b/backend/src/modules/verify-module/repositories/email-verify.repository.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EmailVerification } from 'src/entities'; +import { Repository } from 'typeorm'; + +@Injectable() +export class EmailVerifyRepository { + public constructor( + @InjectRepository(EmailVerification) + private readonly repository: Repository + ) {} + + public async createEmailVerification( + token: string, + expiresAt: Date, + userId: string + ): Promise { + await this.repository.save({ + token, + expiresAt, + user: { id: userId }, + }); + } +} diff --git a/backend/src/modules/verify-module/repositories/index.ts b/backend/src/modules/verify-module/repositories/index.ts new file mode 100644 index 0000000..251e136 --- /dev/null +++ b/backend/src/modules/verify-module/repositories/index.ts @@ -0,0 +1 @@ +export * from './email-verify.repository'; diff --git a/backend/src/modules/verify-module/services/email-verification.service.ts b/backend/src/modules/verify-module/services/email-verification.service.ts new file mode 100644 index 0000000..80b93e1 --- /dev/null +++ b/backend/src/modules/verify-module/services/email-verification.service.ts @@ -0,0 +1,27 @@ +import { randomBytes } from 'crypto'; + +import { Injectable } from '@nestjs/common'; + +import { EmailVerifyRepository } from '../repositories'; + +@Injectable() +export class EmailVerificationService { + public constructor( + private readonly emailVerifyRepository: EmailVerifyRepository + ) {} + + public async generateEmailVerificationToken(userId: string): Promise { + const token = randomBytes(32).toString('hex'); + + // TODO Check users local time zone and set expiration time accordingly + const expiration = new Date(Date.now() + 24 * 60 * 60 * 1000); + + this.emailVerifyRepository.createEmailVerification( + token, + expiration, + userId + ); + + return token; + } +} diff --git a/backend/src/modules/verify-module/services/index.ts b/backend/src/modules/verify-module/services/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/modules/verify-module/verify.module.ts b/backend/src/modules/verify-module/verify.module.ts new file mode 100644 index 0000000..f506909 --- /dev/null +++ b/backend/src/modules/verify-module/verify.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EmailVerification } from 'src/entities'; + +import { EmailVerifyRepository } from './repositories'; +import { EmailVerificationService } from './services/email-verification.service'; + +@Module({ + imports: [ConfigModule, TypeOrmModule.forFeature([EmailVerification])], + providers: [EmailVerifyRepository, EmailVerificationService], + controllers: [], + exports: [EmailVerificationService], +}) +export class VerifyModule {} From fba474bd7c8cea9c714cb78b83226dc7b073d47c Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Wed, 29 May 2024 19:53:15 +0200 Subject: [PATCH 3/4] send verify mail to user and handle db logic --- .../src/modules/auth-module/auth.module.ts | 2 - .../auth-module/common/decorators/index.ts | 1 - .../auth-module/controller/auth.controller.ts | 3 +- .../auth-module/services/auth.service.ts | 12 ++-- .../password-confirmation.mail.service.ts | 11 ++-- .../repositories/user-data.repository.ts | 7 +++ .../controller/verify.controller.ts | 26 ++++++++ .../repositories/email-verify.repository.ts | 26 +++++++- .../services/email-verification.service.ts | 50 ++++++++++++++-- .../modules/verify-module/verify.module.ts | 11 +++- backend/src/shared/decorator/index.ts | 1 + .../decorator}/public.decorator.ts | 0 backend/src/shared/index.ts | 2 + .../utils}/encryption.service.ts | 11 ++-- backend/src/shared/utils/index.ts | 2 + .../src/shared/utils/uri-encoder.service.ts | 13 ++++ frontend/src/app/app.routes.ts | 7 +++ .../email-verify-root.component.html | 16 +++++ .../email-verify-root.component.scss | 28 +++++++++ .../email-verify-root.component.ts | 60 +++++++++++++++++++ .../register-root/register-root.component.ts | 34 ++++++++++- .../src/app/shared/service/auth.service.ts | 3 +- 22 files changed, 297 insertions(+), 29 deletions(-) create mode 100644 backend/src/modules/verify-module/controller/verify.controller.ts create mode 100644 backend/src/shared/decorator/index.ts rename backend/src/{modules/auth-module/common/decorators => shared/decorator}/public.decorator.ts (100%) create mode 100644 backend/src/shared/index.ts rename backend/src/{modules/auth-module/services => shared/utils}/encryption.service.ts (57%) create mode 100644 backend/src/shared/utils/index.ts create mode 100644 backend/src/shared/utils/uri-encoder.service.ts create mode 100644 frontend/src/app/pages/email-verify-root/email-verify-root.component.html create mode 100644 frontend/src/app/pages/email-verify-root/email-verify-root.component.scss create mode 100644 frontend/src/app/pages/email-verify-root/email-verify-root.component.ts diff --git a/backend/src/modules/auth-module/auth.module.ts b/backend/src/modules/auth-module/auth.module.ts index 5688f34..5a1dccb 100644 --- a/backend/src/modules/auth-module/auth.module.ts +++ b/backend/src/modules/auth-module/auth.module.ts @@ -10,7 +10,6 @@ import { VerifyModule } from '../verify-module/verify.module'; import { AuthController } from './controller/auth.controller'; import { UserCredentialsRepository } from './repositories/user-credentials.repository'; import { AuthService } from './services/auth.service'; -import { EncryptionService } from './services/encryption.service'; import { TokenManagementService } from './services/token-management.service'; import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies'; @@ -25,7 +24,6 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies'; providers: [ AuthService, TokenManagementService, - EncryptionService, UserCredentialsRepository, AccessTokenStrategy, RefreshTokenStrategy, diff --git a/backend/src/modules/auth-module/common/decorators/index.ts b/backend/src/modules/auth-module/common/decorators/index.ts index 61c88bb..2a36025 100644 --- a/backend/src/modules/auth-module/common/decorators/index.ts +++ b/backend/src/modules/auth-module/common/decorators/index.ts @@ -1,3 +1,2 @@ export * from './get-user-id.decorator'; export * from './get-user.decorator'; -export * from './public.decorator'; diff --git a/backend/src/modules/auth-module/controller/auth.controller.ts b/backend/src/modules/auth-module/controller/auth.controller.ts index b52e273..bc48206 100644 --- a/backend/src/modules/auth-module/controller/auth.controller.ts +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -7,8 +7,9 @@ import { UseGuards, } from '@nestjs/common'; import { ApiCreatedResponse, ApiHeader, ApiTags } from '@nestjs/swagger'; +import { Public } from 'src/shared/decorator'; -import { GetCurrentUser, GetCurrentUserId, Public } from '../common/decorators'; +import { GetCurrentUser, GetCurrentUserId } from '../common/decorators'; import { RefreshTokenGuard } from '../common/guards'; import { TokensDto, UserCredentialsDto } from '../models/dto'; import { AuthService } from '../services/auth.service'; diff --git a/backend/src/modules/auth-module/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index 5bc70eb..414ecca 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -1,4 +1,5 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; +import { EncryptionService } from 'src/shared'; import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service'; import { UserDataRepository } from '../../user-module/repositories/user-data.repository'; @@ -6,7 +7,6 @@ import { EmailVerificationService } from '../../verify-module/services/email-ver import { TokensDto, UserCredentialsDto } from '../models/dto'; import { UserCredentialsRepository } from '../repositories/user-credentials.repository'; -import { EncryptionService } from './encryption.service'; import { TokenManagementService } from './token-management.service'; @Injectable() @@ -15,15 +15,15 @@ export class AuthService { private readonly userCredentialsRepository: UserCredentialsRepository, private readonly userDataRepository: UserDataRepository, private readonly tokenManagementService: TokenManagementService, - private readonly encryptionService: EncryptionService, private readonly passwordConfirmationMailService: PasswordConfirmationMailService, private readonly emailVerificationService: EmailVerificationService ) {} public async signup(userCredentials: UserCredentialsDto): Promise { - const passwordHashed = await this.encryptionService.hashData( + const passwordHashed = await EncryptionService.hashData( userCredentials.password ); + const user = await this.userCredentialsRepository.createUser( userCredentials.email, passwordHashed @@ -53,7 +53,7 @@ export class AuthService { throw new ForbiddenException('Access Denied'); } - const passwordMatch = await this.encryptionService.compareHash( + const passwordMatch = await EncryptionService.compareHash( userCredentials.password, user.hash ); @@ -75,7 +75,7 @@ export class AuthService { throw new ForbiddenException('Access Denied'); } - const refreshTokenMatch = await this.encryptionService.compareHash( + const refreshTokenMatch = await EncryptionService.compareHash( refreshToken, user.hashedRt ); @@ -104,7 +104,7 @@ export class AuthService { userId, email ); - const hashedRefreshToken = await this.encryptionService.hashData( + const hashedRefreshToken = await EncryptionService.hashData( tokens.refresh_token ); diff --git a/backend/src/modules/sendgrid-module/services/password-confirmation.mail.service.ts b/backend/src/modules/sendgrid-module/services/password-confirmation.mail.service.ts index de78836..1f19474 100644 --- a/backend/src/modules/sendgrid-module/services/password-confirmation.mail.service.ts +++ b/backend/src/modules/sendgrid-module/services/password-confirmation.mail.service.ts @@ -1,5 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import * as SendGridMailApi from '@sendgrid/mail'; +import { UriEncoderService } from 'src/shared'; import { BaseMailService } from './base.mail.service'; import { TemplateConfigService } from './template-config.service'; @@ -11,20 +13,21 @@ export class PasswordConfirmationMailService extends BaseMailService { public constructor( @Inject('SEND_GRID_API_KEY') protected readonly sendGridApiKey: string, - private readonly templateConfigService: TemplateConfigService + private readonly templateConfigService: TemplateConfigService, + private readonly configService: ConfigService ) { super(sendGridApiKey); } public async sendPasswordConfirmationMail( to: string, - token: string + verificationToken: string ): Promise { const templateId: string = this.templateConfigService.getTemplateId( this.PASSWORD_CONFIRMATION_EMAIL ); - const encodedToken = encodeURIComponent(token); + const token = `${verificationToken}|${UriEncoderService.encodeBase64(to)}`; const mailoptions: SendGridMailApi.MailDataRequired = { to, @@ -32,7 +35,7 @@ export class PasswordConfirmationMailService extends BaseMailService { templateId: templateId, dynamicTemplateData: { name: 'Mara', - buttonUrl: `http://localhost:4200/?token=${encodedToken}`, + buttonUrl: `${this.configService.get('APP_URL')}/verify/?token=${token}`, }, }; diff --git a/backend/src/modules/user-module/repositories/user-data.repository.ts b/backend/src/modules/user-module/repositories/user-data.repository.ts index d8a57b1..767a91d 100644 --- a/backend/src/modules/user-module/repositories/user-data.repository.ts +++ b/backend/src/modules/user-module/repositories/user-data.repository.ts @@ -20,4 +20,11 @@ export class UserDataRepository { return this.repository.save(userData); } + + public async updateEmailVerificationStatus(userId: string): Promise { + await this.repository.update( + { user: { id: userId } }, + { isEmailConfirmed: true } + ); + } } diff --git a/backend/src/modules/verify-module/controller/verify.controller.ts b/backend/src/modules/verify-module/controller/verify.controller.ts new file mode 100644 index 0000000..76f9180 --- /dev/null +++ b/backend/src/modules/verify-module/controller/verify.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common'; +import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; +import { Public } from 'src/shared/decorator'; + +import { EmailVerificationService } from '../services/email-verification.service'; + +@ApiTags('Verify') +@Controller('verify') +export class VerifyController { + public constructor( + private readonly emailVerificationService: EmailVerificationService + ) {} + + @ApiCreatedResponse({ + description: 'Email verified successfully', + type: Boolean, + }) + @Public() + @Get() + @HttpCode(HttpStatus.OK) + public async verifyEmail( + @Query('token') tokenToVerify: string + ): Promise { + return this.emailVerificationService.verifyEmail(tokenToVerify); + } +} diff --git a/backend/src/modules/verify-module/repositories/email-verify.repository.ts b/backend/src/modules/verify-module/repositories/email-verify.repository.ts index 5fd075e..9c540c8 100644 --- a/backend/src/modules/verify-module/repositories/email-verify.repository.ts +++ b/backend/src/modules/verify-module/repositories/email-verify.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { EmailVerification } from 'src/entities'; -import { Repository } from 'typeorm'; +import { MoreThan, Repository } from 'typeorm'; @Injectable() export class EmailVerifyRepository { @@ -21,4 +21,28 @@ export class EmailVerifyRepository { user: { id: userId }, }); } + + public async findEmailVerificationByToken(token: string): Promise { + const result = await this.repository.findOne({ + where: { token, expiresAt: MoreThan(new Date()) }, + }); + + return result !== null; + } + + public async deleteEmailVerificationByToken( + tokenToDelete: string + ): Promise { + const emailVerification = await this.repository.findOne({ + where: { token: tokenToDelete }, + relations: ['user'], + }); + + if (emailVerification) { + await this.repository.delete({ token: tokenToDelete }); + return emailVerification; + } + + return null; + } } diff --git a/backend/src/modules/verify-module/services/email-verification.service.ts b/backend/src/modules/verify-module/services/email-verification.service.ts index 80b93e1..272b065 100644 --- a/backend/src/modules/verify-module/services/email-verification.service.ts +++ b/backend/src/modules/verify-module/services/email-verification.service.ts @@ -1,27 +1,69 @@ import { randomBytes } from 'crypto'; import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EmailVerification } from 'src/entities'; +import { UriEncoderService } from 'src/shared'; +import { UserDataRepository } from '../../user-module/repositories/user-data.repository'; import { EmailVerifyRepository } from '../repositories'; @Injectable() export class EmailVerificationService { public constructor( - private readonly emailVerifyRepository: EmailVerifyRepository + private readonly emailVerifyRepository: EmailVerifyRepository, + private readonly userDataRepository: UserDataRepository, + private readonly configService: ConfigService ) {} public async generateEmailVerificationToken(userId: string): Promise { - const token = randomBytes(32).toString('hex'); + const verificationToken = await this.createVerificationToken(); // TODO Check users local time zone and set expiration time accordingly const expiration = new Date(Date.now() + 24 * 60 * 60 * 1000); this.emailVerifyRepository.createEmailVerification( - token, + verificationToken, expiration, userId ); - return token; + return verificationToken; + } + + public async verifyEmail(tokenToVerify: string): Promise { + const isTokenVerified = + await this.emailVerifyRepository.findEmailVerificationByToken( + tokenToVerify + ); + + if (isTokenVerified) { + const emailVerification = + await this.deleteEmailVerificationToken(tokenToVerify); + + if (emailVerification && emailVerification.user) { + await this.userDataRepository.updateEmailVerificationStatus( + emailVerification.user.id + ); + return true; + } else { + return false; + } + } + return false; + } + + private async createVerificationToken(): Promise { + const verifyToken = randomBytes(32).toString('hex'); + + return UriEncoderService.encodeUri(verifyToken); + } + + private async deleteEmailVerificationToken( + tokenToDelete: string + ): Promise { + return await this.emailVerifyRepository.deleteEmailVerificationByToken( + tokenToDelete + ); } } diff --git a/backend/src/modules/verify-module/verify.module.ts b/backend/src/modules/verify-module/verify.module.ts index f506909..8c90fc8 100644 --- a/backend/src/modules/verify-module/verify.module.ts +++ b/backend/src/modules/verify-module/verify.module.ts @@ -3,13 +3,20 @@ import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { EmailVerification } from 'src/entities'; +import { UserModule } from '../user-module/user.module'; + +import { VerifyController } from './controller/verify.controller'; import { EmailVerifyRepository } from './repositories'; import { EmailVerificationService } from './services/email-verification.service'; @Module({ - imports: [ConfigModule, TypeOrmModule.forFeature([EmailVerification])], + imports: [ + ConfigModule, + UserModule, + TypeOrmModule.forFeature([EmailVerification]), + ], providers: [EmailVerifyRepository, EmailVerificationService], - controllers: [], + controllers: [VerifyController], exports: [EmailVerificationService], }) export class VerifyModule {} diff --git a/backend/src/shared/decorator/index.ts b/backend/src/shared/decorator/index.ts new file mode 100644 index 0000000..3f75d99 --- /dev/null +++ b/backend/src/shared/decorator/index.ts @@ -0,0 +1 @@ +export * from './public.decorator'; diff --git a/backend/src/modules/auth-module/common/decorators/public.decorator.ts b/backend/src/shared/decorator/public.decorator.ts similarity index 100% rename from backend/src/modules/auth-module/common/decorators/public.decorator.ts rename to backend/src/shared/decorator/public.decorator.ts diff --git a/backend/src/shared/index.ts b/backend/src/shared/index.ts new file mode 100644 index 0000000..054a5f9 --- /dev/null +++ b/backend/src/shared/index.ts @@ -0,0 +1,2 @@ +export * from './utils/index'; +export * from './decorator/index'; diff --git a/backend/src/modules/auth-module/services/encryption.service.ts b/backend/src/shared/utils/encryption.service.ts similarity index 57% rename from backend/src/modules/auth-module/services/encryption.service.ts rename to backend/src/shared/utils/encryption.service.ts index 79182e7..5e3a3b8 100644 --- a/backend/src/modules/auth-module/services/encryption.service.ts +++ b/backend/src/shared/utils/encryption.service.ts @@ -1,21 +1,22 @@ -import { Injectable } from '@nestjs/common'; import * as argon2 from 'argon2'; import { Options } from 'argon2'; -@Injectable() export class EncryptionService { - private hashOptions: Options = { + private static hashOptions: Options = { type: argon2.argon2id, memoryCost: 2 ** 16, timeCost: 3, parallelism: 1, }; - public async hashData(data: string): Promise { + public static async hashData(data: string): Promise { return await argon2.hash(data, this.hashOptions); } - public async compareHash(data: string, encrypted: string): Promise { + public static async compareHash( + data: string, + encrypted: string + ): Promise { return await argon2.verify(encrypted, data); } } diff --git a/backend/src/shared/utils/index.ts b/backend/src/shared/utils/index.ts new file mode 100644 index 0000000..bce2e01 --- /dev/null +++ b/backend/src/shared/utils/index.ts @@ -0,0 +1,2 @@ +export * from './uri-encoder.service'; +export * from './encryption.service'; diff --git a/backend/src/shared/utils/uri-encoder.service.ts b/backend/src/shared/utils/uri-encoder.service.ts new file mode 100644 index 0000000..ad9eba0 --- /dev/null +++ b/backend/src/shared/utils/uri-encoder.service.ts @@ -0,0 +1,13 @@ +export class UriEncoderService { + public static encodeUri(uri: string): string { + return encodeURIComponent(uri); + } + + public static decodeUri(uri: string): string { + return decodeURIComponent(uri); + } + + public static encodeBase64(uri: string): string { + return btoa(UriEncoderService.encodeUri(uri)); + } +} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 1a9c7a0..5488cff 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -9,4 +9,11 @@ export const routes: Routes = [ (m) => m.RegisterRootComponent ), }, + { + path: 'verify', + loadComponent: () => + import('./pages/email-verify-root/email-verify-root.component').then( + (m) => m.EmailVerifyRootComponent + ), + }, ]; diff --git a/frontend/src/app/pages/email-verify-root/email-verify-root.component.html b/frontend/src/app/pages/email-verify-root/email-verify-root.component.html new file mode 100644 index 0000000..9862d41 --- /dev/null +++ b/frontend/src/app/pages/email-verify-root/email-verify-root.component.html @@ -0,0 +1,16 @@ +
+
+
+ @if (showRedirectMessage()) { +

Es geht gleich los!

+

+ Danke für das bestätigen der E-Mail - Wir leiten dich zum Login + weiter! +

+ } @else { +

Oops, da ist etwas schief gelaufen!

+

Der Link ist nicht mehr gültig

+ } +
+
+
diff --git a/frontend/src/app/pages/email-verify-root/email-verify-root.component.scss b/frontend/src/app/pages/email-verify-root/email-verify-root.component.scss new file mode 100644 index 0000000..eae00e9 --- /dev/null +++ b/frontend/src/app/pages/email-verify-root/email-verify-root.component.scss @@ -0,0 +1,28 @@ +#background { + display: flex; + height: 100%; +} + +.wrapper { + flex: 1; + background-color: lightsalmon; + display: flex; + + .content{ + flex-direction: column; + display: flex; + align-items: flex-start; + justify-content: center; + h1 { + font-size: 4rem; + margin-left: 3rem; + line-height: 1rem; + } + h2 { + margin-left: 3rem; + } + p { + margin-left: 3rem; + } + } +} diff --git a/frontend/src/app/pages/email-verify-root/email-verify-root.component.ts b/frontend/src/app/pages/email-verify-root/email-verify-root.component.ts new file mode 100644 index 0000000..301c2e5 --- /dev/null +++ b/frontend/src/app/pages/email-verify-root/email-verify-root.component.ts @@ -0,0 +1,60 @@ +import { + ChangeDetectionStrategy, + Component, + InputSignal, + OnInit, + WritableSignal, + input, + signal, +} from '@angular/core'; +import { Router } from '@angular/router'; + +import { delay, filter, tap } from 'rxjs'; + +import { VerifyApiService } from '../../api'; + +@Component({ + selector: 'app-email-verify-root', + standalone: true, + imports: [], + providers: [], + templateUrl: './email-verify-root.component.html', + styleUrl: './email-verify-root.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EmailVerifyRootComponent implements OnInit { + public token: InputSignal = input(''); + public verifyStatus: WritableSignal = signal(false); + public showRedirectMessage: WritableSignal = signal(false); + + public constructor( + private readonly api: VerifyApiService, + private readonly router: Router + ) {} + + public ngOnInit(): void { + this.verifyEmail(); + } + + private verifyEmail(): void { + const [verifyToken, email]: string[] = this.token().split('|'); + + this.api + .verifyControllerVerifyEmail(verifyToken) + .pipe( + tap((isVerified: boolean) => { + this.verifyStatus.set(isVerified); + }), + filter((isVerified) => isVerified), + tap(() => { + this.showRedirectMessage.set(true); + }), + delay(10000) + ) + .subscribe(() => { + this.router.navigate(['/signup'], { + queryParams: { verified: true, email: email }, + }); + }); + } +} diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index d25274f..ccf8788 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -7,6 +7,8 @@ import { WritableSignal, signal, effect, + InputSignal, + input, } from '@angular/core'; import { FormBuilder, @@ -16,6 +18,7 @@ import { ReactiveFormsModule, Validators, } from '@angular/forms'; +import { Router } from '@angular/router'; import { ButtonModule } from 'primeng/button'; import { CheckboxModule } from 'primeng/checkbox'; @@ -50,6 +53,8 @@ type AuthAction = 'register' | 'signup'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegisterRootComponent implements OnInit { + public verified: InputSignal = input(false); + public email: InputSignal = input(''); public form: FormGroup | undefined; public isRegisterSignal: WritableSignal = signal(false); public isSignupSignal: WritableSignal = signal(false); @@ -57,10 +62,12 @@ export class RegisterRootComponent implements OnInit { public emailInvalid: WritableSignal = signal(null); public passwordInvalid: WritableSignal = signal(null); public termsInvalid: WritableSignal = signal(null); + private removeQueryParams: WritableSignal = signal(false); public constructor( private readonly formBuilder: FormBuilder, - private readonly authService: AuthService + private readonly authService: AuthService, + private readonly router: Router ) { effect(() => { if (this.form) { @@ -73,12 +80,21 @@ export class RegisterRootComponent implements OnInit { this.form.removeControl('terms'); } } + + if (this.removeQueryParams()) { + this.clearRouteParams(); + } }); } public ngOnInit(): void { this.initializeForm(); this.setupValueChanges(); + + if (this.email() || this.verified()) { + this.handleRedirect(); + this.removeQueryParams.set(true); + } } public toggleAction(action: AuthAction): void { @@ -133,6 +149,22 @@ export class RegisterRootComponent implements OnInit { }); } + private handleRedirect(): void { + console.log('handleRedirect'); + if (this.verified()) { + this.isDisplayButtons.set(false); + this.isRegisterSignal.set(false); + this.isSignupSignal.set(true); + } + if (this.email()) { + this.form?.get('email')?.setValue(decodeURIComponent(atob(this.email()))); + } + } + + private clearRouteParams(): void { + this.router.navigate([], { queryParams: {} }); + } + private setupValueChanges(): void { this.setupEmailValueChanges(); this.setupPasswordValueChanges(); diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts index f1d97e0..c7a6ea3 100644 --- a/frontend/src/app/shared/service/auth.service.ts +++ b/frontend/src/app/shared/service/auth.service.ts @@ -13,7 +13,6 @@ import { SessionStorageService } from './session-storage.service'; providedIn: 'root', }) export class AuthService { - private readonly _path: string = '/api/auth'; private _access_token: string | null = null; private _refresh_token: string | null = null; private _isAuthenticated$: BehaviorSubject = @@ -29,7 +28,7 @@ export class AuthService { private readonly sessionStorageService: SessionStorageService, private readonly authenticationApiService: AuthenticationApiService ) { - this.autoLogin(); + //this.autoLogin(); } public signin(credentials: LoginCredentials): void { From 15520a8e8b4f9be6a980f60f5430580663a171c3 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Wed, 29 May 2024 20:00:25 +0200 Subject: [PATCH 4/4] small fixes --- .../email-verify-root.component.html | 18 +++++++++++------- .../email-verify-root.component.ts | 6 ++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/pages/email-verify-root/email-verify-root.component.html b/frontend/src/app/pages/email-verify-root/email-verify-root.component.html index 9862d41..d0c333d 100644 --- a/frontend/src/app/pages/email-verify-root/email-verify-root.component.html +++ b/frontend/src/app/pages/email-verify-root/email-verify-root.component.html @@ -1,15 +1,19 @@
- @if (showRedirectMessage()) { -

Es geht gleich los!

-

- Danke für das bestätigen der E-Mail - Wir leiten dich zum Login - weiter! -

- } @else { + @if (verifyStatus() === true) { + @if (showRedirectMessage()) { +

Es geht gleich los!

+

+ Danke für das bestätigen der E-Mail - Wir leiten dich zum Login + weiter! +

+ } + } @else if (verifyStatus() === false) {

Oops, da ist etwas schief gelaufen!

Der Link ist nicht mehr gültig

+ } @else { +

Verifizierung wird durchgeführt...

}
diff --git a/frontend/src/app/pages/email-verify-root/email-verify-root.component.ts b/frontend/src/app/pages/email-verify-root/email-verify-root.component.ts index 301c2e5..135d8bc 100644 --- a/frontend/src/app/pages/email-verify-root/email-verify-root.component.ts +++ b/frontend/src/app/pages/email-verify-root/email-verify-root.component.ts @@ -24,7 +24,9 @@ import { VerifyApiService } from '../../api'; }) export class EmailVerifyRootComponent implements OnInit { public token: InputSignal = input(''); - public verifyStatus: WritableSignal = signal(false); + public verifyStatus: WritableSignal = signal( + null + ); public showRedirectMessage: WritableSignal = signal(false); public constructor( @@ -49,7 +51,7 @@ export class EmailVerifyRootComponent implements OnInit { tap(() => { this.showRedirectMessage.set(true); }), - delay(10000) + delay(5000) ) .subscribe(() => { this.router.navigate(['/signup'], {