UPDATE AUTH

This commit is contained in:
2026-03-23 22:24:30 +03:30
parent a98189a7e9
commit 768d5ea543
14 changed files with 390 additions and 96 deletions
+28
View File
@@ -0,0 +1,28 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
User = get_user_model()
class MultiFieldBackend(ModelBackend):
"""
Authenticate against username, email, or phone_number.
Used for password-based login where the user can enter any of the three.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None or password is None:
return None
try:
user = User.objects.get(
Q(username=username) | Q(email=username) | Q(phone_number=username)
)
except (User.DoesNotExist, User.MultipleObjectsReturned):
User().set_password(password)
return None
if user.check_password(password) and self.user_can_authenticate(user):
return user
return None
@@ -0,0 +1,25 @@
# Generated by Django 5.1.15 on 2026-03-23 18:48
import account.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0001_initial'),
]
operations = [
migrations.AlterModelManagers(
name='user',
managers=[
('objects', account.models.CustomUserManager()),
],
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='email address'),
),
]
+21 -3
View File
@@ -1,19 +1,37 @@
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as BaseUserManager
from django.db.models import Q
from django.db import models from django.db import models
class CustomUserManager(BaseUserManager):
"""Manager that allows lookup by username, email, or phone_number."""
def get_by_natural_key(self, username):
return self.get(
Q(username=username) | Q(email=username) | Q(phone_number=username)
)
class User(AbstractUser): class User(AbstractUser):
phone_number = models.CharField( phone_number = models.CharField(
max_length=32, max_length=32,
unique=True, unique=True,
db_index=True, db_index=True,
) )
email = models.EmailField(
"email address",
unique=True,
db_index=True,
)
USERNAME_FIELD = "phone_number" USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["username"] REQUIRED_FIELDS = ["email", "phone_number"]
objects = CustomUserManager()
class Meta: class Meta:
db_table = "users" db_table = "users"
def __str__(self): def __str__(self):
return self.phone_number return self.username
+21 -1
View File
@@ -1,6 +1,27 @@
from rest_framework import serializers from rest_framework import serializers
# --- Register ---
class RegisterSerializer(serializers.Serializer):
"""Request body for POST /api/auth/register/."""
username = serializers.CharField(max_length=150)
email = serializers.EmailField()
phone_number = serializers.CharField(max_length=32)
password = serializers.CharField(min_length=8, write_only=True)
first_name = serializers.CharField(max_length=150, required=False, default="")
last_name = serializers.CharField(max_length=150, required=False, default="")
# --- Login ---
class LoginSerializer(serializers.Serializer):
"""Request body for POST /api/auth/login/.
identifier can be username, email, or phone_number."""
identifier = serializers.CharField()
password = serializers.CharField()
# --- RequestOTP (request-otp/) --- # --- RequestOTP (request-otp/) ---
class RequestOTPSerializer(serializers.Serializer): class RequestOTPSerializer(serializers.Serializer):
"""Request body for POST /api/auth/request-otp/.""" """Request body for POST /api/auth/request-otp/."""
@@ -26,4 +47,3 @@ class AuthUserSerializer(serializers.Serializer):
first_name = serializers.CharField() first_name = serializers.CharField()
last_name = serializers.CharField() last_name = serializers.CharField()
phone_number = serializers.CharField() phone_number = serializers.CharField()
+5 -4
View File
@@ -1,9 +1,10 @@
from django.urls import path from django.urls import path
from .views import AuthenticationView from .views import AuthenticationView, LoginView, RegisterView
urlpatterns = [ urlpatterns = [
path("request-otp/", AuthenticationView.as_view(), name="request-otp"), path("register/", RegisterView.as_view(), name="register"),
path("verify-otp/", AuthenticationView.as_view(), name="verify-otp"), path("login/", LoginView.as_view(), name="login"),
# path("request-otp/", AuthenticationView.as_view(), name="request-otp"),
# path("verify-otp/", AuthenticationView.as_view(), name="verify-otp"),
] ]
+105 -2
View File
@@ -1,15 +1,22 @@
import secrets import secrets
from django.contrib.auth import authenticate
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
from django.db import IntegrityError
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from account.models import User from account.models import User
from .serializers import RequestOTPSerializer, VerifyOTPSerializer from .serializers import (
LoginSerializer,
RegisterSerializer,
RequestOTPSerializer,
VerifyOTPSerializer,
)
from .sms_service import send_otp_sms from .sms_service import send_otp_sms
@@ -31,6 +38,99 @@ def _auth_user_to_data(user):
} }
class RegisterView(APIView):
"""
POST /api/auth/register/
Creates a new user with username, email, phone_number, and password.
All fields are required (first_name, last_name optional).
Returns JWT tokens and user data on success.
"""
def post(self, request):
serializer = RegisterSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
try:
user = User.objects.create_user(
username=data["username"],
email=data["email"],
phone_number=data["phone_number"],
password=data["password"],
first_name=data.get("first_name", ""),
last_name=data.get("last_name", ""),
)
except IntegrityError as exc:
msg = str(exc).lower()
if "username" in msg:
detail = "A user with this username already exists."
elif "email" in msg:
detail = "A user with this email already exists."
elif "phone_number" in msg:
detail = "A user with this phone number already exists."
else:
detail = "A user with these credentials already exists."
return Response(
{"code": 400, "msg": detail},
status=status.HTTP_400_BAD_REQUEST,
)
refresh = RefreshToken.for_user(user)
user_data = _auth_user_to_data(user)
return Response(
{
"code": 201,
"msg": "success",
"data": user_data,
"token": {
"access": str(refresh.access_token),
"refresh": str(refresh),
},
},
status=status.HTTP_201_CREATED,
)
class LoginView(APIView):
"""
POST /api/auth/login/
Accepts identifier (username, email, or phone_number) + password.
Returns JWT tokens and user data on success.
"""
def post(self, request):
serializer = LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
identifier = serializer.validated_data["identifier"]
password = serializer.validated_data["password"]
user = authenticate(request, username=identifier, password=password)
if user is None:
return Response(
{"code": 401, "msg": "Invalid credentials."},
status=status.HTTP_401_UNAUTHORIZED,
)
refresh = RefreshToken.for_user(user)
user_data = _auth_user_to_data(user)
return Response(
{
"code": 200,
"msg": "success",
"data": user_data,
"token": {
"access": str(refresh.access_token),
"refresh": str(refresh),
},
},
status=status.HTTP_200_OK,
)
class AuthenticationView(APIView): class AuthenticationView(APIView):
""" """
Single view for auth flows: request-otp and verify-otp. Single view for auth flows: request-otp and verify-otp.
@@ -91,7 +191,10 @@ class AuthenticationView(APIView):
user, created = User.objects.get_or_create( user, created = User.objects.get_or_create(
phone_number=phone_number, phone_number=phone_number,
defaults={"username": phone_number}, defaults={
"username": phone_number,
"email": f"{phone_number}@otp.local",
},
) )
refresh = RefreshToken.for_user(user) refresh = RefreshToken.for_user(user)
+5 -1
View File
@@ -13,6 +13,10 @@ ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(","
AUTH_USER_MODEL = "account.User" AUTH_USER_MODEL = "account.User"
AUTHENTICATION_BACKENDS = [
"account.backends.MultiFieldBackend",
]
INSTALLED_APPS = [ INSTALLED_APPS = [
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
@@ -22,7 +26,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
"auth.apps.AuthConfig", "auth.apps.AuthConfig",
"account.apps.AccountConfig", "account.apps.AccountConfig",
"sensor_hub", "sensor_hub.apps.SensorHubConfig",
"dashboard", "dashboard",
"crop_zoning", "crop_zoning",
"plant_simulator", "plant_simulator",
+5 -5
View File
@@ -417,10 +417,10 @@ ECONOMIC_OVERVIEW = {
# Unified response for GET /api/farm-dashboard (section 5) # Unified response for GET /api/farm-dashboard (section 5)
ALL_CARDS = { ALL_CARDS = {
"farmOverviewKpis": FARM_OVERVIEW_KPIS, "farmOverviewKpis": FARM_OVERVIEW_KPIS , # این باید سه روز یکبار محتواش محاسبه بشه
"farmWeatherCard": FARM_WEATHER_CARD, "farmWeatherCard": FARM_WEATHER_CARD, # هروز
"farmAlertsTracker": FARM_ALERTS_TRACKER, "farmAlertsTracker": FARM_ALERTS_TRACKER, #هروز
"sensorValuesList": SENSOR_VALUES_LIST, "sensorValuesList": SENSOR_VALUES_LIST,#هروز
"sensorRadarChart": SENSOR_RADAR_CHART, "sensorRadarChart": SENSOR_RADAR_CHART,
"sensorComparisonChart": SENSOR_COMPARISON_CHART, "sensorComparisonChart": SENSOR_COMPARISON_CHART,
"anomalyDetectionCard": ANOMALY_DETECTION_CARD, "anomalyDetectionCard": ANOMALY_DETECTION_CARD,
@@ -430,6 +430,6 @@ ALL_CARDS = {
"yieldPredictionChart": YIELD_PREDICTION_CHART, "yieldPredictionChart": YIELD_PREDICTION_CHART,
"soilMoistureHeatmap": SOIL_MOISTURE_HEATMAP, "soilMoistureHeatmap": SOIL_MOISTURE_HEATMAP,
"ndviHealthCard": NDVI_HEALTH_CARD, "ndviHealthCard": NDVI_HEALTH_CARD,
"recommendationsList": RECOMMENDATIONS_LIST, "recommendationsList": RECOMMENDATIONS_LIST, # این باید حتما از recommendetion ها گرفته بشه
"economicOverview": ECONOMIC_OVERVIEW, "economicOverview": ECONOMIC_OVERVIEW,
} }
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class SensorHubConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "sensor_hub"
+37
View File
@@ -0,0 +1,37 @@
# Generated by Django 5.1.15 on 2026-03-23 18:48
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Sensor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid_sensor', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
('name', models.CharField(max_length=255)),
('is_active', models.BooleanField(default=True)),
('specifications', models.JSONField(blank=True, default=dict)),
('power_source', models.JSONField(blank=True, default=dict)),
('customized_sensors', models.JSONField(blank=True, default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sensors', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'sensors',
'ordering': ['-created_at'],
},
),
]
View File
+27
View File
@@ -0,0 +1,27 @@
import uuid
from django.conf import settings
from django.db import models
class Sensor(models.Model):
uuid_sensor = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="sensors",
)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
specifications = models.JSONField(default=dict, blank=True)
power_source = models.JSONField(default=dict, blank=True)
customized_sensors = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "sensors"
ordering = ["-created_at"]
def __str__(self):
return f"{self.name} ({self.uuid_sensor})"
+28 -8
View File
@@ -1,12 +1,32 @@
from rest_framework import serializers from rest_framework import serializers
class SensorStoreResponseSerializer(serializers.Serializer): from .models import Sensor
"""Schema for static sensor store response (name, uuid_sensor, last_updated, specifications, power_source, customized_sensors)."""
name = serializers.CharField()
uuid_sensor = serializers.CharField() class SensorSerializer(serializers.ModelSerializer):
last_updated = serializers.CharField() last_updated = serializers.DateTimeField(source="updated_at", read_only=True)
specifications = serializers.JSONField()
power_source = serializers.JSONField() class Meta:
customized_sensors = serializers.JSONField() model = Sensor
fields = [
"uuid_sensor",
"name",
"is_active",
"specifications",
"power_source",
"customized_sensors",
"last_updated",
]
read_only_fields = ["uuid_sensor", "last_updated"]
class SensorCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Sensor
fields = [
"name",
"specifications",
"power_source",
"customized_sensors",
]
+77 -72
View File
@@ -1,97 +1,102 @@
"""
Sensor Hub module.
All endpoints require authenticated user (must be registered).
All responses are static; no processing or validation on inputs.
"""
from rest_framework import status from rest_framework import status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from .serializers import SensorStoreResponseSerializer from .models import Sensor
from .serializers import SensorCreateSerializer, SensorSerializer
# Static sensor payload for store (list/get) response.
STORE_DATA = {
"name": "sensor-hub-static",
"uuid_sensor": "550e8400-e29b-41d4-a716-446655440000",
"last_updated": "2025-02-18T12:00:00Z",
"specifications": {
"model": "SH-1",
"firmware": "1.0.0",
"capabilities": ["temperature", "humidity", "light"],
},
"power_source": {
"type": "battery",
"voltage": 3.3,
"backup": "solar",
},
"customized_sensors": {
"thresholds": {"temperature_min": 10, "temperature_max": 35},
"report_interval_sec": 300,
},
}
# Static payload for single-sensor detail response (same shape as store).
SENSOR_DETAIL_DATA = {
"name": "sensor-hub-static",
"uuid_sensor": "550e8400-e29b-41d4-a716-446655440000",
"last_updated": "2025-02-18T12:00:00Z",
"specifications": {
"model": "SH-1",
"firmware": "1.0.0",
"capabilities": ["temperature", "humidity", "light"],
},
"power_source": {
"type": "battery",
"voltage": 3.3,
"backup": "solar",
},
"customized_sensors": {
"thresholds": {"temperature_min": 10, "temperature_max": 35},
"report_interval_sec": 300,
},
}
class SensorHubView(APIView): class SensorHubView(APIView):
""" """
Sensor-hub endpoints. Behavior depends on URL and HTTP method. Sensor-hub CRUD endpoints connected to the database.
No processing or validation is performed on inputs; responses are static.
Routes: Routes:
- GET "" → List: returns code 200, msg "success", data with static sensor list. - GET "" → List sensors for authenticated user.
- GET "<uuid>/" → Detail: uuid (path). Returns code 200, msg "success", data with static sensor payload. - GET "<uuid>/" → Detail of a single sensor.
- POST ""Add: body/query may be sent but not used. Returns code 200, msg "success". No data field. - POST ""Create a new sensor.
- PATCH "<uuid>/" → Update: uuid (path), body/query may be sent but not used. Returns code 200, msg "success". No data field. - PATCH "<uuid>/" → Update an existing sensor.
- DELETE "<uuid>/" → Delete: uuid (path). Returns code 200, msg "success". No data field. - DELETE "<uuid>/" → Delete a sensor.
- POST "active/" → Activate: no input. Returns code 200, msg "success". No data field. - POST "active/" → Activate a sensor (requires uuid_sensor in body).
- POST "deactive/" → Deactivate: no input. Returns code 200, msg "success". No data field. - POST "deactive/" → Deactivate a sensor (requires uuid_sensor in body).
""" """
authentication_classes = [] # No authentication
permission_classes = [] # No permission permission_classes = [IsAuthenticated]
# permission_classes = [IsAuthenticated]
def _get_sensor(self, request, uuid):
try:
return Sensor.objects.get(uuid_sensor=uuid, owner=request.user)
except Sensor.DoesNotExist:
return None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
uuid = kwargs.get("uuid") uuid = kwargs.get("uuid")
if uuid is not None: if uuid is not None:
data = SensorStoreResponseSerializer(SENSOR_DETAIL_DATA).data sensor = self._get_sensor(request, uuid)
else: if sensor is None:
data = SensorStoreResponseSerializer(STORE_DATA).data return Response(
{"code": 404, "msg": "Sensor not found."},
status=status.HTTP_404_NOT_FOUND,
)
data = SensorSerializer(sensor).data
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
sensors = Sensor.objects.filter(owner=request.user)
data = SensorSerializer(sensors, many=True).data
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK) return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
action = kwargs.get("action") action = kwargs.get("action")
if action == "active": if action in ("active", "deactive"):
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK) return self._toggle_active(request, is_active=(action == "active"))
if action == "deactive":
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK) serializer = SensorCreateSerializer(data=request.data)
# POST without action = add serializer.is_valid(raise_exception=True)
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK) sensor = serializer.save(owner=request.user)
data = SensorSerializer(sensor).data
return Response(
{"code": 201, "msg": "success", "data": data},
status=status.HTTP_201_CREATED,
)
def patch(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs):
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK) uuid = kwargs.get("uuid")
sensor = self._get_sensor(request, uuid)
if sensor is None:
return Response(
{"code": 404, "msg": "Sensor not found."},
status=status.HTTP_404_NOT_FOUND,
)
serializer = SensorCreateSerializer(sensor, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
data = SensorSerializer(sensor).data
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
uuid = kwargs.get("uuid")
sensor = self._get_sensor(request, uuid)
if sensor is None:
return Response(
{"code": 404, "msg": "Sensor not found."},
status=status.HTTP_404_NOT_FOUND,
)
sensor.delete()
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
def _toggle_active(self, request, is_active):
uuid_sensor = request.data.get("uuid_sensor")
if not uuid_sensor:
return Response(
{"code": 400, "msg": "uuid_sensor is required."},
status=status.HTTP_400_BAD_REQUEST,
)
sensor = self._get_sensor(request, uuid_sensor)
if sensor is None:
return Response(
{"code": 404, "msg": "Sensor not found."},
status=status.HTTP_404_NOT_FOUND,
)
sensor.is_active = is_active
sensor.save(update_fields=["is_active", "updated_at"])
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK) return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)