From 2331e605a98406fe93ff32a41fc7e8cb89ba08fc Mon Sep 17 00:00:00 2001 From: jpt Date: Sat, 19 Apr 2025 23:18:48 -0500 Subject: [PATCH] DJOK_USER_TYPE --- README.md | 36 ++++++++++++++++++ apps/__init__.py | 0 apps/accounts/__init__.py | 0 apps/accounts/admin.py | 10 +++++ apps/accounts/models.py | 79 +++++++++++++++++++++++++++++++++++++++ config/settings.py | 24 ++++++------ 6 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 apps/__init__.py create mode 100644 apps/accounts/__init__.py create mode 100644 apps/accounts/admin.py create mode 100644 apps/accounts/models.py diff --git a/README.md b/README.md index 963317f..4ad3b43 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ If you are using this library as a baseline, there are a few steps you'll need t 2. **Recommended:** run `uv run pre-commit install` 3. Read through the various sections below to familiarize yourself with the setup. A few of the libraries may require additional setup, documented under the **You:** steps below. +4. Replace this README & the LICENSE file with those appropriate to your project. + (**Caution**: Since this repository is licensed CC-0, failure to do so would mean licensing your code in the same way, likely not what you want.) +5. Before starting, you will need to choose which kind of user account you want. See `DJOK_USER_TYPE` below. ## File System Layout @@ -121,3 +124,36 @@ Augment's django's built in `auth` with commonly-needed views for signup, email - `/accounts/signup` - `/accounts/login/code` - `/accounts/password/reset` + +## DJOK_USER_TYPE + +Should be set to either: + +- `username` - Standard username/password login w/ optional email. +- `email` - Standard email/password login, username is set to email. + Comes with allauth-powered token-based login as well. + +This must be set **before** running initial DB migrations. + +Once set, you can run: + +```shell +just dj makemigrations accounts +just dj migrate +``` + +Changing once the application is live will require careful planning and custom data migration. + + diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/accounts/__init__.py b/apps/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py new file mode 100644 index 0000000..0049bb4 --- /dev/null +++ b/apps/accounts/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from .models import User + + +class UserAdmin(BaseUserAdmin): + list_display = ["is_active", "username", "full_name", "is_staff", "is_superuser"] + + +admin.site.register(User, UserAdmin) diff --git a/apps/accounts/models.py b/apps/accounts/models.py new file mode 100644 index 0000000..5f1f68b --- /dev/null +++ b/apps/accounts/models.py @@ -0,0 +1,79 @@ +from django.db import models +from django.contrib.auth.models import ( + AbstractBaseUser, + PermissionsMixin, + UnicodeUsernameValidator, + UserManager, +) +from django.utils.translation import gettext_lazy as _ +from django.utils import timezone +from django.core.mail import send_mail +from django.conf import settings + +USERNAME_REQUIRED = settings.DJOK_USER_TYPE == "username" +EMAIL_REQUIRED = settings.DJOK_USER_TYPE == "email" +if not (USERNAME_REQUIRED or EMAIL_REQUIRED): + raise ValueError("Must set DJOK_USER_TYPE") + + +class OkUserManager(UserManager): + def create_superuser(self, **kwargs): + if "username" not in kwargs: + kwargs["username"] = kwargs["email"] + super().create_superuser(**kwargs) + + +class User(AbstractBaseUser, PermissionsMixin): + """ + A modification of the built-in Django user that: + - switches first_name & last_name for username & full_name + - keeps other admin-compliant options + """ + + username_validator = UnicodeUsernameValidator() + + email = models.EmailField(_("email address"), unique=EMAIL_REQUIRED, default="") + username = models.CharField( + max_length=255, + unique=True, + validators=[username_validator] if USERNAME_REQUIRED else [], + default="", + ) + full_name = models.CharField(_("full name"), max_length=150, blank=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 = OkUserManager() + + EMAIL_FIELD = "email" + USERNAME_FIELD = "username" if USERNAME_REQUIRED else "email" + REQUIRED_FIELDS = [] + + class Meta: + verbose_name = _("user") + verbose_name_plural = _("users") + + def clean(self): + super().clean() + self.email = self.__class__.objects.normalize_email(self.email) + + def get_short_name(self): + return self.username + + def get_full_name(self): + return self.full_name + + def email_user(self, subject, message, from_email=None, **kwargs): + send_mail(subject, message, from_email, [self.email], **kwargs) diff --git a/config/settings.py b/config/settings.py index 619602e..f9aa0fd 100644 --- a/config/settings.py +++ b/config/settings.py @@ -67,6 +67,7 @@ INSTALLED_APPS = [ "allauth.account", "django_structlog", "django_typer", + "apps.accounts", ] MIDDLEWARE = [ @@ -116,6 +117,8 @@ USE_TZ = True # Authentication ----- +AUTH_USER_MODEL = "accounts.User" + AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "allauth.account.auth_backends.AuthenticationBackend", @@ -136,21 +139,18 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] -DJOK_AUTH_MODE = "username" -# DJOK_AUTH_MODE is a setting we introduce to pick between +# DJOK_USER_TYPE is a setting we introduce to pick between # a few common auth patterns. # -# Things other than 'username' currently experimental. +# It is also used in accounts/models.py. # -# 'username' -# A username-based email -# -# 'email' -# This configures django-allauth with reasonably secure defaults -# for an email-based account. -# -# '' +# WARNING: Changing this setting after initial setup can have +# data-loss consequences. # +# See documentation for explanation of options. +DJOK_USER_TYPE = "email" + + ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False ACCOUNT_PRESERVE_USERNAME_CASING = False ACCOUNT_LOGIN_BY_CODE_ENABLED = True @@ -162,7 +162,7 @@ ACCOUNT_USERNAME_BLACKLIST = ["admin"] # ACCOUNT_LOGIN_BY_CODE_REQUIRED = False # ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" -if DJOK_AUTH_MODE == "email": +if DJOK_USER_TYPE == "email": ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_LOGIN_METHODS = {"email"} ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]