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>
342 lines
9.4 KiB
Markdown
342 lines
9.4 KiB
Markdown
# 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:
|
|
1. Connect to Google Calendar or Outlook Calendar via OAuth
|
|
2. Sync calendar events into the scheduling system
|
|
3. 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`:
|
|
|
|
```python
|
|
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`:
|
|
|
|
```python
|
|
FEATURE_NAMES = {
|
|
# ... other features ...
|
|
'can_use_calendar_sync': 'Calendar Sync',
|
|
}
|
|
```
|
|
|
|
The `HasFeaturePermission` factory function creates a DRF permission class:
|
|
|
|
```python
|
|
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"
|
|
|
|
```python
|
|
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 calendars
|
|
- `CalendarSyncView`: Syncs calendar events
|
|
- `CalendarDeleteView`: Disconnects calendar integration
|
|
- `CalendarStatusView`: Gets calendar sync status
|
|
|
|
#### Permission Pattern in Views
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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):
|
|
|
|
```python
|
|
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()`:
|
|
|
|
```python
|
|
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:
|
|
|
|
```bash
|
|
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
|
docker compose -f docker-compose.local.yml exec django python manage.py migrate
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Test Permission Denied
|
|
|
|
```python
|
|
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
|
|
|
|
1. **Permission is checked at view level**: The `CalendarSyncPermission` checks that the tenant has the feature enabled
|
|
2. **Tenant isolation**: OAuthCredential queries filter by tenant to ensure data isolation
|
|
3. **Token security**: Tokens are stored encrypted at rest (configured in settings)
|
|
4. **CSRF protection**: OAuth state parameter prevents CSRF attacks
|
|
5. **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`
|