CI/CD
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import http.client
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_otp_sms(phone_number: str, otp_code: str) -> bool:
|
||||
"""Send OTP code via SMS.ir bulk API.
|
||||
|
||||
Returns True on success, False on failure.
|
||||
"""
|
||||
api_key = getattr(settings, "SMS_IR_API_KEY", "")
|
||||
line_number = getattr(settings, "SMS_IR_LINE_NUMBER", 300000000000)
|
||||
|
||||
if not api_key:
|
||||
logger.error("SMS_IR_API_KEY is not configured.")
|
||||
return False
|
||||
|
||||
message_text = f"کد تایید شما: {otp_code}"
|
||||
|
||||
payload = json.dumps({
|
||||
"lineNumber": line_number,
|
||||
"messageText": message_text,
|
||||
"mobiles": [phone_number],
|
||||
"sendDateTime": None,
|
||||
})
|
||||
|
||||
headers = {
|
||||
"X-API-KEY": api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
conn = http.client.HTTPSConnection("api.sms.ir")
|
||||
conn.request("POST", "/v1/send/bulk", payload, headers)
|
||||
res = conn.getresponse()
|
||||
data = res.read().decode("utf-8")
|
||||
conn.close()
|
||||
|
||||
response = json.loads(data)
|
||||
status_code = response.get("status")
|
||||
|
||||
if res.status == 200 and status_code == 1:
|
||||
logger.info("SMS sent successfully to %s", phone_number)
|
||||
return True
|
||||
|
||||
logger.warning(
|
||||
"SMS.ir returned unexpected response: HTTP %s, body: %s",
|
||||
res.status,
|
||||
data,
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to send SMS to %s", phone_number)
|
||||
return False
|
||||
+52
-33
@@ -2,12 +2,15 @@ import secrets
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.signing import TimestampSigner
|
||||
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from account.models import User
|
||||
from .serializers import RequestOTPSerializer, VerifyOTPSerializer
|
||||
from .sms_service import send_otp_sms
|
||||
|
||||
|
||||
OTP_TTL_SECONDS = 300
|
||||
@@ -15,24 +18,16 @@ OTP_SIGNER = TimestampSigner(salt="auth.otp")
|
||||
|
||||
|
||||
def _auth_user_to_data(user):
|
||||
"""Build AuthUser-shaped dict from Django User (or mock)."""
|
||||
# if user is None or not getattr(user, "pk", None):
|
||||
# return None
|
||||
# return {
|
||||
# "id": user.id,
|
||||
# "username": getattr(user, "username", "") or "",
|
||||
# "email": getattr(user, "email", "") or "",
|
||||
# "first_name": getattr(user, "first_name", "") or "",
|
||||
# "last_name": getattr(user, "last_name", "") or "",
|
||||
# "phone_number": getattr(user, "phone_number", "") or "",
|
||||
# }
|
||||
"""Build AuthUser-shaped dict from Django User."""
|
||||
if user is None or not getattr(user, "pk", None):
|
||||
return None
|
||||
return {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"email": "testuser@example.com",
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
"phone_number": "09123456789",
|
||||
"id": user.id,
|
||||
"username": getattr(user, "username", "") or "",
|
||||
"email": getattr(user, "email", "") or "",
|
||||
"first_name": getattr(user, "first_name", "") or "",
|
||||
"last_name": getattr(user, "last_name", "") or "",
|
||||
"phone_number": getattr(user, "phone_number", "") or "",
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +53,13 @@ class AuthenticationView(APIView):
|
||||
cache.set(f"otp_code:{phone_number}", otp_code, timeout=OTP_TTL_SECONDS)
|
||||
otp_token = OTP_SIGNER.sign(phone_number)
|
||||
|
||||
sms_sent = send_otp_sms(phone_number, otp_code)
|
||||
|
||||
payload = {"code": 200, "msg": "success", "token": otp_token}
|
||||
if not sms_sent:
|
||||
payload["sms_warning"] = "SMS delivery failed; OTP stored server-side."
|
||||
if settings.DEBUG:
|
||||
payload["debug_note"] = "OTP code is returned only when DEBUG=1."
|
||||
payload["debug_otp"] = otp_code
|
||||
|
||||
return Response(payload, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -68,26 +67,46 @@ class AuthenticationView(APIView):
|
||||
serializer = VerifyOTPSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# TODO: validate token + otp_code, load or create user, issue JWT/session token
|
||||
auth_token = "1234567890"
|
||||
user_data = _auth_user_to_data(getattr(request, "user", None))
|
||||
if user_data is None:
|
||||
user_data = {
|
||||
"id": 0,
|
||||
"username": "",
|
||||
"email": "",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"phone_number": "",
|
||||
}
|
||||
token = serializer.validated_data["token"]
|
||||
otp_code = serializer.validated_data["otp_code"].strip()
|
||||
|
||||
try:
|
||||
phone_number = OTP_SIGNER.unsign(
|
||||
token, max_age=OTP_TTL_SECONDS
|
||||
)
|
||||
except (BadSignature, SignatureExpired):
|
||||
return Response(
|
||||
{"code": 400, "msg": "Token is invalid or expired."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
cached_otp = cache.get(f"otp_code:{phone_number}")
|
||||
if cached_otp is None or cached_otp != otp_code:
|
||||
return Response(
|
||||
{"code": 400, "msg": "OTP code is invalid or expired."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
cache.delete(f"otp_code:{phone_number}")
|
||||
|
||||
user, created = User.objects.get_or_create(
|
||||
phone_number=phone_number,
|
||||
defaults={"username": phone_number},
|
||||
)
|
||||
|
||||
refresh = RefreshToken.for_user(user)
|
||||
|
||||
user_data = _auth_user_to_data(user)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": user_data,
|
||||
"token": auth_token,
|
||||
"token": {
|
||||
"access": str(refresh.access_token),
|
||||
"refresh": str(refresh),
|
||||
},
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user