Restore email OTP verification flow
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
@@ -62,10 +62,12 @@ export class AuthService {
|
|||||||
username: generatedUsername,
|
username: generatedUsername,
|
||||||
password: passwordHash,
|
password: passwordHash,
|
||||||
});
|
});
|
||||||
return {
|
const code = await this.issueEmailVerificationCode(user.id, user.email);
|
||||||
message: 'Registration successful.',
|
const message = 'Registration successful. Verification code sent to email.';
|
||||||
email: user.email,
|
const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
|
||||||
};
|
return nodeEnv !== 'production'
|
||||||
|
? { message, email: user.email, debugCode: code }
|
||||||
|
: { message, email: user.email };
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerBasic(dto: RegisterBasicDto): Promise<{ message: string; email: string; debugCode?: string }> {
|
async registerBasic(dto: RegisterBasicDto): Promise<{ message: string; email: string; debugCode?: string }> {
|
||||||
@@ -83,11 +85,12 @@ export class AuthService {
|
|||||||
email: dto.email,
|
email: dto.email,
|
||||||
password: passwordHash,
|
password: passwordHash,
|
||||||
});
|
});
|
||||||
|
const code = await this.issueEmailVerificationCode(user.id, user.email);
|
||||||
return {
|
const message = 'Registration successful. Verification code sent to email.';
|
||||||
message: 'Registration successful.',
|
const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
|
||||||
email: user.email,
|
return nodeEnv !== 'production'
|
||||||
};
|
? { message, email: user.email, debugCode: code }
|
||||||
|
: { message, email: user.email };
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(dto: LoginDto): Promise<AuthResult> {
|
async login(dto: LoginDto): Promise<AuthResult> {
|
||||||
@@ -114,21 +117,57 @@ export class AuthService {
|
|||||||
): Promise<{ message: string; debugCode?: string }> {
|
): Promise<{ message: string; debugCode?: string }> {
|
||||||
const normalizedEmail = dto.email.toLowerCase();
|
const normalizedEmail = dto.email.toLowerCase();
|
||||||
const user = await this.usersService.findByEmail(normalizedEmail);
|
const user = await this.usersService.findByEmail(normalizedEmail);
|
||||||
const message = 'Account verification is optional and can be requested later';
|
const message = 'If this email exists, a verification code was sent';
|
||||||
if (!user || user.isDisabled) {
|
if (!user || user.isDisabled) {
|
||||||
return { message };
|
return { message };
|
||||||
}
|
}
|
||||||
if (user.isVerified) {
|
if (user.isEmailVerified) {
|
||||||
return { message: 'Account is already verified' };
|
return { message: 'Email is already verified' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { message: 'Account is not verified yet. Verification can be requested later.' };
|
const code = await this.issueEmailVerificationCode(user.id, normalizedEmail);
|
||||||
|
const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
|
||||||
|
return nodeEnv !== 'production' ? { message, debugCode: code } : { message };
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyEmail(_dto: VerifyEmailDto): Promise<{ message: string }> {
|
async verifyEmail(dto: VerifyEmailDto): Promise<{ message: string }> {
|
||||||
return {
|
const normalizedEmail = dto.email.toLowerCase();
|
||||||
message: 'Account verification is optional and can be requested later',
|
const user = await this.usersService.findByEmail(normalizedEmail);
|
||||||
};
|
if (!user || user.isDisabled) {
|
||||||
|
throw new UnauthorizedException('Invalid or expired verification code');
|
||||||
|
}
|
||||||
|
if (user.isEmailVerified) {
|
||||||
|
return { message: 'Email is already verified' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeRecord = await this.authRepository.findLatestActiveEmailVerificationCode(user.id);
|
||||||
|
if (!codeRecord) {
|
||||||
|
throw new UnauthorizedException('Invalid or expired verification code');
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAttempts = this.configService.get<number>('emailVerification.maxAttempts', { infer: true });
|
||||||
|
if (codeRecord.attempts >= maxAttempts) {
|
||||||
|
await this.authRepository.markEmailVerificationCodeUsed(codeRecord.id);
|
||||||
|
throw new UnauthorizedException('Verification code attempts exceeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await compareHash(dto.code, codeRecord.codeHash);
|
||||||
|
if (!isMatch) {
|
||||||
|
await this.authRepository.incrementEmailVerificationAttempts(codeRecord.id);
|
||||||
|
const attemptsAfter = codeRecord.attempts + 1;
|
||||||
|
if (attemptsAfter >= maxAttempts) {
|
||||||
|
await this.authRepository.markEmailVerificationCodeUsed(codeRecord.id);
|
||||||
|
}
|
||||||
|
throw new UnauthorizedException('Invalid or expired verification code');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.usersService.markEmailVerified(user.id),
|
||||||
|
this.authRepository.markEmailVerificationCodeUsed(codeRecord.id),
|
||||||
|
this.authRepository.markAllEmailVerificationCodesUsedByUser(user.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { message: 'Email verified successfully' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh(dto: RefreshTokenDto): Promise<AuthResult> {
|
async refresh(dto: RefreshTokenDto): Promise<AuthResult> {
|
||||||
@@ -210,6 +249,7 @@ export class AuthService {
|
|||||||
password: passwordHash,
|
password: passwordHash,
|
||||||
avatar: googleUser.avatar ?? '',
|
avatar: googleUser.avatar ?? '',
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
|
isEmailVerified: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,15 @@ export class EmailService {
|
|||||||
private async send(to: string, subject: string, text: string, html: string): Promise<void> {
|
private async send(to: string, subject: string, text: string, html: string): Promise<void> {
|
||||||
const enabled = this.configService.get<boolean>('email.enabled', { infer: true });
|
const enabled = this.configService.get<boolean>('email.enabled', { infer: true });
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
|
const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
|
||||||
|
if (nodeEnv !== 'production') {
|
||||||
|
this.logger.warn(`Email delivery is disabled. Skipping email to ${to}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new ServiceUnavailableException('Email delivery is disabled');
|
||||||
|
}
|
||||||
|
|
||||||
const fromName = this.configService.get<string>('email.fromName', { infer: true }) ?? 'Oudelaa';
|
const fromName = this.configService.get<string>('email.fromName', { infer: true }) ?? 'Oudelaa';
|
||||||
const fromEmail = this.configService.get<string>('email.fromEmail', { infer: true }) ?? '';
|
const fromEmail = this.configService.get<string>('email.fromEmail', { infer: true }) ?? '';
|
||||||
if (!fromEmail) {
|
if (!fromEmail) {
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ export class CreateUserDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isVerified?: boolean;
|
isVerified?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(toBoolean)
|
||||||
|
@IsBoolean()
|
||||||
|
isEmailVerified?: boolean;
|
||||||
|
|
||||||
@ApiProperty({ required: false, enum: MusicRole, isArray: true })
|
@ApiProperty({ required: false, enum: MusicRole, isArray: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export class User {
|
|||||||
@Prop({ default: false, index: true })
|
@Prop({ default: false, index: true })
|
||||||
isVerified!: boolean;
|
isVerified!: boolean;
|
||||||
|
|
||||||
|
@Prop({ default: false })
|
||||||
|
isEmailVerified!: boolean;
|
||||||
|
|
||||||
@Prop({ default: '', trim: true, maxlength: 120 })
|
@Prop({ default: '', trim: true, maxlength: 120 })
|
||||||
shopName!: string;
|
shopName!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export class UsersService {
|
|||||||
longitude: dto.longitude,
|
longitude: dto.longitude,
|
||||||
isPrivate: dto.isPrivate ?? false,
|
isPrivate: dto.isPrivate ?? false,
|
||||||
isVerified: dto.isVerified ?? false,
|
isVerified: dto.isVerified ?? false,
|
||||||
|
isEmailVerified: dto.isEmailVerified ?? false,
|
||||||
musicRoles: roles,
|
musicRoles: roles,
|
||||||
experienceLevel: dto.experienceLevel ?? ExperienceLevel.BEGINNER,
|
experienceLevel: dto.experienceLevel ?? ExperienceLevel.BEGINNER,
|
||||||
musicGenres: dto.musicGenres ?? [],
|
musicGenres: dto.musicGenres ?? [],
|
||||||
@@ -295,7 +296,7 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async markEmailVerified(userId: string): Promise<void> {
|
async markEmailVerified(userId: string): Promise<void> {
|
||||||
const updated = await this.usersRepository.updateById(userId, { isVerified: true });
|
const updated = await this.usersRepository.updateById(userId, { isEmailVerified: true });
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم