Files
calendars/openspec/changes/add-ical-subscription-export/tasks.md
Nathan Panchout a96be2c7b7 📝(docs) add OpenSpec workflow
Add OpenSpec workflow for AI-assisted change proposals
including proposal templates, archive commands, and
project configuration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:56:21 +01:00

128 lines
4.9 KiB
Markdown

# Tasks: iCal Subscription Export
## 1. Backend - Data Model
- [x] 1.1 Create `CalendarSubscriptionToken` model in `core/models.py`
- ForeignKey to User (owner, on_delete=CASCADE)
- `caldav_path` field (CharField, max_length=512) - stores CalDAV path directly
- `calendar_name` field (CharField, max_length=255, optional) - for display
- `token` field (UUID, unique, indexed, default=uuid4)
- `is_active` boolean (default=True)
- `last_accessed_at` DateTimeField (nullable)
- UniqueConstraint on (owner, caldav_path)
- [x] 1.2 Create and run database migration
- [x] 1.3 Add model to Django admin for debugging
## 2. SabreDAV - Enable ICSExportPlugin
- [x] 2.1 Add `ICSExportPlugin` to `server.php`:
```php
$server->addPlugin(new CalDAV\ICSExportPlugin());
```
- [x] 2.2 Test that `?export` works via existing CalDAV proxy
## 3. Backend - Public iCal Endpoint
- [x] 3.1 Create `ICalExportView` in `core/api/viewsets_ical.py`
- No authentication required (public endpoint)
- Extract token from URL path
- Lookup `CalendarSubscriptionToken` by token
- Return 404 if token invalid/inactive
- Update `last_accessed_at` on access
- Proxy request to SabreDAV using `token.caldav_path` and `token.owner.email`
- Return ICS response with `Content-Type: text/calendar`
- Set security headers (Cache-Control, Referrer-Policy)
- [x] 3.2 Add URL route: `path('ical/<uuid:token>.ics', ...)`
- [x] 3.3 Write tests for public endpoint (valid token, invalid token, inactive)
## 4. Backend - Standalone Token Management API
- [x] 4.1 Create serializers in `core/api/serializers.py`
- `CalendarSubscriptionTokenSerializer` - fields: token, url, caldav_path, calendar_name, etc.
- `CalendarSubscriptionTokenCreateSerializer` - for POST body validation
- [x] 4.2 Create standalone `SubscriptionTokenViewSet` in `core/api/viewsets.py`:
- `POST /subscription-tokens/` - create token with { caldav_path, calendar_name }
- `GET /subscription-tokens/by-path/?caldav_path=...` - get existing token
- `DELETE /subscription-tokens/by-path/?caldav_path=...` - revoke token
- Permission verification: user's email must be in caldav_path
- [x] 4.3 Register viewset in `core/urls.py`
- [x] 4.4 Write API tests for token management (create, get, delete, permissions)
## 5. Frontend - API Integration
- [x] 5.1 Add API functions in `features/calendar/api.ts`:
- `getSubscriptionToken(caldavPath)` - GET by-path
- `createSubscriptionToken({ caldavPath, calendarName })` - POST
- `deleteSubscriptionToken(caldavPath)` - DELETE by-path
- [x] 5.2 Update React Query hooks in `hooks/useCalendars.ts`
- Use caldavPath instead of calendarId
## 6. Frontend - UI Components
- [x] 6.1 Update `SubscriptionUrlModal` component
- Accept `caldavPath` prop instead of `calendarId`
- Extract caldavPath from calendar URL in parent component
- Display the subscription URL in a copyable field
- "Copy to clipboard" button with success feedback
- Warning text about URL being private
- "Regenerate URL" button with confirmation dialog
- Only show error alert for real errors (not for expected 404)
- [x] 6.2 Update `CalendarList.tsx`
- Extract CalDAV path from calendar URL
- Pass caldavPath to SubscriptionUrlModal
- [x] 6.3 Add translations (i18n) for new UI strings
## 7. Cleanup
- [x] 7.1 Remove old `subscription_token` action from CalendarViewSet
- [x] 7.2 Remove `sync-from-caldav` endpoint (no longer needed)
- [x] 7.3 Remove `syncFromCaldav` from frontend API
## 8. Testing & Validation
- [x] 8.1 Manual test: add URL to Apple Calendar
- [ ] 8.2 Manual test: add URL to Google Calendar
- [x] 8.3 Verify token regeneration invalidates old URL
- [ ] 8.4 E2E test for subscription workflow (optional)
## Dependencies
```
1 (Django model)
2 (ICSExportPlugin) ──────┐
↓ │
3 (Public endpoint) ──────┤ can run in parallel after 1
↓ │
4 (Token API) ────────────┘
5 (Frontend API)
6 (Frontend UI)
7 (Cleanup)
8 (Testing)
```
## Key Files Modified
### Backend
- `src/backend/core/models.py` - CalendarSubscriptionToken model (standalone)
- `src/backend/core/migrations/0002_calendarsubscriptiontoken.py`
- `src/backend/core/api/serializers.py` - Token serializers
- `src/backend/core/api/viewsets.py` - SubscriptionTokenViewSet
- `src/backend/core/api/viewsets_ical.py` - ICalExportView
- `src/backend/core/urls.py` - Route registration
- `src/backend/core/admin.py` - Admin configuration
- `src/backend/core/factories.py` - Test factory
### Frontend
- `src/features/calendar/api.ts` - API functions with caldavPath
- `src/features/calendar/hooks/useCalendars.ts` - React Query hooks
- `src/features/calendar/components/calendar-list/CalendarList.tsx`
- `src/features/calendar/components/calendar-list/SubscriptionUrlModal.tsx`
### SabreDAV
- `docker/sabredav/server.php` - ICSExportPlugin enabled