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,
|
||||
password: passwordHash,
|
||||
});
|
||||
return {
|
||||
message: 'Registration successful.',
|
||||
email: user.email,
|
||||
};
|
||||
const code = await this.issueEmailVerificationCode(user.id, user.email);
|
||||
const message = 'Registration successful. Verification code sent to 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 }> {
|
||||
@@ -83,11 +85,12 @@ export class AuthService {
|
||||
email: dto.email,
|
||||
password: passwordHash,
|
||||
});
|
||||
|
||||
return {
|
||||
message: 'Registration successful.',
|
||||
email: user.email,
|
||||
};
|
||||
const code = await this.issueEmailVerificationCode(user.id, user.email);
|
||||
const message = 'Registration successful. Verification code sent to email.';
|
||||
const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
|
||||
return nodeEnv !== 'production'
|
||||
? { message, email: user.email, debugCode: code }
|
||||
: { message, email: user.email };
|
||||
}
|
||||
|
||||
async login(dto: LoginDto): Promise<AuthResult> {
|
||||
@@ -114,21 +117,57 @@ export class AuthService {
|
||||
): Promise<{ message: string; debugCode?: string }> {
|
||||
const normalizedEmail = dto.email.toLowerCase();
|
||||
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) {
|
||||
return { message };
|
||||
}
|
||||
if (user.isVerified) {
|
||||
return { message: 'Account is already verified' };
|
||||
if (user.isEmailVerified) {
|
||||
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 }> {
|
||||
return {
|
||||
message: 'Account verification is optional and can be requested later',
|
||||
};
|
||||
async verifyEmail(dto: VerifyEmailDto): Promise<{ message: string }> {
|
||||
const normalizedEmail = dto.email.toLowerCase();
|
||||
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> {
|
||||
@@ -210,6 +249,7 @@ export class AuthService {
|
||||
password: passwordHash,
|
||||
avatar: googleUser.avatar ?? '',
|
||||
isVerified: false,
|
||||
isEmailVerified: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,13 @@ export class EmailService {
|
||||
private async send(to: string, subject: string, text: string, html: string): Promise<void> {
|
||||
const enabled = this.configService.get<boolean>('email.enabled', { infer: true });
|
||||
if (!enabled) {
|
||||
return;
|
||||
const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
|
||||
if (nodeEnv !== 'production') {
|
||||
this.logger.warn(`Email delivery is disabled. Skipping email to ${to}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ServiceUnavailableException('Email delivery is disabled');
|
||||
}
|
||||
|
||||
const fromName = this.configService.get<string>('email.fromName', { infer: true }) ?? 'Oudelaa';
|
||||
|
||||
@@ -93,6 +93,12 @@ export class CreateUserDto {
|
||||
@IsBoolean()
|
||||
isVerified?: boolean;
|
||||
|
||||
@ApiProperty({ required: false, default: false })
|
||||
@IsOptional()
|
||||
@Transform(toBoolean)
|
||||
@IsBoolean()
|
||||
isEmailVerified?: boolean;
|
||||
|
||||
@ApiProperty({ required: false, enum: MusicRole, isArray: true })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
|
||||
@@ -96,6 +96,9 @@ export class User {
|
||||
@Prop({ default: false, index: true })
|
||||
isVerified!: boolean;
|
||||
|
||||
@Prop({ default: false })
|
||||
isEmailVerified!: boolean;
|
||||
|
||||
@Prop({ default: '', trim: true, maxlength: 120 })
|
||||
shopName!: string;
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export class UsersService {
|
||||
longitude: dto.longitude,
|
||||
isPrivate: dto.isPrivate ?? false,
|
||||
isVerified: dto.isVerified ?? false,
|
||||
isEmailVerified: dto.isEmailVerified ?? false,
|
||||
musicRoles: roles,
|
||||
experienceLevel: dto.experienceLevel ?? ExperienceLevel.BEGINNER,
|
||||
musicGenres: dto.musicGenres ?? [],
|
||||
@@ -295,7 +296,7 @@ export class UsersService {
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم