mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
feat: implement email verification flow with account activation and secure cookie configuration
This commit is contained in:
@@ -14,7 +14,10 @@ CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
|||||||
# EMAIL
|
# EMAIL
|
||||||
EMAIL_HOST=localhost
|
EMAIL_HOST=localhost
|
||||||
EMAIL_PORT=1025
|
EMAIL_PORT=1025
|
||||||
|
FROM_EMAIL=Pi Ku <no-reply@piku.app>
|
||||||
|
|
||||||
# FRONTEND
|
# FRONTEND
|
||||||
VITE_API_URL=http://localhost:8000
|
VITE_API_URL=http://localhost:8000
|
||||||
FRONTEND_PORT=5173
|
FRONTEND_PORT=5173
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
FRONTEND_DOMAIN=localhost
|
||||||
|
|||||||
@@ -110,8 +110,19 @@ SIMPLE_JWT = {
|
|||||||
"BLACKLIST_AFTER_ROTATION": True,
|
"BLACKLIST_AFTER_ROTATION": True,
|
||||||
"AUTH_HEADER_TYPES": ("Bearer",),
|
"AUTH_HEADER_TYPES": ("Bearer",),
|
||||||
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
|
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
|
||||||
|
"AUTH_COOKIE": "refresh_token",
|
||||||
|
"AUTH_COOKIE_DOMAIN": f".{env('FRONTEND_DOMAIN')}",
|
||||||
|
"AUTH_COOKIE_SECURE": True,
|
||||||
|
"AUTH_COOKIE_HTTPONLY": True,
|
||||||
|
"AUTH_COOKIE_SAMESITE": "Lax", # Allow cross-site for links from email. Otherwise we'd use strict
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Email config
|
||||||
|
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||||
|
FROM_EMAIL = env("FROM_EMAIL")
|
||||||
|
|
||||||
|
FRONTEND_URL = env("FRONTEND_URL")
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class CustomUserManager(BaseUserManager):
|
|||||||
def create_user(self, email, password=None, **extra_fields):
|
def create_user(self, email, password=None, **extra_fields):
|
||||||
if not email:
|
if not email:
|
||||||
raise ValueError(_("The Email must be set"))
|
raise ValueError(_("The Email must be set"))
|
||||||
|
# set default activation state as False to enforce email verification
|
||||||
|
extra_fields.setdefault("is_active", False)
|
||||||
|
|
||||||
email = self.normalize_email(email)
|
email = self.normalize_email(email)
|
||||||
user = self.model(email=email, **extra_fields)
|
user = self.model(email=email, **extra_fields)
|
||||||
@@ -39,6 +41,10 @@ class User(AbstractUser):
|
|||||||
|
|
||||||
full_name = models.CharField(max_length=100)
|
full_name = models.CharField(max_length=100)
|
||||||
email = models.EmailField(_("email address"), unique=True)
|
email = models.EmailField(_("email address"), unique=True)
|
||||||
|
kdf_salt = models.CharField(max_length=128, blank=True, null=True)
|
||||||
|
|
||||||
|
# Default is False to enforce email verification
|
||||||
|
is_active = models.BooleanField(default=False)
|
||||||
|
|
||||||
objects = CustomUserManager()
|
objects = CustomUserManager()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
||||||
|
|
||||||
|
from users.views import ActivationView
|
||||||
|
|
||||||
from .views import MeView, RegisterView
|
from .views import MeView, RegisterView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -11,4 +13,6 @@ urlpatterns = [
|
|||||||
path("refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
path("refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
# Get current user info
|
# Get current user info
|
||||||
path("me/", MeView.as_view(), name="me"),
|
path("me/", MeView.as_view(), name="me"),
|
||||||
|
# Activate user account
|
||||||
|
path("activate/<str:uidb64>/<str:token>/", ActivationView.as_view(), name="activate"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
from django.utils.http import urlsafe_base64_encode
|
||||||
|
|
||||||
|
|
||||||
|
def send_activation_email(user):
|
||||||
|
token = default_token_generator.make_token(user)
|
||||||
|
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||||
|
activation_url = f"{settings.FRONTEND_URL}/activate/{uid}/{token}"
|
||||||
|
subject = "Activate Your Piku Account"
|
||||||
|
message = f"""Hi {user.full_name},
|
||||||
|
|
||||||
|
Welcome to Pi Ku.
|
||||||
|
|
||||||
|
Please click the link below to activate your account:
|
||||||
|
>> {activation_url}
|
||||||
|
|
||||||
|
If you did not create this account, please ignore this email."""
|
||||||
|
send_mail(subject, message, settings.FROM_EMAIL, [user.email], fail_silently=False)
|
||||||
|
return True
|
||||||
+23
-1
@@ -1,5 +1,8 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from rest_framework import generics, permissions
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
|
from django.utils.http import urlsafe_base64_decode
|
||||||
|
from rest_framework import generics, permissions, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from .serializers import UserSerializer
|
from .serializers import UserSerializer
|
||||||
|
|
||||||
@@ -12,6 +15,25 @@ class RegisterView(generics.CreateAPIView):
|
|||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ActivationView(generics.GenericAPIView):
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
|
def get(self, request, uidb64, token):
|
||||||
|
try:
|
||||||
|
uid = urlsafe_base64_decode(uidb64).decode()
|
||||||
|
user = User.objects.get(pk=uid)
|
||||||
|
except (User.DoesNotExist, TypeError, ValueError):
|
||||||
|
return Response({"detail": "Invalid activation link: User Error"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
# validate token
|
||||||
|
if not default_token_generator.check_token(user, token):
|
||||||
|
return Response({"detail": "Invalid activation link: Token Error"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
# activate user
|
||||||
|
user.is_active = True
|
||||||
|
user.save()
|
||||||
|
return Response({"detail": "Account activated successfully"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class MeView(generics.RetrieveAPIView):
|
class MeView(generics.RetrieveAPIView):
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
|||||||
Reference in New Issue
Block a user