Skip to content

Commit

Permalink
Add user and restauth apps
Browse files Browse the repository at this point in the history
  • Loading branch information
pedroKpaxo committed Mar 18, 2022
1 parent cce8ab4 commit fab18d0
Show file tree
Hide file tree
Showing 40 changed files with 1,321 additions and 7 deletions.
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ ARG := $(wordlist 2, $(words $(MAKECMDGOALS)), $(MAKECMDGOALS))
$(eval $(ARG):;@true)

up:
@echo "Running the app from Makefile"
python manage.py runserver

migrations:
Expand All @@ -13,7 +14,7 @@ migrate:
python manage.py migrate

createsuperuser:
@echo "Creating super user"
@echo "Creating super-user"
python manage.py createsuperuser

shell:
Expand All @@ -23,8 +24,11 @@ shell:
startapp:
bash scripts/start-app.sh $(ARG)

setup_common:
bash scripts/setup-common.sh
setup_commom:
bash scripts/setup-commom.sh

setup_venv:
bash scripts/setup-venv.sh
bash scripts/setup-venv.sh

display_author:
@echo "PedroKpaxo"
Empty file added apps/restauth/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions apps/restauth/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.contrib import admin # noqa
from django.utils.translation import gettext_lazy as _ # noqa

# Register your models here.
7 changes: 7 additions & 0 deletions apps/restauth/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class RestauthConfig(AppConfig):
name = 'apps.restauth'
verbose_name = _("Restauths")
48 changes: 48 additions & 0 deletions apps/restauth/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.contrib.auth.forms import PasswordResetForm as DjangoPasswordResetForm
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode

from apps.user.models import User


class PasswordResetForm(DjangoPasswordResetForm):
def get_users(self, email):
return User.objects.filter(email=email, is_active=True)

def save(self, domain_override=None, site_name_override=None,
subject_template_name='restauth/password_reset_subject.txt',
email_template_name='restauth/password_reset_email.html',
use_https=False, token_generator=default_token_generator,
from_email=None, request=None, html_email_template_name=None,
extra_email_context=None):
"""
Generate a one-use only link for resetting password and send it to the
user.
"""
email = self.cleaned_data["email"]
for user in self.get_users(email):
if not domain_override:
current_site = get_current_site(request)
domain = current_site.domain
site_name = current_site.name
else:
# Overwrite django's default to allow site_name override
domain = domain_override
site_name = site_name_override or domain
user_email = user.email
context = {
'email': user_email,
'domain': domain,
'site_name': site_name,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'user': user,
'token': token_generator.make_token(user),
'protocol': 'https' if use_https else 'http',
**(extra_email_context or {}),
}
self.send_mail(
subject_template_name, email_template_name, context, from_email,
user_email, html_email_template_name=html_email_template_name,
)
Empty file.
4 changes: 4 additions & 0 deletions apps/restauth/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.db import models # noqa
from django.utils.translation import gettext_lazy as _ # noqa

# Create your models here.
91 changes: 91 additions & 0 deletions apps/restauth/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from rest_framework import exceptions, serializers
from dj_rest_auth import serializers as dj_rest_auth_serializers
from dj_rest_auth.registration import serializers as dj_rest_auth_registration_serializers
from rest_framework.validators import UniqueValidator

from django.db.transaction import atomic
from django.utils.translation import gettext_lazy as _

from apps.user.models import ExecutiveUser, User
from shared.fields import DocumentField, PhoneNumberField
from shared.validators import NamedBRCPFValidator
from .forms import PasswordResetForm

DUPLICATE_DOCUMENT = _("Já existe um usuário cadastrado com este documento.")
DOCUMENT = 'document'


class PasswordResetSerializer(dj_rest_auth_serializers.PasswordResetSerializer):
password_reset_form_class = PasswordResetForm


class LoginSerializer(dj_rest_auth_serializers.LoginSerializer):
document = serializers.CharField(required=False)

def _validate_document(self, document, password):
user = None

if document and password:
user = self.authenticate(document=document, password=password)
else:
msg = _('Must include "document" and "password".')
raise exceptions.ValidationError(msg)

return user

def get_auth_user(self, document, email, username, password):
from allauth.account import app_settings

if app_settings.AUTHENTICATION_METHOD == DOCUMENT:
return self._validate_document(document, password)

return super().get_auth_user(username, email, password)

def validate(self, attrs):
document = attrs.get('document')
password = attrs.get('password')
email = attrs.get('email')
username = attrs.get('username')
user = self.get_auth_user(document, email, username, password)

if not user:
msg = _('Unable to log in with provided credentials.')
raise exceptions.ValidationError(msg)

self.validate_auth_user_status(user)

attrs['user'] = user
return attrs


class RegisterSerializer(dj_rest_auth_registration_serializers.RegisterSerializer):
"""Serializer to user self-register (only civil society members)."""
name = serializers.CharField()
email = serializers.EmailField()
is_brazilian = serializers.BooleanField()
document = DocumentField(
validators=[UniqueValidator(queryset=User.objects.all(), message=DUPLICATE_DOCUMENT)]
)
cellphone = PhoneNumberField()

def get_cleaned_data(self):
return {
'email': self.validated_data.get('email', ''),
'password': self.validated_data.get('password1', ''),
'name': self.validated_data['name'],
'is_brazilian': self.validated_data['is_brazilian'],
'document': self.validated_data['document'],
'cellphone': self.validated_data['cellphone'],
}

def validate(self, attrs):
if attrs['is_brazilian']:
validator = NamedBRCPFValidator('document')
validator(attrs['document'])
return attrs

@atomic
def save(self, request):
user = ExecutiveUser.objects.create_user(**self.get_cleaned_data())

return user
Empty file added apps/restauth/tests/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions apps/restauth/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pytest # noqa

# Create your tests here.
3 changes: 3 additions & 0 deletions apps/restauth/tests/test_serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pytest # noqa

# Create your tests here.
3 changes: 3 additions & 0 deletions apps/restauth/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pytest # noqa

# Create your tests here.
19 changes: 19 additions & 0 deletions apps/restauth/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.urls import include, path

from . import views

app_name = 'auth'

# The API URLs are now determined automatically by the router.
urlpatterns = [
path('login/', views.LoginView.as_view(), name='rest_login'),
path('password/reset/', views.PasswordResetView.as_view(), name='rest_password_reset'),
path('password/reset/confirm/', views.PasswordResetConfirmView.as_view(),
name='rest_password_reset_confirm'),
# URLs that require a user to be logged in with a valid session / token.
path('logout/', views.LogoutView.as_view(), name='rest_logout'),
path('user/', views.UserDetailsView.as_view(), name='rest_user_details'),
path('user/delete/', views.UserDestroyView.as_view(), name='rest_user_destroy'),
path('password/change/', views.PasswordChangeView.as_view(), name='rest_password_change'),
path('registration/', include('dj_rest_auth.registration.urls')),
]
43 changes: 43 additions & 0 deletions apps/restauth/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from dj_rest_auth import views as dj_rest_auth_views
from rest_framework.generics import DestroyAPIView
from apps.user.serializers import BaseUserSerializer, ExecutiveUserSerializer, TechnicalUserSerializer

from shared.authentication import BearerAuthentication


class LoginView(dj_rest_auth_views.LoginView):
# Prevents clash between session and token authentication
authentication_classes = (BearerAuthentication,)


class PasswordResetView(dj_rest_auth_views.PasswordResetView):
authentication_classes = (BearerAuthentication,)


class PasswordResetConfirmView(dj_rest_auth_views.PasswordResetConfirmView):
authentication_classes = (BearerAuthentication,)


class LogoutView(dj_rest_auth_views.LogoutView):
pass


class UserDetailsView(dj_rest_auth_views.UserDetailsView):
def get_serializer_class(self):
user = self.get_object()
if user.is_civil:
return ExecutiveUserSerializer
elif user.is_technical:
return TechnicalUserSerializer
else:
return BaseUserSerializer


class UserDestroyView(DestroyAPIView):
"""Destroy (delete) logged in user."""
def get_object(self):
return self.request.user


class PasswordChangeView(dj_rest_auth_views.PasswordChangeView):
pass
Empty file added apps/user/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions apps/user/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.contrib import admin # noqa
from django.utils.translation import gettext_lazy as _ # noqa

# Register your models here.
7 changes: 7 additions & 0 deletions apps/user/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class UserConfig(AppConfig):
name = 'apps.user'
verbose_name = _("Users")
73 changes: 73 additions & 0 deletions apps/user/managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from django.contrib.auth.models import BaseUserManager
from django.utils.translation import gettext_lazy as _ # noqa


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.set_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, password, **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)


class ExecutiveUserManager(UserManager):
def _create_user(self, email, password, **extra_fields):
extra_fields.setdefault('type', self.model.Type.EXECUTIVE)

if extra_fields.get('type') != self.model.Type.EXECUTIVE:
raise ValueError("Executive User must have type='Executive'.")

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

def get_queryset(self):
return super().get_queryset().filter(type=self.model.Type.EXECUTIVE)


class TechnicalUserManager(UserManager):
def _create_user(self, email, password, **extra_fields):
extra_fields.setdefault('type', self.model.Type.TECHNICAL)

if extra_fields.get('type') != self.model.Type.TECHNICAL:
raise ValueError("Technical User must have type='technical'.")

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

def get_queryset(self):
return super().get_queryset().filter(type=self.model.Type.TECHNICAL)


class AcademicUserManager(UserManager):
def _create_user(self, email, password, **extra_fields):
extra_fields.setdefault('type', self.model.Type.ACADEMIC)

if extra_fields.get('type') != self.model.Type.ACADEMIC:
raise ValueError("Academic User must have type='academic'.")

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

def get_queryset(self):
return super().get_queryset().filter(type=self.model.Type.ACADEMIC)
Loading

0 comments on commit fab18d0

Please sign in to comment.