From a04b06bb4be509b8780cbea08104507957830321 Mon Sep 17 00:00:00 2001 From: ITQ Date: Thu, 29 Feb 2024 18:12:41 +0300 Subject: [PATCH] Added user registration --- solution/Dockerfile | 2 +- solution/pulse/countries/urls.py | 2 +- solution/pulse/pulse/settings.py | 4 +- solution/pulse/pulse/urls.py | 5 +- solution/pulse/users/__init__.py | 0 solution/pulse/users/apps.py | 6 ++ .../pulse/users/migrations/0001_initial.py | 28 ++++++ solution/pulse/users/migrations/__init__.py | 0 solution/pulse/users/models.py | 38 ++++++++ solution/pulse/users/serializers.py | 19 ++++ solution/pulse/users/urls.py | 11 +++ solution/pulse/users/views.py | 87 +++++++++++++++++++ solution/requirements/prod.txt | 1 + solution/template.env | 2 - 14 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 solution/pulse/users/__init__.py create mode 100644 solution/pulse/users/apps.py create mode 100644 solution/pulse/users/migrations/0001_initial.py create mode 100644 solution/pulse/users/migrations/__init__.py create mode 100644 solution/pulse/users/models.py create mode 100644 solution/pulse/users/serializers.py create mode 100644 solution/pulse/users/urls.py create mode 100644 solution/pulse/users/views.py diff --git a/solution/Dockerfile b/solution/Dockerfile index 9fab785..c7396c0 100644 --- a/solution/Dockerfile +++ b/solution/Dockerfile @@ -15,4 +15,4 @@ WORKDIR /app COPY . . -CMD ["sh", "-c", "cd pulse && exec python3 manage.py runserver 0.0.0.0:$SERVER_PORT"] +CMD ["sh", "-c", "cd pulse && python3 manage.py migrate && exec python3 manage.py runserver 0.0.0.0:$SERVER_PORT"] diff --git a/solution/pulse/countries/urls.py b/solution/pulse/countries/urls.py index f47ea71..d20947b 100644 --- a/solution/pulse/countries/urls.py +++ b/solution/pulse/countries/urls.py @@ -5,7 +5,7 @@ import countries.views urlpatterns = [ path("", countries.views.CountryListView.as_view(), name="countries"), path( - "/", + "/", countries.views.CountryByAlpha2View.as_view(), name="country_by_alpha2", ), diff --git a/solution/pulse/pulse/settings.py b/solution/pulse/pulse/settings.py index 1c81641..63e2699 100644 --- a/solution/pulse/pulse/settings.py +++ b/solution/pulse/pulse/settings.py @@ -28,13 +28,13 @@ INSTALLED_APPS = [ # Developed apps "ping.apps.PingConfig", "countries.apps.CountriesConfig", + "users.apps.UsersConfig", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", @@ -115,6 +115,8 @@ REST_FRAMEWORK = { ] } +APPEND_SLASH = False + if DEBUG: INSTALLED_APPS.insert(0, "debug_toolbar") MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") diff --git a/solution/pulse/pulse/urls.py b/solution/pulse/pulse/urls.py index 3d9904b..bb94c95 100644 --- a/solution/pulse/pulse/urls.py +++ b/solution/pulse/pulse/urls.py @@ -13,8 +13,9 @@ urlpatterns = [ ), ), # API - path("api/ping/", include("ping.urls")), - path("api/countries/", include("countries.urls")), + path("api/ping", include("ping.urls")), + path("api/countries", include("countries.urls")), + path("api/auth/", include("users.urls")), ] if settings.DEBUG: diff --git a/solution/pulse/users/__init__.py b/solution/pulse/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/pulse/users/apps.py b/solution/pulse/users/apps.py new file mode 100644 index 0000000..88f7b17 --- /dev/null +++ b/solution/pulse/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" diff --git a/solution/pulse/users/migrations/0001_initial.py b/solution/pulse/users/migrations/0001_initial.py new file mode 100644 index 0000000..8c01d03 --- /dev/null +++ b/solution/pulse/users/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.10 on 2024-02-29 14:37 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('login', models.CharField(max_length=30, validators=[django.core.validators.RegexValidator('[a-zA-Z0-9-]+')])), + ('email', models.EmailField(max_length=50)), + ('password', models.CharField(max_length=100, validators=[django.core.validators.MinLengthValidator(6), django.core.validators.MaxLengthValidator(100), django.core.validators.RegexValidator('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$')])), + ('countryCode', models.CharField(max_length=2, validators=[django.core.validators.RegexValidator('[a-zA-Z]{2}')])), + ('isPublic', models.BooleanField()), + ('phone', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator('\\+[\\d]+')])), + ('image', models.URLField(blank=True, null=True)), + ], + ), + ] diff --git a/solution/pulse/users/migrations/__init__.py b/solution/pulse/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/pulse/users/models.py b/solution/pulse/users/models.py new file mode 100644 index 0000000..217e351 --- /dev/null +++ b/solution/pulse/users/models.py @@ -0,0 +1,38 @@ +from django.core.validators import ( + MaxLengthValidator, + MinLengthValidator, + RegexValidator, +) +from django.db import models + + +class Profile(models.Model): + login = models.CharField( + max_length=30, + validators=[RegexValidator(r"[a-zA-Z0-9-]+")], + ) + email = models.EmailField(max_length=50) + password = models.CharField( + max_length=100, + validators=[ + MinLengthValidator(6), + MaxLengthValidator(100), + RegexValidator(r"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$"), + ], + ) + # ruff: noqa: DJ001 N815 + countryCode = models.CharField( + max_length=2, + validators=[RegexValidator(r"[a-zA-Z]{2}")], + ) + isPublic = models.BooleanField() + phone = models.CharField( + max_length=20, + validators=[RegexValidator(r"\+[\d]+")], + blank=True, + null=True, + ) + image = models.URLField(max_length=200, blank=True, null=True) + + def __str__(self): + return self.login diff --git a/solution/pulse/users/serializers.py b/solution/pulse/users/serializers.py new file mode 100644 index 0000000..3697084 --- /dev/null +++ b/solution/pulse/users/serializers.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + +from users.models import Profile + + +class UserSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = Profile + fields = [ + "login", + "email", + "password", + "countryCode", + "isPublic", + "phone", + "image", + ] diff --git a/solution/pulse/users/urls.py b/solution/pulse/users/urls.py new file mode 100644 index 0000000..cc4ddbf --- /dev/null +++ b/solution/pulse/users/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +import users.views + +urlpatterns = [ + path( + "register", + users.views.register_user, + name="register", + ), +] diff --git a/solution/pulse/users/views.py b/solution/pulse/users/views.py new file mode 100644 index 0000000..add64ba --- /dev/null +++ b/solution/pulse/users/views.py @@ -0,0 +1,87 @@ +import re + +import bcrypt +from rest_framework import status +from rest_framework.decorators import api_view +from rest_framework.response import Response + +from users.models import Profile +from users.serializers import UserSerializer + +MIN_PASSWORD_LEN = 6 +MAX_PASSWORD_LEN = 100 + + +@api_view(["POST"]) +def register_user(request): + serializer = UserSerializer(data=request.data) + + if serializer.is_valid(): + if ( + Profile.objects.filter( + login=serializer.validated_data["login"] + ).first() + is not None + ): + return Response( + {"error": "User with this login already exists"}, + status=status.HTTP_409_CONFLICT, + ) + if ( + Profile.objects.filter( + email=serializer.validated_data["email"] + ).first() + is not None + ): + return Response( + {"error": "User with this email already exists"}, + status=status.HTTP_409_CONFLICT, + ) + if ( + Profile.objects.filter( + phone=serializer.validated_data["phone"] + ).first() + is not None + ): + return Response( + {"error": "User with this phone already exists"}, + status=status.HTTP_409_CONFLICT, + ) + + password = serializer.validated_data["password"] + password_pattern = re.compile( + r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$" + ) + + if not ( + bool(re.match(password_pattern, password)) + ): + error = {"message": "Your password does not meet our requirements"} + return Response( + error, + status=status.HTTP_400_BAD_REQUEST, + ) + + password_hash = bcrypt.hashpw( + password.encode("utf-8"), bcrypt.gensalt() + ).decode("utf-8") + serializer.validated_data["password"] = password_hash + + user = serializer.save() + + profile = { + "profile": { + "login": user.login, + "email": user.email, + "countryCode": user.countryCode, + "isPublic": user.isPublic, + } + } + if user.phone is not None: + profile["profile"]["phone"] = user.phone + if user.image is not None: + profile["profile"]["image"] = user.image + + return Response(profile, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/solution/requirements/prod.txt b/solution/requirements/prod.txt index 57680ab..0a7dec6 100644 --- a/solution/requirements/prod.txt +++ b/solution/requirements/prod.txt @@ -4,3 +4,4 @@ python-dotenv==1.0.1 psycopg2-binary==2.9.9 dj-database-url==2.1.0 django-filter==23.5 +bcrypt==4.1.2 diff --git a/solution/template.env b/solution/template.env index f999772..b3546f1 100644 --- a/solution/template.env +++ b/solution/template.env @@ -2,8 +2,6 @@ DJANGO_DEBUG = False SECRET_KEY = secret_key DJANGO_ALLOWED_HOSTS = 127.0.0.1 INTERNAL_IPS = 127.0.0.1 -POSTGRES_CONN = postgres://login:pass@localhost:5432/postgres -POSTGRES_JDBC_URL = jdbc:postgresql://host:port/dbname POSTGRES_USERNAME = postgres POSTGRES_PASSWORD = password POSTGRES_HOST = localhost