Feature: E-Mail verify #8
|
@ -12,6 +12,7 @@ import { AccessTokenGuard } from './modules/auth-module/common/guards';
|
||||||
import { DatabaseModule } from './modules/database-module/database.module';
|
import { DatabaseModule } from './modules/database-module/database.module';
|
||||||
import { SendgridModule } from './modules/sendgrid-module/sendgrid.module';
|
import { SendgridModule } from './modules/sendgrid-module/sendgrid.module';
|
||||||
import { UserModule } from './modules/user-module/user.module';
|
import { UserModule } from './modules/user-module/user.module';
|
||||||
|
import { VerifyModule } from './modules/verify-module/verify.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -22,6 +23,7 @@ import { UserModule } from './modules/user-module/user.module';
|
||||||
AuthModule,
|
AuthModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
SendgridModule,
|
SendgridModule,
|
||||||
|
VerifyModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, { provide: 'APP_GUARD', useClass: AccessTokenGuard }],
|
providers: [AppService, { provide: 'APP_GUARD', useClass: AccessTokenGuard }],
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './user-credentials.entity';
|
export * from './user-credentials.entity';
|
||||||
export * from './user-data.entity';
|
export * from './user-data.entity';
|
||||||
|
export * from './email-verification.entity';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
@Entity()
|
@Entity()
|
||||||
export class UserCredentials {
|
export class UserCredentials {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
public id: number;
|
public id: string;
|
||||||
|
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
public email: string;
|
public email: string;
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { UserCredentials } from 'src/entities';
|
||||||
|
|
||||||
import { SendgridModule } from '../sendgrid-module/sendgrid.module';
|
import { SendgridModule } from '../sendgrid-module/sendgrid.module';
|
||||||
import { UserModule } from '../user-module/user.module';
|
import { UserModule } from '../user-module/user.module';
|
||||||
|
import { VerifyModule } from '../verify-module/verify.module';
|
||||||
|
|
||||||
import { AuthController } from './controller/auth.controller';
|
import { AuthController } from './controller/auth.controller';
|
||||||
import { UserCredentialsRepository } from './repositories/user-credentials.repository';
|
import { UserCredentialsRepository } from './repositories/user-credentials.repository';
|
||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
import { EncryptionService } from './services/encryption.service';
|
|
||||||
import { TokenManagementService } from './services/token-management.service';
|
import { TokenManagementService } from './services/token-management.service';
|
||||||
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
||||||
|
|
||||||
|
@ -17,13 +17,13 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
||||||
imports: [
|
imports: [
|
||||||
UserModule,
|
UserModule,
|
||||||
SendgridModule,
|
SendgridModule,
|
||||||
|
VerifyModule,
|
||||||
JwtModule.register({}),
|
JwtModule.register({}),
|
||||||
TypeOrmModule.forFeature([UserCredentials]),
|
TypeOrmModule.forFeature([UserCredentials]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AuthService,
|
AuthService,
|
||||||
TokenManagementService,
|
TokenManagementService,
|
||||||
EncryptionService,
|
|
||||||
UserCredentialsRepository,
|
UserCredentialsRepository,
|
||||||
AccessTokenStrategy,
|
AccessTokenStrategy,
|
||||||
RefreshTokenStrategy,
|
RefreshTokenStrategy,
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * from './get-user-id.decorator';
|
export * from './get-user-id.decorator';
|
||||||
export * from './get-user.decorator';
|
export * from './get-user.decorator';
|
||||||
export * from './public.decorator';
|
|
||||||
|
|
|
@ -7,8 +7,9 @@ import {
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiCreatedResponse, ApiHeader, ApiTags } from '@nestjs/swagger';
|
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 { RefreshTokenGuard } from '../common/guards';
|
||||||
import { TokensDto, UserCredentialsDto } from '../models/dto';
|
import { TokensDto, UserCredentialsDto } from '../models/dto';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
|
@ -50,7 +51,7 @@ export class AuthController {
|
||||||
})
|
})
|
||||||
@Post('logout')
|
@Post('logout')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
public async logout(@GetCurrentUserId() userId: number): Promise<boolean> {
|
public async logout(@GetCurrentUserId() userId: string): Promise<boolean> {
|
||||||
return this.authService.logout(userId);
|
return this.authService.logout(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ export class AuthController {
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
public async refresh(
|
public async refresh(
|
||||||
@GetCurrentUserId() userId: number,
|
@GetCurrentUserId() userId: string,
|
||||||
@GetCurrentUser('refresh_token') refresh_token: string
|
@GetCurrentUser('refresh_token') refresh_token: string
|
||||||
): Promise<TokensDto> {
|
): Promise<TokensDto> {
|
||||||
return this.authService.refresh(userId, refresh_token);
|
return this.authService.refresh(userId, refresh_token);
|
||||||
|
|
|
@ -26,13 +26,13 @@ export class UserCredentialsRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findUserById(
|
public async findUserById(
|
||||||
userId: number
|
userId: string
|
||||||
): Promise<UserCredentials | undefined> {
|
): Promise<UserCredentials | undefined> {
|
||||||
return this.repository.findOne({ where: { id: userId } });
|
return this.repository.findOne({ where: { id: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateUserTokenHash(
|
public async updateUserTokenHash(
|
||||||
userId: number,
|
userId: string,
|
||||||
hashedRt: string | null
|
hashedRt: string | null
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const result = await this.repository.update(userId, { hashedRt });
|
const result = await this.repository.update(userId, { hashedRt });
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||||
|
import { EncryptionService } from 'src/shared';
|
||||||
|
|
||||||
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
||||||
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
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 { TokensDto, UserCredentialsDto } from '../models/dto';
|
||||||
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
||||||
|
|
||||||
import { EncryptionService } from './encryption.service';
|
|
||||||
import { TokenManagementService } from './token-management.service';
|
import { TokenManagementService } from './token-management.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -14,14 +15,15 @@ export class AuthService {
|
||||||
private readonly userCredentialsRepository: UserCredentialsRepository,
|
private readonly userCredentialsRepository: UserCredentialsRepository,
|
||||||
private readonly userDataRepository: UserDataRepository,
|
private readonly userDataRepository: UserDataRepository,
|
||||||
private readonly tokenManagementService: TokenManagementService,
|
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<TokensDto> {
|
public async signup(userCredentials: UserCredentialsDto): Promise<TokensDto> {
|
||||||
const passwordHashed = await this.encryptionService.hashData(
|
const passwordHashed = await EncryptionService.hashData(
|
||||||
userCredentials.password
|
userCredentials.password
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = await this.userCredentialsRepository.createUser(
|
const user = await this.userCredentialsRepository.createUser(
|
||||||
userCredentials.email,
|
userCredentials.email,
|
||||||
passwordHashed
|
passwordHashed
|
||||||
|
@ -29,10 +31,15 @@ export class AuthService {
|
||||||
|
|
||||||
await this.userDataRepository.createInitialUserData(user);
|
await this.userDataRepository.createInitialUserData(user);
|
||||||
|
|
||||||
// TODO Send email confirmation
|
const token =
|
||||||
// await this.passwordConfirmationMailService.sendPasswordConfirmationMail(
|
await this.emailVerificationService.generateEmailVerificationToken(
|
||||||
// user.email
|
user.id
|
||||||
// );
|
);
|
||||||
|
|
||||||
|
await this.passwordConfirmationMailService.sendPasswordConfirmationMail(
|
||||||
|
user.email,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
return this.generateAndPersistTokens(user.id, user.email);
|
return this.generateAndPersistTokens(user.id, user.email);
|
||||||
}
|
}
|
||||||
|
@ -46,7 +53,7 @@ export class AuthService {
|
||||||
throw new ForbiddenException('Access Denied');
|
throw new ForbiddenException('Access Denied');
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMatch = await this.encryptionService.compareHash(
|
const passwordMatch = await EncryptionService.compareHash(
|
||||||
userCredentials.password,
|
userCredentials.password,
|
||||||
user.hash
|
user.hash
|
||||||
);
|
);
|
||||||
|
@ -59,7 +66,7 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refresh(
|
public async refresh(
|
||||||
userId: number,
|
userId: string,
|
||||||
refreshToken: string
|
refreshToken: string
|
||||||
): Promise<TokensDto> {
|
): Promise<TokensDto> {
|
||||||
const user = await this.userCredentialsRepository.findUserById(userId);
|
const user = await this.userCredentialsRepository.findUserById(userId);
|
||||||
|
@ -68,7 +75,7 @@ export class AuthService {
|
||||||
throw new ForbiddenException('Access Denied');
|
throw new ForbiddenException('Access Denied');
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshTokenMatch = await this.encryptionService.compareHash(
|
const refreshTokenMatch = await EncryptionService.compareHash(
|
||||||
refreshToken,
|
refreshToken,
|
||||||
user.hashedRt
|
user.hashedRt
|
||||||
);
|
);
|
||||||
|
@ -80,7 +87,7 @@ export class AuthService {
|
||||||
return this.generateAndPersistTokens(user.id, user.email);
|
return this.generateAndPersistTokens(user.id, user.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(userId: number): Promise<boolean> {
|
public async logout(userId: string): Promise<boolean> {
|
||||||
const affected = await this.userCredentialsRepository.updateUserTokenHash(
|
const affected = await this.userCredentialsRepository.updateUserTokenHash(
|
||||||
userId,
|
userId,
|
||||||
null
|
null
|
||||||
|
@ -90,14 +97,14 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateAndPersistTokens(
|
private async generateAndPersistTokens(
|
||||||
userId: number,
|
userId: string,
|
||||||
email: string
|
email: string
|
||||||
): Promise<TokensDto> {
|
): Promise<TokensDto> {
|
||||||
const tokens = await this.tokenManagementService.generateTokens(
|
const tokens = await this.tokenManagementService.generateTokens(
|
||||||
userId,
|
userId,
|
||||||
email
|
email
|
||||||
);
|
);
|
||||||
const hashedRefreshToken = await this.encryptionService.hashData(
|
const hashedRefreshToken = await EncryptionService.hashData(
|
||||||
tokens.refresh_token
|
tokens.refresh_token
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class TokenManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async generateTokens(
|
public async generateTokens(
|
||||||
userId: number,
|
userId: string,
|
||||||
email: string
|
email: string
|
||||||
): Promise<TokensDto> {
|
): Promise<TokensDto> {
|
||||||
const access_token: string = await this.createAccessToken(userId, email);
|
const access_token: string = await this.createAccessToken(userId, email);
|
||||||
|
@ -36,7 +36,7 @@ export class TokenManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createAccessToken(
|
private async createAccessToken(
|
||||||
userId: number,
|
userId: string,
|
||||||
email: string
|
email: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return this.jwt.signAsync(
|
return this.jwt.signAsync(
|
||||||
|
@ -49,7 +49,7 @@ export class TokenManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createRefreshToken(
|
private async createRefreshToken(
|
||||||
userId: number,
|
userId: string,
|
||||||
email: string
|
email: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return this.jwt.signAsync(
|
return this.jwt.signAsync(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
import { UserCredentials, UserData } from 'src/entities';
|
import { EmailVerification, UserCredentials, UserData } from 'src/entities';
|
||||||
|
|
||||||
export const databaseConfigFactory = (
|
export const databaseConfigFactory = (
|
||||||
configService: ConfigService
|
configService: ConfigService
|
||||||
|
@ -13,5 +13,5 @@ export const databaseConfigFactory = (
|
||||||
database: configService.get('DB_NAME'),
|
database: configService.get('DB_NAME'),
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: true,
|
logging: true,
|
||||||
entities: [UserCredentials, UserData],
|
entities: [UserCredentials, UserData, EmailVerification],
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as SendGridMailApi from '@sendgrid/mail';
|
import * as SendGridMailApi from '@sendgrid/mail';
|
||||||
|
import { UriEncoderService } from 'src/shared';
|
||||||
|
|
||||||
import { BaseMailService } from './base.mail.service';
|
import { BaseMailService } from './base.mail.service';
|
||||||
import { TemplateConfigService } from './template-config.service';
|
import { TemplateConfigService } from './template-config.service';
|
||||||
|
@ -11,23 +13,29 @@ export class PasswordConfirmationMailService extends BaseMailService {
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@Inject('SEND_GRID_API_KEY') protected readonly sendGridApiKey: string,
|
@Inject('SEND_GRID_API_KEY') protected readonly sendGridApiKey: string,
|
||||||
private readonly templateConfigService: TemplateConfigService
|
private readonly templateConfigService: TemplateConfigService,
|
||||||
|
private readonly configService: ConfigService
|
||||||
) {
|
) {
|
||||||
super(sendGridApiKey);
|
super(sendGridApiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendPasswordConfirmationMail(to: string): Promise<void> {
|
public async sendPasswordConfirmationMail(
|
||||||
|
to: string,
|
||||||
|
verificationToken: string
|
||||||
|
): Promise<void> {
|
||||||
const templateId: string = this.templateConfigService.getTemplateId(
|
const templateId: string = this.templateConfigService.getTemplateId(
|
||||||
this.PASSWORD_CONFIRMATION_EMAIL
|
this.PASSWORD_CONFIRMATION_EMAIL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const token = `${verificationToken}|${UriEncoderService.encodeBase64(to)}`;
|
||||||
|
|
||||||
const mailoptions: SendGridMailApi.MailDataRequired = {
|
const mailoptions: SendGridMailApi.MailDataRequired = {
|
||||||
to,
|
to,
|
||||||
from: { email: 'info@igor-propisnov.com', name: 'Ticket App' },
|
from: { email: 'info@igor-propisnov.com', name: 'Ticket App' },
|
||||||
templateId: templateId,
|
templateId: templateId,
|
||||||
dynamicTemplateData: {
|
dynamicTemplateData: {
|
||||||
name: 'Mara',
|
name: 'Mara',
|
||||||
buttonUrl: 'https://igor-propisnov.com',
|
buttonUrl: `${this.configService.get<string>('APP_URL')}/verify/?token=${token}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,4 +20,11 @@ export class UserDataRepository {
|
||||||
|
|
||||||
return this.repository.save(userData);
|
return this.repository.save(userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateEmailVerificationStatus(userId: string): Promise<void> {
|
||||||
|
await this.repository.update(
|
||||||
|
{ user: { id: userId } },
|
||||||
|
{ isEmailConfirmed: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<boolean> {
|
||||||
|
return this.emailVerificationService.verifyEmail(tokenToVerify);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { EmailVerification } from 'src/entities';
|
||||||
|
import { MoreThan, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmailVerifyRepository {
|
||||||
|
public constructor(
|
||||||
|
@InjectRepository(EmailVerification)
|
||||||
|
private readonly repository: Repository<EmailVerification>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async createEmailVerification(
|
||||||
|
token: string,
|
||||||
|
expiresAt: Date,
|
||||||
|
userId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await this.repository.save({
|
||||||
|
token,
|
||||||
|
expiresAt,
|
||||||
|
user: { id: userId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findEmailVerificationByToken(token: string): Promise<boolean> {
|
||||||
|
const result = await this.repository.findOne({
|
||||||
|
where: { token, expiresAt: MoreThan(new Date()) },
|
||||||
|
});
|
||||||
|
|
||||||
|
return result !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteEmailVerificationByToken(
|
||||||
|
tokenToDelete: string
|
||||||
|
): Promise<EmailVerification | null> {
|
||||||
|
const emailVerification = await this.repository.findOne({
|
||||||
|
where: { token: tokenToDelete },
|
||||||
|
relations: ['user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (emailVerification) {
|
||||||
|
await this.repository.delete({ token: tokenToDelete });
|
||||||
|
return emailVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './email-verify.repository';
|
|
@ -0,0 +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 userDataRepository: UserDataRepository,
|
||||||
|
private readonly configService: ConfigService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async generateEmailVerificationToken(userId: string): Promise<string> {
|
||||||
|
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(
|
||||||
|
verificationToken,
|
||||||
|
expiration,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
return verificationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verifyEmail(tokenToVerify: string): Promise<boolean> {
|
||||||
|
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<string> {
|
||||||
|
const verifyToken = randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
return UriEncoderService.encodeUri(verifyToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteEmailVerificationToken(
|
||||||
|
tokenToDelete: string
|
||||||
|
): Promise<EmailVerification | null> {
|
||||||
|
return await this.emailVerifyRepository.deleteEmailVerificationByToken(
|
||||||
|
tokenToDelete
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
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,
|
||||||
|
UserModule,
|
||||||
|
TypeOrmModule.forFeature([EmailVerification]),
|
||||||
|
],
|
||||||
|
providers: [EmailVerifyRepository, EmailVerificationService],
|
||||||
|
controllers: [VerifyController],
|
||||||
|
exports: [EmailVerificationService],
|
||||||
|
})
|
||||||
|
export class VerifyModule {}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './public.decorator';
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './utils/index';
|
||||||
|
export * from './decorator/index';
|
|
@ -1,21 +1,22 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import { Options } from 'argon2';
|
import { Options } from 'argon2';
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class EncryptionService {
|
export class EncryptionService {
|
||||||
private hashOptions: Options = {
|
private static hashOptions: Options = {
|
||||||
type: argon2.argon2id,
|
type: argon2.argon2id,
|
||||||
memoryCost: 2 ** 16,
|
memoryCost: 2 ** 16,
|
||||||
timeCost: 3,
|
timeCost: 3,
|
||||||
parallelism: 1,
|
parallelism: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
public async hashData(data: string): Promise<string> {
|
public static async hashData(data: string): Promise<string> {
|
||||||
return await argon2.hash(data, this.hashOptions);
|
return await argon2.hash(data, this.hashOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async compareHash(data: string, encrypted: string): Promise<boolean> {
|
public static async compareHash(
|
||||||
|
data: string,
|
||||||
|
encrypted: string
|
||||||
|
): Promise<boolean> {
|
||||||
return await argon2.verify(encrypted, data);
|
return await argon2.verify(encrypted, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './uri-encoder.service';
|
||||||
|
export * from './encryption.service';
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,4 +9,11 @@ export const routes: Routes = [
|
||||||
(m) => m.RegisterRootComponent
|
(m) => m.RegisterRootComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'verify',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./pages/email-verify-root/email-verify-root.component').then(
|
||||||
|
(m) => m.EmailVerifyRootComponent
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div id="background">
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="content">
|
||||||
|
@if (verifyStatus() === true) {
|
||||||
|
@if (showRedirectMessage()) {
|
||||||
|
<h1>Es geht gleich los!</h1>
|
||||||
|
<h2>
|
||||||
|
Danke für das bestätigen der E-Mail - Wir leiten dich zum Login
|
||||||
|
weiter!
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
} @else if (verifyStatus() === false) {
|
||||||
|
<h1>Oops, da ist etwas schief gelaufen!</h1>
|
||||||
|
<h2>Der Link ist nicht mehr gültig</h2>
|
||||||
|
} @else {
|
||||||
|
<h1>Verifizierung wird durchgeführt...</h1>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
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<string> = input<string>('');
|
||||||
|
public verifyStatus: WritableSignal<boolean | null> = signal<boolean | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
public showRedirectMessage: WritableSignal<boolean> = signal<boolean>(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(5000)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate(['/signup'], {
|
||||||
|
queryParams: { verified: true, email: email },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,8 @@ import {
|
||||||
WritableSignal,
|
WritableSignal,
|
||||||
signal,
|
signal,
|
||||||
effect,
|
effect,
|
||||||
|
InputSignal,
|
||||||
|
input,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
|
@ -16,6 +18,7 @@ import {
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
Validators,
|
Validators,
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { CheckboxModule } from 'primeng/checkbox';
|
import { CheckboxModule } from 'primeng/checkbox';
|
||||||
|
@ -50,6 +53,8 @@ type AuthAction = 'register' | 'signup';
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class RegisterRootComponent implements OnInit {
|
export class RegisterRootComponent implements OnInit {
|
||||||
|
public verified: InputSignal<boolean> = input<boolean>(false);
|
||||||
|
public email: InputSignal<string> = input<string>('');
|
||||||
public form: FormGroup | undefined;
|
public form: FormGroup | undefined;
|
||||||
public isRegisterSignal: WritableSignal<boolean> = signal(false);
|
public isRegisterSignal: WritableSignal<boolean> = signal(false);
|
||||||
public isSignupSignal: WritableSignal<boolean> = signal(false);
|
public isSignupSignal: WritableSignal<boolean> = signal(false);
|
||||||
|
@ -57,10 +62,12 @@ export class RegisterRootComponent implements OnInit {
|
||||||
public emailInvalid: WritableSignal<string | null> = signal(null);
|
public emailInvalid: WritableSignal<string | null> = signal(null);
|
||||||
public passwordInvalid: WritableSignal<string | null> = signal(null);
|
public passwordInvalid: WritableSignal<string | null> = signal(null);
|
||||||
public termsInvalid: WritableSignal<string | null> = signal(null);
|
public termsInvalid: WritableSignal<string | null> = signal(null);
|
||||||
|
private removeQueryParams: WritableSignal<boolean> = signal(false);
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly formBuilder: FormBuilder,
|
private readonly formBuilder: FormBuilder,
|
||||||
private readonly authService: AuthService
|
private readonly authService: AuthService,
|
||||||
|
private readonly router: Router
|
||||||
) {
|
) {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
if (this.form) {
|
if (this.form) {
|
||||||
|
@ -73,12 +80,21 @@ export class RegisterRootComponent implements OnInit {
|
||||||
this.form.removeControl('terms');
|
this.form.removeControl('terms');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.removeQueryParams()) {
|
||||||
|
this.clearRouteParams();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.initializeForm();
|
this.initializeForm();
|
||||||
this.setupValueChanges();
|
this.setupValueChanges();
|
||||||
|
|
||||||
|
if (this.email() || this.verified()) {
|
||||||
|
this.handleRedirect();
|
||||||
|
this.removeQueryParams.set(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleAction(action: AuthAction): void {
|
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 {
|
private setupValueChanges(): void {
|
||||||
this.setupEmailValueChanges();
|
this.setupEmailValueChanges();
|
||||||
this.setupPasswordValueChanges();
|
this.setupPasswordValueChanges();
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { SessionStorageService } from './session-storage.service';
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private readonly _path: string = '/api/auth';
|
|
||||||
private _access_token: string | null = null;
|
private _access_token: string | null = null;
|
||||||
private _refresh_token: string | null = null;
|
private _refresh_token: string | null = null;
|
||||||
private _isAuthenticated$: BehaviorSubject<boolean> =
|
private _isAuthenticated$: BehaviorSubject<boolean> =
|
||||||
|
@ -29,7 +28,7 @@ export class AuthService {
|
||||||
private readonly sessionStorageService: SessionStorageService,
|
private readonly sessionStorageService: SessionStorageService,
|
||||||
private readonly authenticationApiService: AuthenticationApiService
|
private readonly authenticationApiService: AuthenticationApiService
|
||||||
) {
|
) {
|
||||||
this.autoLogin();
|
//this.autoLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
public signin(credentials: LoginCredentials): void {
|
public signin(credentials: LoginCredentials): void {
|
||||||
|
|
Loading…
Reference in New Issue