Backend Draft
This commit is contained in:
0
backend/accounts/__init__.py
Normal file
0
backend/accounts/__init__.py
Normal file
BIN
backend/accounts/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/accounts/__pycache__/admin.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/admin.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/accounts/__pycache__/apps.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/apps.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/accounts/__pycache__/models.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/accounts/__pycache__/permissions.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/permissions.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/accounts/__pycache__/serializers.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/serializers.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/accounts/__pycache__/tests.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/tests.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/accounts/__pycache__/urls.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/urls.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/accounts/__pycache__/views.cpython-314.pyc
Normal file
BIN
backend/accounts/__pycache__/views.cpython-314.pyc
Normal file
Binary file not shown.
12
backend/accounts/admin.py
Normal file
12
backend/accounts/admin.py
Normal 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
6
backend/accounts/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "accounts"
|
||||
154
backend/accounts/migrations/0001_initial.py
Normal file
154
backend/accounts/migrations/0001_initial.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
20
backend/accounts/migrations/0002_alter_user_managers.py
Normal file
20
backend/accounts/migrations/0002_alter_user_managers.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/accounts/migrations/__init__.py
Normal file
0
backend/accounts/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/accounts/migrations/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/accounts/migrations/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
22
backend/accounts/models.py
Normal file
22
backend/accounts/models.py
Normal 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
|
||||
57
backend/accounts/permissions.py
Normal file
57
backend/accounts/permissions.py
Normal 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
|
||||
44
backend/accounts/serializers.py
Normal file
44
backend/accounts/serializers.py
Normal 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
59
backend/accounts/tests.py
Normal 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
11
backend/accounts/urls.py
Normal 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
33
backend/accounts/views.py
Normal 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')
|
||||
Reference in New Issue
Block a user