Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions Backend/API_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,46 @@ Content-Type: application/json
**Response (201):**
```json
{
"email": "string",
"message": "Registration successful. Please check your email to verify your account."
}
```

---

### 2. Verify Email
```http
POST /auth/verify-email/
Content-Type: application/json
```

**Request:**
```json
{
"email": "string",
"code": "string"
}
```

**Response (200):**
```json
{
"message": "Email verified successfully",
"access": "jwt_token",
"refresh": "jwt_token",
"user": {
"id": 1,
"username": "string",
"email": "string",
"username": "string",
"first_name": "string",
"last_name": "string"
},
"refresh": "jwt_token",
"access": "jwt_token",
"message": "Registration successful"
}
}
```

---

### 2. Login
### 3. Login
```http
POST /auth/login/
Content-Type: application/json
Expand All @@ -62,9 +86,11 @@ Content-Type: application/json
}
```

**Note:** User must verify email before login. Unverified accounts will receive `401 Unauthorized`.

---

### 3. Refresh Token
### 4. Refresh Token
```http
POST /auth/token/refresh/
Content-Type: application/json
Expand Down Expand Up @@ -101,6 +127,13 @@ Authorization: Bearer <access_token>
- **Refresh Token:** 7 days
- Refresh tokens rotate on use (new refresh token returned)

## Email Verification

- Verification codes are **6 characters** (alphanumeric, excluding ambiguous characters)
- Codes expire after **10 minutes**
- Codes are **single-use** and cannot be reused
- Users receive verification email immediately after registration

## Error Responses

```json
Expand All @@ -110,4 +143,4 @@ Authorization: Bearer <access_token>
}
```

Common status codes: `400` (validation error), `401` (unauthorized), `404` (not found)
Common status codes: `400` (validation error), `401` (unauthorized), `404` (not found), `500` (server error)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 5.1.3 on 2025-12-09 19:50

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('authentication', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='user',
name='is_email_verified',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='user',
name='is_active',
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name='EmailVerificationCode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=6)),
('created_at', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField()),
('is_used', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_verification_codes', to=settings.AUTH_USER_MODEL)),
],
),
]
37 changes: 36 additions & 1 deletion Backend/apps/authentication/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils import timezone
from datetime import timedelta
import random

# Create your models here.

class User(AbstractUser):
email = models.EmailField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_email_verified = models.BooleanField(default=False)
is_active = models.BooleanField(default=False)

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
Expand All @@ -15,4 +20,34 @@ def __str__(self):
return self.email

class Meta:
db_table = 'auth_user'
db_table = 'auth_user'

class EmailVerificationCode(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='email_verification_codes'
)
code = models.CharField(max_length=6)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
is_used = models.BooleanField(default=False)

def generate_code(self):
chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
return ''.join(random.choices(chars, k=6))

def save(self, *args, **kwargs):
if not self.code:
self.code = self.generate_code()
if not self.expires_at:
self.expires_at = timezone.now() + timedelta(minutes=10)
super().save(*args, **kwargs)

def is_valid(self):
if timezone.now() > self.expires_at or self.is_used:
return False
return True

def __str__(self):
return f"{self.user.email} - {self.code}"
11 changes: 11 additions & 0 deletions Backend/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,14 @@ def create(self, validated_data):
user.set_password(validated_data['password'])
user.save()
return user

class EmailVerificationCodeSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
code = serializers.CharField(min_length=6, max_length=6, required=True)

def validate_code(self, value):
"""Validate that code contains only allowed characters"""
chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
if not all(char in chars for char in value):
raise serializers.ValidationError("Invalid verification code format.")
return value
67 changes: 67 additions & 0 deletions Backend/apps/authentication/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from mailjet_rest import Client
from django.conf import settings
import os

class EmailService:
def __init__(self):
self.api_key = settings.MAILJET_API_KEY
self.api_secret = settings.MAILJET_API_SECRET
self.client = Client(auth=(self.api_key, self.api_secret), version='v3.1')
self.sender_email = "theshahidkhan.2004@gmail.com"
self.sender_name = "TOC Simulator"

def send_verification_email(self, user_email, user_name, verification_code):
"""
Send verification code to user.

Args:
user_email (str): Recipient email
user_name (str): User's first name for personalization
verification_code (str): 6-digit code

Returns:
bool: True if sent successfully
"""
data = {
'Messages': [
{
"From": {
"Email": self.sender_email,
"Name": self.sender_name
},
"To": [
{
"Email": user_email,
"Name": user_name
}
],
"Subject": "Verify Your Email - TOC Simulator",
"TextPart": f"Your verification code is: {verification_code}",
"HTMLPart": f"""
<h3>Welcome to TOC Simulator, {user_name}!</h3>
<p>Your verification code is:</p>
<h1 style="letter-spacing: 5px; color: #4F46E5;">{verification_code}</h1>
<p>This code will expire in 15 minutes.</p>
<p>If you didn't create an account, please ignore this email.</p>
"""
}
]
}

try:
result = self.client.send.create(data=data)
print(f"📧 Mailjet Response Status: {result.status_code}")
print(f"📧 Mailjet Response: {result.json()}")

if result.status_code == 200:
print(f"✅ Email sent successfully to {user_email}")
return True
else:
print(f"❌ Mailjet returned status {result.status_code}")
print(f"Response: {result.json()}")
return False
except Exception as e:
print(f"❌ Error sending email: {str(e)}")
import traceback
traceback.print_exc()
return False
3 changes: 2 additions & 1 deletion Backend/apps/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import RegisterView
from .views import RegisterView, VerifyEmailView

urlpatterns = [
path('register/', RegisterView.as_view(), name='register'),
path('verify-email/', VerifyEmailView.as_view(), name='verify_email'),
path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
84 changes: 78 additions & 6 deletions Backend/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from .serializers import UserSerializer
from .serializers import UserSerializer, EmailVerificationCodeSerializer
from .models import EmailVerificationCode
from django.utils import timezone
from django.contrib.auth import get_user_model
from .services import EmailService

User = get_user_model()

Expand All @@ -17,11 +20,80 @@ def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
refresh = RefreshToken.for_user(user)
code = user.email_verification_codes.create()
email_service = EmailService()
email_sent = email_service.send_verification_email(
user_email=user.email,
user_name=user.first_name,
verification_code=code.code
)
if not email_sent:
user.delete()
return Response(
{"error": "Failed to send verification email. Please try again."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
res_data = {
"user": serializer.data,
"refresh": str(refresh),
"access": str(refresh.access_token),
"message": "Registration successful"
"email": user.email,
"message": "Registration successful. Please check your email to verify your account."
}
return Response(res_data, status=status.HTTP_201_CREATED)

class VerifyEmailView(APIView):
permission_classes = (AllowAny,)

def post(self, request):
serializer = EmailVerificationCodeSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

email = serializer.validated_data['email']
code_value = serializer.validated_data['code']

try:
# Find user
user = User.objects.get(email=email)

# Find the most recent unused, unexpired code
verification_code = EmailVerificationCode.objects.filter(
user=user,
code=code_value,
is_used=False,
expires_at__gt=timezone.now()
).first()

if not verification_code:
return Response(
{"error": "Expired verification code"},
status=status.HTTP_400_BAD_REQUEST
)

# Mark code as used
verification_code.is_used = True
verification_code.save()

# Activate user
user.is_active = True
user.is_email_verified = True
user.save()

# Generate tokens
refresh = RefreshToken.for_user(user)

return Response({
"message": "Email verified successfully",
"access": str(refresh.access_token),
"refresh": str(refresh),
"user": {
"id": user.id,
"email": user.email,
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
}
}, status=status.HTTP_200_OK)

except User.DoesNotExist:
return Response(
{"error": "User not found"},
status=status.HTTP_404_NOT_FOUND
)
8 changes: 8 additions & 0 deletions Backend/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# exit on error
set -o errexit

pip install -r requirements.txt

python manage.py collectstatic --no-input
python manage.py migrate
Loading
Loading