diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index cd5f2a8..27a3fdb 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -109,8 +109,11 @@ export class AuthService { await this.throttleChallenges('register', email, mobile); - const emailCode = generateOtp(); + const returnOtp = this.shouldReturnOtp(); + const emailOtpEnabled = this.isEmailOtpConfigured(); const mobileCode = generateOtp(); + // تا وقتی سرویس ایمیل نداریم، همان کد پیامک برای تأیید ایمیل هم پذیرفته می‌شود + const emailCode = emailOtpEnabled ? generateOtp() : mobileCode; const passwordHash = await bcrypt.hash(dto.password, 10); const payload: RegisterPayload = { organizationName: dto.organizationName.trim(), @@ -134,8 +137,13 @@ export class AuthService { }, }); - const returnOtp = this.shouldReturnOtp(); - await this.dispatchEmailOtp(email, emailCode, returnOtp); + if (emailOtpEnabled) { + await this.dispatchEmailOtp(email, emailCode, returnOtp); + } else { + this.logger.warn( + `[auth-otp-email-skipped] ${email} — mailer disabled; SMS code covers both`, + ); + } await this.dispatchMobileOtp(mobile, mobileCode, returnOtp); return { @@ -143,8 +151,12 @@ export class AuthService { expiresInSeconds: OTP_TTL_SECONDS, maskedEmail: maskEmail(email), maskedMobile: maskMobile(mobile), + emailOtpRequired: emailOtpEnabled, ...(returnOtp - ? { devEmailCode: emailCode, devMobileCode: mobileCode } + ? { + devEmailCode: emailCode, + devMobileCode: mobileCode, + } : {}), }; } @@ -229,7 +241,7 @@ export class AuthService { return this.buildLoginResponse(organization.users[0]); } - /** مرحله ۱ ورود: ایمیل/موبایل + رمز → ارسال OTP روی همان کانال */ + /** مرحله ۱ ورود: ایمیل/موبایل + رمز → OTP ترجیحاً با پیامک */ async startLogin(dto: LoginDto) { const identifier = (dto.identifier ?? dto.email ?? '').trim(); const isEmail = looksLikeEmail(identifier); @@ -252,13 +264,8 @@ export class AuthService { throw new UnauthorizedException('اطلاعات ورود نادرست است'); } - const channel = email ? 'email' : 'mobile'; - if (channel === 'mobile' && !user.mobile) { - throw new BadRequestException('موبایل برای این حساب ثبت نشده است'); - } - if (channel === 'email' && !user.email) { - throw new BadRequestException('ایمیل برای این حساب ثبت نشده است'); - } + const returnOtp = this.shouldReturnOtp(); + const channel = this.resolveLoginOtpChannel(user, returnOtp); await this.throttleChallenges('login', user.email, user.mobile); @@ -277,7 +284,6 @@ export class AuthService { }, }); - const returnOtp = this.shouldReturnOtp(); if (channel === 'email') { await this.dispatchEmailOtp(user.email, code, returnOtp); } else { @@ -286,7 +292,7 @@ export class AuthService { return { challengeId: challenge.id, - channel: channel as 'email' | 'mobile', + channel, expiresInSeconds: OTP_TTL_SECONDS, maskedDestination: channel === 'email' ? maskEmail(user.email) : maskMobile(user.mobile!), @@ -392,15 +398,44 @@ export class AuthService { ); } + /** تا وقتی SMTP/سرویس ایمیل واقعی نداریم خاموش است */ + private isEmailOtpConfigured(): boolean { + return this.config.get('AUTH_EMAIL_OTP_ENABLED') === 'true'; + } + + private resolveLoginOtpChannel( + user: { email: string; mobile: string | null }, + returnOtp: boolean, + ): 'email' | 'mobile' { + // ترجیح: پیامک (سرویس آماده است) + if (user.mobile) { + if (this.sms.isConfigured() || returnOtp) { + return 'mobile'; + } + } + + if (this.isEmailOtpConfigured() || returnOtp) { + return 'email'; + } + + if (!user.mobile) { + throw new BadRequestException( + 'برای ورود باید موبایل روی حساب ثبت باشد (ارسال ایمیل فعال نیست)', + ); + } + + throw new BadRequestException( + 'سامانه پیامک پیکربندی نشده است؛ با پشتیبانی تماس بگیرید', + ); + } + private async dispatchEmailOtp( email: string, code: string, returnOtp: boolean, ) { - // فعلاً سرویس ایمیل پیکربندی نشده؛ در محیط غیرپروداکشن کد لاگ می‌شود this.logger.log(`[auth-otp-email] ${email} => ${code}`); - if (!returnOtp && this.config.get('NODE_ENV') === 'production') { - // بدون ایمیل واقعی در پروداکشن، ورود/ثبت‌نام با ایمیل ممکن نیست مگر AUTH_RETURN_OTP + if (!this.isEmailOtpConfigured() && !returnOtp) { throw new BadRequestException( 'ارسال ایمیل پیکربندی نشده است؛ با پشتیبانی تماس بگیرید', ); diff --git a/apps/web/src/views/RegisterView.vue b/apps/web/src/views/RegisterView.vue index 0881f71..5f7328e 100644 --- a/apps/web/src/views/RegisterView.vue +++ b/apps/web/src/views/RegisterView.vue @@ -78,11 +78,12 @@ async function submitOtp() { error.value = ''; loading.value = true; try { - await auth.verifyRegister( - challenge.value.challengeId, - emailCode.value.trim(), - mobileCode.value.trim(), - ); + const mobile = mobileCode.value.trim(); + const email = + challenge.value.emailOtpRequired === false + ? mobile + : emailCode.value.trim(); + await auth.verifyRegister(challenge.value.challengeId, email, mobile); await router.push('/'); } catch (e) { error.value = apiError(e, 'تأیید کد ناموفق بود'); @@ -264,20 +265,30 @@ function backToForm() {