Feature: E-Mail verify #8

Merged
igorpropisnov merged 4 commits from feature/mail-verify into main 2024-05-29 20:01:58 +02:00
31 changed files with 430 additions and 39 deletions

View File

@ -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 }],

View File

@ -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;
}

View File

@ -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';

View File

@ -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;

View File

@ -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,

View File

@ -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';

View File

@ -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);

View File

@ -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 });

View File

@ -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
); );

View File

@ -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(

View File

@ -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],
}); });

View File

@ -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}`,
}, },
}; };

View File

@ -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 }
);
}
} }

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export * from './email-verify.repository';

View File

@ -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
);
}
}

View File

@ -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 {}

View File

@ -0,0 +1 @@
export * from './public.decorator';

View File

@ -0,0 +1,2 @@
export * from './utils/index';
export * from './decorator/index';

View File

@ -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);
} }
} }

View File

@ -0,0 +1,2 @@
export * from './uri-encoder.service';
export * from './encryption.service';

View File

@ -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));
}
}

View File

@ -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
),
},
]; ];

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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 },
});
});
}
}

View File

@ -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();

View File

@ -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 {