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>
153 lines
4.3 KiB
Python
153 lines
4.3 KiB
Python
"""
|
|
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)
|