Skip to content

Commit

Permalink
feat: added registration and login
Browse files Browse the repository at this point in the history
  • Loading branch information
Akay7 committed Jun 6, 2024
1 parent ece6fdc commit 5dbaa0b
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 35 deletions.
15 changes: 14 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
{
"configurations": [
{
"name": "Python Debugger: Django",
"type": "debugpy",
"request": "launch",
"preLaunchTask": "docker-compose: dependencies up",
"program": "${workspaceFolder}/backend/manage.py",
"args": [
"runserver"
],
"django": true,
"autoStartBrowser": false,
"justMyCode": false
},
{
"name": "Docker: Python - Django",
"type": "docker",
Expand All @@ -16,4 +29,4 @@
}
}
]
}
}
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"python.envFile": "${workspaceFolder}/env/local.env",
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"dockerRun": {
"image": "ghcr.io/akay7/socialnetwork",
"envFiles": [
"env/dev.env"
"${workspaceFolder}/env/dev.env"
],
"network": "socialnetwork_default",
"volumes": [{
Expand Down
38 changes: 37 additions & 1 deletion backend/social_network/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,17 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites",
# 3rd party
"rest_framework",
"drf_spectacular",
"drf_spectacular_sidecar", # required for Django collectstatic discovery
"allauth",
"allauth.account",
"allauth.socialaccount",
"dj_rest_auth",
"dj_rest_auth.registration",
# local
"user",
]

Expand All @@ -56,6 +64,8 @@
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
# 3rd party
"allauth.account.middleware.AccountMiddleware",
]

ROOT_URLCONF = "social_network.urls"
Expand All @@ -78,6 +88,7 @@

WSGI_APPLICATION = "social_network.wsgi.application"

SITE_ID = 1

# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
Expand Down Expand Up @@ -141,6 +152,10 @@

REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_AUTHENTICATION_CLASSES": (
# 'rest_framework.authentication.SessionAuthentication',
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}


Expand All @@ -151,5 +166,26 @@
"SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
"REDOC_DIST": "SIDECAR",
# OTHER SETTINGS
}


# Dj-rest-auth
# https:/iMerica/dj-rest-auth
# https://dj-rest-auth.readthedocs.io/en/latest/configuration.html

REST_AUTH = {
"USE_JWT": True,
"JWT_AUTH_HTTPONLY": False,
"TOKEN_MODEL": None,
"REGISTER_SERIALIZER": "user.serializers.EmailRegisterSerializer",
}


# Django-allauth
# https://docs.allauth.org/en/latest/account/advanced.html

ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_UNIQUE_EMAIL = True
10 changes: 9 additions & 1 deletion backend/social_network/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
SpectacularRedocView,
SpectacularSwaggerView,
)

from rest_framework_simplejwt.views import (
TokenRefreshView,
)
from user.views import UserViewSet


Expand All @@ -33,7 +35,12 @@

urlpatterns = [
path("api/", include(router.urls)),
path("api/", include("dj_rest_auth.urls")),
path("api/registration/", include("dj_rest_auth.registration.urls")),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
# auth via drf api
path("api-auth/", include("rest_framework.urls")),
# docs
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/schema/swagger-ui/",
Expand All @@ -45,5 +52,6 @@
SpectacularRedocView.as_view(url_name="schema"),
name="redoc",
),
# admin
path("admin/", admin.site.urls),
]
38 changes: 10 additions & 28 deletions backend/user/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Generated by Django 5.0.6 on 2024-06-05 09:00
# Generated by Django 5.0.6 on 2024-06-06 03:01

import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
import user.models
import uuid
from django.db import migrations, models

Expand Down Expand Up @@ -34,18 +33,12 @@ class Migration(migrations.Migration):
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
Expand All @@ -63,7 +56,7 @@ class Migration(migrations.Migration):
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
max_length=254, unique=True, verbose_name="email address"
),
),
(
Expand All @@ -88,15 +81,6 @@ class Migration(migrations.Migration):
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"groups",
models.ManyToManyField(
Expand All @@ -121,12 +105,10 @@ class Migration(migrations.Migration):
),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
("objects", user.models.UserManager()),
],
),
]
68 changes: 66 additions & 2 deletions backend/user/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,72 @@
import uuid

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.utils import timezone
from django.utils.translation import gettext_lazy as _


class User(AbstractUser):
class UserManager(BaseUserManager):
use_in_migrations = True

def _create_user(self, email, password, **extra_fields):
"""Create and save a user with the given email, and
password.
"""
if not email:
raise ValueError("The given email must be set")

email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user

def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields)

def create_superuser(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)

if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")

return self._create_user(email, password, **extra_fields)

def get_by_natural_key(self, username):
return self.get(**{f"{self.model.USERNAME_FIELD}__iexact": username})


class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
first_name = models.CharField(_("first name"), max_length=150, blank=True)
last_name = models.CharField(_("last name"), max_length=150, blank=True)
email = models.EmailField(_("email address"), unique=True)
is_staff = models.BooleanField(
_("staff status"),
default=False,
help_text=_("Designates whether the user can log into this admin site."),
)
is_active = models.BooleanField(
_("active"),
default=True,
help_text=_(
"Designates whether this user should be treated as active. "
"Unselect this instead of deleting accounts."
),
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)

objects = UserManager()

EMAIL_FIELD = "email"
USERNAME_FIELD = "email"

objects = UserManager()
10 changes: 10 additions & 0 deletions backend/user/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from rest_framework import serializers
from dj_rest_auth.registration.serializers import RegisterSerializer

from .models import User

Expand All @@ -11,3 +12,12 @@ class Meta:
"first_name",
"last_name",
]


class EmailRegisterSerializer(RegisterSerializer):
username = None

def validate_email(self, email):
if User.objects.filter(email__iexact=email).exists():
raise serializers.ValidationError("This email is already in use.")
return super().validate_email(email)
70 changes: 69 additions & 1 deletion backend/user/tests.py
Original file line number Diff line number Diff line change
@@ -1 +1,69 @@
# Create your tests here.
import pytest


@pytest.fixture
def email():
return "[email protected]"


@pytest.fixture
def password():
return "safe11password"


@pytest.fixture
def user(django_user_model, email, password):
return django_user_model.objects.create_user(email=email, password=password)


@pytest.mark.django_db
def test_can_register_user(django_user_model, client, email, password):
user_qty = django_user_model.objects.count()

response = client.post(
"/api/registration/",
{
"email": email,
"password1": password,
"password2": password,
},
)

assert response.status_code == 201
assert django_user_model.objects.count() == user_qty + 1


def test_can_login_as_user(user, client, email, password):
response = client.post("/api/login/", {"email": email, "password": password})

assert response.status_code == 200
assert response.data["access"]
assert response.data["refresh"]


def test_can_login_with_email_in_different_case(user, client, email, password):
assert email != email.upper()

response = client.post(
"/api/login/", {"email": email.upper(), "password": password}
)

assert response.status_code == 200
assert response.data["access"]
assert response.data["refresh"]


def test_cant_register_with_email_in_different_case(user, client, password):
assert user.email != user.email.upper()

response = client.post(
"/api/registration/",
{
"email": user.email.upper(),
"password1": password,
"password2": password,
},
)

assert response.status_code == 400
assert response.data["email"]
2 changes: 2 additions & 0 deletions compose.debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ services:
ports:
- 8000:8000
- 5678:5678
env_file:
- env/dev.env

social-network-postgres:
image: postgres:16
Expand Down
Loading

0 comments on commit 5dbaa0b

Please sign in to comment.