71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
import logging
|
|
from typing import Protocol
|
|
|
|
import requests
|
|
from django.conf import settings
|
|
from django.utils.module_loading import import_string
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SMSError(Exception):
|
|
pass
|
|
|
|
|
|
class SMSBackend(Protocol):
|
|
def send_otp(self, phone_number: str, code: str) -> None: ...
|
|
|
|
|
|
class ConsoleSMSBackend:
|
|
"""Dev backend — logs the OTP instead of sending. Never use in production."""
|
|
|
|
def send_otp(self, phone_number: str, code: str) -> None:
|
|
logger.warning("[SMS-CONSOLE] OTP for %s: %s", phone_number, code)
|
|
|
|
|
|
class SMSIRBackend:
|
|
"""sms.ir template-based OTP via /v1/send/verify."""
|
|
|
|
def __init__(self):
|
|
self.api_key = settings.SMSIR_API_KEY
|
|
self.template_id = settings.SMSIR_TEMPLATE_ID
|
|
self.param_name = settings.SMSIR_TEMPLATE_PARAM_NAME
|
|
self.base_url = settings.SMSIR_BASE_URL.rstrip("/")
|
|
self.timeout = 15
|
|
if not self.api_key:
|
|
raise SMSError("SMSIR_API_KEY is not configured")
|
|
if not self.template_id:
|
|
raise SMSError("SMSIR_TEMPLATE_ID is not configured")
|
|
|
|
def send_otp(self, phone_number: str, code: str) -> None:
|
|
url = f"{self.base_url}/v1/send/verify"
|
|
headers = {"x-api-key": self.api_key, "Content-Type": "application/json"}
|
|
payload = {
|
|
"mobile": phone_number,
|
|
"templateId": int(self.template_id),
|
|
"parameters": [{"name": self.param_name, "value": str(code)}],
|
|
}
|
|
try:
|
|
resp = requests.post(url, json=payload, headers=headers, timeout=self.timeout)
|
|
except requests.RequestException as e:
|
|
raise SMSError(f"SMS network error: {e}") from e
|
|
|
|
try:
|
|
data = resp.json()
|
|
except ValueError as e:
|
|
raise SMSError(f"SMS non-JSON response (HTTP {resp.status_code})") from e
|
|
|
|
if not (200 <= resp.status_code < 300) or data.get("status") != 1:
|
|
msg = data.get("message") or f"HTTP {resp.status_code}"
|
|
raise SMSError(f"sms.ir error: {msg}")
|
|
|
|
logger.info("OTP delivered to %s via sms.ir template %s", phone_number, self.template_id)
|
|
|
|
|
|
_DEFAULT_BACKEND = "apps.accounts.services.sms.ConsoleSMSBackend"
|
|
|
|
|
|
def get_sms_backend() -> SMSBackend:
|
|
backend_path = getattr(settings, "SMS_BACKEND", _DEFAULT_BACKEND)
|
|
return import_string(backend_path)()
|