base setup
This commit is contained in:
0
at_django_boilerplate/accounts/__init__.py
Executable file
0
at_django_boilerplate/accounts/__init__.py
Executable file
Binary file not shown.
BIN
at_django_boilerplate/accounts/__pycache__/admin.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/accounts/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
at_django_boilerplate/accounts/__pycache__/apps.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/accounts/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
at_django_boilerplate/accounts/__pycache__/forms.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/accounts/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
at_django_boilerplate/accounts/__pycache__/urls.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/accounts/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
at_django_boilerplate/accounts/__pycache__/utils.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/accounts/__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
at_django_boilerplate/accounts/__pycache__/views.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/accounts/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
29
at_django_boilerplate/accounts/admin.py
Executable file
29
at_django_boilerplate/accounts/admin.py
Executable file
@@ -0,0 +1,29 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import CustomUser
|
||||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
model = CustomUser
|
||||
list_display = ('email', 'name', 'is_staff', 'is_active',)
|
||||
list_filter = ('is_staff', 'is_active',)
|
||||
fieldsets = (
|
||||
(None, {'fields': ('email', 'password')}),
|
||||
('Personal info', {'fields': ('first_name', 'last_name', 'dob', 'contact_number', 'profile_photo')}),
|
||||
('Permissions', {'fields': ('is_staff', 'is_active', 'is_superuser', 'groups', 'user_permissions')}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ('email', 'first_name', 'last_name', 'password1', 'password2', 'is_staff', 'is_active')}
|
||||
),
|
||||
)
|
||||
search_fields = ('email',)
|
||||
ordering = ('email',)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change: # If the user is being created
|
||||
obj.set_password(form.cleaned_data['password1'])
|
||||
obj.save() # Call save with custom_save=True
|
||||
else:
|
||||
obj.save()
|
||||
admin.site.register(CustomUser, CustomUserAdmin)
|
||||
0
at_django_boilerplate/accounts/api/__init__.py
Executable file
0
at_django_boilerplate/accounts/api/__init__.py
Executable file
Binary file not shown.
Binary file not shown.
273
at_django_boilerplate/accounts/api/v1.py
Executable file
273
at_django_boilerplate/accounts/api/v1.py
Executable file
@@ -0,0 +1,273 @@
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.contrib.auth import authenticate
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
|
||||
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.contrib.auth import authenticate
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import password_validation
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from at_django_boilerplate.accounts.models import CustomUser
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoginApiView(APIView):
|
||||
permission_classes = [AllowAny] # This makes the endpoint open to all users
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Get the username and password from the request data
|
||||
username = request.data.get('username')
|
||||
password = request.data.get('password')
|
||||
|
||||
if not username or not password:
|
||||
return Response({"message": "Both username and password are required."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Authenticate the user
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None:
|
||||
# User is authenticated, generate token
|
||||
token, created = Token.objects.get_or_create(user=user)
|
||||
# You can retrieve the user's role or permission-based data here
|
||||
if user.is_superuser:
|
||||
role = "admin"
|
||||
elif user.get_manager():
|
||||
role = "manager"
|
||||
elif user.get_technician():
|
||||
role = "technician"
|
||||
else:
|
||||
role = "customer"
|
||||
|
||||
return Response({
|
||||
"token": token.key,
|
||||
"role": role,
|
||||
"userId": user.id,
|
||||
"username": f"{user.first_name} {user.last_name}",
|
||||
"profile_photo": user.profile_photo.url if user.profile_photo.url else None,
|
||||
"message": "Login successful"
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
# Invalid credentials
|
||||
return Response({"message": "Invalid username or password."}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class ValidatePasswordAPIView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [TokenAuthentication]
|
||||
def post(self, request):
|
||||
password = request.data.get('password')
|
||||
if not password:
|
||||
return Response({"success": False, "message": "Password is required."}, status=400)
|
||||
user = request.user
|
||||
if user.check_password(password):
|
||||
return Response({"success": True, "message": "Password is valid."}, status=200)
|
||||
return Response({"success": False, "message": "Password is incorrect."}, status=400)
|
||||
|
||||
|
||||
class ResetPasswordAPIView(APIView):
|
||||
def post(self, request):
|
||||
# Get the email from the request data
|
||||
email = request.data.get('email')
|
||||
|
||||
# Ensure the email is provided
|
||||
if not email:
|
||||
return Response({"success": False, "message": "Email address is required."}, status=400)
|
||||
|
||||
try:
|
||||
# Check if the user exists in the database
|
||||
user = CustomUser.objects.get(email=email)
|
||||
except CustomUser.DoesNotExist:
|
||||
return Response({"success": False, "message": "User with this email does not exist."}, status=404)
|
||||
|
||||
# Send the password reset email
|
||||
if self.send_reset_email(request, user, email):
|
||||
return Response({"success": True, "message": "Please check your email to reset your password."}, status=200)
|
||||
else:
|
||||
return Response({"success": False, "message": "Failed to send email. Please try again later."}, status=500)
|
||||
|
||||
|
||||
def send_reset_email(self, request, user, email):
|
||||
# Generate a password reset token
|
||||
token = default_token_generator.make_token(user)
|
||||
|
||||
# Convert user.pk (integer) to string and then encode it to base64
|
||||
uid = urlsafe_base64_encode(str(user.pk).encode('utf-8')) # Encode user ID to base64 string
|
||||
|
||||
# Prepare the context for the email template
|
||||
context = {
|
||||
'user': user,
|
||||
'reset_link': f"http://localhost:3000/reset-password/{uid}/{token}/" # Update the URL if needed (to your front-end)
|
||||
}
|
||||
|
||||
# Render the HTML email template with the context
|
||||
html_message = render_to_string('email_template.html', context)
|
||||
|
||||
# Subject of the email
|
||||
subject = "Password Reset Request"
|
||||
|
||||
# Strip HTML tags for the plain-text version of the email
|
||||
message = strip_tags(html_message)
|
||||
|
||||
# From email and recipient list
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
recipient_list = [email]
|
||||
|
||||
try:
|
||||
# Send the email
|
||||
send_mail(subject, message, from_email, recipient_list, html_message=html_message)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error sending email: {str(e)}")
|
||||
return False
|
||||
|
||||
class ResetPasswordConfirmAPIView(APIView):
|
||||
def get(self, request, uidb64, token):
|
||||
try:
|
||||
# Decode the user ID from base64
|
||||
uid = urlsafe_base64_decode(uidb64).decode('utf-8')
|
||||
user = CustomUser.objects.get(pk=uid)
|
||||
|
||||
# Check if the token is valid
|
||||
if default_token_generator.check_token(user, token):
|
||||
return Response(
|
||||
{"success": True, "message": "Token is valid."},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"success": False, "message": "Invalid or expired token."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
|
||||
return Response(
|
||||
{"success": False, "message": "Invalid or expired token."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def post(self, request, uidb64, token):
|
||||
password = request.data.get('password')
|
||||
|
||||
if not password:
|
||||
return Response(
|
||||
{"success": False, "message": "Password is required."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
# Decode the user ID from base64
|
||||
uid = urlsafe_base64_decode(uidb64).decode('utf-8')
|
||||
user = CustomUser.objects.get(pk=uid)
|
||||
except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
|
||||
return Response(
|
||||
{"success": False, "message": "Invalid user ID."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
password_validation.validate_password(password=password)
|
||||
except ValidationError as e:
|
||||
return Response(
|
||||
{"success": False, "message": str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
# Validate the token
|
||||
if default_token_generator.check_token(user, token):
|
||||
# Set and save the new password
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return Response(
|
||||
{"success": True, "message": "Password has been reset successfully."},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"success": False, "message": "Invalid or expired token."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class UserProfileView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [TokenAuthentication]
|
||||
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
|
||||
if user.is_superuser:
|
||||
print('Is Superuser')
|
||||
users = CustomUser.objects.all()
|
||||
data = [
|
||||
{
|
||||
'id': u.id,
|
||||
'email': u.email,
|
||||
'first_name': u.first_name,
|
||||
'last_name': u.last_name,
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
|
||||
elif user.is_manager: # Manager user only sees their own utility's data
|
||||
utility = user.get_utility() # Get Utility
|
||||
users = utility.accounts_in_utility.all()
|
||||
|
||||
data = [{
|
||||
'id': user.id,
|
||||
'email': user.email,
|
||||
'first_name': user.first_name,
|
||||
'last_name': user.last_name,
|
||||
'utility': str(utility)
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
|
||||
elif user.is_technician:
|
||||
utility = user.get_utility() # Get Utility
|
||||
users = utility.accounts_in_utility.all()
|
||||
|
||||
data = [{
|
||||
'id': u.id,
|
||||
'email': u.email,
|
||||
'first_name': u.first_name,
|
||||
'last_name': u.last_name,
|
||||
'utility': str(utility)
|
||||
}
|
||||
for u in users if not u.is_manager and not u.is_superuser
|
||||
]
|
||||
|
||||
elif user.is_customer:
|
||||
utility = user.get_utility() # Get Utility
|
||||
data = [{
|
||||
'id': user.id,
|
||||
'email': user.email,
|
||||
'first_name': user.first_name,
|
||||
'last_name': user.last_name,
|
||||
'utility': str(utility)
|
||||
}]
|
||||
|
||||
else:
|
||||
data = None
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
6
at_django_boilerplate/accounts/apps.py
Executable file
6
at_django_boilerplate/accounts/apps.py
Executable file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'at_django_boilerplate.accounts'
|
||||
51
at_django_boilerplate/accounts/auth_backends.py
Executable file
51
at_django_boilerplate/accounts/auth_backends.py
Executable file
@@ -0,0 +1,51 @@
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth import get_user_model
|
||||
from at_django_boilerplate.utils.hash_utils import hexdigest
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CustomAuthBackend(ModelBackend):
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
print('Username:',username)
|
||||
if username is None:
|
||||
username = kwargs.get('email') # fallback if email is passed explicitly
|
||||
print('Username:',username)
|
||||
|
||||
if not username or not password:
|
||||
return None
|
||||
|
||||
username = username.lower()
|
||||
email_hash = hexdigest(username)
|
||||
user_found=False
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
try:
|
||||
user = UserModel.objects.get_by_email(email=username)
|
||||
if user:
|
||||
user_found=True
|
||||
|
||||
except UserModel.DoesNotExist:
|
||||
logger.info(f'User with email {username} not found.')
|
||||
|
||||
|
||||
try:
|
||||
if not user_found:
|
||||
user = UserModel.objects.get_by_contact_number(contact_number=username)
|
||||
if user:
|
||||
user_found=True
|
||||
except UserModel.DoesNotExist:
|
||||
logger.info(f'User with contact_number {username} not found.')
|
||||
return None
|
||||
|
||||
if user_found:
|
||||
if user.check_password(password) and self.user_can_authenticate(user):
|
||||
return user
|
||||
|
||||
logger.info(f'Authentication failed for user with email hash {email_hash}.')
|
||||
return None
|
||||
|
||||
def user_can_authenticate(self, user):
|
||||
return user.is_active
|
||||
165
at_django_boilerplate/accounts/forms.py
Executable file
165
at_django_boilerplate/accounts/forms.py
Executable file
@@ -0,0 +1,165 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.auth.password_validation import (validate_password,
|
||||
get_password_validators,
|
||||
MinimumLengthValidator,
|
||||
UserAttributeSimilarityValidator,
|
||||
CommonPasswordValidator,
|
||||
NumericPasswordValidator,)
|
||||
from django.conf import settings
|
||||
from .models import CustomUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from at_django_boilerplate.utils.hash_utils import hexdigest
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserSignUpForm(forms.ModelForm):
|
||||
password = forms.CharField(widget=forms.PasswordInput)
|
||||
confirm_password = forms.CharField(widget=forms.PasswordInput)
|
||||
terms_accepted = forms.BooleanField(required=True, label='I accept the terms and conditions')
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['first_name', 'last_name', 'email','contact_number','dob','password','confirm_password','terms_accepted']
|
||||
widgets = {
|
||||
'dob': forms.DateInput(attrs={'type': 'date'}),
|
||||
'first_name': forms.TextInput(attrs={'type': 'text'}),
|
||||
'last_name': forms.TextInput(attrs={'type': 'text'}),
|
||||
'email': forms.EmailInput(attrs={'type': 'email'}),
|
||||
'contact_number': forms.TextInput(attrs={'type': 'text'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['password'].help_text = self.get_password_requirements()
|
||||
|
||||
def get_password_requirements(self):
|
||||
password_validators = get_password_validators(settings.AUTH_PASSWORD_VALIDATORS)
|
||||
requirements = []
|
||||
for validator in password_validators:
|
||||
if isinstance(validator, MinimumLengthValidator):
|
||||
requirements.append(f"*Password must be at least {validator.min_length} characters long.<br>")
|
||||
elif isinstance(validator, UserAttributeSimilarityValidator):
|
||||
requirements.append("*Password cannot be too similar to your other personal information.<br>")
|
||||
elif isinstance(validator, CommonPasswordValidator):
|
||||
requirements.append("*Password cannot be a commonly used password.<br>")
|
||||
elif isinstance(validator, NumericPasswordValidator):
|
||||
requirements.append("*Password cannot be entirely numeric.<br>")
|
||||
return " ".join(requirements)
|
||||
|
||||
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
password = cleaned_data.get("password")
|
||||
confirm_password = cleaned_data.get("confirm_password")
|
||||
|
||||
if password and confirm_password:
|
||||
try:
|
||||
validate_password(password, self.instance)
|
||||
except forms.ValidationError as error:
|
||||
self.add_error('password', error)
|
||||
return cleaned_data
|
||||
|
||||
if password != confirm_password:
|
||||
self.add_error('confirm_password', "Passwords do not match")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
|
||||
class SigninForm(forms.Form):
|
||||
username = forms.CharField(max_length=65)
|
||||
password = forms.CharField(max_length=65, widget=forms.PasswordInput)
|
||||
|
||||
|
||||
class CustomPasswordResetForm(PasswordResetForm):
|
||||
|
||||
def get_users(self, email):
|
||||
"""Given an email, return matching user(s) who should receive a reset."""
|
||||
email_hash = hexdigest(email)
|
||||
active_custom_users = CustomUser.objects.filter(email_hash=email_hash)
|
||||
active_users = [user for user in active_custom_users ]
|
||||
valid_users = [u for u in active_users if u.has_usable_password()]
|
||||
return valid_users
|
||||
|
||||
|
||||
def save(self, domain_override=None,
|
||||
subject_template_name='reset/password_reset_subject.txt',
|
||||
email_template_name='reset/password_reset_email.html',
|
||||
use_https=False, token_generator=default_token_generator,
|
||||
from_email=None, request=None, html_email_template_name=None,
|
||||
extra_email_context=None):
|
||||
"""
|
||||
Generates a one-use only link for resetting password and sends to the user.
|
||||
"""
|
||||
email = self.cleaned_data["email"]
|
||||
for user in self.get_users(email):
|
||||
context = {
|
||||
'email': email,
|
||||
'domain': domain_override or request.get_host(),
|
||||
'request':request,
|
||||
'site_name': 'we-kwick',
|
||||
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
|
||||
'user': user,
|
||||
'token': token_generator.make_token(user),
|
||||
'protocol': 'https' if use_https else 'http',
|
||||
**(extra_email_context or {}),
|
||||
}
|
||||
from_email = settings.EMAIL_HOST_USER
|
||||
# print('From:',from_email)
|
||||
# print('Email Template:',email_template_name)
|
||||
# print('HTML Email Template:',html_email_template_name)
|
||||
self.send_mail(
|
||||
subject_template_name, email_template_name, context, from_email,
|
||||
email, html_email_template_name=html_email_template_name,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ProfileUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = [ 'contact_number']
|
||||
widgets = {
|
||||
'dob': forms.DateInput(attrs={'type': 'date'}),
|
||||
}
|
||||
|
||||
class UpdateProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = [ 'contact_number']
|
||||
widgets = {
|
||||
'dob': forms.DateInput(attrs={'type': 'date'}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
def validate_image(file):
|
||||
valid_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']
|
||||
valid_file_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.svg','.JPG']
|
||||
|
||||
file_mime_type = file.content_type
|
||||
if file_mime_type not in valid_mime_types:
|
||||
raise ValidationError(_('Unsupported file type. Please upload a JPEG, PNG, GIF, or SVG image.'))
|
||||
|
||||
extension = file.name.split('.')[-1].lower()
|
||||
if not any(file.name.endswith(ext) for ext in valid_file_extensions):
|
||||
raise ValidationError(_('Unsupported file extension.'))
|
||||
|
||||
class ProfilePictureUpdateForm(forms.ModelForm):
|
||||
profile_photo = forms.ImageField(
|
||||
widget=forms.ClearableFileInput(attrs={'accept': 'image/jpeg,image/png,image/gif,image/svg+xml'}),
|
||||
validators=[validate_image]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['profile_photo']
|
||||
0
at_django_boilerplate/accounts/management/__init__.py
Executable file
0
at_django_boilerplate/accounts/management/__init__.py
Executable file
Binary file not shown.
0
at_django_boilerplate/accounts/management/commands/__init__.py
Executable file
0
at_django_boilerplate/accounts/management/commands/__init__.py
Executable file
Binary file not shown.
Binary file not shown.
19
at_django_boilerplate/accounts/management/commands/seed_admin_account.py
Executable file
19
at_django_boilerplate/accounts/management/commands/seed_admin_account.py
Executable file
@@ -0,0 +1,19 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from at_django_boilerplate.accounts.models import CustomUser
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Add predefined tags to the database'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
tmp = CustomUser.objects.create(email='admin@rys.com',first_name='Admin',last_name='User')
|
||||
tmp.save()
|
||||
tmp.set_password('1234')
|
||||
tmp.is_superuser = True
|
||||
tmp.is_active = True
|
||||
tmp.is_staff = True
|
||||
tmp.save()
|
||||
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("✅ Seeded Accounts & Businesses"))
|
||||
50
at_django_boilerplate/accounts/migrations/0001_initial.py
Normal file
50
at_django_boilerplate/accounts/migrations/0001_initial.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.2.6 on 2025-12-29 05:20
|
||||
|
||||
import at_django_boilerplate.accounts.models
|
||||
import at_django_boilerplate.utils.custom_fields
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('first_name', at_django_boilerplate.utils.custom_fields.EncryptedTextField()),
|
||||
('last_name', at_django_boilerplate.utils.custom_fields.EncryptedTextField()),
|
||||
('email', at_django_boilerplate.utils.custom_fields.EncryptedSearchableTextField()),
|
||||
('dob', models.DateField(blank=True, null=True, verbose_name='Date Of Birth')),
|
||||
('contact_number', at_django_boilerplate.utils.custom_fields.EncryptedSearchableTextField(blank=True, null=True)),
|
||||
('profile_photo', models.ImageField(blank=True, default='assets/default_profile.png', null=True, upload_to=at_django_boilerplate.accounts.models.user_directory_path)),
|
||||
('is_staff', models.BooleanField(default=False)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('terms_accepted', models.BooleanField(default=False, verbose_name='Terms & Conditions')),
|
||||
('terms_accepted_time', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('source', models.CharField(default='manual', max_length=10)),
|
||||
('last_active', models.DateTimeField(blank=True, null=True)),
|
||||
('is_technician', models.BooleanField(default=False)),
|
||||
('is_manager', models.BooleanField(default=False)),
|
||||
('email_hash', models.CharField(blank=True, editable=False, max_length=64, null=True)),
|
||||
('contact_number_hash', models.CharField(blank=True, editable=False, max_length=64, null=True)),
|
||||
('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')),
|
||||
('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')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.6 on 2025-12-30 09:23
|
||||
|
||||
import at_django_boilerplate.utils.custom_fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='email',
|
||||
field=at_django_boilerplate.utils.custom_fields.EncryptedSearchableTextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
0
at_django_boilerplate/accounts/migrations/__init__.py
Executable file
0
at_django_boilerplate/accounts/migrations/__init__.py
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
178
at_django_boilerplate/accounts/models.py
Executable file
178
at_django_boilerplate/accounts/models.py
Executable file
@@ -0,0 +1,178 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
import uuid
|
||||
from at_django_boilerplate.utils.encryption_utils import EncryptionUtils
|
||||
# ✅ imports fixed according to your project structure
|
||||
from at_django_boilerplate.utils.hash_utils import hexdigest
|
||||
from at_django_boilerplate.utils.custom_fields import (
|
||||
EncryptedTextField,
|
||||
EncryptedSearchableTextField,
|
||||
)
|
||||
from at_django_boilerplate.utils.mixins import UUIDMixin
|
||||
|
||||
def user_directory_path(instance, filename):
|
||||
user_id = str(instance.id).zfill(10)
|
||||
return f'uploads/users/{user_id}/{filename}'
|
||||
|
||||
class CustomUserManager(BaseUserManager):
|
||||
def create_user(self, email=None, contact_number=None, password=None, **extra_fields):
|
||||
"""
|
||||
Create and save a User with the given email and/or contact_number and password.
|
||||
At least one of email or contact_number must be provided.
|
||||
"""
|
||||
if not email and not contact_number:
|
||||
raise ValueError("Either email or contact number must be provided.")
|
||||
|
||||
if email:
|
||||
email = self.normalize_email(email)
|
||||
|
||||
user = self.model(email=email, contact_number=contact_number, **extra_fields)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, email=None, contact_number=None, password=None, **extra_fields):
|
||||
extra_fields.setdefault('is_staff', True)
|
||||
extra_fields.setdefault('is_superuser', True)
|
||||
|
||||
if extra_fields.get('is_staff') is not True:
|
||||
raise ValueError('Superuser must have is_staff=True.')
|
||||
if extra_fields.get('is_superuser') is not True:
|
||||
raise ValueError('Superuser must have is_superuser=True.')
|
||||
|
||||
# Superuser usually needs at least one identifier
|
||||
if not email and not contact_number:
|
||||
raise ValueError('Superuser must have email or contact number.')
|
||||
|
||||
return self.create_user(email, contact_number, password, **extra_fields)
|
||||
|
||||
# Keep your existing search helpers unchanged
|
||||
def get_by_email(self, email):
|
||||
try:
|
||||
return self.get(email_hash=hexdigest(email))
|
||||
except CustomUser.DoesNotExist:
|
||||
return None
|
||||
|
||||
def filter_by_email(self, email):
|
||||
return self.filter(email_hash=hexdigest(email))
|
||||
|
||||
def get_by_contact_number(self, contact_number):
|
||||
try:
|
||||
return self.get(contact_number_hash=hexdigest(contact_number))
|
||||
except CustomUser.DoesNotExist:
|
||||
return None
|
||||
|
||||
def filter_by_contact_number(self, contact_number):
|
||||
return self.filter(contact_number_hash=hexdigest(contact_number))
|
||||
|
||||
class CustomUser(AbstractBaseUser, PermissionsMixin,UUIDMixin):
|
||||
|
||||
first_name = EncryptedTextField()
|
||||
last_name = EncryptedTextField()
|
||||
email = EncryptedSearchableTextField(hash_field='email_hash',null=True, # ← Must be True
|
||||
blank=True)
|
||||
|
||||
dob = models.DateField(_('Date Of Birth'), blank=True, null=True)
|
||||
|
||||
contact_number = EncryptedSearchableTextField( hash_field='contact_number_hash',null=True, blank=True)
|
||||
profile_photo = models.ImageField(
|
||||
upload_to=user_directory_path,
|
||||
default='assets/default_profile.png',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
is_staff = models.BooleanField(default=False)
|
||||
is_active = models.BooleanField(default=True)
|
||||
date_joined = models.DateTimeField(default=timezone.now)
|
||||
|
||||
terms_accepted = models.BooleanField("Terms & Conditions", default=False)
|
||||
terms_accepted_time = models.DateTimeField(default=timezone.now)
|
||||
|
||||
source = models.CharField(max_length=10, default='manual')
|
||||
|
||||
last_active = models.DateTimeField(null=True,blank=True)
|
||||
|
||||
|
||||
is_technician = models.BooleanField(default=False)
|
||||
is_manager = models.BooleanField(default=False)
|
||||
|
||||
|
||||
|
||||
|
||||
objects = CustomUserManager()
|
||||
|
||||
USERNAME_FIELD = 'id'
|
||||
REQUIRED_FIELDS = ['first_name', 'last_name']
|
||||
|
||||
def __str__(self):
|
||||
return self.get_decrypted_name()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_decrypted_name()
|
||||
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
return self.is_superuser
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("user_profile")
|
||||
|
||||
def get_decrypted_first_name(self):
|
||||
return self.first_name or ''
|
||||
|
||||
def get_decrypted_last_name(self):
|
||||
return self.last_name or ''
|
||||
|
||||
def get_decrypted_name(self):
|
||||
return f"{self.get_decrypted_first_name()} {self.get_decrypted_last_name()}".strip()
|
||||
|
||||
def get_decrypted_email(self):
|
||||
return self.email or ''
|
||||
|
||||
def get_decrypted_contact_number(self):
|
||||
return self.contact_number or ''
|
||||
|
||||
def set_contact_number(self, value):
|
||||
self.contact_number = value
|
||||
self.save()
|
||||
|
||||
def set_first_name(self, value):
|
||||
self.first_name = value
|
||||
self.save()
|
||||
|
||||
def set_last_name(self, value):
|
||||
self.last_name = value
|
||||
self.save()
|
||||
|
||||
def set_name(self, value):
|
||||
names = value.strip().split(' ', 1)
|
||||
self.set_first_name(names[0])
|
||||
self.set_last_name(names[1] if len(names) > 1 else '')
|
||||
|
||||
def has_contact_number(self):
|
||||
return bool(self.contact_number)
|
||||
|
||||
|
||||
def is_customer(self):
|
||||
try:
|
||||
return self.connection_account.all().count() >=1
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return False
|
||||
|
||||
def get_connection(self):
|
||||
return self.connection_account.all().first()
|
||||
|
||||
def get_technician(self):
|
||||
return self.is_technician
|
||||
|
||||
|
||||
def get_manager(self):
|
||||
return self.is_manager
|
||||
# def ge
|
||||
75
at_django_boilerplate/accounts/templates/change_password.html
Executable file
75
at_django_boilerplate/accounts/templates/change_password.html
Executable file
@@ -0,0 +1,75 @@
|
||||
{% extends "public_base.html" %}
|
||||
{% block content %}
|
||||
<div class="form-content my-3 p-3">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-lg border-0 rounded-lg mt-0 mb-3">
|
||||
<div class="card-header justify-content-center">
|
||||
<h3 class="font-weight-light my-4 text-center">Change Your Password</h3>
|
||||
</div>
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||
<div id="form_errors">
|
||||
{% for key, value in form.errors.items %}
|
||||
<strong>{{ value }}</strong>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="form-row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="form-group">
|
||||
<label class="small mb-1" for="id_old_password">Old Password</label>
|
||||
<input type="password" name="old_password" autocomplete="new-password"
|
||||
class="form-control" required id="id_old_password"
|
||||
placeholder="Enter Old Password"/>
|
||||
<span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="form-group">
|
||||
<label class="small mb-1" for="id_new_password1">New Password</label>
|
||||
<input type="password" name="new_password1" autocomplete="new-password"
|
||||
class="form-control" required id="id_new_password1"
|
||||
placeholder="Enter New Password"/>
|
||||
<span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="form-group">
|
||||
<label class="small mb-1" for="id_new_password2">New Password Confirmation</label>
|
||||
<input type="password" name="new_password2" autocomplete="new-password"
|
||||
required id="id_new_password2" class="form-control"
|
||||
placeholder="Confirm New Password"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="form-group mt-0 mb-1">
|
||||
<button type="submit" class="col-md-12 btn btn-dark" id="reset">Update Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
596
at_django_boilerplate/accounts/templates/login.html
Executable file
596
at_django_boilerplate/accounts/templates/login.html
Executable file
@@ -0,0 +1,596 @@
|
||||
{% extends 'public_base.html' %}
|
||||
{% block title %}
|
||||
Login
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--accent: #4f46e5;
|
||||
--gradient-start: #6366f1;
|
||||
--gradient-end: #8b5cf6;
|
||||
--bg: #ffffff;
|
||||
--muted: #6b7280;
|
||||
--danger: #ef4444;
|
||||
--success: #10b981;
|
||||
--radius: 16px;
|
||||
--shadow: 0 20px 40px rgba(99, 102, 241, 0.15);
|
||||
--shadow-hover: 0 25px 50px rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: linear-gradient(135deg, #f0f4ff 0%, #fdf2ff 50%, #f0fdf4 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.05) 0%, transparent 70%);
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
||||
25% { transform: translate(10px, 10px) rotate(1deg); }
|
||||
50% { transform: translate(-5px, 15px) rotate(-1deg); }
|
||||
75% { transform: translate(15px, -5px) rotate(1deg); }
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
flex: 1;
|
||||
padding: 4rem 3rem;
|
||||
background: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-left::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, var(--gradient-start), var(--gradient-end));
|
||||
}
|
||||
|
||||
.form-container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
box-shadow: 0 10px 25px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.logo i {
|
||||
font-size: 1.8rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-container h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-container p.subtext {
|
||||
margin-bottom: 2.5rem;
|
||||
font-size: 1rem;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem 1rem 1rem 3rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--accent);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
|
||||
outline: none;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--muted);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus + .input-icon {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.forgot-password:hover {
|
||||
color: var(--gradient-end);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 10px 25px rgba(99, 102, 241, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
|
||||
/* OTP Button */
|
||||
.btn-otp {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-otp:hover {
|
||||
background: linear-gradient(135deg, #059669, #047857);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
|
||||
.signup-link {
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.signup-link a {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
padding: 4rem 3rem;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-right::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9rem;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.alert-success { background: rgba(16,185,129,0.1); border-color: var(--success); color: #065f46; }
|
||||
.alert-danger { background: rgba(239,68,68,0.1); border-color: var(--danger); color: #991b1b; }
|
||||
|
||||
/* Modal Styles (No Bootstrap) */
|
||||
.otp-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.otp-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.otp-modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.otp-modal-header {
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.otp-modal-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.otp-input-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.otp-input {
|
||||
width: 56px;
|
||||
height: 64px;
|
||||
text-align: center;
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.otp-input:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 4px rgba(99,102,241,0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.login-container { flex-direction: column; max-width: 500px; }
|
||||
.login-left::before { display: none; }
|
||||
.login-right { display: none; }
|
||||
.login-left { padding: 3rem 2rem; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<div class="login-left">
|
||||
<div class="form-container">
|
||||
<div class="logo-section">
|
||||
<div class="logo">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
</div>
|
||||
<h3>Welcome Back</h3>
|
||||
<p class="subtext">Sign in to your account to continue</p>
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {% if 'success' in message.tags %}alert-success{% else %}alert-danger{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ request.GET.next }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">Username</label>
|
||||
<div class="input-group">
|
||||
<input id="username" name="username" class="form-control" placeholder="Enter your username" required autofocus type="text">
|
||||
<div class="input-icon"><i class="fas fa-user"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="Enter your password" required>
|
||||
<div class="input-icon"><i class="fas fa-lock"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-options">
|
||||
<label class="remember-me">
|
||||
<input type="checkbox" name="remember"> Remember me
|
||||
</label>
|
||||
<a href="{% url 'password_reset' %}" class="forgot-password">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">
|
||||
<i class="fas fa-sign-in-alt"></i> Sign In
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn-submit btn-otp" id="openOtpModal">
|
||||
<i class="fas fa-mobile-alt"></i> Login with OTP
|
||||
</button>
|
||||
|
||||
<div class="signup-link">
|
||||
Don't have an account? <a href="{% url 'signup' %}">Sign up here</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-right">
|
||||
<h2 class="features-title">Why Choose Us?</h2>
|
||||
<ul class="feature-list">
|
||||
<li class="feature-item">
|
||||
<div class="feature-icon"><i class="fas fa-rocket"></i></div>
|
||||
<div class="feature-text"><strong>Lightning Fast</strong><br>Quick and seamless login experience</div>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<div class="feature-icon"><i class="fas fa-shield-alt"></i></div>
|
||||
<div class="feature-text"><strong>Bank-Level Security</strong><br>Your data is protected with enterprise-grade security</div>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<div class="feature-icon"><i class="fas fa-sync-alt"></i></div>
|
||||
<div class="feature-text"><strong>Sync Across Devices</strong><br>Access your account from anywhere, anytime</div>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<div class="feature-icon"><i class="fas fa-headset"></i></div>
|
||||
<div class="feature-text"><strong>24/7 Support</strong><br>Our team is always here to help you</div>
|
||||
</li>
|
||||
<li class="feature-item">
|
||||
<div class="feature-icon"><i class="fas fa-chart-line"></i></div>
|
||||
<div class="feature-text"><strong>Advanced Analytics</strong><br>Get insights into your business performance</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom OTP Modal (No Bootstrap) -->
|
||||
<div class="otp-modal" id="otpModal">
|
||||
<div class="otp-modal-content">
|
||||
<div class="otp-modal-header">
|
||||
<h5 class="fw-bold">Login with OTP</h5>
|
||||
<button type="button" id="closeOtpModal" style="background:none;border:none;font-size:1.5rem;cursor:pointer;">×</button>
|
||||
</div>
|
||||
<div class="otp-modal-body">
|
||||
<div id="step1">
|
||||
<p class="text-center text-muted mb-4">Enter your registered email or mobile number</p>
|
||||
<input type="text" id="otpIdentifier" class="form-control" placeholder="Email or Phone (+91...)" required autofocus>
|
||||
<button id="sendOtpBtn" class="btn-submit w-100 mt-4">
|
||||
<i class="fas fa-paper-plane"></i> Send OTP
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="step2" style="display:none;">
|
||||
<p class="text-center text-muted mb-4">Enter the 6-digit OTP sent to<br><strong id="sentTo"></strong></p>
|
||||
<div class="otp-input-group">
|
||||
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
|
||||
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
|
||||
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
|
||||
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
|
||||
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
|
||||
<input type="text" class="otp-input" maxlength="1" inputmode="numeric">
|
||||
</div>
|
||||
<button id="verifyOtpBtn" class="btn-submit w-100">
|
||||
<i class="fas fa-check-circle"></i> Verify & Login
|
||||
</button>
|
||||
<p class="text-center mt-3">
|
||||
<a href="#" id="resendOtp" class="text-muted small">Resend OTP</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Modal Controls
|
||||
const modal = document.getElementById('otpModal');
|
||||
const openBtn = document.getElementById('openOtpModal');
|
||||
const closeBtn = document.getElementById('closeOtpModal');
|
||||
const step1 = document.getElementById('step1');
|
||||
const step2 = document.getElementById('step2');
|
||||
const identifierInput = document.getElementById('otpIdentifier');
|
||||
const sentTo = document.getElementById('sentTo');
|
||||
const otpInputs = document.querySelectorAll('.otp-input');
|
||||
|
||||
openBtn.onclick = () => modal.classList.add('active');
|
||||
closeBtn.onclick = () => modal.classList.remove('active');
|
||||
modal.onclick = (e) => { if (e.target === modal) modal.classList.remove('active'); };
|
||||
|
||||
// Auto-focus OTP inputs
|
||||
otpInputs.forEach((input, i) => {
|
||||
input.addEventListener('input', () => {
|
||||
if (input.value && i < 5) otpInputs[i + 1].focus();
|
||||
});
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' && !input.value && i > 0) otpInputs[i - 1].focus();
|
||||
});
|
||||
});
|
||||
|
||||
// Send OTP
|
||||
document.getElementById('sendOtpBtn').onclick = async () => {
|
||||
const identifier = identifierInput.value.trim();
|
||||
if (!identifier) return alert('Please enter email or phone');
|
||||
|
||||
const btn = document.getElementById('sendOtpBtn');
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
||||
btn.disabled = true;
|
||||
|
||||
const res = await fetch("{% url 'request_otp' %}", {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': '{{ csrf_token }}' },
|
||||
body: new URLSearchParams({ 'identifier': identifier })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
step1.style.display = 'none';
|
||||
step2.style.display = 'block';
|
||||
sentTo.textContent = identifier;
|
||||
otpInputs[0].focus();
|
||||
} else {
|
||||
alert('Failed to send OTP');
|
||||
}
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Send OTP';
|
||||
btn.disabled = false;
|
||||
};
|
||||
|
||||
// Verify OTP
|
||||
document.getElementById('verifyOtpBtn').onclick = async () => {
|
||||
const otp = Array.from(otpInputs).map(i => i.value).join('');
|
||||
if (otp.length !== 6) return alert('Enter full 6-digit OTP');
|
||||
|
||||
const btn = document.getElementById('verifyOtpBtn');
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verifying...';
|
||||
btn.disabled = true;
|
||||
|
||||
const res = await fetch("{% url 'verify_otp' %}", {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': '{{ csrf_token }}' },
|
||||
body: new URLSearchParams({ 'otp': otp })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.href = "{% url 'home' %}";
|
||||
} else {
|
||||
alert('Invalid or expired OTP');
|
||||
}
|
||||
btn.innerHTML = '<i class="fas fa-check-circle"></i> Verify & Login';
|
||||
btn.disabled = false;
|
||||
};
|
||||
|
||||
// Resend
|
||||
document.getElementById('resendOtp').onclick = (e) => {
|
||||
e.preventDefault();
|
||||
step2.style.display = 'none';
|
||||
step1.style.display = 'block';
|
||||
identifierInput.value = sentTo.textContent;
|
||||
identifierInput.focus();
|
||||
};
|
||||
|
||||
// Loading spinner on normal login
|
||||
document.querySelector('form').addEventListener('submit', function() {
|
||||
this.querySelector('.btn-submit').innerHTML = '<i class="fas fa-spinner fa-spin"></i> Signing In...';
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
31
at_django_boilerplate/accounts/templates/otp/otp_email.html
Executable file
31
at_django_boilerplate/accounts/templates/otp/otp_email.html
Executable file
@@ -0,0 +1,31 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Your OTP for {{ purpose|capfirst }}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background-color: #f9f9f9; color: #333; }
|
||||
.container { max-width: 600px; margin: auto; padding: 30px; background-color: #fff; border-radius: 8px; }
|
||||
.otp-box { font-size: 24px; font-weight: bold; color: #2c3e50; background: #ecf0f1; padding: 15px; text-align: center; border-radius: 6px; margin: 20px 0; }
|
||||
.footer { font-size: 12px; color: #999; margin-top: 30px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Hello,</h2>
|
||||
<p>You requested an OTP for <strong>{{ purpose|capfirst }}</strong>.</p>
|
||||
<p>Your One-Time Password (OTP) is:</p>
|
||||
<div class="otp-box">{{ otp }}</div>
|
||||
<p>This OTP is valid for <strong>{{ validity_minutes }} minutes</strong>.</p>
|
||||
|
||||
<p><strong>⚠️ Do not share this OTP with anyone.</strong></p>
|
||||
|
||||
<p>If you did not request this OTP, please ignore this email or <a href="mailto:support@example.com">contact support</a> immediately.</p>
|
||||
|
||||
<div class="footer">
|
||||
This is an automated message. Please do not reply.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
66
at_django_boilerplate/accounts/templates/profile/profile.html
Executable file
66
at_django_boilerplate/accounts/templates/profile/profile.html
Executable file
@@ -0,0 +1,66 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}User Profile{% endblock %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Profile Picture Update Modal -->
|
||||
<div class="fixed inset-0 z-50 hidden overflow-y-auto bg-black bg-opacity-60" id="profilePicModal">
|
||||
<div class="flex items-center justify-center min-h-screen px-4">
|
||||
<div class="bg-white rounded-xl shadow-2xl w-full max-w-md">
|
||||
<div class="flex justify-between items-center p-6 border-b border-gray-200">
|
||||
<h3 class="text-xl font-semibold text-[#1E40AF]">Update Profile Picture</h3>
|
||||
<button onclick="document.getElementById('profilePicModal').classList.add('hidden')" class="text-gray-500 hover:text-[#1E40AF] text-2xl transition-colors">×</button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form method="POST" action="{% url 'update_profile_picture' %}" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ update_profile_photo_form|crispy }}
|
||||
<button type="submit" class="mt-4 w-full bg-[#1E40AF] hover:bg-[#1E3A8A] text-white font-semibold py-2.5 px-4 rounded-lg transition-colors duration-200">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Content -->
|
||||
<div class="max-w-6xl mx-auto px-4 py-12">
|
||||
<div class="bg-white rounded-xl shadow-xl overflow-hidden">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
|
||||
<!-- Left Sidebar -->
|
||||
<div class="bg-gray-50 md:w-1/3 flex flex-col items-center justify-center p-8 text-center">
|
||||
<img src="{{ user.profile_photo.url }}" alt="Profile Image" class="w-36 h-36 rounded-full object-cover mb-6 shadow-lg border-4 border-white">
|
||||
<h2 class="text-2xl font-bold text-[#1E40AF]">{{ user }}</h2>
|
||||
<a href="{% url 'update_profile' user.id %}" class="mt-6 w-full bg-[#1E40AF] text-white py-2.5 rounded-lg hover:bg-[#1E3A8A] font-semibold transition-colors duration-200">Update Profile</a>
|
||||
<button onclick="document.getElementById('profilePicModal').classList.remove('hidden')" class="mt-3 w-full bg-[#1E40AF] text-white py-2.5 rounded-lg hover:bg-[#1E3A8A] font-semibold transition-colors duration-200">Update Profile Picture</button>
|
||||
</div>
|
||||
|
||||
<!-- Right Content -->
|
||||
<div class="md:w-2/3 p-8">
|
||||
<h3 class="text-xl font-semibold text-[#1E40AF] mb-4 border-b border-gray-200 pb-3">Account Information</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-500">Email</p>
|
||||
<p class="text-[#1E3A8A]">{{ user.email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-500">Phone</p>
|
||||
<p class="text-[#1E3A8A]">{{ user.get_decrypted_contact_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-semibold text-[#1E40AF] mb-4 border-b border-gray-200 pb-3">Social Media</h3>
|
||||
<div class="flex space-x-6 mt-4">
|
||||
<a href="#" class="text-[#1E40AF] hover:text-[#1E3A8A] text-2xl transition-colors"><i class="mdi mdi-facebook"></i></a>
|
||||
<a href="#" class="text-[#1E40AF] hover:text-[#1E3A8A] text-2xl transition-colors"><i class="mdi mdi-twitter"></i></a>
|
||||
<a href="#" class="text-[#1E40AF] hover:text-[#1E3A8A] text-2xl transition-colors"><i class="mdi mdi-instagram"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
21
at_django_boilerplate/accounts/templates/profile/update_profile.html
Executable file
21
at_django_boilerplate/accounts/templates/profile/update_profile.html
Executable file
@@ -0,0 +1,21 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}
|
||||
Update Profile
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-lg p-6 bg-white rounded-lg shadow-lg mt-10">
|
||||
<h2 class="text-3xl font-bold text-center text-gray-800 mb-6">Update Profile</h2>
|
||||
|
||||
<form method="POST" class="space-y-6">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
|
||||
<button type="submit" class="w-full bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white font-bold py-2 px-4 rounded-lg shadow-lg transition duration-300">
|
||||
Save changes
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
14
at_django_boilerplate/accounts/templates/profile/update_profile_picture.html
Executable file
14
at_django_boilerplate/accounts/templates/profile/update_profile_picture.html
Executable file
@@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}
|
||||
Update Profile Picture
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h2>Update Profile Picture</h2>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
21
at_django_boilerplate/accounts/templates/registration/create_user.html
Executable file
21
at_django_boilerplate/accounts/templates/registration/create_user.html
Executable file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Create User</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Create User</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{% url 'index' %}">Index</a></li>
|
||||
<li><a href="{% url 'home' %}">Home</a></li>
|
||||
<li><a href="{% url 'user_list' %}">User List</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
||||
316
at_django_boilerplate/accounts/templates/registration/signup.html
Executable file
316
at_django_boilerplate/accounts/templates/registration/signup.html
Executable file
@@ -0,0 +1,316 @@
|
||||
{% extends 'public_base.html' %}
|
||||
{% load static %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}Sign Up | Register your startup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-indigo-50 to-purple-50 py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-6xl">
|
||||
<div class="bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100">
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<!-- Left Side - Image Card (Hidden on mobile) -->
|
||||
<div class="hidden lg:block lg:w-2/5 bg-gradient-to-br from-indigo-600 to-purple-700 p-8 lg:p-12">
|
||||
<div class="flex flex-col justify-center items-center text-center text-white h-full">
|
||||
<div class="max-w-sm">
|
||||
<img src="{% static 'img/images or rys/Startup India Registration/hero1.png' %}" alt="Startup Registration" class="w-full h-48 lg:h-56 mb-6 lg:mb-8 rounded-lg">
|
||||
<h2 class="text-2xl font-bold mb-4">Start Your Entrepreneurial Journey</h2>
|
||||
<div class="space-y-4 text-left">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-check-circle text-green-300 mr-3 mt-1 flex-shrink-0"></i>
|
||||
<span class="text-sm">Quick and hassle-free registration process</span>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-check-circle text-green-300 mr-3 mt-1 flex-shrink-0"></i>
|
||||
<span class="text-sm">Expert guidance for startup compliance</span>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-check-circle text-green-300 mr-3 mt-1 flex-shrink-0"></i>
|
||||
<span class="text-sm">Secure and confidential data handling</span>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-check-circle text-green-300 mr-3 mt-1 flex-shrink-0"></i>
|
||||
<span class="text-sm">Dedicated support for your business growth</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side - Form Card -->
|
||||
<div class="w-full lg:w-3/5 py-8 lg:py-10 px-6 lg:px-8">
|
||||
<!-- Mobile Header with Logo -->
|
||||
<div class="lg:hidden mb-6 text-center">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="w-16 h-16 bg-indigo-600 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-rocket text-white text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Create Your Account</h2>
|
||||
<p class="text-sm text-gray-600">Join thousands of successful startups</p>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Header -->
|
||||
<div class="hidden lg:block text-center mb-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-2">Create Your Account</h2>
|
||||
<p class="text-gray-600">Start your entrepreneurial journey with us today</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Display -->
|
||||
{% if form.errors %}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 animate-pulse">
|
||||
<div class="flex items-center">
|
||||
<i class="fa-solid fa-exclamation-circle mr-2"></i>
|
||||
<strong class="font-medium">Please correct the following errors:</strong>
|
||||
</div>
|
||||
<ul class="list-disc list-inside mt-2 ml-2 text-sm">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Email Validation Status -->
|
||||
<div id="email-status" class="flex items-center space-x-2 text-sm mb-4 hidden">
|
||||
<span id="email-icon" class="text-lg"></span>
|
||||
<span id="email-text"></span>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'signup' %}" class="space-y-6" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Personal Information Section -->
|
||||
<div class="space-y-5">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center mr-3">
|
||||
<i class="fas fa-user text-indigo-600 text-sm"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Personal Information</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Name Fields -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- First Name -->
|
||||
<div>
|
||||
<label for="{{ form.first_name.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
First Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
</span>
|
||||
{{ form.first_name|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Name -->
|
||||
<div>
|
||||
<label for="{{ form.last_name.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Last Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
</span>
|
||||
{{ form.last_name|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact & DOB Fields -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Contact Number -->
|
||||
<div>
|
||||
<label for="{{ form.contact_number.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Contact Number
|
||||
</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
|
||||
<i class="fa-solid fa-phone"></i>
|
||||
</span>
|
||||
{{ form.contact_number|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Field -->
|
||||
<div>
|
||||
<label for="{{ form.email.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
</span>
|
||||
{{ form.email|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Section -->
|
||||
<div class="space-y-5">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center mr-3">
|
||||
<i class="fas fa-lock text-indigo-600 text-sm"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Security</h3>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div>
|
||||
<label for="{{ form.password.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</span>
|
||||
{{ form.password|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
|
||||
</div>
|
||||
|
||||
<!-- Compact Password Requirements -->
|
||||
<div class="mt-2">
|
||||
<button type="button" id="password-toggle" class="flex items-center text-xs text-indigo-600 hover:text-indigo-500 transition duration-200">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
View password requirements
|
||||
</button>
|
||||
|
||||
<div id="password-requirements" class="hidden mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p class="text-xs font-medium text-blue-700 mb-2">Password Requirements:</p>
|
||||
<ul class="text-xs text-blue-600 space-y-1">
|
||||
<li class="flex items-start">
|
||||
<span class="text-blue-500 mr-1">•</span>
|
||||
<span>Cannot be too similar to your personal information</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-blue-500 mr-1">•</span>
|
||||
<span>Must be at least 8 characters long</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-blue-500 mr-1">•</span>
|
||||
<span>Cannot be a commonly used password</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-blue-500 mr-1">•</span>
|
||||
<span>Cannot be entirely numeric</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div>
|
||||
<label for="{{ form.confirm_password.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirm Password <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</span>
|
||||
{{ form.confirm_password|add_class:"appearance-none block w-full pl-10 px-3 py-3 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms and Conditions -->
|
||||
<div class="bg-indigo-50 p-4 rounded-lg border border-indigo-100">
|
||||
<div class="flex items-start">
|
||||
{{ form.terms_accepted|add_class:"h-5 w-5 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded mt-0.5" }}
|
||||
<label for="{{ form.terms_accepted.id_for_label }}" class="ml-3 block text-sm text-gray-700">
|
||||
<span class="font-medium text-gray-900">I accept the terms and conditions</span> <span class="text-red-500">*</span>
|
||||
<p class="text-xs text-gray-500 mt-1">By creating an account, you agree to our Terms of Service and Privacy Policy.</p>
|
||||
</label>
|
||||
</div>
|
||||
{% if form.terms_accepted.errors %}
|
||||
<div class="text-red-600 text-sm mt-2 ml-8">
|
||||
{{ form.terms_accepted.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 shadow-lg transform hover:scale-[1.02] active:scale-[0.98]">
|
||||
<i class="fa-solid fa-rocket mr-2"></i>
|
||||
Launch Your Startup Journey
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Redirect -->
|
||||
<div class="text-center pt-4">
|
||||
<p class="text-sm text-gray-600">
|
||||
Already have an account?
|
||||
<a href="{% url 'login' %}" class="font-medium text-indigo-600 hover:text-indigo-500 transition duration-200 ml-1">
|
||||
Sign in here
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email AJAX Validation -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// Email validation
|
||||
$('#id_email').on('blur', function () {
|
||||
const username = $(this).val();
|
||||
if (username) {
|
||||
$.ajax({
|
||||
url: '{% url "check_username" %}',
|
||||
data: { 'username': username },
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
$('#email-status').removeClass('hidden');
|
||||
if (data.is_taken) {
|
||||
$('#email-icon').html('<i class="fa-solid fa-times-circle"></i>').removeClass('text-green-600').addClass('text-red-600');
|
||||
$('#email-text').text('This email is already registered.').removeClass('text-green-600').addClass('text-red-600');
|
||||
$('#id_email').addClass('border-red-500 focus:border-red-500 focus:ring-red-500');
|
||||
} else {
|
||||
$('#email-icon').html('<i class="fa-solid fa-check-circle"></i>').removeClass('text-red-600').addClass('text-green-600');
|
||||
$('#email-text').text('Email is available.').removeClass('text-red-600').addClass('text-green-600');
|
||||
$('#id_email').removeClass('border-red-500 focus:border-red-500 focus:ring-red-500');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$('#email-status').addClass('hidden');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$('#email-status').addClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Password requirements toggle
|
||||
$('#password-toggle').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$('#password-requirements').toggleClass('hidden');
|
||||
if ($('#password-requirements').hasClass('hidden')) {
|
||||
$(this).html('<i class="fas fa-info-circle mr-1"></i>View password requirements');
|
||||
} else {
|
||||
$(this).html('<i class="fas fa-times mr-1"></i>Hide password requirements');
|
||||
}
|
||||
});
|
||||
|
||||
// Hide password requirements when clicking outside
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('#password-toggle, #password-requirements').length) {
|
||||
$('#password-requirements').addClass('hidden');
|
||||
$('#password-toggle').html('<i class="fas fa-info-circle mr-1"></i>View password requirements');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
15
at_django_boilerplate/accounts/templates/registration/verify_email.html
Executable file
15
at_django_boilerplate/accounts/templates/registration/verify_email.html
Executable file
@@ -0,0 +1,15 @@
|
||||
{% extends 'public_base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Verify
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>You need to verify your email</h1>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input class="btn btn-primary max-btn" type="submit" value="Verify">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
{% extends 'public_base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Verify
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="alert alert-success">
|
||||
You have successfully verified your e-mail
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends 'public_base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Verify
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
12
at_django_boilerplate/accounts/templates/registration/verify_email_done.html
Executable file
12
at_django_boilerplate/accounts/templates/registration/verify_email_done.html
Executable file
@@ -0,0 +1,12 @@
|
||||
{% extends 'public_base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Verify
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h5>An email has been sent with instructions to verify your email</h5>
|
||||
<h5>If you have not received the email. Please check the spam folder</h5>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<div style="background-color: #c2c2c2; padding: 15px;">
|
||||
<h2 style="margin: 0; padding: 10px; border-top-left-radius: 10px; border-top-right-radius: 10px; background-color: #fdc038; color: #ffffff;">Verify Email</h2>
|
||||
<div style="margin: 0; padding: 15px; background-color: #ffffff;">
|
||||
<p>Hi {{ user.name }},</p>
|
||||
<p>You created an account on we-kwick, you need to verify your email. Please click on the button below to verify your email.</p>
|
||||
|
||||
<a href="{{ request.scheme }}://{{ domain }}{% url 'verify-email-confirm' uidb64=uid token=token %}"
|
||||
style="border: 0; color: #ffffff; background-color: #fdc038; padding: 15px; font-weight: bold; text-decoration: none; border-radius: 5px; display: inline-block; margin-top: 20px;">
|
||||
Verify Email
|
||||
</a>
|
||||
|
||||
<p style="margin-top: 40px;">Or you can copy the link below to your browser</p>
|
||||
<p>{{ request.scheme }}://{{ domain }}{% url 'verify-email-confirm' uidb64=uid token=token %}</p>
|
||||
<p>The We-Kwick Team</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px 0;">
|
||||
<p>© {% now 'Y' %} <a href="">Blog</a></p>
|
||||
<p>Follow us on <a href="">Twitter</a></p>
|
||||
</div>
|
||||
</div>
|
||||
49
at_django_boilerplate/accounts/templates/registration/verify_otp.html
Executable file
49
at_django_boilerplate/accounts/templates/registration/verify_otp.html
Executable file
@@ -0,0 +1,49 @@
|
||||
{% extends 'public_base.html' %}
|
||||
{% load static %}
|
||||
{% block title %}Verify OTP{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-center min-h-screen bg-gray-100 px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-xl shadow-md p-6">
|
||||
<h4 class="text-2xl font-semibold text-center text-gray-800 mb-6">Verify OTP</h4>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="mb-4 px-4 py-3 rounded text-sm
|
||||
{% if message.tags == 'error' %}
|
||||
bg-red-100 text-red-700
|
||||
{% elif message.tags == 'success' %}
|
||||
bg-green-100 text-green-700
|
||||
{% else %}
|
||||
bg-blue-100 text-blue-700
|
||||
{% endif %}
|
||||
">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="mb-4">
|
||||
<label for="otpInput" class="block text-sm font-medium text-gray-700 mb-1">Enter OTP sent to your email</label>
|
||||
<input type="text" name="otp" id="otpInput" maxlength="6" pattern="\d{6}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter 6-digit OTP" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md text-lg font-medium transition">
|
||||
<i class="fas fa-check-circle mr-2"></i>Verify OTP
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center text-sm text-gray-600">
|
||||
Didn’t receive the OTP?
|
||||
<a href="{% url 'request_otp' %}" class="text-blue-600 hover:underline">Resend</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
62
at_django_boilerplate/accounts/templates/reset/password_reset.html
Executable file
62
at_django_boilerplate/accounts/templates/reset/password_reset.html
Executable file
@@ -0,0 +1,62 @@
|
||||
{% extends "public_base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-center min-h-screen bg-gray-100 px-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white shadow-xl rounded-2xl overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 text-center">
|
||||
<h3 class="text-xl font-semibold text-gray-800">Forgot Password?</h3>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
{% if form.errors %}
|
||||
<div class="m-4 p-3 rounded-md bg-red-50 border border-red-200 text-red-700">
|
||||
<ul class="list-disc list-inside text-sm">
|
||||
{% for key, value in form.errors.items %}
|
||||
<li>{{ value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Body -->
|
||||
<div class="px-6 py-4">
|
||||
<form method="POST" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Email Field -->
|
||||
<div>
|
||||
<label for="id_email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input type="email"
|
||||
name="email"
|
||||
id="id_email"
|
||||
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
placeholder="Enter email"
|
||||
autocomplete="email"
|
||||
maxlength="254"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="w-full py-2 px-4 bg-gray-900 text-white font-medium rounded-lg shadow hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 text-center">
|
||||
<a href="{% url 'login' %}" class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
Back To Login
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- templates/registration/password_reset_complete.html -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Password reset complete{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Password reset complete</h1>
|
||||
<p>Your new password has been set. You can log in now on the <a href="{% url 'login' %}">log in page</a>.</p>
|
||||
{% endblock %}
|
||||
42
at_django_boilerplate/accounts/templates/reset/password_reset_confirm.html
Executable file
42
at_django_boilerplate/accounts/templates/reset/password_reset_confirm.html
Executable file
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Password Reset{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
|
||||
<style>
|
||||
.centered-form {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(255, 255, 255, 1.0); /* Adjust the opacity as needed */
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if validlink %}
|
||||
<div class="container-fluid h-100">
|
||||
<div class="row h-100">
|
||||
<div class="col-sm-8">
|
||||
<div class="centered-form">
|
||||
<h3>Set a New Password</h3>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{{form|crispy}}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Change My Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
26
at_django_boilerplate/accounts/templates/reset/password_reset_done.html
Executable file
26
at_django_boilerplate/accounts/templates/reset/password_reset_done.html
Executable file
@@ -0,0 +1,26 @@
|
||||
<!-- templates/registration/password_reset_done.html -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Email Sent{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert mt-2
|
||||
{% if 'success' in message.tags %} alert-success
|
||||
{% elif 'warning' in message.tags %} alert-warning
|
||||
{% elif 'info' in message.tags %} alert-info
|
||||
{% else %} alert-danger
|
||||
{% endif %}">
|
||||
<span class="text-danger fst-italic">{{ message }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h1>Check your inbox.</h1>
|
||||
<p>We've emailed you instructions for setting your password. You should receive the email shortly!</p>
|
||||
|
||||
{% endblock %}
|
||||
13
at_django_boilerplate/accounts/templates/reset/password_reset_email.html
Executable file
13
at_django_boilerplate/accounts/templates/reset/password_reset_email.html
Executable file
@@ -0,0 +1,13 @@
|
||||
{% autoescape off %}
|
||||
To initiate the password reset process for your we-kwick account,
|
||||
click the link below:
|
||||
|
||||
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
|
||||
|
||||
If clicking the link above doesn't work, please copy and paste the URL below in a new browser
|
||||
window instead.
|
||||
|
||||
Sincerely,
|
||||
we-kwick Team
|
||||
{% endautoescape %}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<div style="background-color: #c2c2c2; padding: 15px;">
|
||||
<h2 style="margin: 0; padding: 10px; border-top-left-radius: 10px; border-top-right-radius: 10px; background-color: #fdc038; color: #ffffff;">
|
||||
Password Reset
|
||||
</h2>
|
||||
<div style="margin: 0; padding: 15px; background-color: #ffffff;">
|
||||
<p>Hi {{ user.name }},</p>
|
||||
<p>
|
||||
To initiate the password reset process for your we-kwick account,
|
||||
click the link below:
|
||||
|
||||
<a href="{{ request.scheme }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}"
|
||||
style="border: 0; color: #ffffff; background-color: #fdc038; padding: 15px; font-weight: bold; text-decoration: none; border-radius: 5px; display: inline-block; margin-top: 20px;">
|
||||
Reset Password
|
||||
</a>
|
||||
|
||||
<p style="margin-top: 40px;">Or you can copy the link below to your browser</p>
|
||||
<p>{{ request.scheme }}://{{ domain }}{% url 'verify-email-confirm' uidb64=uid token=token %}</p>
|
||||
<p>The We-Kwick Team</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px 0;">
|
||||
<p>© {% now 'Y' %} <a href="">Blog</a></p>
|
||||
<p>Follow us on <a href="">Twitter</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
We-Kwick Password Reset
|
||||
25
at_django_boilerplate/accounts/templates/user_list.html
Executable file
25
at_django_boilerplate/accounts/templates/user_list.html
Executable file
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>User List</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>User List</h2>
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>
|
||||
Name: {{ user.name }}, Birthday: {{ user.birthday }},
|
||||
Address: {{ user.address }}, Email: {{ user.email_address }},
|
||||
Contact: {{ user.contact_number }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{% url 'index' %}">Index</a></li>
|
||||
<li><a href="{% url 'home' %}">Home</a></li>
|
||||
<li><a href="{% url 'signup' %}">Create user</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
||||
0
at_django_boilerplate/accounts/templatetags/__init__.py
Executable file
0
at_django_boilerplate/accounts/templatetags/__init__.py
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
7
at_django_boilerplate/accounts/templatetags/custom_tags.py
Executable file
7
at_django_boilerplate/accounts/templatetags/custom_tags.py
Executable file
@@ -0,0 +1,7 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter(name='add_class')
|
||||
def add_class(field, css):
|
||||
return field.as_widget(attrs={"class": css})
|
||||
8
at_django_boilerplate/accounts/templatetags/forms_tags.py
Executable file
8
at_django_boilerplate/accounts/templatetags/forms_tags.py
Executable file
@@ -0,0 +1,8 @@
|
||||
from django import template
|
||||
from django.forms.widgets import Textarea
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def is_textarea(field):
|
||||
return isinstance(field.field.widget, Textarea)
|
||||
3
at_django_boilerplate/accounts/tests.py
Executable file
3
at_django_boilerplate/accounts/tests.py
Executable file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
at_django_boilerplate/accounts/tokens.py
Executable file
10
at_django_boilerplate/accounts/tokens.py
Executable file
@@ -0,0 +1,10 @@
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from six import text_type
|
||||
|
||||
class TokenGenerator(PasswordResetTokenGenerator):
|
||||
def _make_hash_value(self, user, timestamp):
|
||||
return (
|
||||
text_type(user.pk) + text_type(timestamp) +
|
||||
text_type(getattr(user, 'is_active', True))
|
||||
)
|
||||
account_activation_token = TokenGenerator()
|
||||
57
at_django_boilerplate/accounts/urls.py
Executable file
57
at_django_boilerplate/accounts/urls.py
Executable file
@@ -0,0 +1,57 @@
|
||||
# urls.py
|
||||
from django.urls import path
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
from . import views
|
||||
from at_django_boilerplate.accounts.api import v1 as api_v1
|
||||
|
||||
urlpatterns = [
|
||||
path('signup/', views.SignupView.as_view(), name='signup'),
|
||||
path('ajax/check_username/', views.CheckUsernameAvailability.as_view(), name='check_username'),
|
||||
|
||||
path('login/', views.LoginPageView.as_view(), name='login'),
|
||||
path('login/', views.LoginPageView.as_view(), name='signin'),
|
||||
path('login', views.LoginPageView.as_view(), name='login'),
|
||||
path('login', views.LoginPageView.as_view(), name='signin'),
|
||||
|
||||
path('profile/', login_required(views.ProfileView.as_view()), name='user_profile'),
|
||||
path('update_profile/<uuid:pk>', login_required(views.UpdateProfileView.as_view()), name='update_profile'),
|
||||
path('update_profile_picture/', login_required(views.update_profile_picture), name='update_profile_picture'),
|
||||
|
||||
|
||||
path('password-reset/', views.ResetPasswordView.as_view(), name='password_reset'),
|
||||
path('password-reset/applied', views.ResetPasswordDoneView.as_view(), name='password_reset_done'),
|
||||
|
||||
path('password-reset-confirm/<uidb64>/<token>/',
|
||||
auth_views.PasswordResetConfirmView.as_view(template_name='reset/password_reset_confirm.html'),
|
||||
name='password_reset_confirm'),
|
||||
path('password-reset-complete/',
|
||||
auth_views.PasswordResetCompleteView.as_view(template_name='reset/password_reset_complete.html'),
|
||||
name='password_reset_complete'),
|
||||
path('password_change/',views.ChangePasswordView.as_view()),
|
||||
|
||||
path('verify-email/', views.verify_email, name='verify-email'),
|
||||
path('verify-email/done/', views.verify_email_done, name='verify-email-done'),
|
||||
path('verify-email-confirm/<uidb64>/<token>/', views.verify_email_confirm, name='verify-email-confirm'),
|
||||
path('verify-email/complete/', views.verify_email_complete, name='verify-email-complete'),
|
||||
|
||||
path('logout/', views.LogoutView.as_view(), name='logout'),
|
||||
|
||||
# Otp via signin
|
||||
path('request-otp/', views.request_otp_view, name='request_otp'),
|
||||
path('verify-otp/', views.verify_otp_view, name='verify_otp'),
|
||||
|
||||
]
|
||||
|
||||
|
||||
api_v1=[
|
||||
path('api/login/', api_v1.LoginApiView.as_view(), name='login-api'),
|
||||
path('validate-password/', api_v1.ValidatePasswordAPIView.as_view(), name='validate-password-api'),
|
||||
path('api/reset-password/', api_v1.ResetPasswordAPIView.as_view(), name='reset-password-api'),
|
||||
path('reset-password/<uidb64>/<token>/', api_v1.ResetPasswordConfirmAPIView.as_view(), name='reset-password-confirm'),
|
||||
path('api/profile/', api_v1.UserProfileView.as_view(), name='profile-api'),
|
||||
]
|
||||
|
||||
|
||||
urlpatterns +=api_v1
|
||||
189
at_django_boilerplate/accounts/utils.py
Executable file
189
at_django_boilerplate/accounts/utils.py
Executable file
@@ -0,0 +1,189 @@
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django.template.loader import render_to_string
|
||||
from .tokens import account_activation_token
|
||||
from django.core.mail import EmailMessage
|
||||
from django.shortcuts import redirect
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.conf import settings
|
||||
|
||||
from django.contrib.auth import login
|
||||
from .models import CustomUser
|
||||
from at_django_boilerplate.utils.hash_utils import hexdigest
|
||||
from at_django_boilerplate.utils.encryption_utils import EncryptionUtils
|
||||
|
||||
def send_activation_email(request, user, to_email):
|
||||
try:
|
||||
domain = request.get_host()
|
||||
message = render_to_string('registration/verify_email_message.html', {
|
||||
'request': request,
|
||||
'user': user,
|
||||
'domain': domain,
|
||||
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
|
||||
'token': account_activation_token.make_token(user),
|
||||
})
|
||||
|
||||
subject = 'Welcome to we-kwick. Verify your Email'
|
||||
from_email = settings.EMAIL_HOST_USER
|
||||
_to_email = [to_email]
|
||||
text_content = ''
|
||||
html_content = message
|
||||
|
||||
email = EmailMultiAlternatives(subject, text_content, from_email, _to_email)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
email.send()
|
||||
print(f'Sent Joining Email to {to_email} ')
|
||||
return True
|
||||
except Exception as e:
|
||||
print('Failed to Send Email')
|
||||
return False
|
||||
|
||||
|
||||
def redirect_to_next_or_home(request):
|
||||
try:
|
||||
if 'next' in request.POST:
|
||||
_next = request.POST.get('next')
|
||||
if _next is not None:
|
||||
if _next == '' or _next == '/':
|
||||
return redirect('home')
|
||||
else:
|
||||
return redirect(_next)
|
||||
else:
|
||||
return redirect('home')
|
||||
else:
|
||||
return redirect('home')
|
||||
except Exception as e:
|
||||
print('Exception Post:', e)
|
||||
return redirect('home')
|
||||
|
||||
|
||||
def save_user(request,user_form):
|
||||
email = user_form.cleaned_data['email'].lower()
|
||||
contact_number = user_form.cleaned_data['contact_number']
|
||||
email_hash = hexdigest(email)
|
||||
existing_email = CustomUser.objects.filter(email_hash=email_hash)
|
||||
existing_email_exists = existing_email.exists()
|
||||
|
||||
existing_number_exists = False
|
||||
if all([contact_number != '',contact_number != None]):
|
||||
contact_number_hash = hexdigest(contact_number)
|
||||
existing_number = CustomUser.objects.filter(contact_number_hash=contact_number_hash)
|
||||
existing_number_exists = existing_number.exists()
|
||||
|
||||
if existing_email_exists:
|
||||
user_form.add_error('email', 'Email is already in use.')
|
||||
elif existing_number_exists:
|
||||
user_form.add_error('contact_number', 'Contact number is already in use.')
|
||||
else:
|
||||
try:
|
||||
user = user_form.save(commit=False)
|
||||
user.set_password(user.password)
|
||||
user.save(custom_save=True)
|
||||
login(request, user)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
send_activation_email(request=request,user=user,to_email=user.get_decrypted_email())
|
||||
return True,user
|
||||
except Exception as e:
|
||||
return False,user_form
|
||||
|
||||
return False,user_form
|
||||
|
||||
|
||||
|
||||
# Otp
|
||||
import random
|
||||
import re
|
||||
from django.core.mail import send_mail
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
|
||||
PHONE_REGEX = re.compile(r'^\+?\d{10,15}$')
|
||||
|
||||
def generate_otp(length=6):
|
||||
return ''.join([str(random.randint(0, 9)) for _ in range(length)])
|
||||
|
||||
# def send_otp(identifier, purpose='login'):
|
||||
# """
|
||||
# Sends an OTP via email or SMS based on the identifier.
|
||||
# Returns a tuple: (success: bool, method: 'email'|'sms')
|
||||
# """
|
||||
# otp = generate_otp()
|
||||
# cache_key = f'otp_{purpose}_{identifier}'
|
||||
|
||||
# if PHONE_REGEX.match(identifier):
|
||||
# method = 'sms'
|
||||
# cache.set(cache_key, otp, timeout=300)
|
||||
# # Replace with your SMS API logic (e.g., Twilio)
|
||||
# print(f"📲 SMS OTP sent to {identifier}: {otp}")
|
||||
# else:
|
||||
# method = 'email'
|
||||
# cache.set(cache_key, otp, timeout=300)
|
||||
# try:
|
||||
# send_mail(
|
||||
# subject=f"Your {purpose.capitalize()} OTP",
|
||||
# message=f"Your {purpose} OTP is: {otp}\n\nThis OTP is valid for 5 minutes.",
|
||||
# from_email=settings.EMAIL_HOST_USER,
|
||||
# recipient_list=[identifier],
|
||||
# fail_silently=False,
|
||||
# )
|
||||
# except Exception as e:
|
||||
# print(f"❌ Failed to send OTP email to {identifier}: {e}")
|
||||
# return False, 'email'
|
||||
|
||||
# return True, method
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import EmailMessage
|
||||
|
||||
def send_otp(identifier, purpose='login'):
|
||||
"""
|
||||
Sends an OTP via email or SMS based on the identifier.
|
||||
Returns a tuple: (success: bool, method: 'email'|'sms')
|
||||
"""
|
||||
otp = generate_otp()
|
||||
cache_key = f'otp_{purpose}_{identifier}'
|
||||
|
||||
if PHONE_REGEX.match(identifier):
|
||||
method = 'sms'
|
||||
cache.set(cache_key, otp, timeout=300)
|
||||
# Replace with your SMS API logic
|
||||
print(f"📲 SMS OTP sent to {identifier}: {otp}")
|
||||
else:
|
||||
method = 'email'
|
||||
cache.set(cache_key, otp, timeout=300)
|
||||
|
||||
try:
|
||||
email_subject = f"Your {purpose.capitalize()} OTP"
|
||||
email_body = render_to_string('otp/otp_email.html', {
|
||||
'otp': otp,
|
||||
'purpose': purpose,
|
||||
'identifier': identifier,
|
||||
'validity_minutes': 5,
|
||||
})
|
||||
email = EmailMessage(
|
||||
subject=email_subject,
|
||||
body=email_body,
|
||||
from_email=settings.EMAIL_HOST_USER,
|
||||
to=[identifier],
|
||||
)
|
||||
email.content_subtype = 'html' # Important for HTML emails
|
||||
email.send(fail_silently=False)
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send OTP email to {identifier}: {e}")
|
||||
return False, 'email'
|
||||
|
||||
return True, method
|
||||
|
||||
|
||||
def verify_otp(identifier, user_input_otp, purpose='login'):
|
||||
"""
|
||||
Verifies the OTP entered by the user against the cache.
|
||||
"""
|
||||
cache_key = f'otp_{purpose}_{identifier}'
|
||||
cached_otp = cache.get(cache_key)
|
||||
|
||||
if cached_otp and cached_otp == user_input_otp:
|
||||
cache.delete(cache_key)
|
||||
return True
|
||||
return False
|
||||
415
at_django_boilerplate/accounts/views.py
Executable file
415
at_django_boilerplate/accounts/views.py
Executable file
@@ -0,0 +1,415 @@
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import FormView
|
||||
from django.views.generic.edit import UpdateView
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.list import ListView
|
||||
from django.contrib.auth import login,authenticate,logout
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import redirect, render
|
||||
from django.db import IntegrityError
|
||||
import hashlib
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from .forms import (UserSignUpForm,
|
||||
SigninForm,
|
||||
CustomPasswordResetForm,
|
||||
ProfilePictureUpdateForm,
|
||||
UpdateProfileForm)
|
||||
from django.contrib.auth.views import (PasswordResetView,
|
||||
PasswordChangeView)
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
|
||||
from .models import CustomUser
|
||||
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from .tokens import account_activation_token
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
|
||||
from .utils import (send_activation_email,
|
||||
redirect_to_next_or_home,
|
||||
)
|
||||
from at_django_boilerplate.utils.encryption_utils import EncryptionUtils
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from .forms import UserSignUpForm
|
||||
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CheckUsernameAvailability(View):
|
||||
def get(self, request):
|
||||
username = request.GET.get('username', None)
|
||||
is_taken = CustomUser.objects.filter_by_email(username).exists()
|
||||
data = {
|
||||
'is_taken': is_taken
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
class SignupView(FormView):
|
||||
template_name = 'registration/signup.html'
|
||||
form_class = UserSignUpForm
|
||||
success_url = reverse_lazy('home')
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
next_url = request.GET.get('next')
|
||||
user_form = self.form_class(request.POST)
|
||||
|
||||
if user_form.is_valid():
|
||||
email = user_form.cleaned_data['email'].lower()
|
||||
contact_number = user_form.cleaned_data.get('contact_number')
|
||||
|
||||
email_exists = CustomUser.objects.filter_by_email(email).exists()
|
||||
number_exists = False
|
||||
|
||||
if contact_number:
|
||||
number_exists = CustomUser.objects.filter_by_contact_number(contact_number).exists()
|
||||
|
||||
if email_exists:
|
||||
user_form.add_error('email', 'Email is already in use.')
|
||||
elif number_exists:
|
||||
user_form.add_error('contact_number', 'Contact number is already in use.')
|
||||
else:
|
||||
try:
|
||||
user = user_form.save(commit=False)
|
||||
user.set_password(user.password)
|
||||
user.save()
|
||||
login(request, user)
|
||||
|
||||
user.is_active = True
|
||||
user.save()
|
||||
|
||||
send_activation_email(
|
||||
request=request,
|
||||
user=user,
|
||||
to_email=user.get_decrypted_email()
|
||||
)
|
||||
|
||||
# return redirect(next_url or 'verify-email-done')
|
||||
return redirect('dashboard')
|
||||
|
||||
except IntegrityError:
|
||||
# Rollback if needed
|
||||
user.delete()
|
||||
|
||||
return self.render_to_response(self.get_context_data(form=user_form))
|
||||
|
||||
|
||||
def verify_email(request):
|
||||
return render(request, 'registration/verify_email.html')
|
||||
|
||||
|
||||
def verify_email_done(request):
|
||||
return render(request, 'registration/verify_email_done.html')
|
||||
|
||||
|
||||
def verify_email_confirm(request, uidb64, token):
|
||||
try:
|
||||
uid = force_str(urlsafe_base64_decode(uidb64))
|
||||
user = CustomUser.objects.get(pk=uid)
|
||||
except(TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
|
||||
user = None
|
||||
if user is not None and account_activation_token.check_token(user, token):
|
||||
user.is_active = True
|
||||
user.save()
|
||||
messages.success(request, 'Your email has been verified.')
|
||||
return redirect('verify-email-complete')
|
||||
else:
|
||||
messages.warning(request, 'The link is invalid.')
|
||||
return render(request, 'registration/verify_email_confirm.html')
|
||||
|
||||
|
||||
def verify_email_complete(request):
|
||||
return render(request, 'registration/verify_email_complete.html')
|
||||
|
||||
|
||||
|
||||
class LoginPageView(View):
|
||||
template_name = 'login.html'
|
||||
form_class = SigninForm
|
||||
|
||||
def get(self, request):
|
||||
if request.user.is_authenticated:
|
||||
next_url = request.GET.get('next')
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts={request.get_host()}):
|
||||
return redirect(next_url)
|
||||
return redirect('home')
|
||||
|
||||
form = self.form_class()
|
||||
return render(request, self.template_name, {'form': form, 'message': ''})
|
||||
|
||||
def post(self, request):
|
||||
print('Login')
|
||||
form = self.form_class(request.POST)
|
||||
message = 'Login failed!'
|
||||
|
||||
if form.is_valid():
|
||||
print('Form Valid')
|
||||
username = form.cleaned_data['username'].lower()
|
||||
password = form.cleaned_data['password']
|
||||
|
||||
user = authenticate(request, username=username, password=password)
|
||||
# user.backend = 'accounts.auth_backends.CustomAuthBackend' # <--- key line added
|
||||
print(f"Authenticated User: {user}")
|
||||
if user:
|
||||
if user.is_active:
|
||||
login(request, user)
|
||||
return redirect_to_next_or_home(request)
|
||||
else:
|
||||
messages.error(request, 'Please verify your account before logging in.')
|
||||
else:
|
||||
messages.error(request, 'Invalid username or password.')
|
||||
else:
|
||||
messages.error(request, 'Invalid input.')
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'message': message,
|
||||
})
|
||||
|
||||
|
||||
|
||||
class LogoutView(View):
|
||||
def get(self, request):
|
||||
logout(request)
|
||||
return redirect('home')
|
||||
|
||||
|
||||
class ResetPasswordView(SuccessMessageMixin, PasswordResetView):
|
||||
template_name = 'reset/password_reset.html'
|
||||
email_template_name = 'reset/password_reset_email.html'
|
||||
html_email_template_name = 'reset/password_reset_email_html.html'
|
||||
subject_template_name = 'reset/password_reset_subject.txt'
|
||||
|
||||
success_message = "We've emailed you instructions for setting your password, " \
|
||||
"if an account exists with the email you entered. You should receive them shortly." \
|
||||
" If you don't receive an email, " \
|
||||
"please make sure you've entered the address you registered with, and check your spam folder."
|
||||
success_url = reverse_lazy('password_reset_done')
|
||||
token_generator = default_token_generator
|
||||
form_class = CustomPasswordResetForm
|
||||
|
||||
class ResetPasswordDoneView(View):
|
||||
template_name = 'reset/password_reset_done.html'
|
||||
success_message = "We've emailed you instructions for setting your password, " \
|
||||
"if an account exists with the email you entered. You should receive them shortly." \
|
||||
" If you don't receive an email, " \
|
||||
"please make sure you've entered the address you registered with, and check your spam folder."
|
||||
|
||||
def get(self, request):
|
||||
return render(request=request,template_name=self.template_name)
|
||||
|
||||
|
||||
|
||||
class ChangePasswordView(SuccessMessageMixin, PasswordChangeView):
|
||||
template_name = 'change_password.html'
|
||||
success_message = "Successfully Changed Your Password"
|
||||
success_url = reverse_lazy('home')
|
||||
|
||||
|
||||
|
||||
|
||||
class CustomUserListView(ListView):
|
||||
model = CustomUser
|
||||
template_name = 'user_list.html'
|
||||
users = 'CustomUsers'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
users = CustomUser.objects.all()
|
||||
context['users'] = [{
|
||||
'username': custom_user.username,
|
||||
'name': custom_user.get_decrypted_name(),
|
||||
'email': custom_user.get_decrypted_email(),
|
||||
'birthday': custom_user.birthday,
|
||||
'address': custom_user.get_decrypted_address(),
|
||||
'contact_number': custom_user.get_decrypted_contact_number()
|
||||
} for custom_user in users]
|
||||
return context
|
||||
|
||||
class ProfileView(DetailView):
|
||||
model = CustomUser
|
||||
template_name = 'profile/profile.html'
|
||||
context_object_name = 'custom_user'
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Initialize the form with the current user's data
|
||||
form = ProfilePictureUpdateForm(instance=self.get_object())
|
||||
context['update_profile_photo_form'] = form
|
||||
return context
|
||||
|
||||
class UpdateProfileView(UserPassesTestMixin, UpdateView):
|
||||
model = CustomUser
|
||||
form_class = UpdateProfileForm
|
||||
template_name = 'profile/update_profile.html'
|
||||
|
||||
def test_func(self):
|
||||
obj = self.get_object()
|
||||
requested_by_user = self.request.user
|
||||
return (obj == requested_by_user)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
# Decrypt the data before passing it to the form
|
||||
encryption_tool = EncryptionUtils()
|
||||
contact_number = None
|
||||
if self.object.contact_number:
|
||||
contact_number = encryption_tool.decrypt(self.object.contact_number)
|
||||
|
||||
initial_data = {
|
||||
'contact_number': contact_number
|
||||
}
|
||||
form = self.form_class(instance=self.object, initial=initial_data)
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if form.is_valid():
|
||||
obj = form.save(commit=False)
|
||||
encryption_tool = EncryptionUtils()
|
||||
# address = form.cleaned_data['address']
|
||||
# obj.address = encryption_tool.encrypt(address)
|
||||
contact_number = form.cleaned_data['contact_number']
|
||||
if all([contact_number!=None,contact_number!='']):
|
||||
obj.contact_number = encryption_tool.encrypt(contact_number)
|
||||
obj.save()
|
||||
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
# Decrypt the data before re-rendering the form with errors
|
||||
encryption_tool = EncryptionUtils()
|
||||
contact_number = None
|
||||
if self.object.contact_number:
|
||||
contact_number = encryption_tool.decrypt(self.object.contact_number)
|
||||
|
||||
initial_data = {
|
||||
'contact_number': contact_number
|
||||
}
|
||||
form = self.form_class(instance=self.object, initial=initial_data)
|
||||
|
||||
return self.form_invalid(form)
|
||||
|
||||
@login_required
|
||||
def update_profile_picture(request):
|
||||
if request.user.pk !=None:
|
||||
user_profile = request.user
|
||||
if request.method == 'POST':
|
||||
form = ProfilePictureUpdateForm(request.POST, request.FILES, instance=user_profile)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Your profile picture has been updated successfully!')
|
||||
return redirect('user_profile')
|
||||
else:
|
||||
form = ProfilePictureUpdateForm(instance=user_profile)
|
||||
return render(request, 'profile/update_profile_picture.html', {'form': form})
|
||||
|
||||
|
||||
from django.core.validators import EmailValidator, ValidationError
|
||||
from django.core.cache import cache
|
||||
from .utils import send_otp, verify_otp
|
||||
from .models import CustomUser
|
||||
import re
|
||||
|
||||
PHONE_REGEX = re.compile(r'^\+?\d{10,15}$')
|
||||
|
||||
|
||||
|
||||
def is_email(identifier):
|
||||
validator = EmailValidator()
|
||||
try:
|
||||
validator(identifier)
|
||||
return True
|
||||
except ValidationError:
|
||||
return False
|
||||
|
||||
def request_otp_view(request):
|
||||
if request.method == 'POST':
|
||||
identifier = request.POST.get('identifier', '').strip().lower()
|
||||
|
||||
if not identifier:
|
||||
messages.error(request, "Please enter your email or mobile number.")
|
||||
return redirect('login') # Assuming OTP modal is on the login page
|
||||
print(f"Identifier: {identifier}")
|
||||
user = None
|
||||
if is_email(identifier):
|
||||
print(f"Email: {identifier}")
|
||||
user = CustomUser.objects.filter_by_email(email=identifier).first()
|
||||
else:
|
||||
if not PHONE_REGEX.match(identifier):
|
||||
messages.error(request, "Invalid mobile number format.")
|
||||
return redirect('login')
|
||||
print(f"Mobile hash: {identifier}")
|
||||
user = CustomUser.objects.filter_by_contact_number(contact_number=identifier).first()
|
||||
print(f"User: {user}")
|
||||
if not user:
|
||||
messages.error(request, "No user found with this email or mobile number.")
|
||||
return redirect('login')
|
||||
|
||||
# Rate limiting
|
||||
cache_key = f"otp_rate_limit_{identifier}"
|
||||
if cache.get(cache_key):
|
||||
messages.error(request, "Please wait before requesting another OTP.")
|
||||
return redirect('login')
|
||||
|
||||
# Send OTP
|
||||
success, _ = send_otp(identifier, purpose='login')
|
||||
if success:
|
||||
cache.set(cache_key, True, timeout=60) # 60 seconds
|
||||
request.session['otp_identifier'] = identifier
|
||||
messages.success(request, "OTP sent successfully.")
|
||||
|
||||
return redirect('verify_otp')
|
||||
else:
|
||||
messages.error(request, "Failed to send OTP. Please try again.")
|
||||
return redirect('login')
|
||||
|
||||
return redirect('login')
|
||||
|
||||
|
||||
def verify_otp_view(request):
|
||||
identifier = request.session.get('otp_identifier')
|
||||
if not identifier:
|
||||
messages.error(request, "Session expired. Please request a new OTP.")
|
||||
return redirect('login')
|
||||
|
||||
if request.method == 'POST':
|
||||
user_otp = request.POST.get('otp', '').strip()
|
||||
|
||||
if not user_otp:
|
||||
messages.error(request, "Please enter the OTP.")
|
||||
return render(request, 'registration/verify_otp.html')
|
||||
|
||||
if verify_otp(identifier, user_otp):
|
||||
# Match user
|
||||
if is_email(identifier):
|
||||
print(f"Email: {identifier}")
|
||||
user = CustomUser.objects.get_by_email(email=identifier)
|
||||
else:
|
||||
|
||||
user = CustomUser.objects.get_by_contact_number(contact_number=identifier)
|
||||
|
||||
# user.backend = 'django.contrib.auth.backends.ModelBackend' # <--- key line added
|
||||
login(request, user)
|
||||
messages.success(request, "Logged in successfully.")
|
||||
request.session.pop('otp_identifier', None)
|
||||
return redirect('home')
|
||||
else:
|
||||
messages.error(request, "Invalid or expired OTP.")
|
||||
|
||||
return render(request, 'registration/verify_otp.html')
|
||||
Reference in New Issue
Block a user