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>
238 lines
5.8 KiB
Python
238 lines
5.8 KiB
Python
"""
|
|
Smooth Schedule Core App Admin Configuration
|
|
"""
|
|
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
from django.urls import reverse
|
|
from django_tenants.admin import TenantAdminMixin
|
|
from .models import Tenant, Domain, PermissionGrant
|
|
|
|
|
|
@admin.register(Tenant)
|
|
class TenantAdmin(TenantAdminMixin, admin.ModelAdmin):
|
|
"""
|
|
Admin interface for Tenant management.
|
|
"""
|
|
list_display = [
|
|
'name',
|
|
'schema_name',
|
|
'subscription_tier',
|
|
'is_active',
|
|
'created_on',
|
|
'user_count',
|
|
'domain_list',
|
|
]
|
|
|
|
list_filter = [
|
|
'is_active',
|
|
'subscription_tier',
|
|
'created_on',
|
|
]
|
|
|
|
search_fields = [
|
|
'name',
|
|
'schema_name',
|
|
'contact_email',
|
|
]
|
|
|
|
readonly_fields = [
|
|
'schema_name',
|
|
'created_on',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Information', {
|
|
'fields': ('name', 'schema_name', 'created_on')
|
|
}),
|
|
('Subscription', {
|
|
'fields': ('subscription_tier', 'is_active', 'max_users', 'max_resources')
|
|
}),
|
|
('Contact', {
|
|
'fields': ('contact_email', 'phone')
|
|
}),
|
|
)
|
|
|
|
def user_count(self, obj):
|
|
"""Display count of users in this tenant"""
|
|
count = obj.users.count()
|
|
return format_html(
|
|
'<span style="color: {};">{}</span>',
|
|
'green' if count < obj.max_users else 'red',
|
|
count
|
|
)
|
|
user_count.short_description = 'Users'
|
|
|
|
def domain_list(self, obj):
|
|
"""Display list of domains for this tenant"""
|
|
domains = obj.domain_set.all()
|
|
if not domains:
|
|
return '-'
|
|
|
|
domain_links = []
|
|
for domain in domains:
|
|
url = reverse('admin:core_domain_change', args=[domain.pk])
|
|
domain_links.append(f'<a href="{url}">{domain.domain}</a>')
|
|
|
|
return format_html(' | '.join(domain_links))
|
|
domain_list.short_description = 'Domains'
|
|
|
|
|
|
@admin.register(Domain)
|
|
class DomainAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for Domain management.
|
|
"""
|
|
list_display = [
|
|
'domain',
|
|
'tenant',
|
|
'is_primary',
|
|
'is_custom_domain',
|
|
'verified_status',
|
|
]
|
|
|
|
list_filter = [
|
|
'is_primary',
|
|
'is_custom_domain',
|
|
]
|
|
|
|
search_fields = [
|
|
'domain',
|
|
'tenant__name',
|
|
]
|
|
|
|
readonly_fields = [
|
|
'verified_at',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Domain Information', {
|
|
'fields': ('domain', 'tenant', 'is_primary')
|
|
}),
|
|
('Custom Domain Settings', {
|
|
'fields': (
|
|
'is_custom_domain',
|
|
'route53_zone_id',
|
|
'route53_record_set_id',
|
|
'ssl_certificate_arn',
|
|
'verified_at',
|
|
),
|
|
'classes': ('collapse',),
|
|
}),
|
|
)
|
|
|
|
def verified_status(self, obj):
|
|
"""Display verification status with color coding"""
|
|
if obj.is_verified():
|
|
return format_html(
|
|
'<span style="color: green;">✓ Verified</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="color: orange;">⚠ Pending</span>'
|
|
)
|
|
verified_status.short_description = 'Status'
|
|
|
|
|
|
@admin.register(PermissionGrant)
|
|
class PermissionGrantAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for Permission Grant management.
|
|
"""
|
|
list_display = [
|
|
'id',
|
|
'grantor',
|
|
'grantee',
|
|
'action',
|
|
'granted_at',
|
|
'expires_at',
|
|
'status',
|
|
'time_left',
|
|
]
|
|
|
|
list_filter = [
|
|
'action',
|
|
'granted_at',
|
|
'expires_at',
|
|
]
|
|
|
|
search_fields = [
|
|
'grantor__email',
|
|
'grantee__email',
|
|
'action',
|
|
'reason',
|
|
]
|
|
|
|
readonly_fields = [
|
|
'granted_at',
|
|
'grantor',
|
|
'grantee',
|
|
'ip_address',
|
|
'user_agent',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Grant Information', {
|
|
'fields': ('grantor', 'grantee', 'action', 'reason')
|
|
}),
|
|
('Timing', {
|
|
'fields': ('granted_at', 'expires_at', 'revoked_at')
|
|
}),
|
|
('Audit Trail', {
|
|
'fields': ('ip_address', 'user_agent'),
|
|
'classes': ('collapse',),
|
|
}),
|
|
)
|
|
|
|
def status(self, obj):
|
|
"""Display status with color coding"""
|
|
if obj.revoked_at:
|
|
return format_html(
|
|
'<span style="color: red;">✗ Revoked</span>'
|
|
)
|
|
elif obj.is_active():
|
|
return format_html(
|
|
'<span style="color: green;">✓ Active</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="color: gray;">⊘ Expired</span>'
|
|
)
|
|
status.short_description = 'Status'
|
|
|
|
def time_left(self, obj):
|
|
"""Display remaining time"""
|
|
remaining = obj.time_remaining()
|
|
if remaining is None:
|
|
return '-'
|
|
|
|
minutes = int(remaining.total_seconds() / 60)
|
|
if minutes < 5:
|
|
color = 'red'
|
|
elif minutes < 15:
|
|
color = 'orange'
|
|
else:
|
|
color = 'green'
|
|
|
|
return format_html(
|
|
'<span style="color: {};">{} min</span>',
|
|
color,
|
|
minutes
|
|
)
|
|
time_left.short_description = 'Time Left'
|
|
|
|
actions = ['revoke_grants']
|
|
|
|
def revoke_grants(self, request, queryset):
|
|
"""Admin action to revoke permission grants"""
|
|
count = 0
|
|
for grant in queryset:
|
|
if grant.is_active():
|
|
grant.revoke()
|
|
count += 1
|
|
|
|
self.message_user(
|
|
request,
|
|
f'Successfully revoked {count} permission grant(s).'
|
|
)
|
|
revoke_grants.short_description = 'Revoke selected grants'
|