diff --git a/backend/src/entities/email-verification.entity.ts b/backend/src/entities/email-verification.entity.ts index f49655f..36b6b0d 100644 --- a/backend/src/entities/email-verification.entity.ts +++ b/backend/src/entities/email-verification.entity.ts @@ -21,6 +21,9 @@ export class EmailVerification { @Column() public expiresAt: Date; + @Column() + public email: string; + @OneToOne(() => UserCredentials) @JoinColumn({ name: 'userCredentialsId' }) public user: UserCredentials; diff --git a/backend/src/modules/auth-module/controller/auth.controller.ts b/backend/src/modules/auth-module/controller/auth.controller.ts index 5b0baa2..b3829c6 100644 --- a/backend/src/modules/auth-module/controller/auth.controller.ts +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -15,7 +15,11 @@ import { SuccessDto } from 'src/shared'; import { Public } from 'src/shared/decorator'; import { LocalAuthGuard } from '../guard'; -import { SigninResponseDto, UserCredentialsDto } from '../models/dto'; +import { + MagicLinkDto, + SigninResponseDto, + UserCredentialsDto, +} from '../models/dto'; import { AuthService } from '../services/auth.service'; @ApiTags('Authentication') @@ -23,6 +27,20 @@ import { AuthService } from '../services/auth.service'; export class AuthController { public constructor(private readonly authService: AuthService) {} + @ApiCreatedResponse({ + description: 'Magic link sent successfully', + type: SuccessDto, + }) + @ApiBody({ type: MagicLinkDto }) + @Post('send-magic-link') + @HttpCode(HttpStatus.OK) + @Public() + public async sendMagicLink( + @Body() magicLinkDto: MagicLinkDto + ): Promise { + return this.authService.sendMagicLink(magicLinkDto); + } + @ApiCreatedResponse({ description: 'User signed up successfully', type: SuccessDto, diff --git a/backend/src/modules/auth-module/models/dto/index.ts b/backend/src/modules/auth-module/models/dto/index.ts index e0a3cb5..e8eeec3 100644 --- a/backend/src/modules/auth-module/models/dto/index.ts +++ b/backend/src/modules/auth-module/models/dto/index.ts @@ -1,2 +1,3 @@ export * from './user-credentials.dto'; export * from './signin-response.dto'; +export * from './magic-link.dto'; diff --git a/backend/src/modules/auth-module/models/dto/magic-link.dto.ts b/backend/src/modules/auth-module/models/dto/magic-link.dto.ts new file mode 100644 index 0000000..d059252 --- /dev/null +++ b/backend/src/modules/auth-module/models/dto/magic-link.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsEmail } from 'class-validator'; + +export class MagicLinkDto { + @ApiProperty({ + description: 'User email', + example: 'foo@bar.com', + }) + @IsNotEmpty() + @IsEmail() + public email: string; +} diff --git a/backend/src/modules/auth-module/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index 53e70f4..e2d4a1a 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { UserCredentials } from 'src/entities'; import { SessionService } from 'src/modules/session/services/session.service'; import { EncryptionService, SuccessDto } from 'src/shared'; @@ -11,7 +11,11 @@ import { 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 { SigninResponseDto, UserCredentialsDto } from '../models/dto'; +import { + MagicLinkDto, + SigninResponseDto, + UserCredentialsDto, +} from '../models/dto'; import { UserCredentialsRepository } from '../repositories/user-credentials.repository'; @Injectable() @@ -24,6 +28,54 @@ export class AuthService { private readonly sessionService: SessionService ) {} + public async sendMagicLink(magiclink: MagicLinkDto): Promise { + try { + const existingUser = await this.userCredentialsRepository.findUserByEmail( + magiclink.email + ); + + if (existingUser) { + const token = + await this.emailVerificationService.generateEmailVerificationTokenForMagicLink( + magiclink.email + ); + + await this.passwordConfirmationMailService.sendLoginLinkEmail( + magiclink.email, + token + ); + } else { + const isEmailSubmitted: boolean = + await this.emailVerificationService.isEmailSubmitted(magiclink.email); + + if (!isEmailSubmitted) { + const token = + await this.emailVerificationService.generateEmailVerificationTokenForMagicLink( + magiclink.email + ); + + await this.passwordConfirmationMailService.sendRegistrationLinkEmail( + magiclink.email, + token + ); + } else { + throw new ConflictException('EMAIL_ALREADY_SUBMITTED', { + message: 'This email has already been submitted for registration.', + }); + } + } + + return { success: true }; + } catch (error) { + if (error instanceof BadRequestException) { + throw error; + } + throw new InternalServerErrorException('MAGIC_LINK_ERROR', { + cause: error, + }); + } + } + public async signup( userCredentials: UserCredentialsDto ): Promise { @@ -47,15 +99,16 @@ export class AuthService { await this.userDataRepository.createInitialUserData(user); - const token = - await this.emailVerificationService.generateEmailVerificationToken( - user.id - ); + // TODO: Send Welcome Mail + // const token = + // await this.emailVerificationService.generateEmailVerificationToken( + // user.id + // ); - await this.passwordConfirmationMailService.sendPasswordConfirmationMail( - user.email, - token - ); + // await this.passwordConfirmationMailService.sendPasswordConfirmationMail( + // user.email, + // token + // ); return { success: true, 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 1f19474..819793e 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 @@ -41,4 +41,44 @@ export class PasswordConfirmationMailService extends BaseMailService { await this.sendMail(mailoptions); } + + public async sendLoginLinkEmail( + to: string, + loginToken: string + ): Promise { + const token = `${loginToken}|${UriEncoderService.encodeBase64(to)}`; + const loginLink = `${this.configService.get('APP_URL')}/?token=${token}&signin=true`; + + const mailoptions: SendGridMailApi.MailDataRequired = { + to, + from: { email: 'info@igor-propisnov.com', name: 'Ticket App' }, + subject: 'Login to Your Account', + text: `Hi ${to}, Click this link to log in to your account: ${loginLink}`, + html: `

Click here to log in to your account.

`, + }; + + await this.sendMail(mailoptions); + } + + public async sendRegistrationLinkEmail( + to: string, + registrationToken: string + ): Promise { + const token = `${registrationToken}|${UriEncoderService.encodeBase64(to)}`; + const registrationLink = `${this.configService.get('APP_URL')}/?token=${token}&signup=true`; + + console.log('##############'); + console.log(registrationLink); + console.log('##############'); + + const mailoptions: SendGridMailApi.MailDataRequired = { + to, + from: { email: 'info@igor-propisnov.com', name: 'Ticket App' }, + subject: 'Complete Your Registration', + text: `Click this link to complete your registration: ${registrationLink}`, + html: `

Click here to complete your registration.

`, + }; + + await this.sendMail(mailoptions); + } } diff --git a/backend/src/modules/verify-module/controller/verify.controller.ts b/backend/src/modules/verify-module/controller/verify.controller.ts index d10f070..c4273e3 100644 --- a/backend/src/modules/verify-module/controller/verify.controller.ts +++ b/backend/src/modules/verify-module/controller/verify.controller.ts @@ -1,16 +1,6 @@ -import { - Controller, - Get, - Req, - HttpCode, - HttpStatus, - Query, - UseGuards, - Post, -} from '@nestjs/common'; +import { Controller, HttpCode, HttpStatus, Query, Post } from '@nestjs/common'; import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; -import { Request } from 'express'; -import { SessionGuard } from 'src/modules/session/guard'; +import { SuccessDto } from 'src/shared'; import { Public } from 'src/shared/decorator'; import { EmailVerificationService } from '../services/email-verification.service'; @@ -24,25 +14,29 @@ export class VerifyController { @ApiCreatedResponse({ description: 'Verify email', - type: Boolean, + type: SuccessDto, }) @Public() @Post() @HttpCode(HttpStatus.OK) public async verifyEmail( - @Query('token') tokenToVerify: string - ): Promise { - return this.emailVerificationService.verifyEmail(tokenToVerify); + @Query('token') tokenToVerify: string, + @Query('email') emailToVerify: string + ): Promise { + return this.emailVerificationService.verifyEmail( + tokenToVerify, + emailToVerify + ); } - @ApiCreatedResponse({ - description: 'Check if email is verified', - type: Boolean, - }) - @Get('check') - @HttpCode(HttpStatus.OK) - @UseGuards(SessionGuard) - public async isEmailVerified(@Req() request: Request): Promise { - return this.emailVerificationService.isEmailVerified(request.sessionID); - } + // @ApiCreatedResponse({ + // description: 'Check if email is verified', + // type: Boolean, + // }) + // @Get('check') + // @HttpCode(HttpStatus.OK) + // @UseGuards(SessionGuard) + // public async isEmailVerified(@Req() request: Request): Promise { + // return this.emailVerificationService.isEmailVerified(request.sessionID); + // } } 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 9c540c8..a87546a 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 { MoreThan, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; @Injectable() export class EmailVerifyRepository { @@ -13,21 +13,40 @@ export class EmailVerifyRepository { public async createEmailVerification( token: string, expiresAt: Date, - userId: string + email: string, + userId: string | null ): Promise { await this.repository.save({ token, expiresAt, - user: { id: userId }, + email, + user: userId ? { id: userId } : null, }); } - public async findEmailVerificationByToken(token: string): Promise { - const result = await this.repository.findOne({ - where: { token, expiresAt: MoreThan(new Date()) }, + public async findByTokenAndEmail( + token: string, + email: string + ): Promise { + return await this.repository.findOne({ + where: { + token, + email, + }, }); + } - return result !== null; + public async removeEmailVerificationByTokenAndEmail( + token: string, + email: string + ): Promise { + await this.repository.delete({ token, email }); + } + + public async findItemByEmail( + email: string + ): Promise { + return this.repository.findOne({ where: { email } }); } public async deleteEmailVerificationByToken( 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 c0e9214..dfbb8ac 100644 --- a/backend/src/modules/verify-module/services/email-verification.service.ts +++ b/backend/src/modules/verify-module/services/email-verification.service.ts @@ -3,7 +3,7 @@ import { randomBytes } from 'crypto'; import { Injectable } from '@nestjs/common'; import { EmailVerification } from 'src/entities'; import { SessionService } from 'src/modules/session/services/session.service'; -import { UriEncoderService } from 'src/shared'; +import { SuccessDto, UriEncoderService } from 'src/shared'; import { InternalServerErrorException } from 'src/shared/exceptions'; import { UserDataRepository } from '../../user-module/repositories/user-data.repository'; @@ -25,7 +25,8 @@ export class EmailVerificationService { await this.emailVerifyRepository.createEmailVerification( verificationToken, expiration, - userId + userId, + null ); return verificationToken; @@ -40,30 +41,66 @@ export class EmailVerificationService { } } - public async verifyEmail(tokenToVerify: string): Promise { + public async isEmailSubmitted(email: string): Promise { try { const emailVerification = - await this.emailVerifyRepository.findEmailVerificationByToken( - tokenToVerify + await this.emailVerifyRepository.findItemByEmail(email); + + return !!emailVerification; + } catch (error) { + throw new InternalServerErrorException('EMAIL_VERIFICATION_ERROR', { + message: 'An error occurred while verifying the email.', + }); + } + } + + public async generateEmailVerificationTokenForMagicLink( + email: string + ): Promise { + try { + const verificationToken = await this.createVerificationToken(); + const expiration = new Date(Date.now() + 24 * 60 * 60 * 1000); + + await this.emailVerifyRepository.createEmailVerification( + verificationToken, + expiration, + email, + null + ); + + return verificationToken; + } catch (error) { + throw new InternalServerErrorException( + 'EMAIL_VERIFICATION_TOKEN_GENERATION_ERROR', + { + message: + 'An error occurred while generating the email verification token.', + } + ); + } + } + + public async verifyEmail( + tokenToVerify: string, + emailToVerify: string + ): Promise { + try { + const findTokenAndEmail: EmailVerification = + await this.emailVerifyRepository.findByTokenAndEmail( + tokenToVerify, + emailToVerify ); - if (!emailVerification) { - return false; + if (!findTokenAndEmail) { + return { success: false }; } - const deletedVerification = - await this.deleteEmailVerificationToken(tokenToVerify); + await this.emailVerifyRepository.removeEmailVerificationByTokenAndEmail( + tokenToVerify, + emailToVerify + ); - if (deletedVerification && deletedVerification.user) { - const isStatusUpdated = - await this.userDataRepository.updateEmailVerificationStatus( - deletedVerification.user.id - ); - - return isStatusUpdated; - } - - return false; + return { success: true }; } catch (error) { throw new InternalServerErrorException('EMAIL_VERIFICATION_ERROR', { message: 'An error occurred while verifying the email.', diff --git a/backend/src/shared/filters/http-exception.filter.ts b/backend/src/shared/filters/http-exception.filter.ts index d507bb8..7a8915d 100644 --- a/backend/src/shared/filters/http-exception.filter.ts +++ b/backend/src/shared/filters/http-exception.filter.ts @@ -19,6 +19,8 @@ export class HttpExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); + //console.error('Exception caught:', exception); + let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR; let message: string = 'Internal server error'; let error: string = 'INTERNAL_SERVER_ERROR'; 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 48af55f..b2bf3a0 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 @@ -11,8 +11,6 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; -import { delay, filter, tap } from 'rxjs'; - import { VerifyApiService } from '../../api'; import { BackgroundPatternService, ThemeService } from '../../shared/service'; @@ -97,21 +95,21 @@ export class EmailVerifyRootComponent implements OnInit { this.email.set(decodeURIComponent(atob(email))); } - this.api - .verifyControllerVerifyEmail(verifyToken) - .pipe( - delay(1500), - tap((isVerified: boolean) => { - this.verifyStatus.set(isVerified); - }), - filter((isVerified) => isVerified), - tap(() => { - this.showRedirectMessage.set(true); - }), - delay(10000) - ) - .subscribe(() => { - this.navigateToWelcomeScreen(); - }); + // this.api + // .verifyControllerVerifyEmail(verifyToken) + // .pipe( + // delay(1500), + // tap((isVerified: boolean) => { + // this.verifyStatus.set(isVerified); + // }), + // filter((isVerified) => isVerified), + // tap(() => { + // this.showRedirectMessage.set(true); + // }), + // delay(10000) + // ) + // .subscribe(() => { + // this.navigateToWelcomeScreen(); + // }); } } diff --git a/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts b/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts index 59f79bc..ab65b0b 100644 --- a/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts +++ b/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts @@ -24,17 +24,17 @@ export class EventEmptyStateComponent { private readonly verifyApi: VerifyApiService ) {} - public navigateToCreateEvent(): void { - this.verifyApi - .verifyControllerIsEmailVerified() - .subscribe((isVerified: boolean) => { - if (!isVerified) { - this.openEmailVerificationModal(); - } else { - this.router.navigate(['/event/create']); - } - }); - } + // public navigateToCreateEvent(): void { + // this.verifyApi + // .verifyControllerIsEmailVerified() + // .subscribe((isVerified: boolean) => { + // if (!isVerified) { + // this.openEmailVerificationModal(); + // } else { + // this.router.navigate(['/event/create']); + // } + // }); + // } public closeEmailVerificationModal(): void { (this.emailVerificationModal.nativeElement as HTMLDialogElement).close(); diff --git a/frontend/src/app/pages/welcome-root/welcome-root.component.html b/frontend/src/app/pages/welcome-root/welcome-root.component.html index d50117f..d001089 100644 --- a/frontend/src/app/pages/welcome-root/welcome-root.component.html +++ b/frontend/src/app/pages/welcome-root/welcome-root.component.html @@ -1,4 +1,4 @@ -@if (!userSignupSuccess()) { +
@@ -29,7 +28,7 @@ (change)="toggleTheme()" [checked]="isDarkMode" /> - + - +
+
--> + + + +
+ + + +
+
+ +
+ +
+ @if (isTokenVerifing()) { + +
+
+
+
+ @if (isRegistrationMode()) { +
+ } +
+
+
+ } @else { +
+

+ @if (isRegistrationMode()) { + Complete Your Registration + } @else { + Welcome to APP-NAME + } +

+

+ @if (isRegistrationMode()) { + You're one step away from unlocking powerful event management + tools + } @else { + Enter your email to access your account or get started + } +

+
+ + + @if (isRegistrationMode()) { + + } + + +
+ +
+ } + + @if (!isRegistrationMode() && !isTokenVerifing()) { +
+
+

What happens next?

+
    +
  • We'll send a magic link to your email
  • +
  • Click the link in the email to securely log in
  • +
  • If you're new, you'll be prompted to create a password
  • +
  • Existing users will be logged in instantly
  • +
  • The magic link expires in 10 minutes for security
  • +
+
+
+ } @else if (isTokenVerifing()) { +
+
+
+
+
+
+
+
+
+
+
+
+ } +
+ +
+

+ By continuing, you agree to APP-NAME's + Terms of Service + and + Privacy Policy + . +

+
+
+
+ + + + + diff --git a/frontend/src/app/pages/welcome-root/welcome-root.component.ts b/frontend/src/app/pages/welcome-root/welcome-root.component.ts index 7fd00a2..aa94260 100644 --- a/frontend/src/app/pages/welcome-root/welcome-root.component.ts +++ b/frontend/src/app/pages/welcome-root/welcome-root.component.ts @@ -1,4 +1,4 @@ -import { CommonModule } from '@angular/common'; +/* import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { ChangeDetectionStrategy, @@ -398,3 +398,365 @@ export class WelcomeRootComponent implements OnInit { }); } } + */ +import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; +import { + ChangeDetectionStrategy, + Component, + OnInit, + WritableSignal, + signal, + ElementRef, + InputSignal, + input, + effect, + ViewChild, +} from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { Router } from '@angular/router'; + +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { delay, finalize, tap, timer } from 'rxjs'; + +import { + Configuration, + MagicLinkDtoApiModel, + SuccessDtoApiModel, + UserCredentialsDtoApiModel, + VerifyApiService, +} from '../../api'; +import { ApiConfiguration } from '../../config/api-configuration'; +import { + AuthService, + BackgroundPatternService, + ThemeService, +} from '../../shared/service'; +import { customEmailValidator } from '../../shared/validator'; + +@Component({ + selector: 'app-unified-login', + standalone: true, + imports: [ + CommonModule, + FormsModule, + InputTextModule, + ReactiveFormsModule, + ButtonModule, + HttpClientModule, + ], + providers: [ + { + provide: Configuration, + useFactory: (): unknown => + new ApiConfiguration({ withCredentials: true }), + }, + ], + templateUrl: './welcome-root.component.html', + styleUrl: './welcome-root.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WelcomeRootComponent implements OnInit { + @ViewChild('passwordInput') public passwordInput!: ElementRef; + public token: InputSignal = input(''); + public signup: InputSignal = input(false); + public dialogBackgroundStyle: { 'background-image': string } | null = null; + public leftBackgroundStyle: { 'background-image': string } | null = null; + public rightBackgroundStyle: { 'background-image': string } | null = null; + public form!: FormGroup; + public isLoading: WritableSignal = signal(false); + public isEmailSent: WritableSignal = signal(false); + public isTokenVerifing: WritableSignal = signal(false); + public isTokenVerified: WritableSignal = signal(false); + public errorReasons: WritableSignal = signal([]); + public verificationError: WritableSignal = signal< + string | null + >(null); + public isRegistrationMode: WritableSignal = signal(false); + private removeQueryParams: WritableSignal = signal(false); + + public get isDarkMode(): boolean { + return this.themeService.getTheme() === 'dark'; + } + + public constructor( + private readonly formBuilder: FormBuilder, + private readonly authService: AuthService, + private readonly verifyApiService: VerifyApiService, + private readonly router: Router, + private readonly themeService: ThemeService, + private readonly el: ElementRef, + private readonly backgroundPatternService: BackgroundPatternService + ) { + effect(() => { + if (this.removeQueryParams()) { + this.clearRouteParams(); + } + }); + } + + public ngOnInit(): void { + this.setBackground(); + this.initializeForm(); + this.verifySignupMagicLink(); + } + + public verifySignupMagicLink(): void { + if (this.token() && this.signup()) { + const token: string = this.extractVerifyToken(); + const email: string = this.extractEmail(); + + this.removeQueryParams.set(true); + + if (token && email) { + this.isTokenVerifing.set(true); + this.verificationError.set(null); + this.errorReasons.set([]); + this.addPasswordFieldToForm(); + this.isRegistrationMode.set(true); + const decodedEmail = decodeURIComponent(atob(email)); + + this.verifyApiService + .verifyControllerVerifyEmail(token, decodedEmail) + .pipe( + delay(2000), + finalize(() => { + if (!this.verificationError()) { + this.isTokenVerifing.set(false); + this.isTokenVerified.set(true); + // Warte 3 Sekunden, dann schließe das Modal und fokussiere das Passwort-Feld + timer(3000).subscribe(() => { + this.isTokenVerified.set(false); + this.focusPasswordField(); + }); + } + }) + ) + .subscribe({ + next: (response: SuccessDtoApiModel) => { + if (response.success) { + this.isTokenVerifing.set(false); + console.log('Verification successful'); + } else { + console.error('Verification failed'); + this.verificationError.set( + 'Verification failed. Please check the reasons below:' + ); + this.errorReasons.set([ + 'The verification token may have expired.', + 'The email address may not match our records.', + 'The verification link may have been used already.', + ]); + } + }, + error: (error) => { + console.error('Verification failed', error); + this.verificationError.set( + 'An error occurred during verification. Please check the reasons below:' + ); + this.errorReasons.set([ + 'There might be a problem with your internet connection.', + 'Our servers might be experiencing issues.', + 'The verification service might be temporarily unavailable.', + ]); + }, + }); + + this.form.patchValue({ email: decodedEmail }); + const emailControl = this.form.get('email'); + + if (emailControl) { + emailControl.setValue(decodedEmail); + emailControl.disable(); + emailControl.markAsTouched(); + emailControl.setErrors(null); + } + } + } + } + + public getInputClass(controlName: string): string { + const control = this.form.get(controlName); + + if (controlName === 'email' && this.isRegistrationMode()) { + return 'input-success'; + } + + if (control?.touched) { + return control.valid ? 'input-success' : 'input-error'; + } + return ''; + } + + public getErrorMessage(controlName: string): string { + const control = this.form.get(controlName); + + if (control?.touched && control.errors) { + if (control.errors['required']) { + return 'This field is required.'; + } + if (control.errors['email']) { + return 'Please enter a valid email address.'; + } + } + return ''; + } + + public setBackground(): void { + const theme = this.themeService.getTheme(); + let opacity: number; + + if (theme === 'dark') { + opacity = 0.05; + } else { + opacity = 0.1; + } + + const colorPrimary = getComputedStyle( + this.el.nativeElement + ).getPropertyValue('--p'); + + const colorPrimaryC = getComputedStyle( + this.el.nativeElement + ).getPropertyValue('--pc'); + + const svgUrlforDialog = this.backgroundPatternService.getWigglePattern( + colorPrimary, + opacity + ); + const svgUrlForLeft = this.backgroundPatternService.getBankNotePattern( + colorPrimaryC, + opacity + ); + const svgUrlForRight = this.backgroundPatternService.getHideoutPattern( + colorPrimary, + opacity + ); + + this.dialogBackgroundStyle = { + 'background-image': `url("${svgUrlforDialog}")`, + }; + this.leftBackgroundStyle = { + 'background-image': `url("${svgUrlForLeft}")`, + }; + this.rightBackgroundStyle = { + 'background-image': `url("${svgUrlForRight}")`, + }; + } + + public toggleTheme(): void { + this.themeService.toggleTheme(); + this.setBackground(); + } + + public onSubmit(): void { + if (this.form.invalid) { + Object.keys(this.form.controls).forEach((key) => { + const control = this.form.get(key); + + control?.markAsTouched(); + }); + return; + } + + if (this.isRegistrationMode()) { + const signupCredentials: UserCredentialsDtoApiModel = { + email: this.form.value.email, + password: this.form.value.password, + }; + + this.signupNewUser(signupCredentials); + } else { + this.sendLoginEmail(this.form.value.email); + } + } + + private focusPasswordField(): void { + if (this.passwordInput) { + this.passwordInput.nativeElement.focus(); + } + } + + private clearRouteParams(): void { + this.router.navigate([], { queryParams: {} }); + } + + private extractVerifyToken(): string { + const [verifyToken]: string[] = this.token().split('|'); + + return verifyToken; + } + + private extractEmail(): string { + const [, email]: string[] = this.token().split('|'); + + return email; + } + + private initializeForm(): void { + this.form = this.formBuilder.group({ + email: new FormControl( + { value: '', disabled: false }, + { + validators: [Validators.required, customEmailValidator()], + updateOn: 'change', + } + ), + }); + } + + private addPasswordFieldToForm(): void { + this.form.addControl( + 'password', + new FormControl('', { + validators: [Validators.required, Validators.minLength(8)], + updateOn: 'change', + }) + ); + } + + private signupNewUser(signupCredentials: UserCredentialsDtoApiModel): void { + this.isLoading.set(true); + this.authService + .signup(signupCredentials) + .pipe( + delay(1000), + tap(() => this.isLoading.set(true)), + finalize(() => this.isLoading.set(false)) + ) + .subscribe((response: SuccessDtoApiModel) => { + if (response.success) { + console.log('User signed up successfully'); + // TODO: Redirect to Dashbord + } + }); + } + + private sendLoginEmail(email: string): void { + this.isLoading.set(true); + const magiclink: MagicLinkDtoApiModel = { + email: email, + }; + + this.authService + .sendMagicLink(magiclink) + .pipe( + delay(1000), + tap(() => this.isLoading.set(true)), + finalize(() => this.isLoading.set(false)) + ) + .subscribe((response: SuccessDtoApiModel) => { + if (response.success) { + this.isEmailSent.set(true); + } + }); + } +} diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts index 36a4e9e..c0ba5f4 100644 --- a/frontend/src/app/shared/service/auth.service.ts +++ b/frontend/src/app/shared/service/auth.service.ts @@ -6,6 +6,7 @@ import { catchError, shareReplay, tap } from 'rxjs/operators'; import { AuthenticationApiService, + MagicLinkDtoApiModel, SigninResponseDtoApiModel, SuccessDtoApiModel, UserCredentialsDtoApiModel, @@ -27,6 +28,12 @@ export class AuthService { this.statusCheck$ = this.initializeStatusCheck(); } + public sendMagicLink( + email: MagicLinkDtoApiModel + ): Observable { + return this.authenticationApiService.authControllerSendMagicLink(email); + } + public signup( credentials: UserCredentialsDtoApiModel ): Observable { @@ -49,6 +56,7 @@ export class AuthService { .pipe(tap(() => this.isAuthenticatedSignal.set(false))); } + // TODO: Later for Autologin public status(): Observable { if (this.isAuthenticatedSignal()) { return of({ success: true }); @@ -56,6 +64,7 @@ export class AuthService { return this.statusCheck$; } + // TODO Later for AutoLogin private initializeStatusCheck(): Observable { return this.authenticationApiService.authControllerStatus().pipe( tap((response) => this.isAuthenticatedSignal.set(response.success)),