Backend Draft

This commit is contained in:
__init__
2026-02-23 20:31:53 +05:30
commit eec700af51
127 changed files with 2356 additions and 0 deletions

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

12
backend/accounts/admin.py Normal file
View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
class CustomUserAdmin(UserAdmin):
model = User
fieldsets = UserAdmin.fieldsets + (
('Tenant Info', {'fields': ('tenant', 'role')}),
)
list_display = ['username', 'email', 'first_name', 'last_name', 'role', 'tenant', 'is_staff']
admin.site.register(User, CustomUserAdmin)

6
backend/accounts/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "accounts"

View File

@@ -0,0 +1,154 @@
# Generated by Django 6.0.2 on 2026-02-20 18:33
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("tenants", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("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",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"role",
models.CharField(
choices=[
("super_admin", "Super Admin"),
("institution_admin", "Institution Admin"),
("teacher", "Teacher"),
("student", "Student"),
("project_manager", "Project Manager"),
],
default="student",
max_length=20,
),
),
(
"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",
),
),
(
"tenant",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="users",
to="tenants.tenant",
),
),
(
"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={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.28 on 2026-02-20 18:37
import django.contrib.auth.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0001_initial"),
]
operations = [
migrations.AlterModelManagers(
name="user",
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]

View File

View File

@@ -0,0 +1,22 @@
from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models
from tenants.models import Tenant
from tenants.managers import TenantScopedManager
class User(AbstractUser):
ROLE_CHOICES = [
('super_admin', 'Super Admin'),
('institution_admin', 'Institution Admin'),
('teacher', 'Teacher'),
('student', 'Student'),
('project_manager', 'Project Manager'),
]
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='users', null=True, blank=True)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='student')
objects = UserManager() # Default manager for auth operations
tenant_objects = TenantScopedManager() # Custom manager for tenant filtering
def __str__(self):
return self.username

View File

@@ -0,0 +1,57 @@
from rest_framework import permissions
class IsTenantUser(permissions.BasePermission):
"""
Allows access only to authenticated users who belong to a tenant.
Also ensures object-level tenant isolation.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated and request.user.tenant)
def has_object_permission(self, request, view, obj):
if hasattr(obj, 'tenant'):
return obj.tenant == request.user.tenant
return True
class IsAdmin(permissions.BasePermission):
"""
Allows access only to super_admin and institution_admin roles.
"""
def has_permission(self, request, view):
return bool(
request.user and
request.user.is_authenticated and
request.user.role in ['super_admin', 'institution_admin']
)
class IsProjectOwner(permissions.BasePermission):
"""
Allows object level access only to the user who created it.
"""
def has_object_permission(self, request, view, obj):
if hasattr(obj, 'created_by'):
return obj.created_by == request.user
return False
class IsTeacher(permissions.BasePermission):
"""
Allows access only to teacher, institution_admin, or super_admin roles.
"""
def has_permission(self, request, view):
return bool(
request.user and
request.user.is_authenticated and
request.user.role in ['teacher', 'institution_admin', 'super_admin']
)
class IsStudentReadOnly(permissions.BasePermission):
"""
Students get read-only access (GET, HEAD, OPTIONS).
Other roles are allowed (and restricted by other classes).
"""
def has_permission(self, request, view):
if request.user and request.user.is_authenticated:
if request.user.role == 'student':
return request.method in permissions.SAFE_METHODS
return True
return False

View File

@@ -0,0 +1,44 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from tenants.serializers import TenantSerializer
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
User = get_user_model()
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add custom claims
token['role'] = getattr(user, 'role', 'student')
token['tenant_id'] = user.tenant.id if getattr(user, 'tenant', None) else None
return token
class UserSerializer(serializers.ModelSerializer):
tenant = TenantSerializer(read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'role', 'tenant', 'is_active']
read_only_fields = ['id']
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username', 'email', 'password', 'first_name', 'last_name', 'role']
def create(self, validated_data):
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data.get('email', ''),
password=validated_data['password'],
first_name=validated_data.get('first_name', ''),
last_name=validated_data.get('last_name', ''),
role=validated_data.get('role', 'student'),
tenant=self.context['request'].tenant if 'request' in self.context else None
)
return user

59
backend/accounts/tests.py Normal file
View File

@@ -0,0 +1,59 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from tenants.models import Tenant
User = get_user_model()
class AuthTests(TestCase):
def setUp(self):
self.client = APIClient()
self.tenant = Tenant.objects.create(name='Test Tenant', subdomain='testtenant')
self.register_url = reverse('auth_register')
self.login_url = reverse('token_obtain_pair')
self.profile_url = reverse('user_profile')
self.user_data = {
'username': 'testuser',
'email': 'testuser@example.com',
'password': 'strongpassword123',
'first_name': 'Test',
'last_name': 'User',
'role': 'student'
}
def test_registration(self):
# Register a new user
response = self.client.post(self.register_url, self.user_data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
user = User.objects.get(username='testuser')
self.assertTrue(user.check_password('strongpassword123'))
self.assertEqual(user.role, 'student')
def test_login_and_profile(self):
# First create the user
user = User.objects.create_user(
username='testuser',
password='strongpassword123',
role='teacher',
tenant=self.tenant
)
# Login to get tokens
login_data = {'username': 'testuser', 'password': 'strongpassword123'}
response = self.client.post(self.login_url, login_data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('access', response.data)
self.assertIn('refresh', response.data)
access_token = response.data['access']
# Fetch profile using the token
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token)
profile_response = self.client.get(self.profile_url)
self.assertEqual(profile_response.status_code, status.HTTP_200_OK)
self.assertEqual(profile_response.data['username'], 'testuser')
self.assertEqual(profile_response.data['role'], 'teacher')
self.assertEqual(profile_response.data['tenant']['id'], self.tenant.id)

11
backend/accounts/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from .views import RegisterView, CustomTokenObtainPairView, UserProfileView, UserListView
urlpatterns = [
path('register/', RegisterView.as_view(), name='auth_register'),
path('login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('profile/', UserProfileView.as_view(), name='user_profile'),
path('users/', UserListView.as_view(), name='user_list'),
]

33
backend/accounts/views.py Normal file
View File

@@ -0,0 +1,33 @@
from rest_framework import generics, permissions, viewsets
from rest_framework_simplejwt.views import TokenObtainPairView
from django.contrib.auth import get_user_model
from .serializers import RegisterSerializer, UserSerializer, CustomTokenObtainPairSerializer
User = get_user_model()
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
class RegisterView(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = RegisterSerializer
permission_classes = [permissions.AllowAny]
class UserProfileView(generics.RetrieveUpdateAPIView):
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
def get_object(self):
return self.request.user
class UserListView(generics.ListAPIView):
"""
List all users in the current tenant.
Used for task assignment dropdown.
"""
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
pagination_class = None # Return all users as a list, not paginated
def get_queryset(self):
return User.objects.filter(tenant=self.request.tenant, is_active=True).order_by('first_name', 'last_name')