roo roo roo down the river we go...

This commit is contained in:
2025-10-24 21:47:45 -05:00
parent 31f3ba08c2
commit bb115e8665
18 changed files with 699 additions and 8 deletions

View File

@@ -1,12 +1,69 @@
# MOHPortal
MOH is a :
# MOHPortal
- Dev agency
- Managed service provider
- General consulting/contracting staffing management for all of TSYS Group (especially HFNOC).
MOHPortal (MerchantsOfHope.org) — a modular, secure recruiting and staffing platform built to serve TSYS Group and its lines of business. The platform supports multi-tenant operations, agency-style recruiting workflows, managed services, and deep integration with existing TSYS business units.
## Purpose and Goals
- Provide a central, extensible platform for recruiting, contracting, and managed services across TSYS Group.
- Support multiple independent tenants with strict data isolation.
- Ship as a containerized application suitable for Docker Compose and Kubernetes deployments.
- Meet enterprise security, privacy, and accessibility requirements for government and commercial contracts.
Also the platform will be made generally available in an effort to onshore recruiting as RWSCP recapitalizes the American dream.
## Key Capabilities
- Job seeker experience: browse jobs, upload/resume parsing, apply, track application status.
- Employer experience: create/manage job postings, review candidates, manage hiring workflows.
- Candidate lifecycle management: screening stages, interview scheduling, offer management.
- Tenant administration: tenant onboarding, role-based access control, tenant-scoped configuration.
- Integrations: identity providers (OIDC, social login), ATS/HR systems, internal TSYS services, analytics.
Will integrate with each TSYS BU Dolinar instance.
## Architecture & Integration Notes
- Modular microservice-friendly design; services should be containerized and communicate over internal networks.
- Use the current directory name to determine the primary language/runtime for implementation and test artifacts.
- Only expose the main web interface externally; all other service ports remain on internal docker/k8s networks.
- Provide API-first design with versioned REST/GraphQL endpoints and clear schema contracts for downstream integrations.
## Multi-Tenancy & Data Isolation
- Strong tenant separation (logical and storage-level isolation where appropriate).
- Tenant configuration, branding, and feature flags per tenant.
- Admins scoped by tenant; global system admins for platform operations only.
## Authentication & Authorization
- Support OIDC providers and federated social logins (configurable per tenant).
- Role-based access control (RBAC) and least-privilege principles.
- Audit logging for administrative actions and authentication events.
## Accessibility & Compliance
- Target WCAG 2.1 AA at minimum to satisfy government contract accessibility requirements.
- English-only for MVP; ensure UI and content flows are accessible and keyboard-navigable.
- Compliance posture: design with PCI, GDPR, SOC, FedRAMP considerations in mind. Implement data minimization, encryption at rest/in transit, and strong access controls.
- Assume USA law jurisdiction for legal and privacy decisions.
## Security & Privacy
- Encrypt sensitive data at rest and in transit (TLS everywhere).
- Rotate secrets and credentials using secrets management (Vault or cloud-native equivalents).
- Implement rate limiting, WAF patterns, hardened container images, and supply-chain security best practices.
- Logging and monitoring with alerting and observability (prometheus/ELK or equivalent).
## Development Practices
- Follow Test Driven Development (TDD) with comprehensive unit, integration, and E2E tests.
- Maintain a docker-compose.yml for local stacks and a Kubernetes-friendly deployment manifest for production.
- Adopt CI/CD pipelines for automated builds, tests, image scans, and deployments.
- Keep the repo organized by service, tests, and infrastructure-as-code. Do not create artifacts outside the current directory.
## Deployment & Operations
- Ship as Docker container(s). Use a naming convention for artifacts and containers that maps agent-language-function (e.g., copilot-python-api).
- Only expose the main web UI port externally; other services on internal stack networks.
- Prepare for k8s deployment: manifests, helm charts, resource requests/limits, and readiness/liveness probes.
- Define backup, disaster recovery, and tenant migration procedures.
## Governance & Contributing
- Document coding standards, security checklists, and QA acceptance criteria.
- Review process for changes that affect compliance or tenant data handling.
- Add clear contribution guidelines and changelog for tenant-impacting changes.
## Next Steps (MVP)
- Define core user stories (job search, apply, post job, admin tenant onboarding).
- Scaffold services and initial docker-compose stack.
- Implement auth (OIDC), multi-tenant data model, and accessible UI skeleton.
- Establish CI pipeline and baseline security scans.
For questions or to propose changes to platform scope, contact the PMO and reference the project-specific agent guidelines in the repository.

0
jobs/__init__.py Normal file
View File

90
jobs/models.py Normal file
View File

@@ -0,0 +1,90 @@
from django.db import models
from tenants.models import Tenant
from users.models import User
class JobCategory(models.Model):
"""
Job category for organizing job postings.
"""
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Job(models.Model):
"""
Job posting model for the recruiting platform.
"""
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('closed', 'Closed'),
('archived', 'Archived'),
]
title = models.CharField(max_length=200)
description = models.TextField()
requirements = models.TextField()
responsibilities = models.TextField()
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
posted_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='jobs_posted')
category = models.ForeignKey(JobCategory, on_delete=models.SET_NULL, null=True, blank=True)
location = models.CharField(max_length=200)
employment_type = models.CharField(max_length=50, choices=[
('full-time', 'Full-time'),
('part-time', 'Part-time'),
('contract', 'Contract'),
('temporary', 'Temporary'),
('internship', 'Internship'),
('remote', 'Remote'),
])
salary_min = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
salary_max = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
is_remote = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
expires_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"{self.title} - {self.tenant.name}"
@property
def is_active(self):
return self.status == 'published' and self.expires_at and self.expires_at > self.created_at
class Application(models.Model):
"""
Job application model to track candidate applications.
"""
STATUS_CHOICES = [
('submitted', 'Submitted'),
('reviewed', 'Reviewed'),
('shortlisted', 'Shortlisted'),
('interviewed', 'Interviewed'),
('offered', 'Offered'),
('rejected', 'Rejected'),
('withdrawn', 'Withdrawn'),
]
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='applications')
applicant = models.ForeignKey(User, on_delete=models.CASCADE, related_name='applications')
cover_letter = models.TextField(blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='submitted')
applied_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.applicant.username} - {self.job.title}"

40
jobs/serializers.py Normal file
View File

@@ -0,0 +1,40 @@
from rest_framework import serializers
from .models import Job, Application, JobCategory
from users.serializers import UserSerializer
from tenants.serializers import TenantSerializer
class JobCategorySerializer(serializers.ModelSerializer):
"""
Serializer for the JobCategory model.
"""
class Meta:
model = JobCategory
fields = '__all__'
class JobSerializer(serializers.ModelSerializer):
"""
Serializer for the Job model.
"""
posted_by = UserSerializer(read_only=True)
tenant = TenantSerializer(read_only=True)
category = JobCategorySerializer(read_only=True)
class Meta:
model = Job
fields = '__all__'
read_only_fields = ('posted_by', 'tenant', 'created_at', 'updated_at', 'published_at')
class ApplicationSerializer(serializers.ModelSerializer):
"""
Serializer for the Application model.
"""
job = JobSerializer(read_only=True)
applicant = UserSerializer(read_only=True)
class Meta:
model = Application
fields = '__all__'
read_only_fields = ('job', 'applicant', 'applied_at', 'updated_at')

86
jobs/views.py Normal file
View File

@@ -0,0 +1,86 @@
from rest_framework import generics, permissions, status
from rest_framework.response import Response
from rest_framework.decorators import api_view
from .models import Job, Application, JobCategory
from .serializers import JobSerializer, ApplicationSerializer, JobCategorySerializer
class JobListView(generics.ListCreateAPIView):
"""
API view to retrieve list of jobs or create a new job.
"""
queryset = Job.objects.all()
serializer_class = JobSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
queryset = Job.objects.all()
tenant_id = self.request.query_params.get('tenant', None)
if tenant_id is not None:
queryset = queryset.filter(tenant_id=tenant_id)
return queryset
class JobDetailView(generics.RetrieveUpdateDestroyAPIView):
"""
API view to retrieve, update or delete a single job.
"""
queryset = Job.objects.all()
serializer_class = JobSerializer
permission_classes = [permissions.IsAuthenticated]
class ApplicationListView(generics.ListCreateAPIView):
"""
API view to retrieve list of applications or create a new application.
"""
queryset = Application.objects.all()
serializer_class = ApplicationSerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
# Set the applicant to the current user
serializer.save(applicant=self.request.user)
class ApplicationDetailView(generics.RetrieveUpdateDestroyAPIView):
"""
API view to retrieve, update or delete a single application.
"""
queryset = Application.objects.all()
serializer_class = ApplicationSerializer
permission_classes = [permissions.IsAuthenticated]
class JobCategoryListView(generics.ListCreateAPIView):
"""
API view to retrieve list of job categories or create a new category.
"""
queryset = JobCategory.objects.all()
serializer_class = JobCategorySerializer
permission_classes = [permissions.IsAuthenticated]
@api_view(['POST'])
def apply_to_job(request, job_id):
"""
API endpoint to apply to a specific job.
"""
try:
job = Job.objects.get(pk=job_id)
except Job.DoesNotExist:
return Response({'error': 'Job not found'}, status=status.HTTP_404_NOT_FOUND)
# Check if user has already applied
if Application.objects.filter(job=job, applicant=request.user).exists():
return Response({'error': 'Already applied to this job'}, status=status.HTTP_400_BAD_REQUEST)
application = Application.objects.create(
job=job,
applicant=request.user,
cover_letter=request.data.get('cover_letter', ''),
status='submitted'
)
serializer = ApplicationSerializer(application)
return Response(serializer.data, status=status.HTTP_201_CREATED)

22
manage.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mohportal.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

187
mohportal/settings.py Normal file
View File

@@ -0,0 +1,187 @@
"""
Django settings for MOHPortal project.
Generated by 'django-admin startproject' on 2025-10-25.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-development-key-change-in-production')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DEBUG', 'True').lower() == 'true'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'corsheaders',
'rest_framework',
'social_django',
# Custom apps
'users',
'jobs',
'tenants',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'social_django.middleware.SocialAuthExceptionMiddleware',
]
ROOT_URLCONF = 'mohportal.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
},
},
]
WSGI_APPLICATION = 'mohportal.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'mohportal_db'),
'USER': os.environ.get('DB_USER', 'mohportal_user'),
'PASSWORD': os.environ.get('DB_PASSWORD', 'mohportal_password'),
'HOST': os.environ.get('DB_HOST', 'db'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Authentication backends
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'social_core.backends.google.GoogleOpenId',
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.github.GithubOAuth2',
'social_core.backends.azuread_tenant.AzureADTenantOAuth2',
]
# Social auth settings
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = os.environ.get('GOOGLE_OAUTH2_KEY')
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = os.environ.get('GOOGLE_OAUTH2_SECRET')
SOCIAL_AUTH_GITHUB_KEY = os.environ.get('GITHUB_KEY')
SOCIAL_AUTH_GITHUB_SECRET = os.environ.get('GITHUB_SECRET')
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = os.environ.get('AZUREAD_KEY')
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = os.environ.get('AZUREAD_SECRET')
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = os.environ.get('AZUREAD_TENANT_ID')
# Django REST Framework settings
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# Security settings for production
if not DEBUG:
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_SECONDS = 3153600
SECURE_REDIRECT_EXEMPT = []
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'

32
mohportal/urls.py Normal file
View File

@@ -0,0 +1,32 @@
"""MOHPortal URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('auth/', include('social_django.urls', namespace='social')),
path('api/users/', include('users.urls')),
path('api/jobs/', include('jobs.urls')),
path('api/tenants/', include('tenants.urls')),
path('api-auth/', include('rest_framework.urls')),
]
# Serve static files in development
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
mohportal/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for MOHPortal project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mohportal.settings')
application = get_wsgi_application()

14
requirements.txt Normal file
View File

@@ -0,0 +1,14 @@
Django>=4.2.0
djangorestframework>=3.14.0
django-tenant-schemas>=1.11.0
python-social-auth>=0.3.6
django-oauth-toolkit>=2.3.0
psycopg2-binary>=2.9.0
gunicorn>=21.0.0
pytest>=7.0.0
pytest-django>=4.5.0
factory-boy>=3.2.0
selenium>=4.10.0
whitenoise>=6.4.0
django-cors-headers>=4.0.0
cryptography>=41.0.0

0
tenants/__init__.py Normal file
View File

36
tenants/models.py Normal file
View File

@@ -0,0 +1,36 @@
from django.db import models
class Tenant(models.Model):
"""
Tenant model for multi-tenancy support.
Each tenant represents a separate business unit or organization.
"""
name = models.CharField(max_length=200, unique=True)
subdomain = models.SlugField(max_length=100, unique=True)
schema_name = models.CharField(max_length=100, unique=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Tenant-specific configurations
company_name = models.CharField(max_length=200, blank=True)
contact_email = models.EmailField()
phone_number = models.CharField(max_length=15, blank=True)
website = models.URLField(blank=True)
# Branding settings
logo = models.ImageField(upload_to='tenant_logos/', null=True, blank=True)
primary_color = models.CharField(max_length=7, default='#007bff') # Hex color
secondary_color = models.CharField(max_length=7, default='#6c757d') # Hex color
# Feature flags
enable_social_login = models.BooleanField(default=True)
enable_email_notifications = models.BooleanField(default=True)
def __str__(self):
return self.name
class Meta:
verbose_name = 'Tenant'
verbose_name_plural = 'Tenants'

12
tenants/serializers.py Normal file
View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from .models import Tenant
class TenantSerializer(serializers.ModelSerializer):
"""
Serializer for the Tenant model.
"""
class Meta:
model = Tenant
fields = '__all__'
read_only_fields = ('created_at', 'updated_at', 'schema_name')

0
users/__init__.py Normal file
View File

43
users/models.py Normal file
View File

@@ -0,0 +1,43 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from tenants.models import Tenant
class User(AbstractUser):
"""
Custom User model that extends Django's AbstractUser.
Supports multi-tenancy by linking users to specific tenants.
"""
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
related_name='users',
null=True,
blank=True
)
is_job_seeker = models.BooleanField(default=False)
is_employer = models.BooleanField(default=False)
is_tenant_admin = models.BooleanField(default=False)
is_global_admin = models.BooleanField(default=False)
# Additional fields for job seekers
resume = models.FileField(upload_to='resumes/', null=True, blank=True)
bio = models.TextField(max_length=1000, blank=True)
phone_number = models.CharField(max_length=15, blank=True)
# Additional fields for employers
company_name = models.CharField(max_length=200, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.username} ({self.email})"
def get_full_name(self):
"""
Return the user's full name.
"""
if self.first_name and self.last_name:
return f"{self.first_name} {self.last_name}"
return self.username

15
users/serializers.py Normal file
View File

@@ -0,0 +1,15 @@
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import User as CustomUser
class UserSerializer(serializers.ModelSerializer):
"""
Serializer for the User model.
"""
class Meta:
model = CustomUser
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'is_job_seeker',
'is_employer', 'is_tenant_admin', 'is_global_admin', 'bio', 'phone_number',
'company_name', 'created_at', 'updated_at')
read_only_fields = ('id', 'created_at', 'updated_at')

8
users/urls.py Normal file
View File

@@ -0,0 +1,8 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.UserListView.as_view(), name='user-list'),
path('<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
path('current/', views.current_user, name='current-user'),
]

33
users/views.py Normal file
View File

@@ -0,0 +1,33 @@
from django.shortcuts import render
from rest_framework import generics, permissions
from rest_framework.response import Response
from rest_framework.decorators import api_view
from .models import User
from .serializers import UserSerializer
class UserListView(generics.ListAPIView):
"""
API view to retrieve list of users.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
class UserDetailView(generics.RetrieveAPIView):
"""
API view to retrieve a single user.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
@api_view(['GET'])
def current_user(request):
"""
API endpoint to retrieve the current user's profile.
"""
serializer = UserSerializer(request.user)
return Response(serializer.data)