Backend: - Add HasQuota() permission factory for quota limits (resources, users, services, appointments, email templates, automated tasks) - Add HasFeaturePermission() factory for feature-based permissions (SMS, masked calling, custom domains, white label, plugins, webhooks, calendar sync, analytics) - Add has_feature() method to Tenant model for flexible permission checking - Add new tenant permission fields: can_create_plugins, can_use_webhooks, can_use_calendar_sync, can_export_data - Create Data Export API with CSV/JSON support for appointments, customers, resources, services - Create Analytics API with dashboard, appointments, revenue endpoints - Add calendar sync views and URL configuration Frontend: - Add usePlanFeatures hook for checking feature availability - Add UpgradePrompt components (inline, banner, overlay variants) - Add LockedSection wrapper and LockedButton for feature gating - Update settings pages with permission checks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
9.4 KiB
Calendar Sync Feature Integration Guide
This document explains how the calendar sync feature permission system works and provides examples for implementation.
Overview
The calendar sync feature allows tenants to:
- Connect to Google Calendar or Outlook Calendar via OAuth
- Sync calendar events into the scheduling system
- Manage multiple calendar integrations
The feature is gated by the can_use_calendar_sync permission that must be enabled at the tenant level.
Architecture
1. Database Model (Tenant)
Added to /home/poduck/Desktop/smoothschedule2/smoothschedule/core/models.py:
can_use_calendar_sync = models.BooleanField(
default=False,
help_text="Whether this business can sync Google Calendar and other calendar providers"
)
2. Permission Check (HasFeaturePermission)
Updated in /home/poduck/Desktop/smoothschedule2/smoothschedule/core/permissions.py:
FEATURE_NAMES = {
# ... other features ...
'can_use_calendar_sync': 'Calendar Sync',
}
The HasFeaturePermission factory function creates a DRF permission class:
from core.permissions import HasFeaturePermission
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
3. OAuth Integration
Modified in /home/poduck/Desktop/smoothschedule2/smoothschedule/core/oauth_views.py:
GoogleOAuthInitiateView: Checks permission when purpose is "calendar"MicrosoftOAuthInitiateView: Checks permission when purpose is "calendar"
def post(self, request):
purpose = request.data.get('purpose', 'email')
# Check calendar sync permission if purpose is calendar
if purpose == 'calendar':
calendar_permission = HasFeaturePermission('can_use_calendar_sync')
if not calendar_permission().has_permission(request, self):
return Response({
'success': False,
'error': 'Your current plan does not include Calendar Sync.',
}, status=status.HTTP_403_FORBIDDEN)
# Continue with OAuth flow...
4. Calendar Sync Views
Created in /home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/calendar_sync_views.py:
CalendarListView: Lists connected calendarsCalendarSyncView: Syncs calendar eventsCalendarDeleteView: Disconnects calendar integrationCalendarStatusView: Gets calendar sync status
Permission Pattern in Views
from core.permissions import HasFeaturePermission
from rest_framework.permissions import IsAuthenticated
class CalendarSyncPermission(IsAuthenticated):
"""Custom permission combining auth + feature check"""
def has_permission(self, request, view):
if not super().has_permission(request, view):
return False
tenant = getattr(request, 'tenant', None)
if not tenant:
return False
return tenant.has_feature('can_use_calendar_sync')
class CalendarListView(APIView):
permission_classes = [CalendarSyncPermission]
def get(self, request):
# This endpoint is only accessible if:
# 1. User is authenticated
# 2. Tenant has can_use_calendar_sync enabled
...
Usage Examples
1. Enable Calendar Sync for a Tenant
# Via Django shell or management command
from core.models import Tenant
tenant = Tenant.objects.get(schema_name='demo')
tenant.can_use_calendar_sync = True
tenant.save()
# Check if tenant has feature
if tenant.has_feature('can_use_calendar_sync'):
print("Calendar sync enabled!")
2. API Endpoints
Initiate Google Calendar OAuth
POST /api/oauth/google/initiate/
Content-Type: application/json
{
"purpose": "calendar"
}
# Response (if permission granted):
{
"success": true,
"authorization_url": "https://accounts.google.com/o/oauth2/auth?..."
}
# Response (if permission denied):
{
"success": false,
"error": "Your current plan does not include Calendar Sync. Please upgrade..."
}
List Connected Calendars
GET /api/calendar/list/
# Response:
{
"success": true,
"calendars": [
{
"id": 1,
"provider": "Google",
"email": "user@gmail.com",
"is_valid": true,
"is_expired": false,
"last_used_at": "2025-12-02T10:30:00Z",
"created_at": "2025-12-01T08:15:00Z"
}
]
}
Sync Calendar Events
POST /api/calendar/sync/
Content-Type: application/json
{
"credential_id": 1,
"calendar_id": "primary",
"start_date": "2025-12-01",
"end_date": "2025-12-31"
}
# Response:
{
"success": true,
"message": "Calendar sync started for user@gmail.com"
}
Check Calendar Sync Status
GET /api/calendar/status/
# Response (if feature enabled):
{
"success": true,
"can_use_calendar_sync": true,
"total_connected": 2,
"feature_enabled": true
}
# Response (if feature not enabled):
{
"success": true,
"can_use_calendar_sync": false,
"message": "Calendar Sync feature is not available for your plan",
"total_connected": 0
}
Permission Flow Diagram
User Request to Calendar Endpoint
↓
[Is User Authenticated?]
├─ NO → 401 Unauthorized
└─ YES ↓
[Is Request in Tenant Context?]
├─ NO → 400 Bad Request
└─ YES ↓
[Does Tenant have can_use_calendar_sync?]
├─ NO → 403 Permission Denied (upgrade message)
└─ YES ↓
[Process Request]
├─ Success → 200 OK
└─ Error → 500 Server Error
Integration with ViewSets
For ModelViewSet endpoints (like scheduling events from calendar sync):
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from core.permissions import HasFeaturePermission
from schedule.models import Event
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
# Permission for create/update/delete operations
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
def get_queryset(self):
# Only show events that user's tenant can access
return Event.objects.filter(tenant=self.request.tenant)
@action(detail=False, methods=['post'])
def sync_from_calendar(self, request):
"""Create events from a calendar sync"""
# This action is protected by the permission_classes
calendar_id = request.data.get('calendar_id')
# ... implement sync logic ...
Note: If you only want to gate specific actions, override get_permissions():
def get_permissions(self):
if self.action in ['create', 'update', 'destroy', 'sync_from_calendar']:
# Only allow these actions if calendar sync is enabled
return [IsAuthenticated(), HasFeaturePermission('can_use_calendar_sync')()]
# Read-only actions don't require calendar sync permission
return [IsAuthenticated()]
Migration
The migration 0016_tenant_can_use_calendar_sync.py adds the field to existing tenants with default value of False.
To apply the migration:
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
docker compose -f docker-compose.local.yml exec django python manage.py migrate
Testing
Test Permission Denied
from django.test import TestCase, RequestFactory
from rest_framework.test import APITestCase
from core.models import Tenant
from smoothschedule.users.models import User
class CalendarSyncTests(APITestCase):
def setUp(self):
self.tenant = Tenant.objects.create(
schema_name='test',
name='Test Tenant',
can_use_calendar_sync=False # Feature disabled
)
self.user = User.objects.create_user(
email='user@test.com',
password='testpass',
tenant=self.tenant
)
self.client.force_authenticate(user=self.user)
def test_calendar_sync_permission_denied(self):
"""Test that users without permission cannot access calendar sync"""
response = self.client.post('/api/calendar/list/')
# Should return 403 Forbidden with upgrade message
self.assertEqual(response.status_code, 403)
self.assertIn('upgrade', response.json()['error'].lower())
def test_calendar_sync_permission_granted(self):
"""Test that users with permission can access calendar sync"""
self.tenant.can_use_calendar_sync = True
self.tenant.save()
response = self.client.get('/api/calendar/list/')
# Should return 200 OK
self.assertEqual(response.status_code, 200)
Security Considerations
- Permission is checked at view level: The
CalendarSyncPermissionchecks that the tenant has the feature enabled - Tenant isolation: OAuthCredential queries filter by tenant to ensure data isolation
- Token security: Tokens are stored encrypted at rest (configured in settings)
- CSRF protection: OAuth state parameter prevents CSRF attacks
- Audit logging: All calendar sync operations are logged with tenant and user information
Related Files
- Model:
/home/poduck/Desktop/smoothschedule2/smoothschedule/core/models.py(Tenant.can_use_calendar_sync) - Permission:
/home/poduck/Desktop/smoothschedule2/smoothschedule/core/permissions.py(HasFeaturePermission) - OAuth Views:
/home/poduck/Desktop/smoothschedule2/smoothschedule/core/oauth_views.py - Calendar Views:
/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/calendar_sync_views.py - Migration:
/home/poduck/Desktop/smoothschedule2/smoothschedule/core/migrations/0016_tenant_can_use_calendar_sync.py