feature/refactor-login #19

Merged
igorpropisnov merged 26 commits from feature/refactor-login into main 2024-09-19 13:58:12 +02:00
19 changed files with 369 additions and 1415 deletions
Showing only changes of commit d59d41e1ee - Show all commits

View File

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

View File

@ -17,6 +17,7 @@ import { Public } from 'src/shared/decorator';
import { LocalAuthGuard } from '../guard'; import { LocalAuthGuard } from '../guard';
import { import {
MagicLinkDto, MagicLinkDto,
MagicLinkSigninDto,
SigninResponseDto, SigninResponseDto,
UserCredentialsDto, UserCredentialsDto,
} from '../models/dto'; } from '../models/dto';
@ -36,9 +37,12 @@ export class AuthController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Public() @Public()
public async sendMagicLink( public async sendMagicLink(
@Body() magicLinkDto: MagicLinkDto @Body() magicLinkDto: MagicLinkDto,
@Req() request: Request
): Promise<SuccessDto> { ): Promise<SuccessDto> {
return this.authService.sendMagicLink(magicLinkDto); const userAgent = request.headers['user-agent'] || 'Unknown';
return this.authService.sendMagicLink(magicLinkDto, userAgent);
} }
@ApiCreatedResponse({ @ApiCreatedResponse({
@ -58,12 +62,14 @@ export class AuthController {
description: 'User signin successfully', description: 'User signin successfully',
type: SigninResponseDto, type: SigninResponseDto,
}) })
@ApiBody({ type: UserCredentialsDto }) @ApiBody({ type: MagicLinkSigninDto })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@UseGuards(LocalAuthGuard) @UseGuards(LocalAuthGuard)
@Public() @Public()
@Post('signin') @Post('magic-link-signin')
public async signin(@Req() request: Request): Promise<SigninResponseDto> { public async magicLinkSignin(
@Req() request: Request
): Promise<SigninResponseDto> {
return this.authService.getLoginResponse( return this.authService.getLoginResponse(
request.user as SigninResponseDto & { userAgent: string } request.user as SigninResponseDto & { userAgent: string }
); );

View File

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

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString } from 'class-validator';
export class MagicLinkSigninDto {
@ApiProperty()
@IsString()
public token: string;
@ApiProperty()
@IsEmail()
public email: string;
}

View File

@ -1,4 +1,8 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import {
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { UserCredentials } from 'src/entities'; import { UserCredentials } from 'src/entities';
import { SessionService } from 'src/modules/session/services/session.service'; import { SessionService } from 'src/modules/session/services/session.service';
import { EncryptionService, SuccessDto } from 'src/shared'; import { EncryptionService, SuccessDto } from 'src/shared';
@ -28,7 +32,10 @@ export class AuthService {
private readonly sessionService: SessionService private readonly sessionService: SessionService
) {} ) {}
public async sendMagicLink(magiclink: MagicLinkDto): Promise<SuccessDto> { public async sendMagicLink(
magiclink: MagicLinkDto,
userAgent: string
): Promise<SuccessDto> {
try { try {
const existingUser = await this.userCredentialsRepository.findUserByEmail( const existingUser = await this.userCredentialsRepository.findUserByEmail(
magiclink.email magiclink.email
@ -37,7 +44,9 @@ export class AuthService {
if (existingUser) { if (existingUser) {
const token = const token =
await this.emailVerificationService.generateEmailVerificationTokenForMagicLink( await this.emailVerificationService.generateEmailVerificationTokenForMagicLink(
magiclink.email magiclink.email,
userAgent,
existingUser.id
); );
await this.passwordConfirmationMailService.sendLoginLinkEmail( await this.passwordConfirmationMailService.sendLoginLinkEmail(
@ -45,24 +54,16 @@ export class AuthService {
token token
); );
} else { } else {
const isEmailSubmitted: boolean = const token =
await this.emailVerificationService.isEmailSubmitted(magiclink.email); await this.emailVerificationService.generateEmailVerificationTokenForMagicLink(
if (!isEmailSubmitted) {
const token =
await this.emailVerificationService.generateEmailVerificationTokenForMagicLink(
magiclink.email
);
await this.passwordConfirmationMailService.sendRegistrationLinkEmail(
magiclink.email, magiclink.email,
token userAgent
); );
} else {
throw new ConflictException('EMAIL_ALREADY_SUBMITTED', { await this.passwordConfirmationMailService.sendRegistrationLinkEmail(
message: 'This email has already been submitted for registration.', magiclink.email,
}); token
} );
} }
return { success: true }; return { success: true };
@ -105,11 +106,6 @@ export class AuthService {
// user.id // user.id
// ); // );
// await this.passwordConfirmationMailService.sendPasswordConfirmationMail(
// user.email,
// token
// );
return { return {
success: true, success: true,
}; };
@ -125,28 +121,31 @@ export class AuthService {
} }
public async validateUser( public async validateUser(
token: string,
email: string, email: string,
password: string userAgent: string
): Promise<UserCredentials> { ): Promise<UserCredentials> {
try { try {
const verificationResult =
await this.emailVerificationService.verifyEmail(
token,
email,
userAgent
);
if (!verificationResult.success) {
throw new UnauthorizedException('Invalid or expired token');
}
const user = await this.userCredentialsRepository.findUserByEmail(email); const user = await this.userCredentialsRepository.findUserByEmail(email);
if (!user) { if (!user) {
throw new ForbiddenException('INVALID_CREDENTIALS'); throw new UnauthorizedException('User not found');
}
const passwordMatch = await EncryptionService.compareHash(
password,
user.hashedPassword
);
if (!passwordMatch) {
throw new ForbiddenException('INVALID_CREDENTIALS');
} }
return user; return user;
} catch (error) { } catch (error) {
if (error instanceof ForbiddenException) { if (error instanceof UnauthorizedException) {
throw error; throw error;
} else { } else {
throw new InternalServerErrorException('VALIDATION_ERROR', { throw new InternalServerErrorException('VALIDATION_ERROR', {
@ -156,6 +155,10 @@ export class AuthService {
} }
} }
public async getUserByEmail(email: string): Promise<UserCredentials> {
return this.userCredentialsRepository.findUserByEmail(email);
}
public async signout(sessionId: string): Promise<SuccessDto> { public async signout(sessionId: string): Promise<SuccessDto> {
try { try {
await this.sessionService.deleteSessionBySessionId(sessionId); await this.sessionService.deleteSessionBySessionId(sessionId);

View File

@ -1,34 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express'; import { Request } from 'express';
import { Strategy } from 'passport-local'; import { Strategy } from 'passport-local';
import { EmailVerificationService } from 'src/modules/verify-module/services/email-verification.service';
import { SigninResponseDto } from '../models/dto'; import { MagicLinkSigninDto } from '../models/dto';
import { AuthService } from '../services/auth.service'; import { AuthService } from '../services/auth.service';
@Injectable() @Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) { export class LocalStrategy extends PassportStrategy(Strategy) {
public constructor(private readonly authService: AuthService) { public constructor(
private authService: AuthService,
private emailVerificationService: EmailVerificationService
) {
super({ super({
usernameField: 'email', usernameField: 'email',
passwordField: 'password', passwordField: 'token',
passReqToCallback: true, passReqToCallback: true,
}); });
} }
public async validate( public async validate(request: Request): Promise<any> {
request: Request, const { token, email }: MagicLinkSigninDto = request.body;
email: string,
password: string if (!token || !email) {
): Promise<SigninResponseDto & { userAgent: string }> { throw new UnauthorizedException('Missing token or email');
const user = await this.authService.validateUser(email, password); }
const verificationResult = await this.emailVerificationService.verifyEmail(
token as string,
email as string,
request.headers['user-agent']
);
if (!verificationResult.success) {
throw new UnauthorizedException('Invalid or expired token');
}
const user = await this.authService.getUserByEmail(email as string);
if (!user) { if (!user) {
throw new UnauthorizedException(); throw new UnauthorizedException('User not found');
} }
const userAgent = request.headers['user-agent']; const userAgent = request.headers['user-agent'];
return { id: user.id, email: user.email, userAgent: userAgent }; return { id: user.id, email: user.email, userAgent };
} }
} }

View File

@ -67,10 +67,6 @@ export class PasswordConfirmationMailService extends BaseMailService {
const token = `${registrationToken}|${UriEncoderService.encodeBase64(to)}`; const token = `${registrationToken}|${UriEncoderService.encodeBase64(to)}`;
const registrationLink = `${this.configService.get<string>('APP_URL')}/?token=${token}&signup=true`; const registrationLink = `${this.configService.get<string>('APP_URL')}/?token=${token}&signup=true`;
console.log('##############');
console.log(registrationLink);
console.log('##############');
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' },

View File

@ -1,9 +1,5 @@
import { import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
CanActivate, import { SessionException } from 'src/shared/exceptions';
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { SessionService } from '../services/session.service'; import { SessionService } from '../services/session.service';
@ -19,20 +15,20 @@ export class SessionGuard implements CanActivate {
const session = await this.sessionService.findSessionBySessionId(sessionId); const session = await this.sessionService.findSessionBySessionId(sessionId);
if (!session) { if (!session) {
throw new UnauthorizedException('Session not found.'); throw new SessionException('Session not found.');
} }
const isExpired = await this.sessionService.isSessioExpired(session); const isExpired = await this.sessionService.isSessioExpired(session);
if (isExpired) { if (isExpired) {
throw new UnauthorizedException('Session expired.'); throw new SessionException('Session expired.');
} }
const userAgentInSession = JSON.parse(session.json).passport.user const userAgentInSession = JSON.parse(session.json).passport.user
.userAgent as string; .userAgent as string;
if (userAgentInSession !== currentAgent) { if (userAgentInSession !== currentAgent) {
throw new UnauthorizedException('User agent mismatch.'); throw new SessionException('User agent mismatch.');
} }
return true; return true;

View File

@ -1,4 +1,11 @@
import { Controller, HttpCode, HttpStatus, Query, Post } from '@nestjs/common'; import {
Controller,
HttpCode,
HttpStatus,
Query,
Post,
Req,
} from '@nestjs/common';
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
import { SuccessDto } from 'src/shared'; import { SuccessDto } from 'src/shared';
import { Public } from 'src/shared/decorator'; import { Public } from 'src/shared/decorator';
@ -21,22 +28,15 @@ export class VerifyController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
public async verifyEmail( public async verifyEmail(
@Query('token') tokenToVerify: string, @Query('token') tokenToVerify: string,
@Query('email') emailToVerify: string @Query('email') emailToVerify: string,
@Req() request: Request
): Promise<SuccessDto> { ): Promise<SuccessDto> {
const userAgent = request.headers['user-agent'] || 'Unknown';
return this.emailVerificationService.verifyEmail( return this.emailVerificationService.verifyEmail(
tokenToVerify, tokenToVerify,
emailToVerify emailToVerify,
userAgent
); );
} }
// @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 { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { EmailVerification } from 'src/entities'; import { EmailVerification } from 'src/entities';
import { LessThan, MoreThan, Repository } from 'typeorm'; import { LessThan, Repository } from 'typeorm';
@Injectable() @Injectable()
export class EmailVerifyRepository { export class EmailVerifyRepository {
@ -14,30 +14,17 @@ export class EmailVerifyRepository {
token: string, token: string,
expiresAt: Date, expiresAt: Date,
email: string, email: string,
userId: string | null userId: string | null,
userAgent: string
): Promise<void> { ): Promise<void> {
await this.repository.delete({ email });
await this.repository.save({ await this.repository.save({
token, token,
expiresAt, expiresAt,
email, email,
user: userId ? { id: userId } : null, user: userId ? { id: userId } : null,
}); userAgent,
}
public async findValidVerification(
token: string,
email: string
): Promise<EmailVerification | undefined> {
const currentDate = new Date();
const tenMinutesAgo = new Date(currentDate.getTime() - 10 * 60 * 1000);
return await this.repository.findOne({
where: {
token,
email,
createdAt: MoreThan(tenMinutesAgo),
expiresAt: MoreThan(currentDate),
},
}); });
} }
@ -60,12 +47,6 @@ export class EmailVerifyRepository {
await this.repository.delete({ token, email }); await this.repository.delete({ token, email });
} }
public async findItemByEmail(
email: string
): Promise<EmailVerification | null> {
return this.repository.findOne({ where: { email } });
}
public async deleteAllExpiredTokens(): Promise<void> { public async deleteAllExpiredTokens(): Promise<void> {
const currentDate = new Date(); const currentDate = new Date();
@ -73,20 +54,4 @@ export class EmailVerifyRepository {
expiresAt: LessThan(currentDate), expiresAt: LessThan(currentDate),
}); });
} }
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

@ -1,64 +1,25 @@
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EmailVerification } from 'src/entities';
import { SessionService } from 'src/modules/session/services/session.service';
import { SuccessDto, UriEncoderService } from 'src/shared'; import { SuccessDto, UriEncoderService } from 'src/shared';
import { import {
InternalServerErrorException, InternalServerErrorException,
TokenExpiredException, TokenExpiredException,
UserAgentMismatchException,
} from 'src/shared/exceptions'; } from 'src/shared/exceptions';
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
import { EmailVerifyRepository } from '../repositories'; import { EmailVerifyRepository } from '../repositories';
@Injectable() @Injectable()
export class EmailVerificationService { export class EmailVerificationService {
public constructor( public constructor(
private readonly emailVerifyRepository: EmailVerifyRepository, private readonly emailVerifyRepository: EmailVerifyRepository
private readonly userDataRepository: UserDataRepository,
private readonly sessionService: SessionService
) {} ) {}
public async generateEmailVerificationToken(userId: string): Promise<string> {
try {
const verificationToken = await this.createVerificationToken();
const expiration = new Date(Date.now() + 24 * 60 * 60 * 1000);
await this.emailVerifyRepository.createEmailVerification(
verificationToken,
expiration,
userId,
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 isEmailSubmitted(email: string): Promise<boolean> {
try {
const emailVerification =
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( public async generateEmailVerificationTokenForMagicLink(
email: string email: string,
userAgent: string,
userid?: string
): Promise<string> { ): Promise<string> {
try { try {
const verificationToken = await this.createVerificationToken(); const verificationToken = await this.createVerificationToken();
@ -68,7 +29,8 @@ export class EmailVerificationService {
verificationToken, verificationToken,
expiresAt, expiresAt,
email, email,
null userid || null,
userAgent
); );
return verificationToken; return verificationToken;
@ -85,7 +47,8 @@ export class EmailVerificationService {
public async verifyEmail( public async verifyEmail(
tokenToVerify: string, tokenToVerify: string,
emailToVerify: string emailToVerify: string,
userAgent: string
): Promise<SuccessDto> { ): Promise<SuccessDto> {
try { try {
const token = await this.emailVerifyRepository.findByTokenAndEmail( const token = await this.emailVerifyRepository.findByTokenAndEmail(
@ -97,6 +60,13 @@ export class EmailVerificationService {
throw new TokenExpiredException(); throw new TokenExpiredException();
} }
if (token.userAgent !== userAgent) {
throw new UserAgentMismatchException({
message:
'The User Agent does not match the one used to generate the token.',
});
}
const currentDate = new Date(); const currentDate = new Date();
if (token.expiresAt.getTime() < currentDate.getTime()) { if (token.expiresAt.getTime() < currentDate.getTime()) {
@ -113,35 +83,16 @@ export class EmailVerificationService {
if (error instanceof TokenExpiredException) { if (error instanceof TokenExpiredException) {
throw error; throw error;
} }
if (error instanceof UserAgentMismatchException) {
throw error;
}
throw new InternalServerErrorException('EMAIL_VERIFICATION_ERROR', { throw new InternalServerErrorException('EMAIL_VERIFICATION_ERROR', {
message: 'An error occurred while verifying the email.', message: 'An error occurred while verifying the email.',
}); });
} }
} }
public async isEmailVerified(sessionID: string): Promise<boolean> { public async deleteAllExpiredTokens(): Promise<void> {
try {
const userId = await this.sessionService.getUserIdBySessionId(sessionID);
if (!userId) {
return false;
}
const isVerified =
await this.userDataRepository.isEmailConfirmedByUserId(userId);
return isVerified;
} catch (error) {
throw new InternalServerErrorException('EMAIL_VERIFICATION_CHECK_ERROR', {
message:
'An error occurred while checking the email verification status.',
});
}
}
async deleteAllExpiredTokens(): Promise<void> {
const currentDate = new Date();
await this.emailVerifyRepository.deleteAllExpiredTokens(); await this.emailVerifyRepository.deleteAllExpiredTokens();
} }
@ -150,12 +101,4 @@ export class EmailVerificationService {
return UriEncoderService.encodeUri(verifyToken); return UriEncoderService.encodeUri(verifyToken);
} }
private async deleteEmailVerificationToken(
tokenToDelete: string
): Promise<EmailVerification | null> {
return await this.emailVerifyRepository.deleteEmailVerificationByToken(
tokenToDelete
);
}
} }

View File

@ -3,3 +3,5 @@ export * from './forbidden.exception';
export * from './internal-server-error.exception'; export * from './internal-server-error.exception';
export * from './not-found.exception'; export * from './not-found.exception';
export * from './token-expired.exception'; export * from './token-expired.exception';
export * from './useragent-mismatch-exception';
export * from './session.exception';

View File

@ -0,0 +1,12 @@
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class SessionException extends BaseException {
public constructor(message: string, details?: unknown) {
super('Session Error', HttpStatus.UNAUTHORIZED, 'SESSION_ERROR', {
message,
details,
});
}
}

View File

@ -0,0 +1,14 @@
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class UserAgentMismatchException extends BaseException {
public constructor(details?: unknown) {
super(
'User Agent Mismatch',
HttpStatus.UNAUTHORIZED,
'USER_AGENT_MISMATCH',
details
);
}
}

View File

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

View File

@ -24,25 +24,11 @@ export class EventEmptyStateComponent {
private readonly verifyApi: VerifyApiService private readonly verifyApi: VerifyApiService
) {} ) {}
// public navigateToCreateEvent(): void { public navigateToCreateEvent(): void {
// this.verifyApi this.router.navigate(['/event/create']);
// .verifyControllerIsEmailVerified() }
// .subscribe((isVerified: boolean) => {
// if (!isVerified) {
// this.openEmailVerificationModal();
// } else {
// this.router.navigate(['/event/create']);
// }
// });
// }
public closeEmailVerificationModal(): void { public closeEmailVerificationModal(): void {
(this.emailVerificationModal.nativeElement as HTMLDialogElement).close(); (this.emailVerificationModal.nativeElement as HTMLDialogElement).close();
} }
private openEmailVerificationModal(): void {
(
this.emailVerificationModal.nativeElement as HTMLDialogElement
).showModal();
}
} }

View File

@ -1,646 +1,4 @@
<!-- @if (!userSignupSuccess()) {
<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 class="flex-1 items-start flex justify-end pt-16">
@if (isSignupSignal()) {
<button
(click)="toggleAction('signin')"
class="btn btn-primary btn-outline no-animation">
Login
</button>
}
@if (isSigninSignal()) {
@if (displaySkeleton()) {
<div class="skeleton w-36 h-12"></div>
} @else {
<button
(click)="toggleAction('signup')"
class="btn btn-primary btn-outline no-animation">
New here - Register now!
</button>
}
}
</div>
</div>
@if (isSignupSignal()) {
<div
class="animate-fade-down animate-once animate-duration-1000 animate-ease-in-out flex-1 flex flex-col justify-center items-center px-12">
<h1 class="text-3xl font-semibold text-center">Create an Account</h1>
<p class="text-center">
Enter your email below to create your 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>
<div class="form-control w-full">
<label
[ngClass]="{
'w-full': true,
'border-error focus:border-error':
form.get('password')?.invalid &&
(form.get('password')?.dirty ||
form.get('password')?.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
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd" />
</svg>
<input
formControlName="password"
type="password"
class="grow"
value="" />
</label>
</div>
<button class="btn w-full btn-primary font-semibold">
@if (isLoading()) {
<span class="loading loading-spinner"></span>
}
Sign Up with Email
</button>
<p class="text-xs w-full text-center">
By clicking continue, you agree to our
<u class="cursor-pointer">Terms of Service</u>
and
<u class="cursor-pointer">Privacy Policy</u>
.
</p>
</form>
</div>
}
@if (isSigninSignal()) {
<div
class="animate-fade-down animate-once animate-duration-1000 animate-ease-in-out flex-1 flex flex-col justify-center items-center px-12">
@if (displaySkeleton()) {
<div class="flex items-center w-full flex-col max-w-md gap-4">
<div class="skeleton w-36 h-10"></div>
<div class="skeleton w-full h-10 max-w-md"></div>
<div class="skeleton w-full h-10 max-w-md"></div>
<div class="skeleton w-full h-10 max-w-md"></div>
<div class="skeleton w-full h-10 max-w-md"></div>
</div>
} @else {
<h1 class="text-3xl font-semibold text-center">Login</h1>
<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>
<div class="form-control w-full">
<label
[ngClass]="{
'w-full': true,
'border-error focus:border-error':
form.get('password')?.invalid &&
(form.get('password')?.dirty ||
form.get('password')?.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
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd" />
</svg>
<input
formControlName="password"
type="password"
class="grow"
value="" />
</label>
</div>
<div class="form-control w-full">
<div class="flex items-center justify-between">
<label class="label cursor-pointer">
<input
[formControl]="rememberMe"
type="checkbox"
checked="checked"
class="checkbox checkbox-md checkbox-primary" />
<span class="label-text ml-1.5">Remember me</span>
</label>
<a class="text-primary label-text cursor-pointer">
Forgot password?
</a>
</div>
</div>
<button class="btn w-full btn-primary font-semibold">
@if (isLoading()) {
<span class="loading loading-spinner"></span>
}
Sign In
</button>
<div class="flex gap-1">
<span class="text-xs">Not registered yet?</span>
<a
(click)="toggleAction('signup')"
(keypress)="toggleAction('signup')"
tabindex="0"
class="text-primary cursor-pointer text-xs">
Create An Account
</a>
</div>
</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>
} @else {
<div class="flex h-screen w-screen bg-primary">
<div class="hidden md:flex md:flex-col md:w-1/1"></div>
</div>
}
<div class="modal modal-open" *ngIf="isDialogOpen()">
<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">
Hey, to start using [APP-NAME], we need to verify your email.
</p>
<p class="pt-1">
We´ve already sent out the verification link. Please check it and
<br />
confirm it´s really you.
</p>
</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"> <div class="flex h-screen w-screen bg-base-200">
<!-- Left side (hidden on mobile) -->
<div <div
[ngStyle]="leftBackgroundStyle" [ngStyle]="leftBackgroundStyle"
class="hidden lg:flex lg:flex-col lg:w-1/2 bg-primary text-primary-content"> class="hidden lg:flex lg:flex-col lg:w-1/2 bg-primary text-primary-content">
@ -648,7 +6,7 @@
<h1 class="text-4xl font-bold">APP-NAME</h1> <h1 class="text-4xl font-bold">APP-NAME</h1>
</div> </div>
<div class="flex-1 flex flex-col justify-center px-12"> <div class="flex-1 flex flex-col justify-center px-12">
@if (isTokenVerifing()) { @if (displaySkeleton()) {
<!-- Skeleton loader for the main content --> <!-- Skeleton loader for the main content -->
<div class="space-y-6"> <div class="space-y-6">
<div class="skeleton h-8 w-3/4"></div> <div class="skeleton h-8 w-3/4"></div>
@ -823,8 +181,7 @@
} }
</div> </div>
<div class="flex-1 flex flex-col justify-end pb-16 px-12"> <div class="flex-1 flex flex-col justify-end pb-16 px-12">
@if (isTokenVerifing()) { @if (displaySkeleton()) {
<!-- Skeleton loader for the quote -->
<div class="p-6"> <div class="p-6">
<div class="skeleton h-4 w-full mb-2"></div> <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-5/6 mb-2"></div>
@ -888,8 +245,7 @@
<div <div
class="flex-1 flex flex-col justify-center items-center px-6 sm:px-12 overflow-y-auto"> class="flex-1 flex flex-col justify-center items-center px-6 sm:px-12 overflow-y-auto">
@if (isTokenVerifing()) { @if (displaySkeleton()) {
<!-- Skeleton loader for the form -->
<div class="w-full max-w-md space-y-6"> <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-10 w-3/4 mx-auto"></div>
<div class="skeleton h-4 w-1/2 mx-auto"></div> <div class="skeleton h-4 w-1/2 mx-auto"></div>
@ -1012,7 +368,7 @@
</div> </div>
} }
@if (!isRegistrationMode() && !isTokenVerifing()) { @if (!isRegistrationMode() && !displaySkeleton()) {
<div class="w-full max-w-md mt-12"> <div class="w-full max-w-md mt-12">
<div class="bg-base-200 p-6 rounded-lg"> <div class="bg-base-200 p-6 rounded-lg">
<h3 class="text-lg font-semibold mb-4">What happens next?</h3> <h3 class="text-lg font-semibold mb-4">What happens next?</h3>
@ -1025,7 +381,7 @@
</ul> </ul>
</div> </div>
</div> </div>
} @else if (isTokenVerifing()) { } @else if (displaySkeleton()) {
<div class="w-full max-w-md mt-12"> <div class="w-full max-w-md mt-12">
<div class="bg-base-200 p-6 rounded-lg"> <div class="bg-base-200 p-6 rounded-lg">
<div class="skeleton h-6 w-3/4 mb-4"></div> <div class="skeleton h-6 w-3/4 mb-4"></div>
@ -1101,7 +457,7 @@
<div <div
class="modal modal-open" class="modal modal-open"
*ngIf="isTokenVerifing() || isTokenVerified() || verificationError()" *ngIf="isTokenVerified() || isVerifying() || verificationError()"
tabindex="-1" tabindex="-1"
aria-labelledby="verify-modal-title" aria-labelledby="verify-modal-title"
aria-describedby="verify-modal-description" aria-describedby="verify-modal-description"
@ -1113,8 +469,8 @@
<div class="relative w-10 h-10"> <div class="relative w-10 h-10">
<div <div
class="absolute inset-0 transition-opacity duration-300 ease-in-out" class="absolute inset-0 transition-opacity duration-300 ease-in-out"
[class.opacity-100]="isTokenVerifing() && !verificationError()" [class.opacity-100]="isVerifying() && !verificationError()"
[class.opacity-0]="!isTokenVerifing() || verificationError()"> [class.opacity-0]="!isVerifying() || verificationError()">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>
</div> </div>
<div <div
@ -1184,8 +540,8 @@
persists, contact our support team. persists, contact our support team.
</p> </p>
@if (isTokenVerified() || verificationError()) { @if (isTokenVerified() || verificationError()) {
<div class="mt-6"> <div class="mt-6 flex justify-center">
<a href="/" class="btn btn-primary">Back to Welcome Page</a> <a href="/" class="btn btn-primary">Back to Login</a>
</div> </div>
} }
</div> </div>
@ -1193,56 +549,3 @@
</div> </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,404 +1,3 @@
/* import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import {
ChangeDetectionStrategy,
Component,
OnInit,
WritableSignal,
signal,
effect,
InputSignal,
input,
ElementRef,
} from '@angular/core';
import {
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { Router } from '@angular/router';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { PasswordModule } from 'primeng/password';
import { delay, finalize, takeWhile, tap } from 'rxjs';
import {
Configuration,
SigninResponseDtoApiModel,
SuccessDtoApiModel,
UserCredentialsDtoApiModel,
} from '../../api';
import { ApiConfiguration } from '../../config/api-configuration';
import {
AuthService,
BackgroundPatternService,
ThemeService,
} from '../../shared/service';
import { LocalStorageService } from '../../shared/service/local-storage.service';
import {
customEmailValidator,
customPasswordValidator,
} from '../../shared/validator';
type AuthAction = 'signin' | 'signup';
@Component({
selector: 'app-register-root',
standalone: true,
imports: [
CommonModule,
FormsModule,
InputTextModule,
ReactiveFormsModule,
ButtonModule,
CheckboxModule,
PasswordModule,
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 {
public dialogBackgroundStyle: { 'background-image': string } | null = null;
public leftBackgroundStyle: { 'background-image': string } | null = null;
public rightBackgroundStyle: { 'background-image': string } | null = null;
public verified: InputSignal<boolean> = input<boolean>(false);
public login: InputSignal<boolean> = input<boolean>(false);
public email: InputSignal<string> = input<string>('');
public signedOut: InputSignal<boolean> = input<boolean>(true);
public form!: FormGroup;
public rememberMe: FormControl = new FormControl(false);
public isSigninSignal: WritableSignal<boolean> = signal(false);
public isSignupSignal: WritableSignal<boolean> = signal(true);
public isSignUpSuccess: WritableSignal<boolean> = signal(false);
public userSignupSuccess: WritableSignal<boolean> = signal(false);
public isDialogOpen: WritableSignal<boolean> = signal(false);
public isLoading: WritableSignal<boolean> = signal(false);
public displaySkeleton: WritableSignal<boolean> = signal(true);
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 router: Router,
private readonly themeService: ThemeService,
private readonly el: ElementRef,
private readonly backgroundPatternService: BackgroundPatternService,
private readonly localStorageService: LocalStorageService
) {
effect(() => {
if (this.removeQueryParams()) {
this.clearRouteParams();
}
});
}
public ngOnInit(): void {
this.autologin();
this.setBackground();
this.initializeForm();
this.setupValueChanges();
if ((this.email() && this.verified()) || this.login()) {
this.handleRedirect();
this.removeQueryParams.set(true);
}
}
public autologin(): void {
const rememberMe = this.localStorageService.getItem<boolean>('remember-me');
if (rememberMe && !this.signedOut()) {
this.authService
.status()
.pipe(
delay(1500),
takeWhile((response: SuccessDtoApiModel) => response.success, true),
tap({
next: (response: SuccessDtoApiModel) => {
if (response.success) {
this.router.navigate(['/dashboard']);
}
},
finalize: () => this.displaySkeleton.set(false),
})
)
.subscribe();
} else {
this.displaySkeleton.set(false);
}
}
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 openModal(): void {
this.isDialogOpen.set(true);
}
public closeModal(): void {
this.isDialogOpen.set(false);
}
public toggleTheme(): void {
this.themeService.toggleTheme();
this.setBackground();
}
public toggleAction(action: AuthAction): void {
this.resetFormValidation();
if (action === 'signin') {
this.handlePreselect();
this.isSigninSignal.set(true);
this.isSignupSignal.set(false);
} else {
this.isSigninSignal.set(false);
this.isSignupSignal.set(true);
}
}
public onSubmit(): void {
this.markControlsAsTouchedAndDirty(['email', 'password']);
if (this.form?.valid) {
if (this.isSigninSignal()) {
this.signin(this.form.value);
} else {
this.signup(this.form.value);
}
}
}
private handlePreselect(): void {
const rememberMe = this.localStorageService.getItem<boolean>('remember-me');
const email = this.localStorageService.getItem<string>('email');
if (rememberMe) {
this.isSigninSignal.set(true);
this.isSignupSignal.set(false);
}
if (email) {
this.form?.get('email')?.setValue(email);
}
this.rememberMe.setValue(rememberMe);
}
private initializeForm(): void {
const rememberMeValue =
this.localStorageService.getItem<boolean>('remember-me');
const email = this.localStorageService.getItem<string>('email');
if (rememberMeValue) {
this.isSigninSignal.set(true);
this.isSignupSignal.set(false);
}
const emailValue = rememberMeValue && email ? email : '';
this.form = this.formBuilder.group({
email: new FormControl(emailValue, {
validators: [Validators.required, customEmailValidator()],
updateOn: 'change',
}),
password: new FormControl('', {
validators: [Validators.required, customPasswordValidator()],
updateOn: 'change',
}),
});
this.rememberMe.setValue(rememberMeValue);
}
private handleRedirect(): void {
if (this.verified()) {
this.isSigninSignal.set(true);
this.isSignupSignal.set(false);
}
if (this.email()) {
this.form?.get('email')?.setValue(decodeURIComponent(atob(this.email())));
}
if (this.login()) {
this.isSignupSignal.set(true);
this.isSigninSignal.set(false);
}
}
private clearRouteParams(): void {
this.router.navigate([], { queryParams: {} });
}
private setupValueChanges(): void {
this.setupEmailValueChanges();
this.setupPasswordValueChanges();
}
private setupEmailValueChanges(): void {
const emailControl = this.form?.get('email');
emailControl?.valueChanges.subscribe((value: string) => {
if (value?.length >= 4) {
emailControl.setValidators([
Validators.required,
customEmailValidator(),
]);
} else {
emailControl.setValidators([
Validators.required,
Validators.minLength(4),
]);
}
emailControl.updateValueAndValidity({ emitEvent: false });
});
}
private setupPasswordValueChanges(): void {
const passwordControl = this.form?.get('password');
passwordControl?.valueChanges.subscribe((value: string) => {
if (value?.length >= 8) {
passwordControl.setValidators([
Validators.required,
customPasswordValidator(),
]);
} else {
passwordControl.setValidators([
Validators.required,
Validators.minLength(8),
]);
}
passwordControl.updateValueAndValidity({ emitEvent: false });
});
}
private markControlsAsTouchedAndDirty(controlNames: string[]): void {
controlNames.forEach((controlName: string) => {
const control = this.form?.get(controlName);
if (control) {
control.markAsTouched();
control.markAsDirty();
control.updateValueAndValidity();
}
});
}
private resetFormValidation(): void {
['email', 'password'].forEach((controlName: string) => {
this.resetControlValidation(controlName);
});
}
private resetControlValidation(controlName: string): void {
const control = this.form?.get(controlName);
if (control) {
control.reset();
control.markAsPristine();
control.markAsUntouched();
control.updateValueAndValidity();
}
}
private signin(logiCredentials: UserCredentialsDtoApiModel): void {
const rememberMe: boolean = this.rememberMe.value;
if (rememberMe) {
this.localStorageService.setItem<string>('email', logiCredentials.email);
this.localStorageService.setItem<boolean>('remember-me', rememberMe);
}
this.authService
.signin(logiCredentials)
.pipe(
tap(() => this.isLoading.set(true)),
delay(1000),
finalize(() => this.isLoading.set(false))
)
.subscribe((response: SigninResponseDtoApiModel) => {
if (response) {
this.router.navigate(['/dashboard']);
}
});
}
private signup(logiCredentials: UserCredentialsDtoApiModel): void {
this.isLoading.set(true);
this.authService
.signup(logiCredentials)
.pipe(
delay(1000),
tap(() => this.isLoading.set(true)),
finalize(() => this.isLoading.set(false))
)
.subscribe((response: SuccessDtoApiModel) => {
if (response.success) {
this.openModal();
this.userSignupSuccess.set(true);
}
});
}
}
*/
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { import {
@ -425,11 +24,12 @@ import { Router } from '@angular/router';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext';
import { delay, finalize, tap, timer } from 'rxjs'; import { delay, finalize, switchMap, takeWhile, tap, timer } from 'rxjs';
import { import {
Configuration, Configuration,
MagicLinkDtoApiModel, MagicLinkDtoApiModel,
SigninResponseDtoApiModel,
SuccessDtoApiModel, SuccessDtoApiModel,
UserCredentialsDtoApiModel, UserCredentialsDtoApiModel,
VerifyApiService, VerifyApiService,
@ -467,20 +67,24 @@ import { customEmailValidator } from '../../shared/validator';
export class WelcomeRootComponent implements OnInit { export class WelcomeRootComponent implements OnInit {
@ViewChild('passwordInput') public passwordInput!: ElementRef; @ViewChild('passwordInput') public passwordInput!: ElementRef;
public token: InputSignal<string> = input<string>(''); public token: InputSignal<string> = input<string>('');
public signedOut: InputSignal<boolean> = input<boolean>(false);
public signup: InputSignal<boolean> = input<boolean>(false); public signup: InputSignal<boolean> = input<boolean>(false);
public signin: InputSignal<boolean> = input<boolean>(false);
public dialogBackgroundStyle: { 'background-image': string } | null = null; public dialogBackgroundStyle: { 'background-image': string } | null = null;
public leftBackgroundStyle: { 'background-image': string } | null = null; public leftBackgroundStyle: { 'background-image': string } | null = null;
public rightBackgroundStyle: { 'background-image': string } | null = null; public rightBackgroundStyle: { 'background-image': string } | null = null;
public form!: FormGroup; public form!: FormGroup;
public isLoading: WritableSignal<boolean> = signal(false); public isLoading: WritableSignal<boolean> = signal(false);
public isEmailSent: WritableSignal<boolean> = signal(false); public isEmailSent: WritableSignal<boolean> = signal(false);
public isTokenVerifing: WritableSignal<boolean> = signal(false); public displaySkeleton: WritableSignal<boolean> = signal(false);
public isVerifying: WritableSignal<boolean> = signal(false);
public isTokenVerified: WritableSignal<boolean> = signal(false); public isTokenVerified: WritableSignal<boolean> = signal(false);
public errorReasons: WritableSignal<string[]> = signal<string[]>([]); public errorReasons: WritableSignal<string[]> = signal<string[]>([]);
public verificationError: WritableSignal<string | null> = signal< public verificationError: WritableSignal<string | null> = signal<
string | null string | null
>(null); >(null);
public isRegistrationMode: WritableSignal<boolean> = signal(false); public isRegistrationMode: WritableSignal<boolean> = signal(false);
public isAutoLoginInProgress: WritableSignal<boolean> = signal(false);
private removeQueryParams: WritableSignal<boolean> = signal(false); private removeQueryParams: WritableSignal<boolean> = signal(false);
public get isDarkMode(): boolean { public get isDarkMode(): boolean {
@ -498,89 +102,54 @@ export class WelcomeRootComponent implements OnInit {
) { ) {
effect(() => { effect(() => {
if (this.removeQueryParams()) { if (this.removeQueryParams()) {
this.clearRouteParams(); //this.clearRouteParams();
} }
}); });
} }
public ngOnInit(): void { public ngOnInit(): void {
this.autologin();
this.setBackground(); this.setBackground();
this.initializeForm(); this.initializeForm();
this.verifySignupMagicLink(); this.verifySignupMagicLink();
this.verifySigninMagicLink();
}
public autologin(): void {
if (
!this.token() &&
(!this.signin() || !this.signup()) &&
!this.signedOut()
) {
this.isAutoLoginInProgress.set(true);
this.displaySkeleton.set(true);
timer(2000)
.pipe(
switchMap(() => this.authService.status()),
takeWhile((response: SuccessDtoApiModel) => response.success, true),
tap({
next: (response: SuccessDtoApiModel) => {
if (response.success) {
this.router.navigate(['/dashboard']);
}
},
}),
finalize(() => {
this.isAutoLoginInProgress.set(false);
this.displaySkeleton.set(false);
})
)
.subscribe();
}
}
public verifySigninMagicLink(): void {
this.verifyMagicLink(false);
} }
public verifySignupMagicLink(): void { public verifySignupMagicLink(): void {
if (this.token() && this.signup()) { this.verifyMagicLink(true);
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 { public getInputClass(controlName: string): string {
@ -669,8 +238,8 @@ export class WelcomeRootComponent implements OnInit {
if (this.isRegistrationMode()) { if (this.isRegistrationMode()) {
const signupCredentials: UserCredentialsDtoApiModel = { const signupCredentials: UserCredentialsDtoApiModel = {
email: this.form.value.email, email: this.form.getRawValue().email.trim(),
password: this.form.value.password, password: this.form.getRawValue().password.trim(),
}; };
this.signupNewUser(signupCredentials); this.signupNewUser(signupCredentials);
@ -685,6 +254,127 @@ export class WelcomeRootComponent implements OnInit {
} }
} }
private verifyMagicLink(isSignup: boolean): void {
if (this.token() && (isSignup ? this.signup() : this.signin())) {
const token: string = this.extractVerifyToken();
const email: string = this.extractEmail();
const decodedEmail: string = decodeURIComponent(atob(email));
if (token && email) {
if (isSignup) {
this.setupEmailField(decodedEmail);
this.removeQueryParams.set(true);
this.addPasswordFieldToForm();
this.isRegistrationMode.set(true);
}
this.isVerifying.set(true);
this.verificationError.set(null);
this.errorReasons.set([]);
timer(2500)
.pipe(
tap(() => {
this.isVerifying.set(false);
}),
switchMap(() =>
this.verifyApiService.verifyControllerVerifyEmail(
token,
decodedEmail
)
),
tap((response: SuccessDtoApiModel) => {
if (response.success) {
this.isTokenVerified.set(true);
}
}),
delay(1000),
finalize(() => this.handleVerificationFinalize())
)
.subscribe({
next: (response: SuccessDtoApiModel) =>
this.handleVerificationResponse(
response,
isSignup,
decodedEmail,
token
),
error: () => this.handleVerificationError(),
});
}
}
}
private handleVerificationFinalize(): void {
if (!this.verificationError()) {
this.displaySkeleton.set(false);
this.isTokenVerified.set(true);
timer(2000).subscribe(() => {
this.isTokenVerified.set(false);
this.focusPasswordField();
});
}
}
private handleVerificationResponse(
response: SuccessDtoApiModel,
isSignup: boolean,
email: string,
token: string
): void {
if (response.success) {
this.displaySkeleton.set(false);
this.isTokenVerified.set(true);
if (!isSignup) {
timer(2000).subscribe(() => {
this.authService
.signinMagicLink({ email, token })
.subscribe((response: SigninResponseDtoApiModel) => {
if (response) {
this.router.navigate(['/dashboard']);
}
});
});
}
} else {
this.handleVerificationFailure(
'Verification failed. Please check the reasons below:'
);
}
}
private handleVerificationError(): void {
this.isVerifying.set(false);
this.handleVerificationFailure(
'An error occurred during verification. Please check the reasons below:'
);
}
private handleVerificationFailure(message: string): void {
this.verificationError.set(message);
this.errorReasons.set([
'The verification token may have expired.',
'The device you are using may not match the one used to generate the token.',
'The email address may not match our records.',
'The verification link may have been used already.',
'There might be a problem with your internet connection.',
'Our servers might be experiencing issues.',
'The verification service might be temporarily unavailable.',
]);
}
private setupEmailField(email: string): void {
this.form.patchValue({ email });
this.form.get('email')?.setValue(email);
const emailControl = this.form.get('email');
if (emailControl) {
emailControl.disable({ onlySelf: true, emitEvent: false });
emailControl.markAsTouched();
emailControl.setErrors(null);
}
}
private clearRouteParams(): void { private clearRouteParams(): void {
this.router.navigate([], { queryParams: {} }); this.router.navigate([], { queryParams: {} });
} }
@ -703,13 +393,10 @@ export class WelcomeRootComponent implements OnInit {
private initializeForm(): void { private initializeForm(): void {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
email: new FormControl( email: new FormControl('', {
{ value: '', disabled: false }, validators: [Validators.required, customEmailValidator()],
{ updateOn: 'change',
validators: [Validators.required, customEmailValidator()], }),
updateOn: 'change',
}
),
}); });
} }
@ -734,8 +421,7 @@ export class WelcomeRootComponent implements OnInit {
) )
.subscribe((response: SuccessDtoApiModel) => { .subscribe((response: SuccessDtoApiModel) => {
if (response.success) { if (response.success) {
console.log('User signed up successfully'); // Display Modal // You have successfully signed up. Please check your email for the magic link.
// TODO: Redirect to Dashbord
} }
}); });
} }

View File

@ -7,6 +7,7 @@ import { catchError, shareReplay, tap } from 'rxjs/operators';
import { import {
AuthenticationApiService, AuthenticationApiService,
MagicLinkDtoApiModel, MagicLinkDtoApiModel,
MagicLinkSigninDtoApiModel,
SigninResponseDtoApiModel, SigninResponseDtoApiModel,
SuccessDtoApiModel, SuccessDtoApiModel,
UserCredentialsDtoApiModel, UserCredentialsDtoApiModel,
@ -28,6 +29,14 @@ export class AuthService {
this.statusCheck$ = this.initializeStatusCheck(); this.statusCheck$ = this.initializeStatusCheck();
} }
public signinMagicLink(
credentials: MagicLinkSigninDtoApiModel
): Observable<SigninResponseDtoApiModel> {
return this.authenticationApiService
.authControllerMagicLinkSignin(credentials)
.pipe(tap(() => this.isAuthenticatedSignal.set(true)));
}
public sendMagicLink( public sendMagicLink(
email: MagicLinkDtoApiModel email: MagicLinkDtoApiModel
): Observable<SuccessDtoApiModel> { ): Observable<SuccessDtoApiModel> {
@ -42,13 +51,13 @@ export class AuthService {
.pipe(tap(() => this.isAuthenticatedSignal.set(true))); .pipe(tap(() => this.isAuthenticatedSignal.set(true)));
} }
public signin( // public signin(
credentials: UserCredentialsDtoApiModel // credentials: UserCredentialsDtoApiModel
): Observable<SigninResponseDtoApiModel> { // ): Observable<SigninResponseDtoApiModel> {
return this.authenticationApiService // return this.authenticationApiService
.authControllerSignin(credentials) // .authControllerSignin(credentials)
.pipe(tap(() => this.isAuthenticatedSignal.set(true))); // .pipe(tap(() => this.isAuthenticatedSignal.set(true)));
} // }
public signout(): Observable<SuccessDtoApiModel> { public signout(): Observable<SuccessDtoApiModel> {
return this.authenticationApiService return this.authenticationApiService