mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
refactor: clean up scaffolding backend
This commit is contained in:
@@ -1 +0,0 @@
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 6.0.4 on 2026-04-15 18:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("users", "0004_alter_user_public_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="kdf_salt",
|
||||
),
|
||||
]
|
||||
+4
-13
@@ -7,13 +7,12 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
class CustomUserManager(BaseUserManager):
|
||||
"""
|
||||
General User Model
|
||||
Creates and saves a User with email and password.
|
||||
"""
|
||||
|
||||
def create_user(self, email, password=None, **extra_fields):
|
||||
if not email:
|
||||
raise ValueError(_("The Email must be set"))
|
||||
# set default activation state as False to enforce email verification
|
||||
extra_fields.setdefault("is_active", False)
|
||||
|
||||
email = self.normalize_email(email)
|
||||
@@ -24,7 +23,7 @@ class CustomUserManager(BaseUserManager):
|
||||
|
||||
def create_superuser(self, email, password, **extra_fields):
|
||||
"""
|
||||
Admin Model
|
||||
Creates a Superuser with email and password.
|
||||
"""
|
||||
extra_fields.update({"is_staff": True, "is_superuser": True, "is_active": True})
|
||||
|
||||
@@ -33,30 +32,22 @@ class CustomUserManager(BaseUserManager):
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Database table structure.
|
||||
Note: We use the default integer ID internally for database performance (joins/indexes)
|
||||
but expose a random 'public_id' (UUID) in URLs and APIs for real privacy.
|
||||
Creates a User with email as primary identifier.
|
||||
Requires manual email verification for activation.
|
||||
"""
|
||||
|
||||
public_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)
|
||||
|
||||
# Reset default fields
|
||||
username = None
|
||||
first_name = None
|
||||
last_name = None
|
||||
|
||||
full_name = models.CharField(max_length=100)
|
||||
email = models.EmailField(_("email address"), unique=True)
|
||||
|
||||
# salt for client-side key derivation
|
||||
kdf_salt = models.CharField(max_length=128, blank=True, null=True)
|
||||
|
||||
# Default is False to enforce email verification
|
||||
is_active = models.BooleanField(default=False)
|
||||
|
||||
objects = CustomUserManager()
|
||||
|
||||
# Login uses email instead of username
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
|
||||
@@ -8,10 +8,18 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Specifies the public_id as readonly for the system to auto generate
|
||||
"""
|
||||
|
||||
model = User
|
||||
fields = ("public_id", "email", "full_name", "password")
|
||||
read_only_fields = ("public_id",)
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Validates and creates a new user with the given data.
|
||||
"""
|
||||
user = User.objects.create_user(
|
||||
email=validated_data["email"],
|
||||
password=validated_data["password"],
|
||||
|
||||
+13
-9
@@ -1,4 +1,3 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.urls import reverse
|
||||
@@ -21,30 +20,35 @@ class AuthTests(APITestCase):
|
||||
self.logout_url = reverse("logout")
|
||||
|
||||
def test_login_sets_secure_cookie(self):
|
||||
"""
|
||||
Tests if the Login API can generate access token and set secure cookie for refresh token.
|
||||
"""
|
||||
data = {"email": self.user.email, "password": self.password}
|
||||
cookie_name = "refresh_token"
|
||||
|
||||
response = self.client.post(self.login_url, data)
|
||||
cookie_name = settings.SIMPLE_JWT["AUTH_COOKIE"]
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn("access", response.data)
|
||||
self.assertNotIn("refresh", response.data)
|
||||
self.assertIn(cookie_name, response.cookies)
|
||||
# verify the cookie has a value
|
||||
self.assertTrue(response.cookies[cookie_name].value)
|
||||
self.assertTrue(response.cookies[cookie_name].httponly)
|
||||
self.assertEqual(response.cookies[cookie_name]["samesite"], "Lax")
|
||||
|
||||
|
||||
class ActivationTests(APITestCase):
|
||||
def test_user_activation(self):
|
||||
# initial user state
|
||||
user = User.objects.create_user(email="inactive@test.com", password="password1234", is_active=False)
|
||||
# generate activation link
|
||||
"""
|
||||
Tests if the Activation API can activate an inactive user.
|
||||
"""
|
||||
user = User.objects.create_user(email="inactiveuser@test.com", password="password1234", is_active=False)
|
||||
uidb64 = urlsafe_base64_encode(force_bytes(user.public_id))
|
||||
token = default_token_generator.make_token(user)
|
||||
# call activation url
|
||||
activation_url = reverse("activate", kwargs={"uidb64": uidb64, "token": token})
|
||||
|
||||
response = self.client.get(activation_url)
|
||||
user.refresh_from_db()
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# check user is activated
|
||||
user.refresh_from_db()
|
||||
self.assertTrue(user.is_active)
|
||||
|
||||
@@ -27,12 +27,12 @@ def set_response_cookies(response, refresh_token):
|
||||
if "refresh" in _response.data:
|
||||
del _response.data["refresh"] # remove refresh token from response body
|
||||
_response.set_cookie(
|
||||
key=settings.SIMPLE_JWT["AUTH_COOKIE"],
|
||||
key=settings.AUTH_COOKIE["NAME"],
|
||||
value=refresh_token,
|
||||
max_age=settings.SIMPLE_JWT["REFRESH_TOKEN_LIFETIME"].total_seconds(),
|
||||
httponly=settings.SIMPLE_JWT["AUTH_COOKIE_HTTPONLY"],
|
||||
secure=settings.SIMPLE_JWT["AUTH_COOKIE_SECURE"],
|
||||
samesite=settings.SIMPLE_JWT["AUTH_COOKIE_SAMESITE"],
|
||||
domain=settings.SIMPLE_JWT["AUTH_COOKIE_DOMAIN"],
|
||||
httponly=settings.AUTH_COOKIE["HTTPONLY"],
|
||||
secure=settings.AUTH_COOKIE["SECURE"],
|
||||
samesite=settings.AUTH_COOKIE["SAMESITE"],
|
||||
domain=settings.AUTH_COOKIE["DOMAIN"],
|
||||
)
|
||||
return _response
|
||||
|
||||
@@ -74,7 +74,7 @@ class RefreshTokenView(TokenRefreshView):
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
refresh_token = request.COOKIES.get(settings.SIMPLE_JWT["AUTH_COOKIE"])
|
||||
refresh_token = request.COOKIES.get(settings.AUTH_COOKIE["NAME"])
|
||||
if not refresh_token:
|
||||
return Response({"detail": "Refresh token not found"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
request.data["refresh"] = refresh_token
|
||||
@@ -92,9 +92,9 @@ class LogoutView(generics.GenericAPIView):
|
||||
response = Response({"detail": "Successfully logged out"}, status=status.HTTP_200_OK)
|
||||
# Clear the secure cookie
|
||||
response.delete_cookie(
|
||||
key=settings.SIMPLE_JWT["AUTH_COOKIE"],
|
||||
domain=settings.SIMPLE_JWT.get("AUTH_COOKIE_DOMAIN"),
|
||||
samesite=settings.SIMPLE_JWT.get("AUTH_COOKIE_SAMESITE"),
|
||||
key=settings.AUTH_COOKIE["NAME"],
|
||||
domain=settings.AUTH_COOKIE.get("DOMAIN"),
|
||||
samesite=settings.AUTH_COOKIE.get("SAMESITE"),
|
||||
path="/",
|
||||
)
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user