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:
237
core/admin.py
Normal file
237
core/admin.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
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'
|
||||
Reference in New Issue
Block a user