diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000..72b1401 --- /dev/null +++ b/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/users/base.py b/users/base.py new file mode 100644 index 0000000..74a1e81 --- /dev/null +++ b/users/base.py @@ -0,0 +1,124 @@ +from django.db import models +from django.contrib import auth +from django.core.mail import send_mail +from django.contrib.auth.base_user import BaseUserManager, AbstractBaseUser +from django.contrib.auth.models import PermissionsMixin +from django.utils import timezone +from django.utils.text import slugify +from django.utils.translation import gettext_lazy as _ + +from .validators.username import usernameValidator + +class PuckoBase_Manager(BaseUserManager): + use_in_migrations = True + + def _create_user(self, username, email, password, **extra_fields): + if not username: + raise ValueError('Du måste ha ett användarnamn!') + email = self.normalize_email(email) + username = self.model.normalize_username(username) + user = self.model(username=username, email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + + def create_user(self, username, email=None, password=None, **extra_fields): + extra_fields.setdefault('is_staff', False) + extra_fields.setdefault('is_superuser', False) + return self._create_user(username, email, password, **extra_fields) + + def create_superuser(self, username, 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(username, email, password, **extra_fields) + + def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None): + if backend is None: + backends = auth._get_backends(return_tuples=True) + if len(backends) == 1: + backend, tmp = backends[0] + else: + raise ValueError( + (f'You have multiple authentication backends configured and ' + f'therefor must provide the `backend` argument.') + ) + elif not isinstance(backend, str): + raise TypeError( + f'backend must be a dotted import path string (got {backend}).' + ) + else: + backend = auth.load_backend(backend) + if hasattr(backend, 'with_perm'): + return backend.with_perm( + perm, + is_active=is_active, + include_superusers=include_superusers, + obj=obj, + ) + return self.none() + +class PuckoBase_User(AbstractBaseUser, PermissionsMixin): + username = models.CharField(_('username'), max_length=150, unique=True, validators=[usernameValidator]) + email = models.EmailField(_('email address'), blank=True, null=True) + date_joined = models.DateTimeField(_('date joined'), default=timezone.now) + is_active = models.BooleanField(_('active'), default=True) + is_staff = models.BooleanField(_('staff status'), default=False) + slug = models.SlugField(max_length=70, unique=True, blank=True, null=True) + + objects = PuckoBase_Manager() + EMAIL_FIELD = 'email' + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = [] + + class Meta: + verbose_name = _('pucko user') + verbose_name_plural = _('pucko users') + abstract = True + + def clean(self): + super().clean() + if self.email: + self.email = self.__class__.objects.normalize_email(self.email) + + def save(self, *args, **kwargs): + self.slug = slugify(self.username.lower()) + super(PuckoBase_User, self).save(*args, **kwargs) + + @classmethod + def this(cls, username): + return cls.objects.get(username=username) + + @classmethod + def create(cls, username, password, first_name=None, last_name=None, superuser=False): + if superuser: + cls.objects.create_superuser( + username, password=password, + first_name=first_name, last_name=last_name + ) + else: + cls.objects.create_user( + username, password=password, + first_name=first_name, last_name=last_name + ) + return cls.this(username) + + @classmethod + def get_all(cls): + return cls.objects.all() + + def get_username(self): + return f'{self.username}' + + def email_user(self, subject, message, from_email=None, **kwargs): + if not self.email: + return False + send_mail(subject, message, from_email, [self.email], **kwargs) + return True + + def __str__(self): + return self.username diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..2977602 --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 5.1.3 on 2025-02-18 19:46 + +import django.utils.timezone +import users.base +import users.validators.username +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Puckopruttis', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(max_length=150, unique=True, validators=[users.validators.username.usernameValidator], verbose_name='username')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='email address')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('is_active', models.BooleanField(default=True, verbose_name='active')), + ('is_staff', models.BooleanField(default=False, verbose_name='staff status')), + ('slug', models.SlugField(blank=True, max_length=70, null=True, unique=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'puckopruttis', + 'verbose_name_plural': 'puckopruttare', + }, + managers=[ + ('objects', users.base.PuckoBase_Manager()), + ], + ), + ] diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..0bbd133 --- /dev/null +++ b/users/models.py @@ -0,0 +1,21 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .base import PuckoBase_Manager +from .base import PuckoBase_User + +class Puckopruttis_Manager(PuckoBase_Manager): + pass + +class Puckopruttis(PuckoBase_User): + + class Meta: + verbose_name = _("puckopruttis") + verbose_name_plural = _("puckopruttare") + + def __init__(self, *args, **kwargs): + super(Puckopruttis, self).__init__(*args, **kwargs) + + def save(self, *args, **kwargs): + super(Puckopruttis, self).save(*args, **kwargs) + diff --git a/users/serializer.py b/users/serializer.py new file mode 100644 index 0000000..76835d1 --- /dev/null +++ b/users/serializer.py @@ -0,0 +1,64 @@ +from django.contrib.auth import get_user_model +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers +from rest_framework.validators import UniqueValidator + +from .validators.username import usernameValidator + +class Pucko_User_Serializer(serializers.ModelSerializer): + + class Meta: + model = get_user_model() + fields = ( + 'username', + 'is_staff', + 'date_joined', + 'slug' + ) + read_only_fields = ('is_staff', 'date_joined', 'slug') + lookup_field = 'slug' + extra_kwargs = {'url': {'lookup_field': 'slug'}} + +class Pucko_CreateUser_Serializer(serializers.ModelSerializer): + username = serializers.CharField(required=True, validators=[UniqueValidator(queryset=get_user_model().objects.all()), usernameValidator()]) + password = serializers.CharField(required=True, write_only=True) + password2 = serializers.CharField(required=True, write_only=True) + + class Meta: + model = get_user_model() + fields = ('username', 'password', 'password2') + + def validate(self, attrs): + if attrs['password'] != attrs['password2']: + raise serializers.ValidationError({"password": _("Password fields didn't match")}) + return attrs + + def create(self, validated_data): + user = get_user_model().objects.create( + username=validated_data['username'] + ) + user.set_password(validated_data['password']) + user.save() + return user + +class Pucko_Login_Serializer(serializers.ModelSerializer): + username = serializers.CharField(required=True) + password = serializers.CharField(required=True, write_only=True) + + class Meta: + model = get_user_model() + fields = ('username', 'password') + +class Pucko_Renew_Serializer(serializers.Serializer): + expiry = serializers.DateTimeField(read_only=True) + token = serializers.CharField(max_length=65, read_only=True) + user = Pucko_User_Serializer(read_only=True) + + class Meta: + fields = ("expiry", "token", "user") + read_only_fields = ("expiry", "token", "user") + +class Logout_Serializer(serializers.Serializer): + + class Meta: + fields = () diff --git a/users/tests.py b/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..5d7a09f --- /dev/null +++ b/users/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from .views import LoginView +from .views import RenewView +from .views import LogoutView +from .views import LogoutAllView +from .views import RegisterView + +urlpatterns = [ + path("create-token/", LoginView.as_view(), name="create_token"), + path("refresh-token/", RenewView.as_view(), name="refresh-token"), + path("signup/", RegisterView.as_view(), name="signup"), + path("logout/", LogoutView.as_view(), name="logout"), + path("logout-all/", LogoutAllView.as_view(), name="logout-all") +] \ No newline at end of file diff --git a/users/validators/username.py b/users/validators/username.py new file mode 100644 index 0000000..0aa94d5 --- /dev/null +++ b/users/validators/username.py @@ -0,0 +1,11 @@ +from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator +from django.utils.translation import gettext_lazy as _ + +class usernameValidator(RegexValidator): + + def __init__(self, inverse_match=None, flags=None): + regex = r"^[A-Za-zåäöÅÄÖ][A-Za-z0-9åäöÅÄÖ_]{2,34}$" + message = _("Username is invalid. username must be between 3 and 35 characters long and start with a character between a-ö all other characters can be a-ö, 0-9 or _") + code = "invalid_username" + super(usernameValidator, self).__init__(regex=regex, message=message, code=code, inverse_match=inverse_match, flags=flags) \ No newline at end of file diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..b653217 --- /dev/null +++ b/users/views.py @@ -0,0 +1,67 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth import login + +from rest_framework.authtoken.serializers import AuthTokenSerializer +from rest_framework.authentication import BasicAuthentication +from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response + +from knox.auth import TokenAuthentication +from knox.views import LoginView as KnoxLoginView +from knox.views import LogoutView as KnoxLogoutView +from knox.views import LogoutAllView as KnoxLogoutAllView + +from .serializer import Pucko_CreateUser_Serializer +from .serializer import Pucko_Renew_Serializer +from .serializer import Pucko_User_Serializer +from .serializer import Pucko_Login_Serializer + +class LoginView(KnoxLoginView): + permission_classes = (AllowAny,) + serializer_class = Pucko_Login_Serializer + + def post(self, request, format=None): + """ + Creates a new token for user in Authorization header + or from username/password sent in post request. + """ + serializer = AuthTokenSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + login(request, user) + return super(LoginView, self).post(request, format=None) + +class RenewView(KnoxLoginView): + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAuthenticated,) + serializer_class = Pucko_Renew_Serializer + + def post(self, request, format=None): + try: + login_token = request.headers["Authorization"].split(" ")[1] + except Exception: + return Response({"message", "you need to be logged in."}, status="204") + + return super(RenewView, self).post(request, format=None) + +class LogoutView(KnoxLogoutView): + """ + Logout from current session. + """ + pass + +class LogoutAllView(KnoxLogoutAllView): + """ + Logout from all devices. + """ + pass + +class RegisterView(CreateAPIView): + """ + Create a user. + """ + queryset = get_user_model().objects.all() + permission_classes = (AllowAny,) + serializer_class = Pucko_CreateUser_Serializer