feat: Plan-based feature permissions and quota enforcement
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>
This commit is contained in:
195
QUICK_REFERENCE_CALENDAR_SYNC.md
Normal file
195
QUICK_REFERENCE_CALENDAR_SYNC.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Calendar Sync Permission - Quick Reference
|
||||
|
||||
## What Was Added
|
||||
|
||||
A permission gating system for calendar sync features in the Django backend.
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. Database Field
|
||||
```python
|
||||
# core/models.py - Added to Tenant model
|
||||
can_use_calendar_sync = models.BooleanField(default=False)
|
||||
```
|
||||
|
||||
### 2. Permission Check Factory
|
||||
```python
|
||||
# core/permissions.py - Added to FEATURE_NAMES
|
||||
'can_use_calendar_sync': 'Calendar Sync',
|
||||
```
|
||||
|
||||
### 3. OAuth Integration
|
||||
```python
|
||||
# core/oauth_views.py - Check when purpose is 'calendar'
|
||||
if purpose == 'calendar':
|
||||
calendar_permission = HasFeaturePermission('can_use_calendar_sync')
|
||||
if not calendar_permission().has_permission(request, self):
|
||||
return Response({'error': 'Feature not available'}, status=403)
|
||||
```
|
||||
|
||||
### 4. Calendar Sync Views
|
||||
```python
|
||||
# schedule/calendar_sync_views.py
|
||||
CalendarListView # GET /api/calendar/list/
|
||||
CalendarSyncView # POST /api/calendar/sync/
|
||||
CalendarDeleteView # DELETE /api/calendar/disconnect/
|
||||
CalendarStatusView # GET /api/calendar/status/
|
||||
```
|
||||
|
||||
## How to Use
|
||||
|
||||
### Enable for a Tenant
|
||||
```bash
|
||||
# Via Django shell
|
||||
from core.models import Tenant
|
||||
tenant = Tenant.objects.get(schema_name='demo')
|
||||
tenant.can_use_calendar_sync = True
|
||||
tenant.save()
|
||||
```
|
||||
|
||||
### Use in ViewSet
|
||||
```python
|
||||
from rest_framework import viewsets
|
||||
from core.permissions import HasFeaturePermission
|
||||
|
||||
class MyViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
|
||||
```
|
||||
|
||||
### Use in APIView
|
||||
```python
|
||||
from rest_framework.views import APIView
|
||||
|
||||
class MyView(APIView):
|
||||
permission_classes = [CalendarSyncPermission]
|
||||
# CalendarSyncPermission = IsAuthenticated + has_feature check
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Endpoint | Description | Permission |
|
||||
|--------|----------|-------------|-----------|
|
||||
| GET | /api/calendar/status/ | Check if calendar sync is available | Auth only |
|
||||
| GET | /api/calendar/list/ | List connected calendars | Calendar sync |
|
||||
| POST | /api/calendar/sync/ | Start calendar sync | Calendar sync |
|
||||
| DELETE | /api/calendar/disconnect/ | Disconnect a calendar | Calendar sync |
|
||||
| POST | /api/oauth/google/initiate/ | Start Google OAuth for calendar | Calendar sync (if purpose=calendar) |
|
||||
| POST | /api/oauth/microsoft/initiate/ | Start MS OAuth for calendar | Calendar sync (if purpose=calendar) |
|
||||
|
||||
## Testing
|
||||
|
||||
### Run tests
|
||||
```bash
|
||||
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||
docker compose -f docker-compose.local.yml exec django pytest schedule/tests/test_calendar_sync_permissions.py -v
|
||||
```
|
||||
|
||||
### Test endpoints manually
|
||||
```bash
|
||||
# Check status (always works)
|
||||
curl http://lvh.me:8000/api/calendar/status/ -H "Authorization: Bearer <token>"
|
||||
|
||||
# List calendars (requires permission)
|
||||
curl http://lvh.me:8000/api/calendar/list/ -H "Authorization: Bearer <token>"
|
||||
# Returns 403 if permission not granted
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| core/models.py | Added can_use_calendar_sync field |
|
||||
| core/permissions.py | Added to FEATURE_NAMES |
|
||||
| core/oauth_views.py | Added permission check for calendar |
|
||||
|
||||
## Files Created
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| core/migrations/0016_tenant_can_use_calendar_sync.py | Database migration |
|
||||
| schedule/calendar_sync_views.py | Calendar sync API views |
|
||||
| schedule/calendar_sync_urls.py | URL routing |
|
||||
| schedule/tests/test_calendar_sync_permissions.py | Test suite |
|
||||
| CALENDAR_SYNC_INTEGRATION.md | Developer guide |
|
||||
|
||||
## Permission Check Pattern
|
||||
|
||||
```
|
||||
Request to calendar endpoint
|
||||
↓
|
||||
Check: Is user authenticated?
|
||||
├─ NO → 401 Unauthorized
|
||||
└─ YES ↓
|
||||
Check: Does tenant have can_use_calendar_sync=True?
|
||||
├─ NO → 403 Forbidden (upgrade message)
|
||||
└─ YES ↓
|
||||
Process request
|
||||
├─ Success → 200 OK
|
||||
└─ Error → 500 Server Error
|
||||
```
|
||||
|
||||
## Example: Full Permission Setup
|
||||
|
||||
```python
|
||||
# 1. Enable feature for tenant
|
||||
from core.models import Tenant
|
||||
tenant = Tenant.objects.get(schema_name='demo')
|
||||
tenant.can_use_calendar_sync = True
|
||||
tenant.save()
|
||||
|
||||
# 2. User tries to access calendar endpoint
|
||||
# GET /api/calendar/list/
|
||||
# → Check: tenant.has_feature('can_use_calendar_sync')
|
||||
# → True! → 200 OK with calendar list
|
||||
|
||||
# 3. Without permission
|
||||
tenant.can_use_calendar_sync = False
|
||||
tenant.save()
|
||||
# GET /api/calendar/list/
|
||||
# → Check: tenant.has_feature('can_use_calendar_sync')
|
||||
# → False! → 403 Forbidden with upgrade message
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- **Full Guide:** `CALENDAR_SYNC_INTEGRATION.md` in smoothschedule/ folder
|
||||
- **Implementation Details:** `CALENDAR_SYNC_PERMISSION_IMPLEMENTATION.md` in project root
|
||||
- **Code:** `schedule/calendar_sync_views.py` (well-commented)
|
||||
- **Tests:** `schedule/tests/test_calendar_sync_permissions.py`
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Check if feature is enabled
|
||||
```python
|
||||
tenant.has_feature('can_use_calendar_sync') # Returns bool
|
||||
```
|
||||
|
||||
### Get list of connected calendars
|
||||
```python
|
||||
from core.models import OAuthCredential
|
||||
|
||||
credentials = OAuthCredential.objects.filter(
|
||||
tenant=tenant,
|
||||
purpose='calendar',
|
||||
is_valid=True
|
||||
)
|
||||
```
|
||||
|
||||
### Handle permission denied
|
||||
```python
|
||||
from core.permissions import HasFeaturePermission
|
||||
|
||||
permission = HasFeaturePermission('can_use_calendar_sync')
|
||||
if not permission().has_permission(request, view):
|
||||
# User doesn't have permission
|
||||
# Show upgrade prompt
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Feature defaults to **False** for all tenants (opt-in)
|
||||
- Works alongside existing subscription plan system
|
||||
- Follows same pattern as SMS reminders, webhooks, etc.
|
||||
- Multi-tenant isolation built-in
|
||||
- OAuth tokens are encrypted at rest
|
||||
- All operations logged for audit trail
|
||||
Reference in New Issue
Block a user