Files
smoothschedule/core/models.py
poduck 2e111364a2 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>
2025-11-27 01:43:20 -05:00

272 lines
7.7 KiB
Python

"""
Smooth Schedule Core Models
Multi-tenancy, Domain management, and Permission Grant system
"""
from django.db import models
from django.utils import timezone
from django_tenants.models import TenantMixin, DomainMixin
from datetime import timedelta
class Tenant(TenantMixin):
"""
Tenant model for multi-schema architecture.
Each tenant gets their own PostgreSQL schema for complete data isolation.
"""
name = models.CharField(max_length=100)
created_on = models.DateField(auto_now_add=True)
# Subscription & billing
is_active = models.BooleanField(default=True)
subscription_tier = models.CharField(
max_length=50,
choices=[
('FREE', 'Free Trial'),
('STARTER', 'Starter'),
('PROFESSIONAL', 'Professional'),
('ENTERPRISE', 'Enterprise'),
],
default='FREE'
)
# Feature flags
max_users = models.IntegerField(default=5)
max_resources = models.IntegerField(default=10)
# Metadata
contact_email = models.EmailField(blank=True)
phone = models.CharField(max_length=20, blank=True)
# Auto-created fields from TenantMixin:
# - schema_name (unique, indexed)
# - auto_create_schema
# - auto_drop_schema
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Domain(DomainMixin):
"""
Domain model for tenant routing.
Supports both subdomain (tenant.chronoflow.com) and custom domains.
"""
# Inherited from DomainMixin:
# - domain (unique, primary key)
# - tenant (ForeignKey to Tenant)
# - is_primary (boolean)
# Route53 integration fields for custom domains
is_custom_domain = models.BooleanField(
default=False,
help_text="True if this is a custom domain (not a subdomain of smoothschedule.com)"
)
route53_zone_id = models.CharField(
max_length=100,
blank=True,
null=True,
help_text="AWS Route53 Hosted Zone ID for this custom domain"
)
route53_record_set_id = models.CharField(
max_length=100,
blank=True,
null=True,
help_text="Route53 Record Set ID for DNS verification"
)
# SSL certificate management (for future AWS ACM integration)
ssl_certificate_arn = models.CharField(
max_length=200,
blank=True,
null=True,
help_text="AWS ACM Certificate ARN for HTTPS"
)
verified_at = models.DateTimeField(
null=True,
blank=True,
help_text="When the custom domain was verified via DNS"
)
class Meta:
ordering = ['domain']
def __str__(self):
domain_type = "Custom" if self.is_custom_domain else "Subdomain"
return f"{self.domain} ({domain_type})"
def is_verified(self):
"""Check if custom domain is verified"""
if not self.is_custom_domain:
return True # Subdomains are always verified
return self.verified_at is not None
class PermissionGrant(models.Model):
"""
Time-limited permission grants (30-minute window).
Used for temporary elevated access without permanently changing user permissions.
Example use cases:
- Support agent needs temporary access to tenant data
- Sales demo requiring elevated permissions
- Cross-tenant operations during migrations
"""
grantor = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='granted_permissions',
help_text="User who granted the permission"
)
grantee = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='received_permissions',
help_text="User who received the permission"
)
action = models.CharField(
max_length=100,
help_text="Specific action or permission being granted (e.g., 'view_billing', 'edit_settings')"
)
reason = models.TextField(
blank=True,
help_text="Justification for granting this permission (audit trail)"
)
granted_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField(
help_text="When this permission grant expires"
)
revoked_at = models.DateTimeField(
null=True,
blank=True,
help_text="If manually revoked before expiration"
)
# Audit metadata
ip_address = models.GenericIPAddressField(
null=True,
blank=True,
help_text="IP address where grant was created"
)
user_agent = models.TextField(
blank=True,
help_text="Browser/client user agent"
)
class Meta:
ordering = ['-granted_at']
indexes = [
models.Index(fields=['grantee', 'expires_at']),
models.Index(fields=['grantor', 'granted_at']),
]
def __str__(self):
return f"{self.grantor}{self.grantee}: {self.action} (expires {self.expires_at})"
def is_active(self):
"""
Check if this permission grant is currently active.
Returns False if expired or revoked.
"""
now = timezone.now()
# Check if revoked
if self.revoked_at is not None:
return False
# Check if expired
if now >= self.expires_at:
return False
return True
def revoke(self):
"""Manually revoke this permission grant"""
if self.revoked_at is None:
self.revoked_at = timezone.now()
self.save(update_fields=['revoked_at'])
def time_remaining(self):
"""Get timedelta of remaining active time (or None if expired/revoked)"""
if not self.is_active():
return None
now = timezone.now()
return self.expires_at - now
@classmethod
def create_grant(cls, grantor, grantee, action, reason="", duration_minutes=30, ip_address=None, user_agent=""):
"""
Factory method to create a time-limited permission grant.
Args:
grantor: User granting the permission
grantee: User receiving the permission
action: Permission action string
reason: Justification for audit trail
duration_minutes: How long the grant is valid (default 30)
ip_address: IP address of the request
user_agent: Browser user agent
Returns:
PermissionGrant instance
"""
now = timezone.now()
expires_at = now + timedelta(minutes=duration_minutes)
return cls.objects.create(
grantor=grantor,
grantee=grantee,
action=action,
reason=reason,
expires_at=expires_at,
ip_address=ip_address,
user_agent=user_agent
)
class TierLimit(models.Model):
"""
Defines resource limits for each subscription tier.
Used by HasQuota permission to enforce hard blocks.
"""
tier = models.CharField(
max_length=50,
choices=[
('FREE', 'Free Trial'),
('STARTER', 'Starter'),
('PROFESSIONAL', 'Professional'),
('ENTERPRISE', 'Enterprise'),
]
)
feature_code = models.CharField(
max_length=100,
help_text="Feature code (e.g., 'MAX_RESOURCES', 'MAX_USERS')"
)
limit = models.IntegerField(
default=0,
help_text="Maximum allowed count for this feature"
)
class Meta:
unique_together = ['tier', 'feature_code']
ordering = ['tier', 'feature_code']
def __str__(self):
return f"{self.tier} - {self.feature_code}: {self.limit}"