diff --git a/.docker/selfhost/schema.json b/.docker/selfhost/schema.json index d578625a7..09afa59af 100644 --- a/.docker/selfhost/schema.json +++ b/.docker/selfhost/schema.json @@ -469,6 +469,13 @@ "type": "string", "description": "The account id for the cloudflare r2 storage provider." }, + "jurisdiction": { + "type": "string", + "enum": [ + "eu" + ], + "description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets." + }, "usePresignedURL": { "type": "object", "description": "The presigned url config for the cloudflare r2 storage provider.", @@ -486,13 +493,6 @@ "description": "The presigned key for the cloudflare r2 storage provider." } } - }, - "jurisdiction": { - "type": "string", - "enum": [ - "eu" - ], - "description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets." } } } @@ -667,6 +667,13 @@ "type": "string", "description": "The account id for the cloudflare r2 storage provider." }, + "jurisdiction": { + "type": "string", + "enum": [ + "eu" + ], + "description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets." + }, "usePresignedURL": { "type": "object", "description": "The presigned url config for the cloudflare r2 storage provider.", @@ -684,13 +691,6 @@ "description": "The presigned key for the cloudflare r2 storage provider." } } - }, - "jurisdiction": { - "type": "string", - "enum": [ - "eu" - ], - "description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets." } } } @@ -861,11 +861,14 @@ "properties": { "google": { "type": "object", - "description": "Google Calendar integration config\n@default {\"enabled\":false,\"clientId\":\"\",\"clientSecret\":\"\",\"externalWebhookUrl\":\"\",\"webhookVerificationToken\":\"\",\"requestTimeoutMs\":10000}\n@link https://developers.google.com/calendar/api/guides/push", + "description": "Google Calendar integration config\n@default {\"enabled\":false,\"allowNewAccounts\":true,\"clientId\":\"\",\"clientSecret\":\"\",\"externalWebhookUrl\":\"\",\"webhookVerificationToken\":\"\",\"requestTimeoutMs\":10000}\n@link https://developers.google.com/calendar/api/guides/push", "properties": { "enabled": { "type": "boolean" }, + "allowNewAccounts": { + "type": "boolean" + }, "clientId": { "type": "string" }, @@ -884,6 +887,7 @@ }, "default": { "enabled": false, + "allowNewAccounts": true, "clientId": "", "clientSecret": "", "externalWebhookUrl": "", @@ -1296,6 +1300,13 @@ "type": "string", "description": "The account id for the cloudflare r2 storage provider." }, + "jurisdiction": { + "type": "string", + "enum": [ + "eu" + ], + "description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets." + }, "usePresignedURL": { "type": "object", "description": "The presigned url config for the cloudflare r2 storage provider.", @@ -1313,13 +1324,6 @@ "description": "The presigned key for the cloudflare r2 storage provider." } } - }, - "jurisdiction": { - "type": "string", - "enum": [ - "eu" - ], - "description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets." } } } diff --git a/packages/backend/server/src/plugins/calendar/__tests__/service.spec.ts b/packages/backend/server/src/plugins/calendar/__tests__/service.spec.ts index 9cb2ac468..d6aeb406c 100644 --- a/packages/backend/server/src/plugins/calendar/__tests__/service.spec.ts +++ b/packages/backend/server/src/plugins/calendar/__tests__/service.spec.ts @@ -209,47 +209,77 @@ test('listAccounts includes calendars count', async t => { t.is(counts.get(accountB.id), 1); }); -test('assertCanLinkProvider blocks new google calendar accounts when disabled', async t => { - config.calendar.google.allowNewAccounts = false; - const user = await module.create(Mockers.User); +test.serial( + 'assertCanLinkProvider blocks new google calendar accounts when disabled', + async t => { + config.calendar.google.allowNewAccounts = false; + const user = await module.create(Mockers.User); - const error = await t.throwsAsync( - calendarService.assertCanLinkProvider(user.id, CalendarProviderName.Google) - ); - t.true(error instanceof GraphqlBadRequest); - t.is( - (error as GraphqlBadRequest).data?.code, - 'calendar_provider_link_disabled' - ); -}); + const error = await t.throwsAsync( + calendarService.assertCanLinkProvider( + user.id, + CalendarProviderName.Google + ) + ); + t.true(error instanceof GraphqlBadRequest); + t.is( + (error as GraphqlBadRequest).data?.code, + 'calendar_provider_link_disabled' + ); + } +); -test('assertCanLinkProvider allows users with an existing google calendar account', async t => { - config.calendar.google.allowNewAccounts = false; - const user = await module.create(Mockers.User); - await createAccount(user.id); +test.serial( + 'assertCanLinkProvider allows users with an existing google calendar account', + async t => { + config.calendar.google.allowNewAccounts = false; + const user = await module.create(Mockers.User); + await createAccount(user.id); - await t.notThrowsAsync( - calendarService.assertCanLinkProvider(user.id, CalendarProviderName.Google) - ); -}); + await t.notThrowsAsync( + calendarService.assertCanLinkProvider( + user.id, + CalendarProviderName.Google + ) + ); + } +); -test('handleOAuthCallback does not persist new google account when linking is disabled', async t => { - config.calendar.google.allowNewAccounts = false; - const provider = new MockCalendarProvider(); - providerFactory.register(provider); - const user = await module.create(Mockers.User); +test.serial( + 'handleOAuthCallback does not persist new google account when linking is disabled', + async t => { + config.calendar.google.allowNewAccounts = false; + const provider = new MockCalendarProvider(); + mock.method(providerFactory, 'get', () => provider); + const user = await module.create(Mockers.User); - const error = await t.throwsAsync( - calendarService.handleOAuthCallback({ - provider: CalendarProviderName.Google, - code: 'code', - redirectUri: 'https://example.com/callback', - userId: user.id, - }) - ); - t.true(error instanceof GraphqlBadRequest); - t.is((await models.calendarAccount.listByUser(user.id)).length, 0); -}); + const error = await t.throwsAsync( + calendarService.handleOAuthCallback({ + provider: CalendarProviderName.Google, + code: 'code', + redirectUri: 'https://example.com/callback', + userId: user.id, + }) + ); + t.true(error instanceof GraphqlBadRequest); + t.is((await models.calendarAccount.listByUser(user.id)).length, 0); + } +); + +test.serial( + 'canLinkProvider returns false for new google calendar accounts when disabled', + async t => { + config.calendar.google.allowNewAccounts = false; + const user = await module.create(Mockers.User); + + t.false( + await calendarService.canLinkProvider( + user.id, + CalendarProviderName.Google + ) + ); + } +); test('syncSubscription resets invalid sync token and maps events', async t => { const user = await module.create(Mockers.User); diff --git a/packages/backend/server/src/plugins/calendar/resolver.ts b/packages/backend/server/src/plugins/calendar/resolver.ts index 271e38b4d..75ed602b5 100644 --- a/packages/backend/server/src/plugins/calendar/resolver.ts +++ b/packages/backend/server/src/plugins/calendar/resolver.ts @@ -33,17 +33,20 @@ import { export class CalendarServerConfigResolver { constructor( private readonly providerFactory: CalendarProviderFactory, - private readonly config: Config + private readonly config: Config, + private readonly calendar: CalendarService ) {} @ResolveField(() => [CalendarProviderName]) - calendarProviders() { - return this.providerFactory.providers.filter(provider => { - return ( - provider !== CalendarProviderName.Google || - this.config.calendar.google.allowNewAccounts !== false - ); - }); + async calendarProviders(@CurrentUser() user?: CurrentUser) { + const providers = []; + for (const provider of this.providerFactory.providers) { + if (!(await this.calendar.canLinkProvider(user?.id, provider))) { + continue; + } + providers.push(provider); + } + return providers; } @ResolveField(() => [CalendarCalDAVProviderPresetObjectType]) diff --git a/packages/backend/server/src/plugins/calendar/service.ts b/packages/backend/server/src/plugins/calendar/service.ts index 2785215bb..dc988d8b8 100644 --- a/packages/backend/server/src/plugins/calendar/service.ts +++ b/packages/backend/server/src/plugins/calendar/service.ts @@ -571,18 +571,28 @@ export class CalendarService { } async assertCanLinkProvider(userId: string, provider: CalendarProviderName) { - if (this.canCreateNewAccounts(provider)) { - return; - } - - const accounts = await this.models.calendarAccount.listByUser(userId); - if (accounts.some(account => account.provider === provider)) { + if (await this.canLinkProvider(userId, provider)) { return; } throw this.providerLinkDisabledError(provider); } + async canLinkProvider( + userId: string | null | undefined, + provider: CalendarProviderName + ) { + if (this.canCreateNewAccounts(provider)) { + return true; + } + if (!userId) { + return false; + } + + const accounts = await this.models.calendarAccount.listByUser(userId); + return accounts.some(account => account.provider === provider); + } + getAuthUrl( provider: CalendarProviderName, state: string,