diff --git a/src/apps/lyric/admin.py b/src/apps/lyric/admin.py index 8c38f3f..4ae762f 100644 --- a/src/apps/lyric/admin.py +++ b/src/apps/lyric/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin +from .models import Token, User -# Register your models here. + +admin.site.register(User) +admin.site.register(Token) diff --git a/src/apps/lyric/authentication.py b/src/apps/lyric/authentication.py index 5e48494..355fbc7 100644 --- a/src/apps/lyric/authentication.py +++ b/src/apps/lyric/authentication.py @@ -13,7 +13,7 @@ class PasswordlessAuthenticationBackend: try: return User.objects.get(email=token.email) except User.DoesNotExist: - return User.objects.create(email=token.email) + return User.objects.create_user(email=token.email) def get_user(self, user_id): try: diff --git a/src/apps/lyric/migrations/0001_initial.py b/src/apps/lyric/migrations/0001_initial.py index dbbc8fe..cb9b75d 100644 --- a/src/apps/lyric/migrations/0001_initial.py +++ b/src/apps/lyric/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0 on 2026-02-08 01:19 +# Generated by Django 6.0 on 2026-02-20 00:48 import uuid from django.db import migrations, models @@ -15,9 +15,16 @@ class Migration(migrations.Migration): migrations.CreateModel( name='User', fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('email', models.EmailField(max_length=254, unique=True)), + ('is_staff', models.BooleanField(default=False)), + ('is_superuser', models.BooleanField(default=False)), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='Token', diff --git a/src/apps/lyric/models.py b/src/apps/lyric/models.py index 9879063..b0ad876 100644 --- a/src/apps/lyric/models.py +++ b/src/apps/lyric/models.py @@ -1,16 +1,38 @@ import uuid + +from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.db import models + +class UserManager(BaseUserManager): + def create_user(self, email): + user = self.model(email=email) + user.set_unusable_password() + user.save(using=self._db) + return user + + def create_superuser(self, email, password): + user = self.model(email=email, is_staff=True, is_superuser=True) + user.set_password(password) + user.save(using=self._db) + return user + class Token(models.Model): email = models.EmailField() uid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) -class User(models.Model): +class User(AbstractBaseUser): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = models.EmailField(unique=True) + is_staff = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) + objects = UserManager() REQUIRED_FIELDS = [] USERNAME_FIELD = "email" + + def has_perm(self, perm, obj=None): + return self.is_superuser - is_authenticated = True - is_anonymous =False + def has_module_perms(self, app_label): + return self.is_superuser diff --git a/src/apps/lyric/tests/integrated/test_models.py b/src/apps/lyric/tests/integrated/test_models.py index d6db7d0..5e3137d 100644 --- a/src/apps/lyric/tests/integrated/test_models.py +++ b/src/apps/lyric/tests/integrated/test_models.py @@ -11,6 +11,7 @@ class UserModelTest(TestCase): def test_user_is_valid_with_email_only(self): user = User(email="a@b.cde") + user.set_unusable_password() user.full_clean() # should not raise def test_id_is_primary_key(self): @@ -23,3 +24,19 @@ class TokenModelTest(TestCase): token2 = Token.objects.create(email="v@w.xyz") self.assertNotEqual(token1.pk, token2.pk) self.assertIsInstance(token1.pk, uuid.UUID) + +class UserManagerTest(TestCase): + def test_create_superuser_sets_is_staff_and_is_superuser(self): + user = User.objects.create_superuser( + email="admin@example.com", + password="correct-password", + ) + self.assertTrue(user.is_staff) + self.assertTrue(user.is_superuser) + + def test_create_superuser_sets_usable_password(self): + user = User.objects.create_superuser( + email="admin@example.com", + password="correct-password", + ) + self.assertTrue(user.check_password("correct-password")) diff --git a/src/apps/lyric/tests/unit/test_authentication.py b/src/apps/lyric/tests/unit/test_authentication.py index 6cdfcfb..efca801 100644 --- a/src/apps/lyric/tests/unit/test_authentication.py +++ b/src/apps/lyric/tests/unit/test_authentication.py @@ -2,6 +2,7 @@ from django.http import HttpRequest from django.test import SimpleTestCase from apps.lyric.authentication import PasswordlessAuthenticationBackend +from apps.lyric.models import User class SimpleAuthenticateTest(SimpleTestCase): @@ -15,3 +16,16 @@ class SimpleAuthenticateTest(SimpleTestCase): result = PasswordlessAuthenticationBackend().authenticate(HttpRequest()) self.assertIsNone(result) +class UserPermissionsTest(SimpleTestCase): + def test_superuser_has_perm(self): + user = User(is_superuser=True) + self.assertTrue(user.has_perm("any.permission")) + + def test_superuser_has_module_perms(self): + user = User(is_superuser=True) + self.assertTrue(user.has_module_perms("any_app")) + + def test_non_superuser_has_no_perm(self): + user = User(is_superuser=False) + self.assertFalse(user.has_perm("any.permission")) + diff --git a/src/core/settings.py b/src/core/settings.py index 3a1c3ae..e2e59ac 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -39,7 +39,7 @@ else: INSTALLED_APPS = [ # Django apps - # 'django.contrib.admin', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -123,6 +123,7 @@ AUTH_PASSWORD_VALIDATORS = [ AUTH_USER_MODEL = "lyric.User" AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", "apps.lyric.authentication.PasswordlessAuthenticationBackend", ] diff --git a/src/core/urls.py b/src/core/urls.py index 5589f24..8ba67c7 100644 --- a/src/core/urls.py +++ b/src/core/urls.py @@ -1,10 +1,10 @@ -# from django.contrib import admin +from django.contrib import admin from django.http import HttpResponse from django.urls import include, path from apps.dashboard import views as dash_views urlpatterns = [ - # path('admin/', admin.site.urls), + path('admin/', admin.site.urls), path('', dash_views.home_page, name='home'), path('apps/dashboard/', include('apps.dashboard.urls')), path('apps/lyric/', include('apps.lyric.urls')), diff --git a/src/functional_tests/container_commands.py b/src/functional_tests/container_commands.py index d781406..c5e100a 100644 --- a/src/functional_tests/container_commands.py +++ b/src/functional_tests/container_commands.py @@ -1,5 +1,6 @@ import subprocess + USER = "discoman" diff --git a/src/functional_tests/management/commands/create_session.py b/src/functional_tests/management/commands/create_session.py index 215b93c..78c082d 100644 --- a/src/functional_tests/management/commands/create_session.py +++ b/src/functional_tests/management/commands/create_session.py @@ -1,5 +1,10 @@ from django.conf import settings -from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model +from django.contrib.auth import ( + BACKEND_SESSION_KEY, + HASH_SESSION_KEY, + SESSION_KEY, + get_user_model, +) from django.contrib.sessions.backends.db import SessionStore from django.core.management.base import BaseCommand @@ -18,6 +23,7 @@ def create_pre_authenticated_session(email): user = User.objects.create(email=email) session = SessionStore() session[SESSION_KEY] = str(user.pk) # Convert UUID to string for JSON serialization - session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0] + session[BACKEND_SESSION_KEY] = "apps.lyric.authentication.PasswordlessAuthenticationBackend" + session[HASH_SESSION_KEY] = user.get_session_auth_hash() session.save() return session.session_key diff --git a/src/functional_tests/test_admin.py b/src/functional_tests/test_admin.py new file mode 100644 index 0000000..9f73b9a --- /dev/null +++ b/src/functional_tests/test_admin.py @@ -0,0 +1,28 @@ +from selenium.webdriver.common.by import By + +from .base import FunctionalTest +from apps.lyric.models import User + + +class AdminLoginTest(FunctionalTest): + def setUp(self): + super().setUp() + self.superuser = User.objects.create_superuser( + email="admin@example.com", + password="correct-password", + ) + + def test_can_access_admin(self): + self.browser.get(self.live_server_url + "/admin/") + + self.wait_for(lambda: self.browser.find_element(By.ID, "id_username")) + self.browser.find_element(By.ID, "id_username").send_keys("admin@example.com") + self.browser.find_element(By.ID, "id_password").send_keys("correct-password") + self.browser.find_element(By.CSS_SELECTOR, "input[type=submit]").click() + + body = self.wait_for( + lambda: self.browser.find_element(By.TAG_NAME, "body") + ) + self.assertIn("Site administration", body.text) + self.assertIn("Users", body.text) + self.assertIn("Tokens", body.text) diff --git a/src/functional_tests/test_layout_and_styling.py b/src/functional_tests/test_layout_and_styling.py index 338e2be..3f0546f 100644 --- a/src/functional_tests/test_layout_and_styling.py +++ b/src/functional_tests/test_layout_and_styling.py @@ -4,6 +4,7 @@ from selenium.webdriver.common.keys import Keys from .base import FunctionalTest from .list_page import ListPage + class LayoutAndStylingTest(FunctionalTest): def test_layout_and_styling(self): self.browser.get(self.live_server_url) diff --git a/src/functional_tests/test_login.py b/src/functional_tests/test_login.py index a12b073..047fa68 100644 --- a/src/functional_tests/test_login.py +++ b/src/functional_tests/test_login.py @@ -2,11 +2,14 @@ import re from unittest.mock import patch from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys + from .base import FunctionalTest + TEST_EMAIL = "discoman@example.com" SUBJECT = "A magic login link to your Dashboard" + class LoginTest(FunctionalTest): @patch('apps.lyric.views.requests.post') def test_login_using_magic_link(self, mock_post):