Browse Source

feat: add core backend application structure, enhanced auth middleware, and API components

main
sina_sajjadi 4 weeks ago
parent
commit
ebd8f876c7
  1. 1
      apps/__init__.py
  2. 7
      apps/account/admin/notification.py
  3. 9
      apps/account/models/user.py
  4. 79
      apps/api/tests.py
  5. 31
      apps/api/views/api_views.py
  6. 10
      config/enhanced_auth_middleware.py
  7. 1
      config/settings/base.py
  8. 1
      dynamic_preferences/admin.py
  9. 1
      requirements.txt
  10. 3031
      schema.yml

1
apps/__init__.py

@ -0,0 +1 @@
# apps package

7
apps/account/admin/notification.py

@ -1,11 +1,12 @@
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ajaxdatatable.admin import AjaxDatatable
from unfold.admin import ModelAdmin
from utils.admin import project_admin_site
from apps.account.models import User, Notification from apps.account.models import User, Notification
@admin.register(Notification)
class NotificationAdmin(AjaxDatatable):
@admin.register(Notification, site=project_admin_site)
class NotificationAdmin(ModelAdmin):
list_display = ('title', 'user', 'is_read', 'created_at') list_display = ('title', 'user', 'is_read', 'created_at')
list_filter = ('is_read', 'created_at') list_filter = ('is_read', 'created_at')
search_fields = ('title', 'message', 'user__fullname') search_fields = ('title', 'message', 'user__fullname')

9
apps/account/models/user.py

@ -20,9 +20,16 @@ class User(AbstractUser):
web = 'web', _('Web') web = 'web', _('Web')
class UserType(models.TextChoices): class UserType(models.TextChoices):
CLIENT = 'client', _('Client')
CLIENT = 'client', _('Client / User')
STUDENT = 'student', _('Student / Member')
PROFESSOR = 'professor', _('Professor / Scholar')
CONSULTANT = 'consultant', _('Consultant')
ADMIN = 'admin', _('Admin') ADMIN = 'admin', _('Admin')
SUPER_ADMIN = 'super_admin', _('Super Admin') SUPER_ADMIN = 'super_admin', _('Super Admin')
REGIONAL_ADMIN = 'regional_admin', _('Regional Admin')
INSTITUTION_ADMIN = 'institution_admin', _('Institution Admin')
EDITOR = 'editor', _('Editor')
VIEWER = 'viewer', _('Viewer')
class GenderChoices(models.TextChoices): class GenderChoices(models.TextChoices):
MALE = 'male', _('Male') MALE = 'male', _('Male')

79
apps/api/tests.py

@ -1,3 +1,80 @@
from django.test import TestCase 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
# Create your tests here.
User = get_user_model()
class Phase0InfrastructureTests(TestCase):
"""
Automated verification tests for Phase 0 infrastructure:
- OpenAPI 3.0 schema endpoints via drf-spectacular
- Swagger UI & ReDoc interfaces
- JWT token issue, refresh, and verification endpoints
- Core API health checks
"""
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
email='[email protected]',
username='[email protected]',
password='SecurePassword123!',
fullname='Tester User'
)
def test_health_check_endpoint(self):
response = self.client.get('/api/v1/health/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('status'), 'healthy')
self.assertEqual(response.data.get('version'), '1.0.0')
def test_openapi_schema_endpoint(self):
response = self.client.get('/api/schema/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('openapi', response.content.decode('utf-8'))
def test_swagger_ui_endpoint(self):
response = self.client.get('/api/schema/swagger-ui/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_redoc_endpoint(self):
response = self.client.get('/api/schema/redoc/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_swagger_legacy_redirect(self):
response = self.client.get('/swagger/')
self.assertIn(response.status_code, [status.HTTP_302_FOUND, status.HTTP_301_MOVED_PERMANENTLY])
self.assertEqual(response.url, '/api/schema/swagger-ui/')
def test_jwt_token_obtain_pair(self):
# Obtain JWT tokens
payload = {
'email': '[email protected]',
'password': 'SecurePassword123!'
}
response = self.client.post('/api/v1/auth/token/', payload, 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']
refresh_token = response.data['refresh']
# Verify token
verify_response = self.client.post(
'/api/v1/auth/token/verify/',
{'token': access_token},
format='json'
)
self.assertEqual(verify_response.status_code, status.HTTP_200_OK)
# Refresh token
refresh_response = self.client.post(
'/api/v1/auth/token/refresh/',
{'refresh': refresh_token},
format='json'
)
self.assertEqual(refresh_response.status_code, status.HTTP_200_OK)
self.assertIn('access', refresh_response.data)

31
apps/api/views/api_views.py

@ -7,6 +7,9 @@ from django.utils import timezone
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from drf_spectacular.utils import extend_schema, OpenApiResponse, inline_serializer
from rest_framework import serializers
from apps.api.models import AppVersion, SupportMessage from apps.api.models import AppVersion, SupportMessage
from apps.api.serializers import AppVersionSerializer, SupportMessageCreateSerializer from apps.api.serializers import AppVersionSerializer, SupportMessageCreateSerializer
@ -17,17 +20,16 @@ class HealthCheckView(APIView):
""" """
permission_classes = [AllowAny] permission_classes = [AllowAny]
@swagger_auto_schema(
operation_description="Check API server health status",
@extend_schema(
summary="Check API server health status",
description="Returns system status, current timestamp, and API version",
responses={ responses={
200: openapi.Response(
description="Server is healthy",
examples={
"application/json": {
"status": "healthy",
"timestamp": "2026-09-13T12:00:00Z",
"version": "1.0.0"
}
200: inline_serializer(
name="HealthCheckResponse",
fields={
"status": serializers.CharField(default="healthy"),
"timestamp": serializers.DateTimeField(),
"version": serializers.CharField(default="1.0.0"),
} }
) )
} }
@ -46,11 +48,12 @@ class AppVersionView(APIView):
""" """
permission_classes = [AllowAny] permission_classes = [AllowAny]
@swagger_auto_schema(
operation_description="Get the latest active mobile application version",
@extend_schema(
summary="Get active application version",
description="Returns latest active mobile/web application version details",
responses={ responses={
200: AppVersionSerializer(),
404: openapi.Response("No active version found")
200: AppVersionSerializer,
404: OpenApiResponse(description="No active version found")
} }
) )
def get(self, request): def get(self, request):

10
config/enhanced_auth_middleware.py

@ -13,8 +13,16 @@ def enhanced_auth_middleware(get_response):
Handles custom documentation system authentication Handles custom documentation system authentication
""" """
def middleware(request): def middleware(request):
# Exclude drf-spectacular OpenAPI endpoints so Swagger UI and ReDoc can be accessed
if request.path.startswith("/api/schema/"):
return get_response(request)
# Legacy swagger redirect allow
if request.path == "/swagger/":
return get_response(request)
# Define protected paths that require staff access # Define protected paths that require staff access
protected_paths = ["/swagger", "/redoc", "/docs"]
protected_paths = ["/docs"]
is_protected_path = any(path in request.path for path in protected_paths) is_protected_path = any(path in request.path for path in protected_paths)
if is_protected_path: if is_protected_path:

1
config/settings/base.py

@ -9,7 +9,6 @@ from django.urls import reverse_lazy
import environ import environ
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import sentry_sdk import sentry_sdk
from utils.admin import admin_url_generator
# Build paths inside the project: BASE_DIR / 'subdir'. # Build paths inside the project: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent BASE_DIR = Path(__file__).resolve().parent.parent.parent

1
dynamic_preferences/admin.py

@ -1,4 +1,3 @@
from ajaxdatatable.admin import AjaxDatatable
from django.contrib import admin from django.contrib import admin
from django import forms from django import forms

1
requirements.txt

@ -44,3 +44,4 @@ cachetools>=5.3.0
Unidecode>=1.3.0 Unidecode>=1.3.0
persisting-theory>=1.0 persisting-theory>=1.0
django-dynamic-preferences>=1.17.0 django-dynamic-preferences>=1.17.0
geoip2>=4.8.0

3031
schema.yml
File diff suppressed because it is too large
View File

Loading…
Cancel
Save