feat: basic caldav support (#14372)

fix #13531 

#### PR Dependency Tree


* **PR #14372** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* CalDAV calendar integration: link and sync CalDAV-compatible calendars
(discovery, listing, event sync).
* New UI flow and dialog to link CalDAV accounts with provider
selection, credentials, and display name.

* **API / Config**
* Server exposes CalDAV provider presets in config and new GraphQL
mutation to link CalDAV accounts.
  * New calendar config section for CalDAV with validation and defaults.

* **Tests**
  * Comprehensive CalDAV integration test suite added.

* **Chores**
* Removed analytics tokens from build configuration and reduced Cloud
E2E test shards.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-05 03:04:21 +08:00
committed by GitHub
parent 403f16b404
commit a655b79166
34 changed files with 2995 additions and 217 deletions
@@ -7,7 +7,7 @@ import {
Resolver,
} from '@nestjs/graphql';
import { ActionForbidden, AuthenticationRequired } from '../../base';
import { ActionForbidden, AuthenticationRequired, Config } from '../../base';
import { CurrentUser } from '../../core/auth';
import { ServerConfigType } from '../../core/config/types';
import { AccessController } from '../../core/permission';
@@ -19,8 +19,10 @@ import { CalendarProviderFactory, CalendarProviderName } from './providers';
import { CalendarService } from './service';
import {
CalendarAccountObjectType,
CalendarCalDAVProviderPresetObjectType,
CalendarEventObjectType,
CalendarSubscriptionObjectType,
LinkCalDAVAccountInput,
LinkCalendarAccountInput,
UpdateWorkspaceCalendarsInput,
WorkspaceCalendarObjectType,
@@ -28,12 +30,29 @@ import {
@Resolver(() => ServerConfigType)
export class CalendarServerConfigResolver {
constructor(private readonly providerFactory: CalendarProviderFactory) {}
constructor(
private readonly providerFactory: CalendarProviderFactory,
private readonly config: Config
) {}
@ResolveField(() => [CalendarProviderName])
calendarProviders() {
return this.providerFactory.providers;
}
@ResolveField(() => [CalendarCalDAVProviderPresetObjectType])
calendarCalDAVProviders() {
const caldavConfig = this.config.calendar.caldav;
if (!caldavConfig?.enabled) {
return [];
}
return caldavConfig.providers.map(provider => ({
id: provider.id,
label: provider.label,
requiresAppPassword: provider.requiresAppPassword ?? null,
docsUrl: provider.docsUrl ?? null,
}));
}
}
@Resolver(() => UserType)
@@ -140,6 +159,21 @@ export class CalendarMutationResolver {
return this.calendar.getAuthUrl(input.provider, state, callbackUrl);
}
@Mutation(() => CalendarAccountObjectType)
async linkCalDAVAccount(
@CurrentUser() user: CurrentUser | null,
@Args('input') input: LinkCalDAVAccountInput
) {
if (!user) {
throw new AuthenticationRequired();
}
return await this.calendar.linkCalDAVAccount({
userId: user.id,
input,
});
}
@Mutation(() => CalendarAccountObjectType, { nullable: true })
async updateCalendarAccount(
@CurrentUser() user: CurrentUser,