work in progress
This commit is contained in:
parent
c9a8e9967a
commit
d59d41e1ee
|
@ -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;
|
||||||
|
|
|
@ -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 }
|
||||||
);
|
);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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' },
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> -->
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue