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 UserManager as BaseUserManager
from django.db.models import Q
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):
phone_number = models.CharField(
max_length=32,
unique=True,
db_index=True,
)
email = models.EmailField(
"email address",
unique=True,
db_index=True,
)
USERNAME_FIELD = "phone_number"
REQUIRED_FIELDS = ["username"]
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email", "phone_number"]
objects = CustomUserManager()
class Meta:
db_table = "users"
def __str__(self):
return self.phone_number
return self.username
+21 -1
View File
@@ -1,6 +1,27 @@
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/) ---
class RequestOTPSerializer(serializers.Serializer):
"""Request body for POST /api/auth/request-otp/."""
@@ -26,4 +47,3 @@ class AuthUserSerializer(serializers.Serializer):
first_name = serializers.CharField()
last_name = serializers.CharField()
phone_number = serializers.CharField()
+5 -4
View File
@@ -1,9 +1,10 @@
from django.urls import path
from .views import AuthenticationView
from .views import AuthenticationView, LoginView, RegisterView
urlpatterns = [
path("request-otp/", AuthenticationView.as_view(), name="request-otp"),
path("verify-otp/", AuthenticationView.as_view(), name="verify-otp"),
path("register/", RegisterView.as_view(), name="register"),
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
from django.contrib.auth import authenticate
from django.conf import settings
from django.core.cache import cache
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
from django.db import IntegrityError
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 .serializers import (
LoginSerializer,
RegisterSerializer,
RequestOTPSerializer,
VerifyOTPSerializer,
)
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):
"""
Single view for auth flows: request-otp and verify-otp.
@@ -91,7 +191,10 @@ class AuthenticationView(APIView):
user, created = User.objects.get_or_create(
phone_number=phone_number,
defaults={"username": phone_number},
defaults={
"username": phone_number,
"email": f"{phone_number}@otp.local",
},
)
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"
AUTHENTICATION_BACKENDS = [
"account.backends.MultiFieldBackend",
]
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
@@ -22,7 +26,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"auth.apps.AuthConfig",
"account.apps.AccountConfig",
"sensor_hub",
"sensor_hub.apps.SensorHubConfig",
"dashboard",
"crop_zoning",
"plant_simulator",
+5 -5
View File
@@ -417,10 +417,10 @@ ECONOMIC_OVERVIEW = {
# Unified response for GET /api/farm-dashboard (section 5)
ALL_CARDS = {
"farmOverviewKpis": FARM_OVERVIEW_KPIS,
"farmWeatherCard": FARM_WEATHER_CARD,
"farmAlertsTracker": FARM_ALERTS_TRACKER,
"sensorValuesList": SENSOR_VALUES_LIST,
"farmOverviewKpis": FARM_OVERVIEW_KPIS , # این باید سه روز یکبار محتواش محاسبه بشه
"farmWeatherCard": FARM_WEATHER_CARD, # هروز
"farmAlertsTracker": FARM_ALERTS_TRACKER, #هروز
"sensorValuesList": SENSOR_VALUES_LIST,#هروز
"sensorRadarChart": SENSOR_RADAR_CHART,
"sensorComparisonChart": SENSOR_COMPARISON_CHART,
"anomalyDetectionCard": ANOMALY_DETECTION_CARD,
@@ -430,6 +430,6 @@ ALL_CARDS = {
"yieldPredictionChart": YIELD_PREDICTION_CHART,
"soilMoistureHeatmap": SOIL_MOISTURE_HEATMAP,
"ndviHealthCard": NDVI_HEALTH_CARD,
"recommendationsList": RECOMMENDATIONS_LIST,
"recommendationsList": RECOMMENDATIONS_LIST, # این باید حتما از recommendetion ها گرفته بشه
"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
class SensorStoreResponseSerializer(serializers.Serializer):
"""Schema for static sensor store response (name, uuid_sensor, last_updated, specifications, power_source, customized_sensors)."""
from .models import Sensor
name = serializers.CharField()
uuid_sensor = serializers.CharField()
last_updated = serializers.CharField()
specifications = serializers.JSONField()
power_source = serializers.JSONField()
customized_sensors = serializers.JSONField()
class SensorSerializer(serializers.ModelSerializer):
last_updated = serializers.DateTimeField(source="updated_at", read_only=True)
class Meta:
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.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import SensorStoreResponseSerializer
# 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,
},
}
from .models import Sensor
from .serializers import SensorCreateSerializer, SensorSerializer
class SensorHubView(APIView):
"""
Sensor-hub endpoints. Behavior depends on URL and HTTP method.
No processing or validation is performed on inputs; responses are static.
Sensor-hub CRUD endpoints connected to the database.
Routes:
- GET "" → List: returns code 200, msg "success", data with static sensor list.
- GET "<uuid>/" → Detail: uuid (path). Returns code 200, msg "success", data with static sensor payload.
- POST ""Add: body/query may be sent but not used. Returns code 200, msg "success". No data field.
- PATCH "<uuid>/" → Update: uuid (path), body/query may be sent but not used. Returns code 200, msg "success". No data field.
- DELETE "<uuid>/" → Delete: uuid (path). Returns code 200, msg "success". No data field.
- POST "active/" → Activate: no input. Returns code 200, msg "success". No data field.
- POST "deactive/" → Deactivate: no input. Returns code 200, msg "success". No data field.
- GET "" → List sensors for authenticated user.
- GET "<uuid>/" → Detail of a single sensor.
- POST ""Create a new sensor.
- PATCH "<uuid>/" → Update an existing sensor.
- DELETE "<uuid>/" → Delete a sensor.
- POST "active/" → Activate a sensor (requires uuid_sensor in body).
- 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):
uuid = kwargs.get("uuid")
if uuid is not None:
data = SensorStoreResponseSerializer(SENSOR_DETAIL_DATA).data
else:
data = SensorStoreResponseSerializer(STORE_DATA).data
sensor = self._get_sensor(request, uuid)
if sensor is None:
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)
def post(self, request, *args, **kwargs):
action = kwargs.get("action")
if action == "active":
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
if action == "deactive":
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
# POST without action = add
return Response({"code": 200, "msg": "success"}, status=status.HTTP_200_OK)
if action in ("active", "deactive"):
return self._toggle_active(request, is_active=(action == "active"))
serializer = SensorCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
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):
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):
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)