First commit

This commit is contained in:
2026-02-19 01:19:22 +03:30
commit a39d83c241
32 changed files with 1350 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
+7
View File
@@ -0,0 +1,7 @@
from django.apps import AppConfig
class AuthConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "auth"
label = "auth_api" # Avoid clash with django.contrib.auth (label "auth")
+59
View File
@@ -0,0 +1,59 @@
{
"info": {
"name": "Auth",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"description": "Auth API. Request OTP, Verify OTP."
},
"item": [
{
"name": "Request OTP",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"phone_number\": \"\"\n}"
},
"url": "{{baseUrl}}/api/auth/request-otp/",
"description": "Request OTP for the given phone number. In DEBUG mode, response includes debug_note."
},
"response": [
{
"name": "Success",
"status": "OK",
"code": 200,
"body": "{\n \"code\": 200,\n \"msg\": \"success\",\n \"token\": \"\"\n}"
}
]
},
{
"name": "Verify OTP",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"token\": \"\",\n \"otp_code\": \"\"\n}"
},
"url": "{{baseUrl}}/api/auth/verify-otp/",
"description": "Verify OTP with token from request-otp and otp_code sent to user."
},
"response": [
{
"name": "Success",
"status": "OK",
"code": 200,
"body": "{\n \"code\": 200,\n \"msg\": \"success\",\n \"data\": {\n \"id\": 0,\n \"username\": \"\",\n \"email\": \"\",\n \"first_name\": \"\",\n \"last_name\": \"\",\n \"phone_number\": \"\"\n },\n \"token\": \"\"\n}"
}
]
}
],
"variable": [
{"key": "baseUrl", "value": "http://localhost:8000"},
{"key": "token", "value": ""}
]
}
+29
View File
@@ -0,0 +1,29 @@
from rest_framework import serializers
# --- RequestOTP (request-otp/) ---
class RequestOTPSerializer(serializers.Serializer):
"""Request body for POST /api/auth/request-otp/."""
phone_number = serializers.CharField(max_length=32)
# --- VerifyOTP (verify-otp/) ---
class VerifyOTPSerializer(serializers.Serializer):
"""Request body for POST /api/auth/verify-otp/."""
token = serializers.CharField()
otp_code = serializers.CharField(max_length=10)
# --- AuthUser (used in VerifyOTPResponse and UpdateProfileResponse) ---
class AuthUserSerializer(serializers.Serializer):
"""User data returned in auth/account responses."""
id = serializers.IntegerField()
username = serializers.CharField()
email = serializers.EmailField(allow_blank=True)
first_name = serializers.CharField()
last_name = serializers.CharField()
phone_number = serializers.CharField()
+9
View File
@@ -0,0 +1,9 @@
from django.urls import path
from .views import AuthenticationView
urlpatterns = [
path("request-otp/", AuthenticationView.as_view(), name="request-otp"),
path("verify-otp/", AuthenticationView.as_view(), name="verify-otp"),
]
+93
View File
@@ -0,0 +1,93 @@
import secrets
from django.conf import settings
from django.core.cache import cache
from django.core.signing import TimestampSigner
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import RequestOTPSerializer, VerifyOTPSerializer
OTP_TTL_SECONDS = 300
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 "",
# }
return {
"id": 1,
"username": "testuser",
"email": "testuser@example.com",
"first_name": "Test",
"last_name": "User",
"phone_number": "09123456789",
}
class AuthenticationView(APIView):
"""
Single view for auth flows: request-otp and verify-otp.
Dispatches by path: .../request-otp/ -> request_otp, .../verify-otp/ -> verify_otp.
Response format: RequestOTPResponse / VerifyOTPResponse (code, msg, token, data when applicable).
"""
def post(self, request):
if "verify-otp" in request.path:
return self._verify_otp(request)
return self._request_otp(request)
def _request_otp(self, request):
serializer = RequestOTPSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
phone_number = serializer.validated_data["phone_number"].strip()
otp_code = f"{secrets.randbelow(1_000_000):06d}"
cache.set(f"otp_code:{phone_number}", otp_code, timeout=OTP_TTL_SECONDS)
otp_token = OTP_SIGNER.sign(phone_number)
payload = {"code": 200, "msg": "success", "token": otp_token}
if settings.DEBUG:
payload["debug_note"] = "OTP code is returned only when DEBUG=1."
return Response(payload, status=status.HTTP_200_OK)
def _verify_otp(self, request):
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": "",
}
return Response(
{
"code": 200,
"msg": "success",
"data": user_data,
"token": auth_token,
},
status=status.HTTP_200_OK,
)