This commit is contained in:
Igor Hrenowitsch Propisnov 2024-09-09 02:27:42 +02:00
parent 8e82733dd0
commit 786e4a59b8
15 changed files with 1592 additions and 97 deletions

View File

@ -21,6 +21,9 @@ export class EmailVerification {
@Column()
public expiresAt: Date;
@Column()
public email: string;
@OneToOne(() => UserCredentials)
@JoinColumn({ name: 'userCredentialsId' })
public user: UserCredentials;

View File

@ -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<SuccessDto> {
return this.authService.sendMagicLink(magicLinkDto);
}
@ApiCreatedResponse({
description: 'User signed up successfully',
type: SuccessDto,

View File

@ -1,2 +1,3 @@
export * from './user-credentials.dto';
export * from './signin-response.dto';
export * from './magic-link.dto';

View File

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

View File

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

View File

@ -41,4 +41,44 @@ export class PasswordConfirmationMailService extends BaseMailService {
await this.sendMail(mailoptions);
}
public async sendLoginLinkEmail(
to: string,
loginToken: string
): Promise<void> {
const token = `${loginToken}|${UriEncoderService.encodeBase64(to)}`;
const loginLink = `${this.configService.get<string>('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: `<p>Click <a href="${loginLink}">here</a> to log in to your account.</p>`,
};
await this.sendMail(mailoptions);
}
public async sendRegistrationLinkEmail(
to: string,
registrationToken: string
): Promise<void> {
const token = `${registrationToken}|${UriEncoderService.encodeBase64(to)}`;
const registrationLink = `${this.configService.get<string>('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: `<p>Click <a href="${registrationLink}">here</a> to complete your registration.</p>`,
};
await this.sendMail(mailoptions);
}
}

View File

@ -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<boolean> {
return this.emailVerificationService.verifyEmail(tokenToVerify);
@Query('token') tokenToVerify: string,
@Query('email') emailToVerify: string
): Promise<SuccessDto> {
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<boolean> {
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<boolean> {
// return this.emailVerificationService.isEmailVerified(request.sessionID);
// }
}

View File

@ -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<void> {
await this.repository.save({
token,
expiresAt,
user: { id: userId },
email,
user: userId ? { id: userId } : null,
});
}
public async findEmailVerificationByToken(token: string): Promise<boolean> {
const result = await this.repository.findOne({
where: { token, expiresAt: MoreThan(new Date()) },
public async findByTokenAndEmail(
token: string,
email: string
): Promise<EmailVerification | undefined> {
return await this.repository.findOne({
where: {
token,
email,
},
});
}
return result !== null;
public async removeEmailVerificationByTokenAndEmail(
token: string,
email: string
): Promise<void> {
await this.repository.delete({ token, email });
}
public async findItemByEmail(
email: string
): Promise<EmailVerification | null> {
return this.repository.findOne({ where: { email } });
}
public async deleteEmailVerificationByToken(

View File

@ -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<boolean> {
public async isEmailSubmitted(email: string): Promise<boolean> {
try {
const emailVerification =
await this.emailVerifyRepository.findEmailVerificationByToken(
tokenToVerify
);
await this.emailVerifyRepository.findItemByEmail(email);
if (!emailVerification) {
return false;
return !!emailVerification;
} catch (error) {
throw new InternalServerErrorException('EMAIL_VERIFICATION_ERROR', {
message: 'An error occurred while verifying the email.',
});
}
}
const deletedVerification =
await this.deleteEmailVerificationToken(tokenToVerify);
public async generateEmailVerificationTokenForMagicLink(
email: string
): Promise<string> {
try {
const verificationToken = await this.createVerificationToken();
const expiration = new Date(Date.now() + 24 * 60 * 60 * 1000);
if (deletedVerification && deletedVerification.user) {
const isStatusUpdated =
await this.userDataRepository.updateEmailVerificationStatus(
deletedVerification.user.id
await this.emailVerifyRepository.createEmailVerification(
verificationToken,
expiration,
email,
null
);
return isStatusUpdated;
return verificationToken;
} catch (error) {
throw new InternalServerErrorException(
'EMAIL_VERIFICATION_TOKEN_GENERATION_ERROR',
{
message:
'An error occurred while generating the email verification token.',
}
);
}
}
return false;
public async verifyEmail(
tokenToVerify: string,
emailToVerify: string
): Promise<SuccessDto> {
try {
const findTokenAndEmail: EmailVerification =
await this.emailVerifyRepository.findByTokenAndEmail(
tokenToVerify,
emailToVerify
);
if (!findTokenAndEmail) {
return { success: false };
}
await this.emailVerifyRepository.removeEmailVerificationByTokenAndEmail(
tokenToVerify,
emailToVerify
);
return { success: true };
} catch (error) {
throw new InternalServerErrorException('EMAIL_VERIFICATION_ERROR', {
message: 'An error occurred while verifying the email.',

View File

@ -19,6 +19,8 @@ export class HttpExceptionFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
//console.error('Exception caught:', exception);
let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
let message: string = 'Internal server error';
let error: string = 'INTERNAL_SERVER_ERROR';

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@if (!userSignupSuccess()) {
<!-- @if (!userSignupSuccess()) {
<div class="flex h-screen w-screen">
<div
[ngStyle]="leftBackgroundStyle"
@ -19,7 +19,6 @@
</div>
</div>
<!-- Rechter Bereich, immer sichtbar -->
<div [ngStyle]="rightBackgroundStyle" class="flex flex-col w-full md:w-1/2">
<div class="flex px-12 gap-3">
<div class="flex items-start justify-end pt-16">
@ -29,7 +28,7 @@
(change)="toggleTheme()"
[checked]="isDarkMode" />
<!-- sun icon -->
<svg
class="swap-on h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
@ -38,7 +37,7 @@
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<!-- moon icon -->
<svg
class="swap-off h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
@ -298,4 +297,952 @@
</div>
</div>
</div>
</div> -->
<!-- <div class="flex h-screen w-screen">
<div
[ngStyle]="leftBackgroundStyle"
class="hidden md:flex md:flex-col md:w-1/2 bg-primary">
<div class="flex-1 flex items-start pt-16 px-12">
<h1 class="text-3xl text-base-100">[LOGO] APP-NAME</h1>
</div>
<div class="flex-1 flex flex-col justify-end pb-16 px-12">
<blockquote>
<p class="text-xl text-base-100 font-semibold">
"This library has saved me countless hours of work and helped me
deliver stunning designs to my clients faster than ever before."
</p>
<small class="block text-sm font-light text-base-100 mt-4">
— Sofia Davis
</small>
</blockquote>
</div>
</div>
<div [ngStyle]="rightBackgroundStyle" class="flex flex-col w-full md:w-1/2">
<div class="flex px-12 gap-3">
<div class="flex items-start justify-end pt-16">
<label class="swap swap-rotate">
<input
type="checkbox"
(change)="toggleTheme()"
[checked]="isDarkMode" />
<svg
class="swap-on h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<svg
class="swap-off h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div>
</div>
<div class="flex-1 flex flex-col justify-center items-center px-12">
<h1 class="text-3xl font-semibold text-center">Login or Register</h1>
<p class="text-center">Enter your email to login or create an account</p>
<form
[formGroup]="form"
(ngSubmit)="onSubmit()"
class="flex gap-4 flex-col items-center py-6 w-full max-w-md">
<div class="form-control w-full">
<label
[ngClass]="{
'w-full': true,
'border-error focus:border-error':
form.get('email')?.invalid &&
(form.get('email')?.dirty || form.get('email')?.touched)
}"
class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70">
<path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
<path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
</svg>
<input
formControlName="email"
type="text"
class="grow"
placeholder="name@example.com" />
</label>
</div>
<button class="btn w-full btn-primary font-semibold">
@if (isLoading()) {
<span class="loading loading-spinner"></span>
}
Send Login Link
</button>
</form>
</div>
<div class="flex flex-col items-center justify-center py-12">
<footer>
<p class="text-xs">Made with ♥️ in Germany</p>
</footer>
</div>
</div>
</div>
<div class="modal modal-open" *ngIf="isEmailSent()">
<div
[ngStyle]="dialogBackgroundStyle"
class="modal-box w-11/12 h-2/6 max-w-5xl flex">
<div class="w-full flex flex-col justify-center items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1"
stroke="currentColor"
class="size-28 animate-jump animate-once animate-duration-[2000ms] animate-delay-500 animate-ease-in-out animate-normal">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 0 0 1.183 1.981l6.478 3.488m8.839 2.51-4.66-2.51m0 0-1.023-.55a2.25 2.25 0 0 0-2.134 0l-1.022.55m0 0-4.661 2.51m16.5 1.615a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V8.844a2.25 2.25 0 0 1 1.183-1.981l7.5-4.039a2.25 2.25 0 0 1 2.134 0l7.5 4.039a2.25 2.25 0 0 1 1.183 1.98V19.5Z" />
</svg>
<h1 class="font-bold text-3xl pt-5">Check your inbox, please!</h1>
<div class="flex flex-col items-center text-center">
<p class="pt-3">We've sent you an email with a login link.</p>
<p class="pt-1">
Please check your inbox and click the link to continue.
</p>
</div>
</div>
</div>
</div> -->
<!-- <div class="flex h-screen w-screen bg-base-200">
<div
[ngStyle]="leftBackgroundStyle"
class="hidden lg:flex lg:flex-col lg:w-1/2 bg-primary text-primary-content">
<div class="flex-1 flex items-start pt-16 px-12">
<h1 class="text-4xl font-bold">APP-NAME</h1>
</div>
<div class="flex-1 flex flex-col justify-center px-12">
<h2 class="text-3xl font-semibold mb-6">Create Unforgettable Events</h2>
<ul class="space-y-4">
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7" />
</svg>
Easy event creation and management
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7" />
</svg>
Seamless ticket sales and distribution
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7" />
</svg>
Powerful analytics for event success
</li>
</ul>
</div>
<div class="flex-1 flex flex-col justify-end pb-16 px-12">
<blockquote>
<p class="text-xl font-semibold">
"Eventbreriber has transformed how we manage our events. From creation
to ticket sales, it's all seamless and intuitive."
</p>
<footer class="mt-4">
<p class="font-semibold">Sarah Johnson</p>
<p class="text-sm">Event Manager, Stellar Conferences</p>
</footer>
</blockquote>
</div>
</div>
<div
[ngStyle]="rightBackgroundStyle"
class="flex flex-col w-full lg:w-1/2 bg-base-100">
<div class="flex px-12 gap-3">
<div class="flex items-start justify-end pt-16">
<label class="swap swap-rotate">
<input
type="checkbox"
(change)="toggleTheme()"
[checked]="isDarkMode" />
<svg
class="swap-on h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<svg
class="swap-off h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div>
</div>
<div class="flex-1 flex flex-col justify-center items-center px-6 sm:px-12">
<div class="w-full max-w-md">
<h1 class="text-3xl font-bold text-center mb-2">Welcome to APP-NAME</h1>
<p class="text-center text-base-content/60 mb-8">
Enter your email to login or create an account
</p>
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-6">
<label class="form-control w-full mb-4">
<div class="label">
<span class="label-text"></span>
<span class="label-text-alt"></span>
</div>
<label
class="input input-bordered flex items-center gap-2"
[ngClass]="getInputClass('email')">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70">
<path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
<path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
</svg>
<input
formControlName="email"
type="text"
class="grow"
placeholder="name@example.com" />
</label>
<div class="label">
<span class="label-text-alt"></span>
<span class="label-text-alt text-error">
{{ getErrorMessage('email') }}
</span>
</div>
</label>
<button
type="submit"
class="btn btn-primary w-full"
[disabled]="isLoading()">
@if (isLoading()) {
<span class="loading loading-spinner"></span>
}
Send Magic Link
</button>
</form>
<div class="divider my-8">OR</div>
<div class="flex flex-col space-y-4">
<button class="btn btn-outline gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor">
<path
d="M6.29 18.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0020 3.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.073 4.073 0 01.8 7.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 010 16.407a11.616 11.616 0 006.29 1.84" />
</svg>
Continue with X
</button>
<button class="btn btn-outline gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor">
<path
fill-rule="evenodd"
d="M10 0C4.477 0 0 4.477 0 10c0 4.991 3.657 9.128 8.438 9.879V12.89h-2.54V10h2.54V7.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V10h2.773l-.443 2.89h-2.33v6.989C16.343 19.129 20 14.99 20 10c0-5.523-4.477-10-10-10z"
clip-rule="evenodd" />
</svg>
Continue with Facebook
</button>
</div>
</div>
</div>
<div class="flex justify-center py-6">
<p class="text-sm text-base-content/60">
By continuing, you agree to Eventbreriber's
<a href="#" class="link link-primary">Terms of Service</a>
and
<a href="#" class="link link-primary">Privacy Policy</a>
.
</p>
</div>
</div>
</div>
<div class="modal modal-open" *ngIf="isEmailSent()">
<div class="modal-box w-11/12 max-w-lg">
<div class="flex flex-col items-center text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-24 w-24 text-primary mb-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 19v-8.93a2 2 0 01.89-1.664l7-4.666a2 2 0 012.22 0l7 4.666A2 2 0 0121 10.07V19M3 19a2 2 0 002 2h14a2 2 0 002-2M3 19l6.75-4.5M21 19l-6.75-4.5M3 10l6.75 4.5M21 10l-6.75 4.5m0 0l-1.14.76a2 2 0 01-2.22 0l-1.14-.76" />
</svg>
<h2 class="text-2xl font-bold mb-2">Check your inbox</h2>
<p class="mb-4">
We've sent you an email with a magic link. Click the link to access your
Eventbreriber account.
</p>
<button class="btn btn-primary">Got it</button>
</div>
</div>
</div> -->
<div class="flex h-screen w-screen bg-base-200">
<!-- Left side (hidden on mobile) -->
<div
[ngStyle]="leftBackgroundStyle"
class="hidden lg:flex lg:flex-col lg:w-1/2 bg-primary text-primary-content">
<div class="flex-1 flex items-start pt-16 px-12">
<h1 class="text-4xl font-bold">APP-NAME</h1>
</div>
<div class="flex-1 flex flex-col justify-center px-12">
@if (isTokenVerifing()) {
<!-- Skeleton loader for the main content -->
<div class="space-y-6">
<div class="skeleton h-8 w-3/4"></div>
<div class="space-y-4">
<div class="flex items-center">
<div class="skeleton h-8 w-8 mr-4"></div>
<div class="skeleton h-6 w-1/2"></div>
</div>
<div class="flex items-center">
<div class="skeleton h-8 w-8 mr-4"></div>
<div class="skeleton h-6 w-2/3"></div>
</div>
<div class="flex items-center">
<div class="skeleton h-8 w-8 mr-4"></div>
<div class="skeleton h-6 w-1/2"></div>
</div>
<div class="flex items-center">
<div class="skeleton h-8 w-8 mr-4"></div>
<div class="skeleton h-6 w-3/5"></div>
</div>
</div>
</div>
} @else {
@if (isRegistrationMode()) {
<h2 class="text-3xl font-semibold mb-8">
Complete Your Registration
</h2>
<ul class="space-y-6">
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-xl">Secure Account Creation</span>
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="text-xl">Quick and Easy Setup</span>
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span class="text-xl">Enhanced Security Measures</span>
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
<span class="text-xl">Personalized Experience</span>
</li>
</ul>
} @else {
<h2 class="text-3xl font-semibold mb-8">
Elevate Your Event Management
</h2>
<ul class="space-y-6">
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span class="text-xl">Streamlined Event Creation</span>
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span class="text-xl">Integrated Ticketing System</span>
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span class="text-xl">Advanced Analytics Dashboard</span>
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span class="text-xl">Virtual Event Integration</span>
</li>
<li class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-xl">Automated Reminder System</span>
</li>
</ul>
}
}
</div>
<div class="flex-1 flex flex-col justify-end pb-16 px-12">
@if (isTokenVerifing()) {
<!-- Skeleton loader for the quote -->
<div class="p-6">
<div class="skeleton h-4 w-full mb-2"></div>
<div class="skeleton h-4 w-5/6 mb-2"></div>
<div class="skeleton h-4 w-4/5 mb-4"></div>
<div class="skeleton h-3 w-1/4 mb-1"></div>
<div class="skeleton h-3 w-1/3"></div>
</div>
} @else {
<blockquote
class="backdrop-blur-sm p-6 rounded-lg border-base-100 border-2 shadow-lg">
@if (isRegistrationMode()) {
<p class="text-xl font-semibold italic">
"Registering for APP-NAME was a breeze. In just a few minutes, I
had access to powerful event management tools that transformed our
business."
</p>
<footer class="mt-4">
<p class="font-semibold">Alex Johnson</p>
<p class="text-sm">Event Coordinator, InnovateConferences</p>
</footer>
} @else {
<p class="text-xl font-semibold italic">
"APP-NAME has revolutionized our event management process. The
efficiency and insights it provides are unparalleled."
</p>
<footer class="mt-4">
<p class="font-semibold">Emily Chen</p>
<p class="text-sm">Director of Events, TechCorp International</p>
</footer>
}
</blockquote>
}
</div>
</div>
<div
[ngStyle]="rightBackgroundStyle"
class="flex flex-col w-full lg:w-1/2 bg-base-100 h-screen">
<div class="flex justify-end items-center pt-16 px-12">
<label class="swap swap-rotate">
<input
type="checkbox"
(change)="toggleTheme()"
[checked]="isDarkMode" />
<svg
class="swap-on h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<svg
class="swap-off h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div>
<div
class="flex-1 flex flex-col justify-center items-center px-6 sm:px-12 overflow-y-auto">
@if (isTokenVerifing()) {
<!-- Skeleton loader for the form -->
<div class="w-full max-w-md space-y-6">
<div class="skeleton h-10 w-3/4 mx-auto"></div>
<div class="skeleton h-4 w-1/2 mx-auto"></div>
<div class="skeleton h-12 w-full"></div>
@if (isRegistrationMode()) {
<div class="skeleton h-12 w-full"></div>
}
<div class="skeleton h-12 w-full"></div>
<div class="skeleton h-4 w-1/4 mx-auto"></div>
</div>
} @else {
<div class="w-full max-w-md">
<h1 class="text-4xl font-bold text-center mb-2">
@if (isRegistrationMode()) {
Complete Your Registration
} @else {
Welcome to APP-NAME
}
</h1>
<p class="text-center text-base-content/60 mb-8">
@if (isRegistrationMode()) {
You're one step away from unlocking powerful event management
tools
} @else {
Enter your email to access your account or get started
}
</p>
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-3">
<label class="form-control w-full">
<label
class="input input-bordered flex items-center gap-2 p-3"
[ngClass]="{
'bg-base-200/50': !isRegistrationMode(),
'bg-base-300/50': isRegistrationMode(),
'input-success !border-success':
isRegistrationMode() ||
getInputClass('email') === 'input-success',
'input-error':
!isRegistrationMode() &&
getInputClass('email') === 'input-error'
}"
[style.border-color]="
isRegistrationMode() ||
getInputClass('email') === 'input-success'
? 'var(--success)'
: null
">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="h-5 w-5 opacity-70">
<path
d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
<path
d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>
<input
formControlName="email"
type="text"
class="grow bg-transparent focus:outline-none"
[attr.readonly]="isRegistrationMode() ? '' : null"
[ngClass]="{ 'cursor-not-allowed': isRegistrationMode() }"
placeholder="name@example.com" />
</label>
<div class="label">
<span class="label-text-alt text-error">
{{ getErrorMessage('email') }}
</span>
</div>
</label>
@if (isRegistrationMode()) {
<label class="form-control w-full">
<label
class="input input-bordered flex items-center gap-2 p-3 bg-base-200/50"
[ngClass]="getInputClass('password')">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="h-5 w-5 opacity-70">
<path
fill-rule="evenodd"
d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z"
clip-rule="evenodd" />
</svg>
<input
#passwordInput
formControlName="password"
type="password"
class="grow bg-transparent focus:outline-none"
placeholder="Create a strong password" />
</label>
<div class="label">
<span class="label-text-alt text-error">
{{ getErrorMessage('password') }}
</span>
</div>
</label>
}
<button
type="submit"
class="btn btn-primary text-lg w-full h-auto"
[disabled]="isLoading()">
@if (isLoading()) {
<span class="loading loading-spinner"></span>
}
@if (isRegistrationMode()) {
Complete Registration
} @else {
Send Magic Link
}
</button>
</form>
<div class="mt-8 text-center">
<a href="#" class="text-primary hover:underline">Need help?</a>
</div>
</div>
}
@if (!isRegistrationMode() && !isTokenVerifing()) {
<div class="w-full max-w-md mt-12">
<div class="bg-base-200 p-6 rounded-lg">
<h3 class="text-lg font-semibold mb-4">What happens next?</h3>
<ul class="list-disc list-inside space-y-2 text-sm">
<li>We'll send a magic link to your email</li>
<li>Click the link in the email to securely log in</li>
<li>If you're new, you'll be prompted to create a password</li>
<li>Existing users will be logged in instantly</li>
<li>The magic link expires in 10 minutes for security</li>
</ul>
</div>
</div>
} @else if (isTokenVerifing()) {
<div class="w-full max-w-md mt-12">
<div class="bg-base-200 p-6 rounded-lg">
<div class="skeleton h-6 w-3/4 mb-4"></div>
<div class="space-y-2">
<div class="skeleton h-4 w-full"></div>
<div class="skeleton h-4 w-5/6"></div>
<div class="skeleton h-4 w-4/5"></div>
<div class="skeleton h-4 w-full"></div>
<div class="skeleton h-4 w-5/6"></div>
</div>
</div>
</div>
}
</div>
<div class="flex justify-center py-6">
<p class="text-sm text-base-content/60 text-center max-w-md">
By continuing, you agree to APP-NAME's
<a href="#" class="link link-primary">Terms of Service</a>
and
<a href="#" class="link link-primary">Privacy Policy</a>
.
</p>
</div>
</div>
</div>
<div
class="modal modal-open"
*ngIf="isEmailSent()"
tabindex="-1"
aria-labelledby="modal-title"
aria-describedby="modal-description"
aria-modal="true"
role="dialog">
<div
class="modal-box w-11/12 max-w-2xl mx-auto bg-base-100 shadow-xl rounded-lg transition-all transform duration-300 ease-out">
<div class="flex flex-col items-center text-center p-6 space-y-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-24 w-24 text-primary mb-4 animate-bounce"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 19v-8.93a2 2 0 01.89-1.664l7-4.666a2 2 0 012.22 0l7 4.666A2 2 0 0121 10.07V19M3 19a2 2 0 002 2h14a2 2 0 002-2M3 19l6.75-4.5M21 19l-6.75-4.5M3 10l6.75 4.5M21 10l-6.75 4.5m0 0l-1.14.76a2 2 0 01-2.22 0l-1.14-.76" />
</svg>
<h2 id="modal-title" class="text-3xl font-semibold mb-2">
Please Check Your Email
</h2>
<p id="modal-description">
A confirmation email has been sent. Follow the instructions in the email
to activate your account.
</p>
<ul class="text-left p-4 rounded-lg w-full max-w-lg list-disc">
<li>Open your email inbox and look for the confirmation email.</li>
<li>If you don't see it, check your spam or junk folder.</li>
<li>
Click the confirmation link in the email to complete your
registration.
</li>
<li>Ensure your email client does not block emails from our domain.</li>
</ul>
<div class="mt-6 flex items-center justify-center">
You can now close this tab.
</div>
</div>
</div>
</div>
<div
class="modal modal-open"
*ngIf="isTokenVerifing() || isTokenVerified() || verificationError()"
tabindex="-1"
aria-labelledby="verify-modal-title"
aria-describedby="verify-modal-description"
aria-modal="true"
role="dialog">
<div
class="modal-box w-11/12 max-w-2xl mx-auto bg-base-100 shadow-xl rounded-lg transition-all transform duration-300 ease-out">
<div class="flex flex-col items-center text-center p-6 space-y-4">
<div class="relative w-10 h-10">
<div
class="absolute inset-0 transition-opacity duration-300 ease-in-out"
[class.opacity-100]="isTokenVerifing() && !verificationError()"
[class.opacity-0]="!isTokenVerifing() || verificationError()">
<span class="loading loading-spinner loading-lg"></span>
</div>
<div
class="absolute inset-0 transition-opacity duration-300 ease-in-out"
[class.opacity-0]="!isTokenVerified() || verificationError()"
[class.opacity-100]="isTokenVerified() && !verificationError()">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10 text-success"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7" />
</svg>
</div>
<div
class="absolute inset-0 transition-opacity duration-300 ease-in-out"
[class.opacity-0]="!verificationError()"
[class.opacity-100]="verificationError()">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10 text-error"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
</div>
<h2 id="verify-modal-title" class="text-3xl font-semibold mb-2">
{{
isTokenVerified() && !verificationError()
? 'Verification Complete'
: verificationError()
? 'Verification Failed'
: 'Verifying Your Account'
}}
</h2>
<p id="verify-modal-description">
{{
isTokenVerified() && !verificationError()
? 'Your email has been successfully verified.'
: verificationError()
? verificationError()
: 'Please wait while we verify your email and token.'
}}
</p>
@if (errorReasons().length > 0) {
<div class="mt-4 text-left">
<p class="font-semibold">Possible reasons:</p>
<ul class="list-disc list-inside">
@for (reason of errorReasons(); track reason) {
<li>{{ reason }}</li>
}
</ul>
<p class="mt-4 font-semibold">What to do next:</p>
<p>
Please try to start the registration process again. If the problem
persists, contact our support team.
</p>
@if (isTokenVerified() || verificationError()) {
<div class="mt-6">
<a href="/" class="btn btn-primary">Back to Welcome Page</a>
</div>
}
</div>
}
</div>
</div>
</div>
<!--
<div
class="modal modal-open"
*ngIf="isTokenVerifing() || isTokenVerified() || verificationError()"
tabindex="-1"
aria-labelledby="verify-modal-title"
aria-describedby="verify-modal-description"
aria-modal="true"
role="dialog">
<div
class="modal-box w-11/12 max-w-2xl mx-auto bg-base-100 shadow-xl rounded-lg transition-all transform duration-300 ease-out">
<div class="flex flex-col items-center text-center p-6 space-y-4">
<div class="relative w-10 h-10">
<div
class="absolute inset-0 transition-opacity duration-300 ease-in-out"
[class.opacity-0]="!verificationError()"
[class.opacity-100]="verificationError()">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10 text-error"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
</div>
<h2 id="verify-modal-title" class="text-3xl font-semibold mb-2">
{{
isTokenVerified()
? 'Verification Complete'
: verificationError()
? 'Verification Failed'
: 'Verifying Your Account'
}}
</h2>
<p id="verify-modal-description">
{{
isTokenVerified()
? 'Your email has been successfully verified.'
: verificationError()
? verificationError()
: 'Please wait while we verify your email and token.'
}}
</p>
</div>
</div>
</div> -->

View File

@ -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<string> = input<string>('');
public signup: InputSignal<boolean> = input<boolean>(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<boolean> = signal(false);
public isEmailSent: WritableSignal<boolean> = signal(false);
public isTokenVerifing: WritableSignal<boolean> = signal(false);
public isTokenVerified: WritableSignal<boolean> = signal(false);
public errorReasons: WritableSignal<string[]> = signal<string[]>([]);
public verificationError: WritableSignal<string | null> = signal<
string | null
>(null);
public isRegistrationMode: WritableSignal<boolean> = signal(false);
private removeQueryParams: WritableSignal<boolean> = 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);
}
});
}
}

View File

@ -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<SuccessDtoApiModel> {
return this.authenticationApiService.authControllerSendMagicLink(email);
}
public signup(
credentials: UserCredentialsDtoApiModel
): Observable<SuccessDtoApiModel> {
@ -49,6 +56,7 @@ export class AuthService {
.pipe(tap(() => this.isAuthenticatedSignal.set(false)));
}
// TODO: Later for Autologin
public status(): Observable<SuccessDtoApiModel> {
if (this.isAuthenticatedSignal()) {
return of({ success: true });
@ -56,6 +64,7 @@ export class AuthService {
return this.statusCheck$;
}
// TODO Later for AutoLogin
private initializeStatusCheck(): Observable<SuccessDtoApiModel> {
return this.authenticationApiService.authControllerStatus().pipe(
tap((response) => this.isAuthenticatedSignal.set(response.success)),