feat(backend): migrate auth and notifications off email
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-21 10:28:04 +03:30
parent b4903f7cb1
commit b7b21a6cc6
35 changed files with 2784 additions and 1390 deletions

View File

@@ -1,99 +1,90 @@
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.conf import settings
from django.utils.html import strip_tags
from celery import shared_task
import logging
from apps.users.models import User
import requests
from celery import shared_task
from django.conf import settings
logger = logging.getLogger(__name__)
SMS_ENDPOINT = "https://api.sms.ir/v1/send/verify"
SMS_TEMPLATE_MAP = {
"auth_register_otp": "SMS_AUTH_OTP_TEMPLATE_ID",
"auth_login_otp": "SMS_AUTH_OTP_TEMPLATE_ID",
"auth_reset_password_otp": "SMS_AUTH_OTP_TEMPLATE_ID",
"auth_verify_mobile_otp": "SMS_AUTH_OTP_TEMPLATE_ID",
"event_cancellation": "SMS_EVENT_CANCELLATION_TEMPLATE_ID",
"event_reschedule": "SMS_EVENT_RESCHEDULE_TEMPLATE_ID",
"payment_status": "SMS_PAYMENT_STATUS_TEMPLATE_ID",
}
def _template_id_for_kind(kind: str) -> str:
setting_name = SMS_TEMPLATE_MAP.get(kind, "")
return getattr(settings, setting_name, "") if setting_name else ""
def _send_sms(receptor: str, template_id: str | int, variables: list[dict] | None = None):
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"x-api-key": settings.SMS_APIKEY,
}
payload = {
"mobile": receptor,
"templateId": int(template_id),
"parameters": variables or [],
}
response = requests.post(
SMS_ENDPOINT,
json=payload,
headers=headers,
timeout=10,
)
response.raise_for_status()
return response
@shared_task(bind=True, max_retries=3)
def send_critical_sms(self, mobile: str, kind: str, code_or_message: str):
try:
template_id = _template_id_for_kind(kind)
if not template_id or not settings.SMS_APIKEY:
logger.info(
"SMS skipped for mobile=%s kind=%s template=%s configured=%s payload=%s",
mobile,
kind,
bool(template_id),
bool(settings.SMS_APIKEY),
code_or_message,
)
return {"mobile": mobile, "kind": kind, "sent": False}
variables = [{"name": "OTP", "value": str(code_or_message)}]
if kind in {"event_cancellation", "event_reschedule", "payment_status"}:
variables = [{"name": "MESSAGE", "value": str(code_or_message)}]
_send_sms(mobile, template_id, variables=variables)
logger.info("SMS sent to %s for kind=%s", mobile, kind)
return {"mobile": mobile, "kind": kind, "sent": True}
except Exception as exc:
logger.error("Failed to send SMS to %s for kind=%s: %s", mobile, kind, exc)
raise self.retry(exc=exc, countdown=60)
@shared_task(bind=True, max_retries=1)
def send_verification_email(self, user_id, verification_url):
try:
user = User.objects.get(id=user_id)
subject = 'تایید ایمیل | انجمن علمی مهندسی کامپیوتر'
html_message = render_to_string('emails/verification_email.html', {
'user': user,
'verification_url': verification_url,
})
plain_message = strip_tags(html_message)
send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
html_message=html_message,
fail_silently=False,
)
logger.info(f"Verification email sent to {user.email}")
return f"Verification email sent to {user.email}"
except Exception as exc:
logger.error(f"Failed to send verification email: {exc}")
raise self.retry(exc=exc, countdown=60)
logger.info("Legacy verification email task skipped for user=%s url=%s", user_id, verification_url)
return {"skipped": True}
@shared_task(bind=True, max_retries=3)
@shared_task(bind=True, max_retries=1)
def send_password_reset_email(self, user_id, reset_url):
try:
user = User.objects.get(id=user_id)
subject = 'بازیابی رمز عبور | انجمن علمی مهندسی کامپیوتر'
html_message = render_to_string('emails/password_reset_email.html', {
'user': user,
'reset_url': reset_url,
})
plain_message = strip_tags(html_message)
send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
html_message=html_message,
fail_silently=False,
)
logger.info(f"Password reset email sent to {user.email}")
return f"Password reset email sent to {user.email}"
except Exception as exc:
logger.error(f"Failed to send password reset email: {exc}")
raise self.retry(exc=exc, countdown=60)
logger.info("Legacy password reset email task skipped for user=%s url=%s", user_id, reset_url)
return {"skipped": True}
@shared_task(bind=True, max_retries=3)
@shared_task(bind=True, max_retries=1)
def send_email_verified_success(self, user_id: int):
"""
ارسال ایمیل «ایمیل شما با موفقیت تأیید شد» پس از تغییر وضعیت تأیید.
"""
try:
user = User.objects.get(pk=user_id)
subject = "تأیید ایمیل شما با موفقیت انجام شد"
context = {
"user": user,
"home_url": getattr(settings, "FRONTEND_ROOT", "/"),
}
html_message = render_to_string("emails/verification_success.html", context)
plain_message = strip_tags(html_message)
send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
html_message=html_message,
fail_silently=False,
)
logger.info(f"verified success email sent to {user.email}")
return f"verified success email sent to {user.email}"
except Exception as exc:
logger.error(f"Failed to send verified success email: {exc}")
raise self.retry(exc=exc, countdown=60)
logger.info("Legacy verification success email task skipped for user=%s", user_id)
return {"skipped": True}