はじめに
自分用の個人開発のメモ/備忘録として記録していきます。
環境
django5.0
↓ dockerでの開発環境作成はこちら
やりたいこと
E-mailとPasswordでの認証に変更し、IDをULIDへ変更したい。
ULIDについて
UUIDと同様に一意性保証されていて、生成時刻のミリ秒単位で時系列ソートが可能なULIDを選択
パッケージインストール
pip install ulid-py
django-ulid
を使おうと思ったが、フィールドを
ULIDField(default=default, primary_key=True, editable=False)
にするとなぜか badly formed hexadecimal UUID string
のエラーが発生する。
カスタムユーザークラス作成
usersアプリを作成する
名前は任意なのでusersではなくてもよい。
python manage.py startapp users
models.pyにカスタムユーザークラスを作成
・認証時にemailを利用
・ユーザー名(認証用ではなく任意)としてnameを追加
・created_atとupdated_atを追加
・is_staff : ユーザーが admin サイトへのアクセス権を持っているか
・is_active : ユーザーアカウントが現在アクティブか
・is_superuser : ユーザーが全てのパーミッションを持っているか
from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
import ulid
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None):
if not email:
raise ValueError("Users must have an email address")
user = self.model(
email=self.normalize_email(email),
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None):
user = self.create_user(
email,
password=password,
)
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
class CustomUser(AbstractBaseUser, PermissionsMixin):
id = models.CharField(default=ulid.new, max_length=26, primary_key=True, editable=False)
email = models.EmailField(
verbose_name="email address",
max_length=255,
unique=True,
)
name = models.CharField(
verbose_name="user name",
max_length=30,
null=True,
blank=True
)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
last_login = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = CustomUserManager()
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
def __str__(self):
return self.email
settings.pyの設定変更
customauthをアプリとして追加
INSTALLED_APPS = [
...
# Local apps
'users.apps.UsersConfig',
]
認証ユーザーモデルを作成したカスタムユーザークラスへ変更
# Custom settings
AUTH_USER_MODEL = 'users.CustomUser'
admin.pyを作成
カテゴリ | is_superuser | is_staff | 閲覧 | 新規追加 | 編集 | PW変更 | 権限変更 |
---|---|---|---|---|---|---|---|
スーパーユーザー | TRUE | TRUE | ○ | ○ | ○ | ○ | ○ |
一般スタッフ | FALSE | TRUE | ○ | × | ○ | × | × |
それ以外 | FALSE | FALSE | × | × | × | × | × |
※基本的にはスーパーユーザーのみが、ユーザー管理を行うというもの
※一般スタッフは基本情報確認のみという考えで設定
class UserCreationForm
・新規登録画面
・forms.ModelFormをベースにしている
・passwordとpassword_confirmフィールドは入力内容を隠すためforms.PasswordInputとする
・作成画面で、パスワード確認欄を追加して一致しない場合はエラーを表示
class UserChangeForm
・一般スタッフ用編集画面
・forms.ModelFormをベースにしている
・パスワードは読み取れないフィールドとしている
class UserChangeFormOnlySuperuser
・スーパーユーザー用の編集画面
・UserCreationFormをベースにパスワード入力のrequiredをfalseにした
class UserAdmin
save_modelをオーバーライド
・スーパーユーザーかつパスワードの入力がある場合は、入力値を保存
※新規作成はスーパーユーザーのみを想定
・それ以外は元のパスワードを保持
get_formをオーバーライド
・スーパーユーザーとそれ以外で編集用フォームを切り替え
add_viewをオーバーライド
・新規登録はスーパーユーザーのみとなり、権限のないアクセスはパーミッションエラーを出す
get_fieldsetsをオーバーライド
・スーパーユーザーとそれ以外で表示するフィールドを切り替え
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError
from users.models import CustomUser
from django.core.exceptions import PermissionDenied
class UserCreationForm(forms.ModelForm):
password = forms.CharField(label="Password", widget=forms.PasswordInput)
password_confirm = forms.CharField(
label="Password confirmation", widget=forms.PasswordInput
)
class Meta:
model = CustomUser
fields = ["email", "password", "name", "is_staff", "is_superuser", "is_active"]
def clean_password_confirm(self):
password = self.cleaned_data.get("password")
password_confirm = self.cleaned_data.get("password_confirm")
if (password or password_confirm) and password != password_confirm:
raise ValidationError("Passwords don't match")
return password_confirm
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField()
class Meta:
model = CustomUser
fields = ["email", "name"]
class UserChangeFormOnlySuperuser(UserCreationForm):
password = forms.CharField(label="Password", widget=forms.PasswordInput, required=False)
password_confirm = forms.CharField(
label="Password confirmation", widget=forms.PasswordInput, required=False
)
class UserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
list_display = ['id', 'email', 'name', "is_staff", 'is_superuser', 'is_active', 'last_login', 'created_at', 'updated_at']
list_filter = ["is_staff", "is_superuser","is_active"]
search_fields = ["email", "name"]
ordering = ["created_at"]
filter_horizontal = []
def save_model(self, request, obj, form, change):
if request.user.is_superuser and obj.password:
obj.set_password(obj.password)
else:
obj.password = form.initial.get("password")
super().save_model(request, obj, form, change)
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
kwargs["form"] = UserChangeFormOnlySuperuser
else:
kwargs["form"] = UserChangeForm
return super().get_form(request, obj, **kwargs)
def add_view(self, request, form_url='', extra_context=None):
if not request.user.is_superuser:
raise PermissionDenied
return super().add_view(request, form_url, extra_context)
def get_fieldsets(self, request, obj=None):
if request.user.is_superuser:
return (
(None, {"fields": ["email", "password", "password_confirm"]}),
("Personal info", {"fields": ["name"]}),
("Permissions", {"fields": ["is_staff", "is_superuser", "is_active" ,"groups"]}),
)
else:
return (
(None, {"fields": ["email", "password"]}),
("Personal info", {"fields": ["name"]}),
)
# Now register the new UserAdmin...
admin.site.register(CustomUser, UserAdmin)
マイグレーション
※必要に応じてDBリセット
python manage.py makemigrations
python manage.py migrate
管理画面に入ってみる
スーパーユーザーを作成
python manage.py createsuperuser
管理画面へログイン
http://localhost:*****/admin
おまけ
Documentでは、アプリでは User を直接参照する代わりに、django.contrib.auth.get_user_model() を使ってユーザモデル参照を推奨されていた。
from django.conf import settings
from django.db import models
class Article(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)