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

152
users/admin.py Normal file
View File

@@ -0,0 +1,152 @@
"""
Smooth Schedule Users App Admin Configuration
"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.html import format_html
from hijack.contrib.admin import HijackUserAdminMixin
from .models import User
@admin.register(User)
class UserAdmin(HijackUserAdminMixin, BaseUserAdmin):
"""
Custom User admin with role-based filtering and masquerade button.
"""
list_display = [
'email',
'first_name',
'last_name',
'role_badge',
'tenant',
'is_active',
'is_temporary',
'created_at',
'hijack_field', # Adds the masquerade button
]
list_filter = [
'role',
'is_active',
'is_staff',
'is_superuser',
'is_temporary',
'created_at',
]
search_fields = [
'email',
'first_name',
'last_name',
'tenant__name',
]
ordering = ['-created_at']
fieldsets = (
('Authentication', {
'fields': ('email', 'password')
}),
('Personal Information', {
'fields': ('first_name', 'last_name', 'phone', 'job_title')
}),
('Permissions & Role', {
'fields': ('role', 'tenant', 'is_temporary', 'is_active', 'is_staff', 'is_superuser')
}),
('Groups & Permissions', {
'fields': ('groups', 'user_permissions'),
'classes': ('collapse',),
}),
('Important Dates', {
'fields': ('last_login', 'date_joined', 'created_at', 'updated_at'),
'classes': ('collapse',),
}),
('Security', {
'fields': ('last_login_ip',),
'classes': ('collapse',),
}),
)
readonly_fields = [
'created_at',
'updated_at',
'date_joined',
'last_login',
'last_login_ip',
]
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'email',
'password1',
'password2',
'role',
'tenant',
'first_name',
'last_name',
'is_temporary',
),
}),
)
def role_badge(self, obj):
"""Display role with color-coded badge"""
role_colors = {
'SUPERUSER': '#d32f2f', # Dark red
'PLATFORM_MANAGER': '#f57c00', # Orange
'PLATFORM_SALES': '#fbc02d', # Yellow
'PLATFORM_SUPPORT': '#7cb342', # Light green
'TENANT_OWNER': '#1976d2', # Blue
'TENANT_MANAGER': '#0288d1', # Light blue
'TENANT_STAFF': '#0097a7', # Cyan
'CUSTOMER': '#5e35b1', # Purple
}
color = role_colors.get(obj.role, '#777')
badge_html = f'''
<span style="
background-color: {color};
color: white;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
white-space: nowrap;
">{obj.get_role_display()}</span>
'''
if obj.is_temporary:
badge_html += ' <span style="color: orange; font-size: 11px;">🎭 DEMO</span>'
return format_html(badge_html)
role_badge.short_description = 'Role'
def get_queryset(self, request):
"""
Filter users based on who is viewing.
Platform users see all, tenant users only see their tenant.
"""
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
# If user is a tenant-level admin, only show users from their tenant
if request.user.tenant:
return qs.filter(tenant=request.user.tenant)
return qs
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""
Limit tenant choices based on who is editing.
"""
if db_field.name == "tenant":
# If editing user is tenant-scoped, they can only assign to their own tenant
if request.user.tenant and not request.user.is_superuser:
kwargs["queryset"] = request.user.get_accessible_tenants()
return super().formfield_for_foreignkey(db_field, request, **kwargs)

143
users/models.py Normal file
View File

@@ -0,0 +1,143 @@
"""
Smooth Schedule Custom User Model
Implements strict role hierarchy and multi-tenant user management
"""
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
class User(AbstractUser):
"""
Custom User model with strict role-based access control.
Replaces Django's default User model.
"""
# Role Choices - Strict Hierarchy
class Role(models.TextChoices):
# Platform-level roles (access across all tenants)
SUPERUSER = 'SUPERUSER', _('Platform Superuser')
PLATFORM_MANAGER = 'PLATFORM_MANAGER', _('Platform Manager')
PLATFORM_SALES = 'PLATFORM_SALES', _('Platform Sales')
PLATFORM_SUPPORT = 'PLATFORM_SUPPORT', _('Platform Support')
# Tenant-level roles (access within single tenant)
TENANT_OWNER = 'TENANT_OWNER', _('Tenant Owner')
TENANT_MANAGER = 'TENANT_MANAGER', _('Tenant Manager')
TENANT_STAFF = 'TENANT_STAFF', _('Tenant Staff')
# Customer role (end users of the tenant)
CUSTOMER = 'CUSTOMER', _('Customer')
# Core fields
role = models.CharField(
max_length=20,
choices=Role.choices,
default=Role.CUSTOMER,
help_text="User's role in the system hierarchy"
)
# For multi-tenancy: link users to their tenant
# Note: This is only for tenant-level users. Platform users can access all tenants.
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='users',
help_text="Tenant this user belongs to (null for platform-level users)"
)
# Special flags
is_temporary = models.BooleanField(
default=False,
help_text="True for sales demo accounts - can be masqueraded by Platform Sales"
)
# Additional profile fields
phone = models.CharField(max_length=20, blank=True)
job_title = models.CharField(max_length=100, blank=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_login_ip = models.GenericIPAddressField(null=True, blank=True)
class Meta:
ordering = ['email']
indexes = [
models.Index(fields=['role', 'tenant']),
models.Index(fields=['email', 'is_active']),
]
def __str__(self):
return f"{self.email} ({self.get_role_display()})"
def is_platform_user(self):
"""Check if user has platform-level access"""
return self.role in [
self.Role.SUPERUSER,
self.Role.PLATFORM_MANAGER,
self.Role.PLATFORM_SALES,
self.Role.PLATFORM_SUPPORT,
]
def is_tenant_user(self):
"""Check if user is tenant-scoped"""
return self.role in [
self.Role.TENANT_OWNER,
self.Role.TENANT_MANAGER,
self.Role.TENANT_STAFF,
self.Role.CUSTOMER,
]
def can_manage_users(self):
"""Check if user can manage other users"""
return self.role in [
self.Role.SUPERUSER,
self.Role.PLATFORM_MANAGER,
self.Role.TENANT_OWNER,
self.Role.TENANT_MANAGER,
]
def can_access_billing(self):
"""Check if user can access billing information"""
return self.role in [
self.Role.SUPERUSER,
self.Role.PLATFORM_MANAGER,
self.Role.TENANT_OWNER,
]
def get_accessible_tenants(self):
"""
Get list of tenants this user can access.
Platform users can access all tenants.
Tenant users can only access their own tenant.
"""
from core.models import Tenant
if self.is_platform_user():
return Tenant.objects.all()
elif self.tenant:
return Tenant.objects.filter(pk=self.tenant.pk)
else:
return Tenant.objects.none()
def save(self, *args, **kwargs):
"""
Override save to enforce business rules
"""
# Superusers must be staff and have is_superuser flag
if self.role == self.Role.SUPERUSER:
self.is_staff = True
self.is_superuser = True
# Platform users should not be tied to a tenant
if self.is_platform_user():
self.tenant = None
# Tenant users must have a tenant
if self.is_tenant_user() and not self.tenant:
raise ValueError(f"Users with role {self.role} must be assigned to a tenant")
super().save(*args, **kwargs)