django-authnest 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. django_authnest-0.1.0/LICENSE +21 -0
  2. django_authnest-0.1.0/PKG-INFO +26 -0
  3. django_authnest-0.1.0/authnest/__init__.py +0 -0
  4. django_authnest-0.1.0/authnest/admin.py +143 -0
  5. django_authnest-0.1.0/authnest/apps.py +10 -0
  6. django_authnest-0.1.0/authnest/backends.py +46 -0
  7. django_authnest-0.1.0/authnest/conf.py +97 -0
  8. django_authnest-0.1.0/authnest/exceptions.py +16 -0
  9. django_authnest-0.1.0/authnest/managers.py +100 -0
  10. django_authnest-0.1.0/authnest/migrations/0001_initial.py +50 -0
  11. django_authnest-0.1.0/authnest/migrations/__init__.py +0 -0
  12. django_authnest-0.1.0/authnest/models.py +100 -0
  13. django_authnest-0.1.0/authnest/serializers.py +334 -0
  14. django_authnest-0.1.0/authnest/services/__init__.py +0 -0
  15. django_authnest-0.1.0/authnest/services/cookies.py +47 -0
  16. django_authnest-0.1.0/authnest/services/emails.py +98 -0
  17. django_authnest-0.1.0/authnest/services/tokens.py +84 -0
  18. django_authnest-0.1.0/authnest/templates/authnest/email/account_deleted.html +11 -0
  19. django_authnest-0.1.0/authnest/templates/authnest/email/account_deleted.txt +5 -0
  20. django_authnest-0.1.0/authnest/templates/authnest/email/base.html +8 -0
  21. django_authnest-0.1.0/authnest/templates/authnest/email/email_change.html +12 -0
  22. django_authnest-0.1.0/authnest/templates/authnest/email/email_change.txt +8 -0
  23. django_authnest-0.1.0/authnest/templates/authnest/email/email_changed_notice.html +7 -0
  24. django_authnest-0.1.0/authnest/templates/authnest/email/email_changed_notice.txt +5 -0
  25. django_authnest-0.1.0/authnest/templates/authnest/email/password_changed.html +7 -0
  26. django_authnest-0.1.0/authnest/templates/authnest/email/password_changed.txt +6 -0
  27. django_authnest-0.1.0/authnest/templates/authnest/email/password_reset.html +12 -0
  28. django_authnest-0.1.0/authnest/templates/authnest/email/password_reset.txt +8 -0
  29. django_authnest-0.1.0/authnest/templates/authnest/email/verify_email.html +12 -0
  30. django_authnest-0.1.0/authnest/templates/authnest/email/verify_email.txt +7 -0
  31. django_authnest-0.1.0/authnest/urls.py +24 -0
  32. django_authnest-0.1.0/authnest/utils.py +9 -0
  33. django_authnest-0.1.0/authnest/views.py +377 -0
  34. django_authnest-0.1.0/django_authnest.egg-info/PKG-INFO +26 -0
  35. django_authnest-0.1.0/django_authnest.egg-info/SOURCES.txt +46 -0
  36. django_authnest-0.1.0/django_authnest.egg-info/dependency_links.txt +1 -0
  37. django_authnest-0.1.0/django_authnest.egg-info/requires.txt +9 -0
  38. django_authnest-0.1.0/django_authnest.egg-info/top_level.txt +1 -0
  39. django_authnest-0.1.0/pyproject.toml +45 -0
  40. django_authnest-0.1.0/setup.cfg +4 -0
  41. django_authnest-0.1.0/tests/test_account.py +416 -0
  42. django_authnest-0.1.0/tests/test_backend.py +105 -0
  43. django_authnest-0.1.0/tests/test_conf.py +42 -0
  44. django_authnest-0.1.0/tests/test_login_tokens.py +359 -0
  45. django_authnest-0.1.0/tests/test_managers.py +203 -0
  46. django_authnest-0.1.0/tests/test_password.py +349 -0
  47. django_authnest-0.1.0/tests/test_profile.py +247 -0
  48. django_authnest-0.1.0/tests/test_register_verify.py +232 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mithalesh Pal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-authnest
3
+ Version: 0.1.0
4
+ Summary: Reusable Django REST auth: email/phone/username login, JWT, email verification, password flows, profile, soft delete.
5
+ Author-email: Mithalesh Pal <mithleshpallearning@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mith1995/custom-auth
8
+ Project-URL: Issues, https://github.com/mith1995/custom-auth/issues
9
+ Keywords: django,drf,authentication,jwt,auth
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 5.1
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: django>=5.1
19
+ Requires-Dist: djangorestframework
20
+ Requires-Dist: djangorestframework-simplejwt
21
+ Requires-Dist: pillow
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest; extra == "dev"
24
+ Requires-Dist: pytest-django; extra == "dev"
25
+ Requires-Dist: pytest-cov; extra == "dev"
26
+ Dynamic: license-file
File without changes
@@ -0,0 +1,143 @@
1
+ from django.contrib import admin, messages
2
+ from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
3
+
4
+ from django import forms
5
+ from django.contrib.auth.forms import BaseUserCreationForm
6
+
7
+ from .conf import authnest_settings
8
+
9
+ from .models import User
10
+ from .utils import normalize_email, normalize_phone
11
+
12
+
13
+ class DeletedFilter(admin.SimpleListFilter):
14
+ title = "account status"
15
+ parameter_name = "account"
16
+
17
+ def lookups(self, request, model_admin):
18
+ return [("alive", "Alive"), ("deleted", "Soft deleted")]
19
+
20
+ def queryset(self, request, queryset):
21
+ if self.value() == "alive":
22
+ return queryset.alive()
23
+ if self.value() == "deleted":
24
+ return queryset.deleted()
25
+ return queryset
26
+
27
+ class AdminUserCreationForm(BaseUserCreationForm):
28
+ class Meta(BaseUserCreationForm.Meta):
29
+ model = User
30
+ fields = ("username", "email", "phone")
31
+
32
+ def clean_email(self):
33
+ email = normalize_email(self.cleaned_data.get("email"))
34
+ # all_objects: soft-deleted user ki email bhi reserved hai
35
+ if email and User.all_objects.filter(email__iexact=email).exists():
36
+ raise forms.ValidationError("Is email se account pehle se maujood hai.")
37
+ return email
38
+
39
+ def clean_phone(self):
40
+ phone = normalize_phone(self.cleaned_data.get("phone"))
41
+ if phone and User.all_objects.filter(phone=phone).exists():
42
+ raise forms.ValidationError("Is phone number se account pehle se maujood hai.")
43
+ return phone
44
+
45
+ def clean(self):
46
+ cleaned = super().clean()
47
+ if not cleaned.get("email") and not cleaned.get("phone"):
48
+ raise forms.ValidationError(
49
+ "Email ya phone mein se kam se kam ek dena zaroori hai."
50
+ )
51
+ return cleaned
52
+
53
+ @admin.register(User)
54
+ class UserAdmin(DjangoUserAdmin):
55
+ list_display = (
56
+ "identifier", "email", "phone", "is_staff", "is_active",
57
+ "is_email_verified", "status", "date_joined",
58
+ )
59
+ list_filter = (DeletedFilter, "is_staff", "is_active", "is_email_verified")
60
+ search_fields = ("email", "phone", "username", "first_name", "last_name")
61
+ ordering = ("-date_joined",)
62
+ readonly_fields = ("last_login", "date_joined", "deleted_at")
63
+ filter_horizontal = ("groups", "user_permissions")
64
+ actions = ["restore_users"]
65
+
66
+ fieldsets = (
67
+ (None, {"fields": ("username", "password")}),
68
+ ("Identity", {"fields": ("email", "phone", "is_email_verified", "is_phone_verified")}),
69
+ ("Profile", {"fields": ("first_name", "last_name", "dob", "profile_picture")}),
70
+ ("Permissions", {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
71
+ ("Dates", {"fields": ("last_login", "date_joined", "deleted_at")}),
72
+ )
73
+
74
+ def get_queryset(self, request):
75
+ # Soft delete on ho to all_objects (deleted bhi dikhein), off ho to sirf alive
76
+ manager = (
77
+ self.model.all_objects
78
+ if authnest_settings.SOFT_DELETE
79
+ else self.model.objects
80
+ )
81
+ queryset = manager.all()
82
+ ordering = self.get_ordering(request)
83
+ return queryset.order_by(*ordering) if ordering else queryset
84
+
85
+ def get_list_display(self, request):
86
+ if authnest_settings.SOFT_DELETE:
87
+ return self.list_display
88
+ return [column for column in self.list_display if column != "status"]
89
+
90
+ def get_list_filter(self, request):
91
+ if authnest_settings.SOFT_DELETE:
92
+ return self.list_filter
93
+ return [item for item in self.list_filter if item is not DeletedFilter]
94
+
95
+ def get_actions(self, request):
96
+ actions = super().get_actions(request)
97
+ if not authnest_settings.SOFT_DELETE:
98
+ actions.pop("restore_users", None)
99
+ actions.pop("hard_delete_users", None)
100
+ return actions
101
+
102
+ def get_fieldsets(self, request, obj=None):
103
+ fieldsets = super().get_fieldsets(request, obj)
104
+ if obj is None or authnest_settings.SOFT_DELETE:
105
+ return fieldsets
106
+ return [
107
+ (
108
+ name,
109
+ {
110
+ **options,
111
+ "fields": tuple(f for f in options["fields"] if f != "deleted_at"),
112
+ },
113
+ )
114
+ for name, options in fieldsets
115
+ ]
116
+
117
+ def has_add_permission(self, request):
118
+ # Users registration ya createsuperuser se bante hain, admin me nahi
119
+ return False
120
+
121
+ add_form = AdminUserCreationForm
122
+ add_fieldsets = (
123
+ (
124
+ None,
125
+ {
126
+ "classes": ("wide",),
127
+ "fields": ("username", "email", "phone", "password1", "password2"),
128
+ },
129
+ ),
130
+ )
131
+
132
+ @admin.display(description="User")
133
+ def identifier(self, obj):
134
+ return str(obj)
135
+
136
+ @admin.display(description="Status")
137
+ def status(self, obj):
138
+ return "Deleted" if obj.is_deleted else "Alive"
139
+
140
+ @admin.action(description="Restore selected users")
141
+ def restore_users(self, request, queryset):
142
+ count = queryset.deleted().restore()
143
+ self.message_user(request, f"{count} user restore ho gaye.", messages.SUCCESS)
@@ -0,0 +1,10 @@
1
+ from django.apps import AppConfig
2
+
3
+ class AuthnestConfig(AppConfig):
4
+ name = "authnest"
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ verbose_name = "Authnest"
7
+
8
+ def ready(self):
9
+ from .conf import authnest_settings
10
+ authnest_settings.validate()
@@ -0,0 +1,46 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.contrib.auth.backends import ModelBackend
3
+
4
+ from .conf import authnest_settings
5
+ from .utils import normalize_phone, normalize_email
6
+
7
+ class AuthnestBackend(ModelBackend):
8
+ """Email / phone / username se login. Allowed methods settings se aate hain."""
9
+
10
+ def authenticate(self, request, username = None, password = None, scope="admin", **kwargs):
11
+
12
+ if scope not in ("user", "admin") or not username or password is None:
13
+ return None
14
+
15
+ UserModel = get_user_model()
16
+ user = self._find_user(UserModel, username.strip(), scope)
17
+
18
+ if user is None:
19
+ # User na mile tab bhi hash chalao, taaki response time se pata na chale
20
+ UserModel().set_password(password)
21
+ return None
22
+
23
+ if user.check_password(password) and self.user_can_authenticate(user):
24
+ return user
25
+
26
+ return None
27
+
28
+ def _find_user(self, UserModel, identifier, scope):
29
+ if scope == "user":
30
+ allowed = authnest_settings.USER_LOGIN_METHODS
31
+ else:
32
+ allowed = authnest_settings.ADMIN_LOGIN_METHODS
33
+
34
+ lookups = []
35
+ if "email" in allowed and "@" in identifier:
36
+ lookups.append({"email__iexact": normalize_email(identifier)})
37
+ if "phone" in allowed and "@" not in identifier:
38
+ lookups.append({"phone": normalize_phone(identifier)})
39
+ if "username" in allowed:
40
+ lookups.append({"username": identifier})
41
+
42
+ for lookup in lookups:
43
+ user = UserModel._default_manager.filter(**lookup).first()
44
+ if user is not None and user.is_staff == (scope == "admin"):
45
+ return user
46
+ return None
@@ -0,0 +1,97 @@
1
+ from django.conf import settings
2
+ from django.core.exceptions import ImproperlyConfigured
3
+
4
+ DEFAULTS = {
5
+ "SOFT_DELETE": True,
6
+ "USER_LOGIN_METHODS": ["email"],
7
+ "ADMIN_LOGIN_METHODS": ["username"],
8
+ "REQUIRE_EMAIL_VERIFICATION": True,
9
+ "ENABLE_2FA": False,
10
+ "SEND_EMAILS_ASYNC": False,
11
+ "FRONTEND_URL": "http://localhost:5173",
12
+ "VERIFY_EMAIL_PATH": "/verify-email", # frontend ka route jahan verify link khulega (jaise /verify-email)
13
+ "RESET_PASSWORD_PATH": "/reset-password", # reset password page ka route (jaise /reset-password)
14
+ "EMAIL_VERIFICATION_TIMEOUT": 24 * 3600, # verify link kitne seconds me expire ho (jaise 1 din)
15
+ "PASSWORD_RESET_TIMEOUT": 3600, # reset link ki expiry (jaise 1 ghanta)
16
+ "ACCESS_TOKEN_LIFETIME": 15 * 60, # seconds (15 minute)
17
+ "REFRESH_TOKEN_LIFETIME": 7 * 24 * 3600, # seconds (7 din)
18
+ "REFRESH_TOKEN_IN_COOKIE": True, # True: httpOnly cookie, False: response body
19
+ "REFRESH_TOKEN_ROTATION": True, # har refresh par naya token, purana blacklist
20
+ "REFRESH_COOKIE_NAME": "authnest_refresh",
21
+ "REFRESH_COOKIE_PATH": "/",
22
+ "REFRESH_COOKIE_SECURE": None, # None = auto (DEBUG band ho to Secure)
23
+ "REFRESH_COOKIE_SAMESITE": "Lax",
24
+ "PROFILE_PICTURE_MAX_SIZE": 2 * 1024 * 1024, # 2MB
25
+ "CONFIRM_EMAIL_CHANGE_PATH": "/confirm-email-change",
26
+ }
27
+
28
+ ALLOWED_USER_LOGIN_METHODS = {"email", "phone"}
29
+ ALLOWED_ADMIN_LOGIN_METHODS = {"email", "username"}
30
+
31
+ class AuthnestSettings:
32
+ def __getattr__(self, name):
33
+ # settings.py me Authnest na ho to khali dict (lazy: access ke time padha)
34
+ user_settings = getattr(settings, "AUTHNEST", {})
35
+
36
+ if name in user_settings:
37
+ return user_settings[name]
38
+ if name in DEFAULTS:
39
+ return DEFAULTS[name]
40
+ raise AttributeError(f"Invalid Authnest setting: '{name}'")
41
+
42
+ def validate(self):
43
+ self.validate_methods("USER_LOGIN_METHODS", ALLOWED_USER_LOGIN_METHODS)
44
+ self.validate_methods("ADMIN_LOGIN_METHODS", ALLOWED_ADMIN_LOGIN_METHODS)
45
+
46
+ if not isinstance(self.SOFT_DELETE, bool):
47
+ raise ImproperlyConfigured(
48
+ "AUTHNEST['SOFT_DELETE'] must be True or False."
49
+ )
50
+
51
+ for name in ("ACCESS_TOKEN_LIFETIME", "REFRESH_TOKEN_LIFETIME"):
52
+ value = getattr(self, name)
53
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
54
+ raise ImproperlyConfigured(
55
+ f"AUTHNEST['{name}'] must be a positive integer (seconds)."
56
+ )
57
+
58
+ for name in ("REFRESH_TOKEN_IN_COOKIE", "REFRESH_TOKEN_ROTATION"):
59
+ if not isinstance(getattr(self, name), bool):
60
+ raise ImproperlyConfigured(
61
+ f"AUTHNEST['{name}'] must be True or False."
62
+ )
63
+
64
+ if self.REFRESH_COOKIE_SAMESITE not in ("Lax", "Strict", "None"):
65
+ raise ImproperlyConfigured(
66
+ "AUTHNEST['REFRESH_COOKIE_SAMESITE'] must be 'Lax', 'Strict', or 'None'."
67
+ )
68
+
69
+ if "rest_framework_simplejwt.token_blacklist" not in settings.INSTALLED_APPS:
70
+ raise ImproperlyConfigured(
71
+ "Add 'rest_framework_simplejwt.token_blacklist' to INSTALLED_APPS "
72
+ "(Authnest needs it for logout and refresh token rotation)."
73
+ )
74
+
75
+ size = self.PROFILE_PICTURE_MAX_SIZE
76
+ if isinstance(size, bool) or not (isinstance(size, int)) or size <= 0:
77
+ raise ImproperlyConfigured(
78
+ f"AUTHNEST['PROFILE_PICTURE_MAX_SIZE'] must be a positive integer (bytes)."
79
+ )
80
+
81
+ def validate_methods(self, setting_name, allowed):
82
+ methods = getattr(self, setting_name)
83
+
84
+ if not isinstance(methods, (list, tuple, set)) or not methods:
85
+ raise ImproperlyConfigured(
86
+ f"AUTHNEST['{setting_name}'] must be a non-empty list. "
87
+ f"Allowed values: {sorted(allowed)}"
88
+ )
89
+
90
+ invalid = set(methods) - allowed
91
+ if invalid:
92
+ raise ImproperlyConfigured(
93
+ f"AUTHNEST['{setting_name}'] has invalid values {sorted(invalid)}. "
94
+ f"Allowed values: {sorted(allowed)}"
95
+ )
96
+
97
+ authnest_settings = AuthnestSettings()
@@ -0,0 +1,16 @@
1
+ from rest_framework import status
2
+ from rest_framework.exceptions import APIException
3
+
4
+ class InvalidCredentials(APIException):
5
+ status_code = status.HTTP_401_UNAUTHORIZED
6
+ default_detail = {
7
+ "detail": "Login details galat hain",
8
+ "code": "invalid_credentials",
9
+ }
10
+
11
+ class EmailNotVerified(APIException):
12
+ status_code = status.HTTP_403_FORBIDDEN
13
+ default_detail = {
14
+ "detail": "Pehle apna email verify karein.",
15
+ "code": "email_not_verified",
16
+ }
@@ -0,0 +1,100 @@
1
+ import uuid
2
+
3
+ from django.contrib.auth.models import BaseUserManager
4
+ from django.db import models
5
+ from django.utils import timezone
6
+ from .utils import normalize_email, normalize_phone
7
+ from .conf import authnest_settings
8
+
9
+ class UserQuerySet(models.QuerySet):
10
+ def alive(self):
11
+ return self.filter(deleted_at__isnull=True)
12
+
13
+ def deleted(self):
14
+ return self.filter(deleted_at__isnull=False)
15
+
16
+ def delete(self):
17
+ if not authnest_settings.SOFT_DELETE:
18
+ return self.hard_delete()
19
+
20
+ # Jo rows pehle se soft-deleted hain unke pk abhi nikalo (update se pehle)
21
+ already_deleted_pks = list(self.deleted().values_list("pk", flat=True))
22
+
23
+ # Alive rows soft delete
24
+ soft_count = self.alive().update(deleted_at=timezone.now(), is_active=False)
25
+
26
+ # Pehle se soft-deleted rows asli me delete
27
+ total, per_model = 0, {}
28
+ if already_deleted_pks:
29
+ total, per_model = self.model.all_objects.filter(
30
+ pk__in=already_deleted_pks
31
+ ).hard_delete()
32
+
33
+ per_model = dict(per_model)
34
+ if soft_count:
35
+ label = self.model._meta.label
36
+ per_model[label] = per_model.get(label, 0) + soft_count
37
+ return total + soft_count, per_model
38
+ delete.alters_data = True
39
+ delete.queryset_only = True
40
+
41
+ def hard_delete(self):
42
+ return super().delete()
43
+ hard_delete.alters_data = True
44
+ hard_delete.queryset_only = True
45
+
46
+ def restore(self):
47
+ return self.update(deleted_at=None, is_active=True)
48
+ restore.queryset_only = True
49
+ restore.alters_data = True
50
+
51
+ class UserManager(BaseUserManager.from_queryset(UserQuerySet)):
52
+ """objects: sirf alive users. Normal app code isi se kaam karega."""
53
+
54
+ use_in_migrations = True
55
+
56
+ def get_queryset(self):
57
+ return super().get_queryset().alive()
58
+
59
+ # ---------- helpers ----------
60
+ def _generate_username(self):
61
+ while True:
62
+ username = f"user_{uuid.uuid4().hex[:12]}"
63
+ if not self.model._base_manager.filter(username=username).exists():
64
+ return username
65
+
66
+ # ---------- creation ----------
67
+ def create_user(self, email=None, phone=None, password=None, username=None, **extra_fields):
68
+ email = normalize_email(email)
69
+ phone = normalize_phone(phone)
70
+ if not email and not phone:
71
+ raise ValueError("Email ya phone mein se kam se kam ek zaroori hai")
72
+
73
+ extra_fields.setdefault("is_staff", False)
74
+ extra_fields.setdefault("is_superuser", False)
75
+
76
+ user = self.model(
77
+ email=email,
78
+ phone=phone,
79
+ username=username or self._generate_username(),
80
+ **extra_fields,
81
+ )
82
+
83
+ user.set_password(password)
84
+ user.save(using=self._db)
85
+ return user
86
+
87
+ def create_superuser(self, username, email=None, password=None, **extra_fields):
88
+ extra_fields.setdefault("is_staff", True)
89
+ extra_fields.setdefault("is_superuser", True)
90
+ extra_fields.setdefault("is_email_verified", True)
91
+
92
+ if extra_fields["is_staff"] is not True:
93
+ raise ValueError("Superuser ke liye is_staff=True zaroori hai.")
94
+ if extra_fields["is_superuser"] is not True:
95
+ raise ValueError("Superuser ke liye is_superuser=True zaroori hai.")
96
+
97
+ return self.create_user(email=email, password=password, username=username, **extra_fields)
98
+
99
+ class AllUsersManager(models.Manager.from_queryset(UserQuerySet)):
100
+ """all_objects: deleted users bhi. Registration check, restore aur admin ke liye."""
@@ -0,0 +1,50 @@
1
+ # Generated by Django 6.1.2 on 2026-10-31 06:34
2
+
3
+ import authnest.managers
4
+ import django.core.validators
5
+ import django.utils.timezone
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ initial = True
12
+
13
+ dependencies = [
14
+ ('auth', '0012_alter_user_first_name_max_length'),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name='User',
20
+ fields=[
21
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
+ ('password', models.CharField(max_length=128, verbose_name='password')),
23
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
24
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
25
+ ('email', models.EmailField(blank=True, max_length=255, null=True, unique=True)),
26
+ ('phone', models.CharField(blank=True, max_length=16, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Phone number must be international format, like +919876543210', regex='^\\+[1-9]\\d{7,14}$')])),
27
+ ('username', models.CharField(blank=True, max_length=150, null=True, unique=True)),
28
+ ('first_name', models.CharField(blank=True, max_length=150)),
29
+ ('last_name', models.CharField(blank=True, max_length=150)),
30
+ ('dob', models.DateField(blank=True, null=True)),
31
+ ('profile_picture', models.ImageField(blank=True, null=True, upload_to='authnest/profiles/')),
32
+ ('is_active', models.BooleanField(default=True)),
33
+ ('is_staff', models.BooleanField(default=False)),
34
+ ('is_email_verified', models.BooleanField(default=False)),
35
+ ('is_phone_verified', models.BooleanField(default=False)),
36
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
37
+ ('deleted_at', models.DateTimeField(blank=True, null=True)),
38
+ ('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')),
39
+ ('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')),
40
+ ],
41
+ options={
42
+ 'verbose_name': 'user',
43
+ 'verbose_name_plural': 'users',
44
+ 'constraints': [models.CheckConstraint(condition=models.Q(('email__isnull', False), ('phone__isnull', False), _connector='OR'), name='authnest_user_email_or_phone_required')],
45
+ },
46
+ managers=[
47
+ ('objects', authnest.managers.UserManager()),
48
+ ],
49
+ ),
50
+ ]
File without changes
@@ -0,0 +1,100 @@
1
+ from django.db import models
2
+ from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
3
+ from django.core.validators import RegexValidator
4
+ from django.db.models import Q
5
+ from django.utils import timezone
6
+ from .conf import authnest_settings
7
+ from .managers import AllUsersManager, UserManager
8
+
9
+ phone_validator = RegexValidator(
10
+ regex=r"^\+[1-9]\d{7,14}$",
11
+ message="Phone number must be international format, like +919876543210"
12
+ )
13
+
14
+ class User(AbstractBaseUser, PermissionsMixin):
15
+ # --- Identity (login ke liye) ---
16
+ email = models.EmailField(
17
+ max_length=255,
18
+ unique=True,
19
+ null=True,
20
+ blank=True
21
+ )
22
+ phone = models.CharField(
23
+ max_length=16,
24
+ unique=True,
25
+ null=True,
26
+ blank=True,
27
+ validators=[phone_validator]
28
+ )
29
+ username = models.CharField(
30
+ max_length=150,
31
+ unique=True,
32
+ null=True,
33
+ blank=True
34
+ )
35
+
36
+ # --- Profile ---
37
+ first_name = models.CharField(max_length=150, blank=True)
38
+ last_name = models.CharField(max_length=150, blank=True)
39
+ dob = models.DateField(null=True, blank=True)
40
+ profile_picture = models.ImageField(
41
+ upload_to="authnest/profiles/",
42
+ null=True,
43
+ blank=True
44
+ )
45
+
46
+ # --- Status ---
47
+ is_active = models.BooleanField(default=True)
48
+ is_staff = models.BooleanField(default=False)
49
+ is_email_verified = models.BooleanField(default=False)
50
+ is_phone_verified = models.BooleanField(default=False)
51
+ date_joined = models.DateTimeField(default=timezone.now)
52
+
53
+ # --- Soft delete ---
54
+ deleted_at = models.DateTimeField(null=True, blank=True)
55
+
56
+ objects = UserManager()
57
+ all_objects = AllUsersManager()
58
+
59
+ USERNAME_FIELD = "username"
60
+ EMAIL_FIELD = "email"
61
+ REQUIRED_FIELDS = ["email"]
62
+
63
+ class Meta:
64
+ verbose_name = "user"
65
+ verbose_name_plural = "users"
66
+ constraints = [
67
+ models.CheckConstraint(
68
+ condition=Q(email__isnull=False) | Q(phone__isnull=False),
69
+ name="authnest_user_email_or_phone_required"
70
+ )
71
+ ]
72
+
73
+ def __str__(self):
74
+ return self.email or self.phone or self.username or f"User {self.pk}"
75
+
76
+ @property
77
+ def is_deleted(self):
78
+ return self.deleted_at is not None
79
+
80
+ @property
81
+ def full_name(self):
82
+ return f"{self.first_name} {self.last_name}".strip()
83
+
84
+ def delete(self, using=None, keep_parents=False):
85
+ if not authnest_settings.SOFT_DELETE or self.is_deleted:
86
+ return self.hard_delete(using=using, keep_parents=keep_parents)
87
+
88
+
89
+ self.deleted_at = timezone.now()
90
+ self.is_active = False
91
+ self.save(update_fields=["deleted_at", "is_active"])
92
+
93
+ def restore(self):
94
+ self.deleted_at = None
95
+ self.is_active = True
96
+ self.save(update_fields=["deleted_at", "is_active"])
97
+
98
+ def hard_delete(self, using=None, keep_parents=False):
99
+ # Asli delete, jo Django ka original delete() chalata hai
100
+ return super().delete(using=using, keep_parents=keep_parents)