work in progress

This commit is contained in:
Igor Hrenowitsch Propisnov 2024-09-09 14:40:55 +02:00
parent c9a8e9967a
commit d59d41e1ee
19 changed files with 369 additions and 1415 deletions

View File

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

View File

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

View File

@ -1,3 +1,4 @@
export * from './user-credentials.dto';
export * from './signin-response.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 { SessionService } from 'src/modules/session/services/session.service';
import { EncryptionService, SuccessDto } from 'src/shared';
@ -28,7 +32,10 @@ export class AuthService {
private readonly sessionService: SessionService
) {}
public async sendMagicLink(magiclink: MagicLinkDto): Promise<SuccessDto> {
public async sendMagicLink(
magiclink: MagicLinkDto,
userAgent: string
): Promise<SuccessDto> {
try {
const existingUser = await this.userCredentialsRepository.findUserByEmail(
magiclink.email
@ -37,7 +44,9 @@ export class AuthService {
if (existingUser) {
const token =
await this.emailVerificationService.generateEmailVerificationTokenForMagicLink(
magiclink.email
magiclink.email,
userAgent,
existingUser.id
);
await this.passwordConfirmationMailService.sendLoginLinkEmail(
@ -45,24 +54,16 @@ export class AuthService {
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(
const token =
await this.emailVerificationService.generateEmailVerificationTokenForMagicLink(
magiclink.email,
token
userAgent
);
} else {
throw new ConflictException('EMAIL_ALREADY_SUBMITTED', {
message: 'This email has already been submitted for registration.',
});
}
await this.passwordConfirmationMailService.sendRegistrationLinkEmail(
magiclink.email,
token
);
}
return { success: true };
@ -105,11 +106,6 @@ export class AuthService {
// user.id
// );
// await this.passwordConfirmationMailService.sendPasswordConfirmationMail(
// user.email,
// token
// );
return {
success: true,
};
@ -125,28 +121,31 @@ export class AuthService {
}
public async validateUser(
token: string,
email: string,
password: string
userAgent: string
): Promise<UserCredentials> {
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);
if (!user) {
throw new ForbiddenException('INVALID_CREDENTIALS');
}
const passwordMatch = await EncryptionService.compareHash(
password,
user.hashedPassword
);
if (!passwordMatch) {
throw new ForbiddenException('INVALID_CREDENTIALS');
throw new UnauthorizedException('User not found');
}
return user;
} catch (error) {
if (error instanceof ForbiddenException) {
if (error instanceof UnauthorizedException) {
throw error;
} else {
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> {
try {
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 { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
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';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
public constructor(private readonly authService: AuthService) {
public constructor(
private authService: AuthService,
private emailVerificationService: EmailVerificationService
) {
super({
usernameField: 'email',
passwordField: 'password',
passwordField: 'token',
passReqToCallback: true,
});
}
public async validate(
request: Request,
email: string,
password: string
): Promise<SigninResponseDto & { userAgent: string }> {
const user = await this.authService.validateUser(email, password);
public async validate(request: Request): Promise<any> {
const { token, email }: MagicLinkSigninDto = request.body;
if (!token || !email) {
throw new UnauthorizedException('Missing token or email');
}
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) {
throw new UnauthorizedException();
throw new UnauthorizedException('User not found');
}
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 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' },

View File

@ -1,9 +1,5 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { SessionException } from 'src/shared/exceptions';
import { SessionService } from '../services/session.service';
@ -19,20 +15,20 @@ export class SessionGuard implements CanActivate {
const session = await this.sessionService.findSessionBySessionId(sessionId);
if (!session) {
throw new UnauthorizedException('Session not found.');
throw new SessionException('Session not found.');
}
const isExpired = await this.sessionService.isSessioExpired(session);
if (isExpired) {
throw new UnauthorizedException('Session expired.');
throw new SessionException('Session expired.');
}
const userAgentInSession = JSON.parse(session.json).passport.user
.userAgent as string;
if (userAgentInSession !== currentAgent) {
throw new UnauthorizedException('User agent mismatch.');
throw new SessionException('User agent mismatch.');
}
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 { SuccessDto } from 'src/shared';
import { Public } from 'src/shared/decorator';
@ -21,22 +28,15 @@ export class VerifyController {
@HttpCode(HttpStatus.OK)
public async verifyEmail(
@Query('token') tokenToVerify: string,
@Query('email') emailToVerify: string
@Query('email') emailToVerify: string,
@Req() request: Request
): Promise<SuccessDto> {
const userAgent = request.headers['user-agent'] || 'Unknown';
return this.emailVerificationService.verifyEmail(
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 { InjectRepository } from '@nestjs/typeorm';
import { EmailVerification } from 'src/entities';
import { LessThan, MoreThan, Repository } from 'typeorm';
import { LessThan, Repository } from 'typeorm';
@Injectable()
export class EmailVerifyRepository {
@ -14,30 +14,17 @@ export class EmailVerifyRepository {
token: string,
expiresAt: Date,
email: string,
userId: string | null
userId: string | null,
userAgent: string
): Promise<void> {
await this.repository.delete({ email });
await this.repository.save({
token,
expiresAt,
email,
user: userId ? { id: userId } : null,
});
}
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),
},
userAgent,
});
}
@ -60,12 +47,6 @@ export class EmailVerifyRepository {
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> {
const currentDate = new Date();
@ -73,20 +54,4 @@ export class EmailVerifyRepository {
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 { 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 {
InternalServerErrorException,
TokenExpiredException,
UserAgentMismatchException,
} from 'src/shared/exceptions';
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
import { EmailVerifyRepository } from '../repositories';
@Injectable()
export class EmailVerificationService {
public constructor(
private readonly emailVerifyRepository: EmailVerifyRepository,
private readonly userDataRepository: UserDataRepository,
private readonly sessionService: SessionService
private readonly emailVerifyRepository: EmailVerifyRepository
) {}
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(
email: string
email: string,
userAgent: string,
userid?: string
): Promise<string> {
try {
const verificationToken = await this.createVerificationToken();
@ -68,7 +29,8 @@ export class EmailVerificationService {
verificationToken,
expiresAt,
email,
null
userid || null,
userAgent
);
return verificationToken;
@ -85,7 +47,8 @@ export class EmailVerificationService {
public async verifyEmail(
tokenToVerify: string,
emailToVerify: string
emailToVerify: string,
userAgent: string
): Promise<SuccessDto> {
try {
const token = await this.emailVerifyRepository.findByTokenAndEmail(
@ -97,6 +60,13 @@ export class EmailVerificationService {
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();
if (token.expiresAt.getTime() < currentDate.getTime()) {
@ -113,35 +83,16 @@ export class EmailVerificationService {
if (error instanceof TokenExpiredException) {
throw error;
}
if (error instanceof UserAgentMismatchException) {
throw error;
}
throw new InternalServerErrorException('EMAIL_VERIFICATION_ERROR', {
message: 'An error occurred while verifying the email.',
});
}
}
public async isEmailVerified(sessionID: string): Promise<boolean> {
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();
public async deleteAllExpiredTokens(): Promise<void> {
await this.emailVerifyRepository.deleteAllExpiredTokens();
}
@ -150,12 +101,4 @@ export class EmailVerificationService {
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 './not-found.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 response = ctx.getResponse<Response>();
//console.error('Exception caught:', exception);
console.error('Exception caught:', exception);
let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
let message: string = 'Internal server error';

View File

@ -24,25 +24,11 @@ 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.router.navigate(['/event/create']);
}
public closeEmailVerificationModal(): void {
(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">
<!-- Left side (hidden on mobile) -->
<div
[ngStyle]="leftBackgroundStyle"
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>
</div>
<div class="flex-1 flex flex-col justify-center px-12">
@if (isTokenVerifing()) {
@if (displaySkeleton()) {
<!-- Skeleton loader for the main content -->
<div class="space-y-6">
<div class="skeleton h-8 w-3/4"></div>
@ -823,8 +181,7 @@
}
</div>
<div class="flex-1 flex flex-col justify-end pb-16 px-12">
@if (isTokenVerifing()) {
<!-- Skeleton loader for the quote -->
@if (displaySkeleton()) {
<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>
@ -888,8 +245,7 @@
<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 -->
@if (displaySkeleton()) {
<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>
@ -1012,7 +368,7 @@
</div>
}
@if (!isRegistrationMode() && !isTokenVerifing()) {
@if (!isRegistrationMode() && !displaySkeleton()) {
<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>
@ -1025,7 +381,7 @@
</ul>
</div>
</div>
} @else if (isTokenVerifing()) {
} @else if (displaySkeleton()) {
<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>
@ -1101,7 +457,7 @@
<div
class="modal modal-open"
*ngIf="isTokenVerifing() || isTokenVerified() || verificationError()"
*ngIf="isTokenVerified() || isVerifying() || verificationError()"
tabindex="-1"
aria-labelledby="verify-modal-title"
aria-describedby="verify-modal-description"
@ -1113,8 +469,8 @@
<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()">
[class.opacity-100]="isVerifying() && !verificationError()"
[class.opacity-0]="!isVerifying() || verificationError()">
<span class="loading loading-spinner loading-lg"></span>
</div>
<div
@ -1184,8 +540,8 @@
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 class="mt-6 flex justify-center">
<a href="/" class="btn btn-primary">Back to Login</a>
</div>
}
</div>
@ -1193,56 +549,3 @@
</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 { HttpClientModule } from '@angular/common/http';
import {
@ -425,11 +24,12 @@ import { Router } from '@angular/router';
import { ButtonModule } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { delay, finalize, tap, timer } from 'rxjs';
import { delay, finalize, switchMap, takeWhile, tap, timer } from 'rxjs';
import {
Configuration,
MagicLinkDtoApiModel,
SigninResponseDtoApiModel,
SuccessDtoApiModel,
UserCredentialsDtoApiModel,
VerifyApiService,
@ -467,20 +67,24 @@ import { customEmailValidator } from '../../shared/validator';
export class WelcomeRootComponent implements OnInit {
@ViewChild('passwordInput') public passwordInput!: ElementRef;
public token: InputSignal<string> = input<string>('');
public signedOut: 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 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 displaySkeleton: WritableSignal<boolean> = signal(false);
public isVerifying: 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);
public isAutoLoginInProgress: WritableSignal<boolean> = signal(false);
private removeQueryParams: WritableSignal<boolean> = signal(false);
public get isDarkMode(): boolean {
@ -498,89 +102,54 @@ export class WelcomeRootComponent implements OnInit {
) {
effect(() => {
if (this.removeQueryParams()) {
this.clearRouteParams();
//this.clearRouteParams();
}
});
}
public ngOnInit(): void {
this.autologin();
this.setBackground();
this.initializeForm();
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 {
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);
}
}
}
this.verifyMagicLink(true);
}
public getInputClass(controlName: string): string {
@ -669,8 +238,8 @@ export class WelcomeRootComponent implements OnInit {
if (this.isRegistrationMode()) {
const signupCredentials: UserCredentialsDtoApiModel = {
email: this.form.value.email,
password: this.form.value.password,
email: this.form.getRawValue().email.trim(),
password: this.form.getRawValue().password.trim(),
};
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 {
this.router.navigate([], { queryParams: {} });
}
@ -703,13 +393,10 @@ export class WelcomeRootComponent implements OnInit {
private initializeForm(): void {
this.form = this.formBuilder.group({
email: new FormControl(
{ value: '', disabled: false },
{
validators: [Validators.required, customEmailValidator()],
updateOn: 'change',
}
),
email: new FormControl('', {
validators: [Validators.required, customEmailValidator()],
updateOn: 'change',
}),
});
}
@ -734,8 +421,7 @@ export class WelcomeRootComponent implements OnInit {
)
.subscribe((response: SuccessDtoApiModel) => {
if (response.success) {
console.log('User signed up successfully');
// TODO: Redirect to Dashbord
// Display Modal // You have successfully signed up. Please check your email for the magic link.
}
});
}

View File

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