Initial commit: SmoothSchedule multi-tenant scheduling platform

This commit includes:
- Django backend with multi-tenancy (django-tenants)
- React + TypeScript frontend with Vite
- Platform administration API with role-based access control
- Authentication system with token-based auth
- Quick login dev tools for testing different user roles
- CORS and CSRF configuration for local development
- Docker development environment setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-11-27 01:43:20 -05:00
commit 2e111364a2
567 changed files with 96410 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
"""Platform admin app for managing tenants and platform-level operations."""
default_app_config = 'platform_admin.apps.PlatformAdminConfig'

View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class PlatformAdminConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'platform_admin'
verbose_name = 'Platform Management'

View File

@@ -0,0 +1,43 @@
"""
Platform Permissions
Custom permission classes for platform-level operations
"""
from rest_framework import permissions
from smoothschedule.users.models import User
class IsPlatformAdmin(permissions.BasePermission):
"""
Permission class that only allows platform admins (superuser, platform_manager)
"""
message = "Platform admin access required."
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Allow superusers and platform managers
return request.user.role in [
User.Role.SUPERUSER,
User.Role.PLATFORM_MANAGER
]
class IsPlatformUser(permissions.BasePermission):
"""
Permission class that allows any platform-level user
(superuser, platform_manager, platform_support, platform_sales)
"""
message = "Platform user access required."
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Allow any platform-level role
return request.user.role in [
User.Role.SUPERUSER,
User.Role.PLATFORM_MANAGER,
User.Role.PLATFORM_SUPPORT,
User.Role.PLATFORM_SALES,
]

View File

@@ -0,0 +1,111 @@
"""
Platform Serializers
Serializers for platform-level operations (viewing tenants, users, metrics)
"""
from rest_framework import serializers
from core.models import Tenant, Domain
from smoothschedule.users.models import User
class TenantSerializer(serializers.ModelSerializer):
"""Serializer for Tenant (Business) listing"""
subdomain = serializers.SerializerMethodField()
tier = serializers.CharField(source='subscription_tier')
user_count = serializers.SerializerMethodField()
owner = serializers.SerializerMethodField()
class Meta:
model = Tenant
fields = [
'id', 'name', 'subdomain', 'tier', 'is_active',
'created_on', 'user_count', 'owner', 'max_users',
'max_resources', 'contact_email', 'phone'
]
read_only_fields = fields
def get_subdomain(self, obj):
"""Get primary subdomain for this tenant"""
primary_domain = obj.domains.filter(is_primary=True, is_custom_domain=False).first()
if primary_domain:
# Extract subdomain from domain (e.g., 'business1.lvh.me' -> 'business1')
return primary_domain.domain.split('.')[0]
return obj.schema_name
def get_user_count(self, obj):
"""Get count of users in this tenant's schema"""
# This requires querying the tenant schema
# For now, return 0 - we can optimize this with annotations
return 0
def get_owner(self, obj):
"""Get the tenant owner user"""
# Query public schema for users with this tenant as their business
try:
owner = User.objects.filter(
role=User.Role.TENANT_OWNER,
# Note: We need to add a tenant reference to User model
).first()
if owner:
return {
'id': owner.id,
'username': owner.username,
'full_name': owner.full_name,
'email': owner.email,
'role': owner.role,
}
except:
pass
return None
class PlatformUserSerializer(serializers.ModelSerializer):
"""Serializer for User listing (platform view)"""
business = serializers.SerializerMethodField()
business_name = serializers.SerializerMethodField()
business_subdomain = serializers.SerializerMethodField()
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = [
'id', 'email', 'username', 'first_name', 'last_name', 'full_name', 'role',
'is_active', 'is_staff', 'is_superuser',
'business', 'business_name', 'business_subdomain',
'date_joined', 'last_login'
]
read_only_fields = fields
def get_full_name(self, obj):
"""Get user's full name"""
return obj.full_name
def get_business(self, obj):
"""Get tenant ID if user belongs to a tenant"""
if obj.tenant:
return obj.tenant.id
return None
def get_business_name(self, obj):
"""Get tenant name if user belongs to a tenant"""
if obj.tenant:
return obj.tenant.name
return None
def get_business_subdomain(self, obj):
"""Get tenant subdomain if user belongs to a tenant"""
if obj.tenant:
primary_domain = obj.tenant.domains.filter(is_primary=True, is_custom_domain=False).first()
if primary_domain:
return primary_domain.domain.split('.')[0]
return obj.tenant.schema_name
return None
class PlatformMetricsSerializer(serializers.Serializer):
"""Serializer for platform dashboard metrics"""
total_tenants = serializers.IntegerField()
active_tenants = serializers.IntegerField()
total_users = serializers.IntegerField()
mrr = serializers.DecimalField(max_digits=10, decimal_places=2)
growth_rate = serializers.FloatField()

View File

@@ -0,0 +1,16 @@
"""
Platform URL Configuration
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TenantViewSet, PlatformUserViewSet
app_name = 'platform'
router = DefaultRouter()
router.register(r'businesses', TenantViewSet, basename='business')
router.register(r'users', PlatformUserViewSet, basename='user')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -0,0 +1,77 @@
"""
Platform Views
API views for platform-level operations
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.db.models import Count
from core.models import Tenant
from smoothschedule.users.models import User
from .serializers import TenantSerializer, PlatformUserSerializer, PlatformMetricsSerializer
from .permissions import IsPlatformAdmin, IsPlatformUser
class TenantViewSet(viewsets.ReadOnlyModelViewSet):
"""
ViewSet for viewing tenants (businesses).
Platform admins only.
"""
queryset = Tenant.objects.all().order_by('-created_on')
serializer_class = TenantSerializer
permission_classes = [IsAuthenticated, IsPlatformAdmin]
def get_queryset(self):
"""Optionally filter by active status"""
queryset = super().get_queryset()
is_active = self.request.query_params.get('is_active')
if is_active is not None:
queryset = queryset.filter(is_active=is_active.lower() == 'true')
return queryset
@action(detail=False, methods=['get'])
def metrics(self, request):
"""Get platform-wide tenant metrics"""
total_tenants = Tenant.objects.count()
active_tenants = Tenant.objects.filter(is_active=True).count()
metrics = {
'total_tenants': total_tenants,
'active_tenants': active_tenants,
'total_users': User.objects.count(),
'mrr': 0, # TODO: Calculate from billing
'growth_rate': 0.0, # TODO: Calculate growth
}
serializer = PlatformMetricsSerializer(metrics)
return Response(serializer.data)
class PlatformUserViewSet(viewsets.ReadOnlyModelViewSet):
"""
ViewSet for viewing all users across the platform.
Platform admins only.
"""
queryset = User.objects.all().order_by('-date_joined')
serializer_class = PlatformUserSerializer
permission_classes = [IsAuthenticated, IsPlatformAdmin]
def get_queryset(self):
"""Optionally filter by business or role"""
queryset = super().get_queryset()
# Filter by role
role = self.request.query_params.get('role')
if role:
queryset = queryset.filter(role=role)
# Filter by active status
is_active = self.request.query_params.get('is_active')
if is_active is not None:
queryset = queryset.filter(is_active=is_active.lower() == 'true')
# TODO: Filter by business when we add tenant reference to User
return queryset