emailengine-app 2.63.4 → 2.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.github/workflows/test.yml +4 -0
  2. package/CHANGELOG.md +70 -0
  3. package/copy-static-files.sh +1 -1
  4. package/data/google-crawlers.json +1 -1
  5. package/eslint.config.js +2 -0
  6. package/lib/account.js +13 -9
  7. package/lib/api-routes/account-routes.js +7 -1
  8. package/lib/consts.js +17 -1
  9. package/lib/email-client/gmail/gmail-api.js +1 -12
  10. package/lib/email-client/imap-client.js +5 -3
  11. package/lib/email-client/outlook/graph-api.js +9 -15
  12. package/lib/email-client/outlook-client.js +406 -177
  13. package/lib/export.js +17 -0
  14. package/lib/imapproxy/imap-server.js +3 -2
  15. package/lib/oauth/gmail.js +12 -1
  16. package/lib/oauth/outlook.js +99 -1
  17. package/lib/oauth/pubsub/google.js +253 -85
  18. package/lib/oauth2-apps.js +620 -389
  19. package/lib/outbox.js +1 -1
  20. package/lib/routes-ui.js +193 -238
  21. package/lib/schemas.js +189 -12
  22. package/lib/ui-routes/account-routes.js +7 -2
  23. package/lib/ui-routes/admin-entities-routes.js +3 -3
  24. package/lib/ui-routes/oauth-routes.js +27 -175
  25. package/package.json +21 -21
  26. package/sbom.json +1 -1
  27. package/server.js +54 -22
  28. package/static/licenses.html +30 -90
  29. package/translations/de.mo +0 -0
  30. package/translations/de.po +54 -42
  31. package/translations/en.mo +0 -0
  32. package/translations/en.po +55 -43
  33. package/translations/et.mo +0 -0
  34. package/translations/et.po +54 -42
  35. package/translations/fr.mo +0 -0
  36. package/translations/fr.po +54 -42
  37. package/translations/ja.mo +0 -0
  38. package/translations/ja.po +54 -42
  39. package/translations/messages.pot +93 -71
  40. package/translations/nl.mo +0 -0
  41. package/translations/nl.po +54 -42
  42. package/translations/pl.mo +0 -0
  43. package/translations/pl.po +54 -42
  44. package/views/config/oauth/app.hbs +12 -0
  45. package/views/config/oauth/edit.hbs +2 -0
  46. package/views/config/oauth/index.hbs +4 -1
  47. package/views/config/oauth/new.hbs +2 -0
  48. package/views/config/oauth/subscriptions.hbs +175 -0
  49. package/views/error.hbs +4 -4
  50. package/views/partials/oauth_form.hbs +179 -4
  51. package/views/partials/oauth_tabs.hbs +8 -0
  52. package/views/partials/scope_info.hbs +10 -0
  53. package/workers/api.js +174 -96
  54. package/workers/documents.js +1 -0
  55. package/workers/export.js +6 -2
  56. package/workers/imap.js +33 -49
  57. package/workers/smtp.js +1 -0
  58. package/workers/submit.js +1 -0
  59. package/workers/webhooks.js +42 -30
package/lib/outbox.js CHANGED
@@ -11,7 +11,7 @@ async function list(options) {
11
11
 
12
12
  let jobCounts = await submitQueue.getJobCounts();
13
13
 
14
- let jobStates = ['delayed', 'paused', 'wait', 'active'];
14
+ let jobStates = ['delayed', 'paused', 'wait', 'active', 'failed'];
15
15
 
16
16
  let totalJobs = jobStates.map(state => Number(jobCounts[state]) || 0).reduce((previousValue, currentValue) => previousValue + currentValue);
17
17
 
package/lib/routes-ui.js CHANGED
@@ -37,7 +37,7 @@ const { Account } = require('./account');
37
37
  const { Gateway } = require('./gateway');
38
38
  const { redis, submitQueue, notifyQueue, documentsQueue } = require('./db');
39
39
  const psl = require('psl');
40
- const { oauth2Apps, LEGACY_KEYS, OAUTH_PROVIDERS, oauth2ProviderData } = require('./oauth2-apps');
40
+ const { oauth2Apps, OAUTH_PROVIDERS, oauth2ProviderData, SERVICE_ACCOUNT_PROVIDERS } = require('./oauth2-apps');
41
41
  const { autodetectImapSettings } = require('./autodetect-imap-settings');
42
42
  const getSecret = require('./get-secret');
43
43
  const os = require('os');
@@ -45,10 +45,9 @@ const {
45
45
  ADDRESS_STRATEGIES,
46
46
  settingsSchema,
47
47
  oauthCreateSchema,
48
+ oauthUpdateSchema,
48
49
  accountIdSchema,
49
50
  defaultAccountTypeSchema,
50
- googleProjectIdSchema,
51
- googleWorkspaceAccountsSchema,
52
51
  exportIdSchema
53
52
  } = require('./schemas');
54
53
  const fs = require('fs');
@@ -58,7 +57,6 @@ const { Client: ElasticSearch } = require('@elastic/elasticsearch');
58
57
  const { llmPreProcess } = require('./llm-pre-process');
59
58
  const { locales } = require('./translations');
60
59
  const capa = require('./capa');
61
- const exampleWebhookPayloads = require('./payload-examples-webhooks.json');
62
60
  const exampleDocumentsPayloads = require('./payload-examples-documents.json');
63
61
  const { defaultMappings } = require('./es');
64
62
  const { getESClient } = require('../lib/document-store');
@@ -358,150 +356,6 @@ const OKTA_OAUTH2_CLIENT_ID = readEnvValue('OKTA_OAUTH2_CLIENT_ID');
358
356
  const OKTA_OAUTH2_CLIENT_SECRET = readEnvValue('OKTA_OAUTH2_CLIENT_SECRET');
359
357
  const USE_OKTA_AUTH = !!(OKTA_OAUTH2_ISSUER && OKTA_OAUTH2_CLIENT_ID && OKTA_OAUTH2_CLIENT_SECRET);
360
358
 
361
- const oauthUpdateSchema = {
362
- app: Joi.string().empty('').max(255).example('gmail').label('Provider').required(),
363
-
364
- provider: Joi.string()
365
- .trim()
366
- .empty('')
367
- .max(256)
368
- .valid(...Object.keys(OAUTH_PROVIDERS))
369
- .example('gmail')
370
- .required()
371
- .description('OAuth2 provider'),
372
-
373
- name: Joi.string()
374
- .trim()
375
- .empty('')
376
- .max(256)
377
- .example('My Gmail App')
378
- .description('Application name')
379
- .when('app', {
380
- not: Joi.string().valid(...LEGACY_KEYS),
381
- then: Joi.required(),
382
- otherwise: Joi.optional().valid(false, null)
383
- }),
384
- description: Joi.string().trim().allow('').max(1024).example('My cool app').description('Application description'),
385
-
386
- title: Joi.string().allow('').trim().max(256).example('App title').description('Title for the application button'),
387
-
388
- enabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false).description('Enable this app'),
389
-
390
- clientId: Joi.string()
391
- .trim()
392
- .allow('')
393
- .max(256)
394
- .when('provider', {
395
- not: 'gmailService',
396
- then: Joi.required(),
397
- otherwise: Joi.optional().valid(false, null)
398
- })
399
- .description('OAuth2 Client ID'),
400
-
401
- clientSecret: Joi.string()
402
- .trim()
403
- .empty('', false, null)
404
- .max(256)
405
- .when('provider', {
406
- not: 'gmailService',
407
- then: Joi.optional(),
408
- otherwise: Joi.forbidden()
409
- })
410
- .description('OAuth2 Client Secret'),
411
-
412
- pubSubApp: Joi.string()
413
- .empty('')
414
- .base64({ paddingRequired: false, urlSafe: true })
415
- .max(512)
416
- .example('AAAAAQAACnA')
417
- .description('Cloud Pub/Sub app for Gmail API webhooks'),
418
-
419
- extraScopes: Joi.string()
420
- .allow('')
421
- .trim()
422
- .max(10 * 1024)
423
- .description('OAuth2 Extra Scopes'),
424
-
425
- skipScopes: Joi.string()
426
- .allow('')
427
- .trim()
428
- .max(10 * 1024)
429
- .description('OAuth2 scopes to skip from the base set'),
430
-
431
- serviceClient: Joi.string()
432
- .trim()
433
- .allow('')
434
- .max(256)
435
- .when('provider', {
436
- is: 'gmailService',
437
- then: Joi.required(),
438
- otherwise: Joi.optional().valid(false, null)
439
- })
440
- .description('OAuth2 Service Client ID'),
441
-
442
- googleProjectId: googleProjectIdSchema,
443
-
444
- googleWorkspaceAccounts: googleWorkspaceAccountsSchema.when('provider', {
445
- is: 'gmail',
446
- then: Joi.optional().default(false)
447
- }),
448
-
449
- serviceClientEmail: Joi.string()
450
- .trim()
451
- .allow('')
452
- .email()
453
- .when('provider', {
454
- is: 'gmailService',
455
- then: Joi.required(),
456
- otherwise: Joi.optional().valid(false, null)
457
- })
458
- .example('name@project-123.iam.gserviceaccount.com')
459
- .description('Service Client Email for 2-legged OAuth2 applications'),
460
-
461
- serviceKey: Joi.string()
462
- .trim()
463
- .empty('', false, null)
464
- .max(100 * 1024)
465
- .when('provider', {
466
- is: 'gmailService',
467
- then: Joi.optional(),
468
- otherwise: Joi.forbidden()
469
- })
470
- .description('OAuth2 Secret Service Key'),
471
-
472
- authority: Joi.string()
473
- .trim()
474
- .empty('')
475
- .max(1024)
476
- .when('provider', {
477
- is: 'outlook',
478
- then: Joi.required(),
479
- otherwise: Joi.optional().valid(false, null)
480
- })
481
- .example(false)
482
- .label('SupportedAccountTypes'),
483
-
484
- cloud: Joi.string()
485
- .trim()
486
- .empty('')
487
- .valid('global', 'gcc-high', 'dod', 'china')
488
- .example('global')
489
- .description('Azure cloud type for Outlook OAuth2 applications')
490
- .label('AzureCloud'),
491
-
492
- tenant: Joi.string().trim().empty('').max(1024).example('f8cdef31-a31e-4b4a-93e4-5f571e91255a').label('Directorytenant'),
493
-
494
- redirectUrl: Joi.string()
495
- .allow('')
496
- .uri({ scheme: ['http', 'https'], allowRelative: false })
497
- .when('provider', {
498
- not: 'gmailService',
499
- then: Joi.required(),
500
- otherwise: Joi.optional().valid(false, null)
501
- })
502
- .description('OAuth2 Callback URL')
503
- };
504
-
505
359
  function formatAccountData(account, gt) {
506
360
  account.type = {};
507
361
 
@@ -719,34 +573,6 @@ async function getOpenAiError(gt) {
719
573
  return openAiError;
720
574
  }
721
575
 
722
- async function getExampleWebhookPayloads() {
723
- let serviceUrl = await settings.get('serviceUrl');
724
- let date = new Date().toISOString();
725
-
726
- let examplePayloads = structuredClone(exampleWebhookPayloads);
727
-
728
- examplePayloads.forEach(payload => {
729
- if (payload && payload.content) {
730
- if (typeof payload.content.serviceUrl === 'string') {
731
- payload.content.serviceUrl = serviceUrl;
732
- }
733
-
734
- if (typeof payload.content.date === 'string') {
735
- payload.content.date = date;
736
- }
737
-
738
- if (payload.content.data && typeof payload.content.data.date === 'string') {
739
- payload.content.data.date = date;
740
- }
741
-
742
- if (payload.content.data && typeof payload.content.data.created === 'string') {
743
- payload.content.data.created = date;
744
- }
745
- }
746
- });
747
- return examplePayloads;
748
- }
749
-
750
576
  async function getExampleDocumentsPayloads() {
751
577
  let date = new Date().toISOString();
752
578
 
@@ -886,6 +712,21 @@ function applyRoutes(server, call) {
886
712
  throw error;
887
713
  }
888
714
 
715
+ /**
716
+ * Fetch the list of Pub/Sub apps and mark the one matching selectedId as selected.
717
+ * Returns the apps array ready for template rendering.
718
+ */
719
+ async function getPubSubAppsForSelect(selectedId) {
720
+ let result = await oauth2Apps.list(0, 1000, { pubsub: true });
721
+ let apps = (result && result.apps) || [];
722
+ for (let app of apps) {
723
+ if (app.id === selectedId) {
724
+ app.selected = true;
725
+ }
726
+ }
727
+ return apps;
728
+ }
729
+
889
730
  // List exports for account
890
731
  server.route({
891
732
  method: 'GET',
@@ -2568,6 +2409,7 @@ return true;`
2568
2409
  pageTitle: 'OAuth2',
2569
2410
  menuConfig: true,
2570
2411
  menuConfigOauth: true,
2412
+ activeApplications: true,
2571
2413
 
2572
2414
  newLink: newLink.pathname + newLink.search,
2573
2415
 
@@ -2614,6 +2456,160 @@ return true;`
2614
2456
  }
2615
2457
  });
2616
2458
 
2459
+ // GET /admin/config/oauth/subscriptions - Gmail Pub/Sub subscriptions list
2460
+ server.route({
2461
+ method: 'GET',
2462
+ path: '/admin/config/oauth/subscriptions',
2463
+ async handler(request, h) {
2464
+ try {
2465
+ let data = await oauth2Apps.list(request.query.page - 1, request.query.pageSize, { pubsub: true });
2466
+
2467
+ let gmailSubscriptionTtl = await settings.get('gmailSubscriptionTtl');
2468
+
2469
+ // Compute human-readable expiration for each app
2470
+ // meta.subscriptionExpiration is:
2471
+ // undefined - no data yet (app predates this feature or ensurePubsub hasn't run)
2472
+ // null - indefinite (no TTL set, ensurePubsub confirmed this)
2473
+ // "Ns" - TTL in seconds (e.g. "2678400s" for 31 days)
2474
+ let gt = request.app.gt;
2475
+ for (let app of data.apps) {
2476
+ if (!app.pubSubSubscription) {
2477
+ app.expirationLabel = '';
2478
+ continue;
2479
+ }
2480
+
2481
+ let meta = app.meta || {};
2482
+ if (!('subscriptionExpiration' in meta)) {
2483
+ app.expirationLabel = gt.gettext('Unknown');
2484
+ continue;
2485
+ }
2486
+
2487
+ let seconds = parseInt(meta.subscriptionExpiration, 10);
2488
+ if (seconds > 0) {
2489
+ let days = Math.round(seconds / 86400);
2490
+ app.expirationLabel = util.format(gt.ngettext('%d day', '%d days', days), days);
2491
+ } else {
2492
+ app.expirationLabel = gt.gettext('Indefinite');
2493
+ }
2494
+ }
2495
+
2496
+ let nextPage = false;
2497
+ let prevPage = false;
2498
+
2499
+ let getPagingUrl = page => {
2500
+ let url = new URL(`admin/config/oauth/subscriptions`, 'http://localhost');
2501
+
2502
+ if (page) {
2503
+ url.searchParams.append('page', page);
2504
+ }
2505
+
2506
+ if (request.query.pageSize !== DEFAULT_PAGE_SIZE) {
2507
+ url.searchParams.append('pageSize', request.query.pageSize);
2508
+ }
2509
+
2510
+ return url.pathname + url.search;
2511
+ };
2512
+
2513
+ if (data.pages > data.page + 1) {
2514
+ nextPage = getPagingUrl(data.page + 2);
2515
+ }
2516
+
2517
+ if (data.page > 0) {
2518
+ prevPage = getPagingUrl(data.page);
2519
+ }
2520
+
2521
+ return h.view(
2522
+ 'config/oauth/subscriptions',
2523
+ {
2524
+ pageTitle: 'OAuth2',
2525
+ menuConfig: true,
2526
+ menuConfigOauth: true,
2527
+ activeSubscriptions: true,
2528
+
2529
+ showPaging: data.pages > 1,
2530
+ nextPage,
2531
+ prevPage,
2532
+ firstPage: data.page === 0,
2533
+ pageLinks: new Array(data.pages || 1).fill(0).map((z, i) => ({
2534
+ url: getPagingUrl(i + 1),
2535
+ title: i + 1,
2536
+ active: i === data.page
2537
+ })),
2538
+
2539
+ apps: data.apps,
2540
+
2541
+ values: {
2542
+ gmailSubscriptionTtl: typeof gmailSubscriptionTtl === 'number' ? gmailSubscriptionTtl : ''
2543
+ }
2544
+ },
2545
+ {
2546
+ layout: 'app'
2547
+ }
2548
+ );
2549
+ } catch (err) {
2550
+ request.logger.error({ msg: 'Failed to load subscriptions page', err });
2551
+ throwAsBoom(err);
2552
+ }
2553
+ },
2554
+
2555
+ options: {
2556
+ validate: {
2557
+ options: {
2558
+ stripUnknown: true,
2559
+ abortEarly: false,
2560
+ convert: true
2561
+ },
2562
+
2563
+ async failAction(request, h /*, err*/) {
2564
+ return h.redirect('/admin/config/oauth/subscriptions').takeover();
2565
+ },
2566
+
2567
+ query: Joi.object({
2568
+ page: Joi.number().integer().min(1).max(1000000).default(1),
2569
+ pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE)
2570
+ })
2571
+ }
2572
+ }
2573
+ });
2574
+
2575
+ server.route({
2576
+ method: 'POST',
2577
+ path: '/admin/config/oauth/subscriptions',
2578
+ async handler(request, h) {
2579
+ try {
2580
+ // Joi .empty('').allow(null) ensures this is either null or a number
2581
+ let ttl = request.payload.gmailSubscriptionTtl != null ? request.payload.gmailSubscriptionTtl : null;
2582
+ await settings.set('gmailSubscriptionTtl', ttl);
2583
+
2584
+ await request.flash({ type: 'info', message: 'Configuration updated' });
2585
+ return h.redirect('/admin/config/oauth/subscriptions');
2586
+ } catch (err) {
2587
+ await request.flash({ type: 'danger', message: 'Failed to save settings' });
2588
+ request.logger.error({ msg: 'Failed to save subscription settings', err });
2589
+ return h.redirect('/admin/config/oauth/subscriptions');
2590
+ }
2591
+ },
2592
+ options: {
2593
+ validate: {
2594
+ options: {
2595
+ stripUnknown: true,
2596
+ abortEarly: false,
2597
+ convert: true
2598
+ },
2599
+
2600
+ async failAction(request, h /*, err*/) {
2601
+ await request.flash({ type: 'danger', message: 'Invalid setting value' });
2602
+ return h.redirect('/admin/config/oauth/subscriptions').takeover();
2603
+ },
2604
+
2605
+ payload: Joi.object({
2606
+ gmailSubscriptionTtl: Joi.number().integer().empty('').allow(null).min(0).max(365),
2607
+ crumb: Joi.string().max(256)
2608
+ })
2609
+ }
2610
+ }
2611
+ });
2612
+
2617
2613
  server.route({
2618
2614
  method: 'GET',
2619
2615
  path: '/admin/config/oauth/app/{app}',
@@ -2724,6 +2720,12 @@ return true;`
2724
2720
  try {
2725
2721
  await oauth2Apps.del(request.payload.app);
2726
2722
 
2723
+ try {
2724
+ await call({ cmd: 'googlePubSubRemove', app: request.payload.app });
2725
+ } catch (err) {
2726
+ request.logger.error({ msg: 'Failed to notify workers about OAuth2 app deletion', err, app: request.payload.app });
2727
+ }
2728
+
2727
2729
  await request.flash({ type: 'info', message: `OAuth2 app deleted` });
2728
2730
 
2729
2731
  return h.redirect('/admin/config/oauth');
@@ -2743,7 +2745,7 @@ return true;`
2743
2745
 
2744
2746
  async failAction(request, h, err) {
2745
2747
  await request.flash({ type: 'danger', message: `Couldn't delete OAuth2 app. Try again.` });
2746
- request.logger.error({ msg: 'Failed to delete delete the OAuth2 application', err });
2748
+ request.logger.error({ msg: 'Failed to delete the OAuth2 application', err });
2747
2749
 
2748
2750
  return h.redirect('/admin/config/oauth').takeover();
2749
2751
  },
@@ -2768,8 +2770,6 @@ return true;`
2768
2770
  defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
2769
2771
  }
2770
2772
 
2771
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
2772
-
2773
2773
  return h.view(
2774
2774
  'config/oauth/new',
2775
2775
  {
@@ -2787,7 +2787,7 @@ return true;`
2787
2787
  baseScopesApi: false,
2788
2788
  baseScopesPubsub: false,
2789
2789
 
2790
- pubSubApps: pubSubApps && pubSubApps.apps,
2790
+ pubSubApps: await getPubSubAppsForSelect(null),
2791
2791
 
2792
2792
  azureClouds: structuredClone(AZURE_CLOUDS).map(entry => {
2793
2793
  if (entry.id === 'global') {
@@ -2858,7 +2858,7 @@ return true;`
2858
2858
  throw new Error('Unexpected result');
2859
2859
  }
2860
2860
 
2861
- if (oauth2App && oauth2App.pubsubUpdates && oauth2App.pubsubUpdates.pubSubSubscription) {
2861
+ if (oauth2App && oauth2App.pubsubUpdates && Object.keys(oauth2App.pubsubUpdates).length > 0) {
2862
2862
  await call({ cmd: 'googlePubSub', app: oauth2App.id });
2863
2863
  }
2864
2864
 
@@ -2881,8 +2881,6 @@ return true;`
2881
2881
  defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
2882
2882
  }
2883
2883
 
2884
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
2885
-
2886
2884
  return h.view(
2887
2885
  'config/oauth/new',
2888
2886
  {
@@ -2896,15 +2894,7 @@ return true;`
2896
2894
  providerData,
2897
2895
  defaultRedirectUrl,
2898
2896
 
2899
- pubSubApps:
2900
- pubSubApps &&
2901
- pubSubApps.apps &&
2902
- pubSubApps.apps.map(app => {
2903
- if (app.id === request.payload.pubSubApp) {
2904
- app.selected = true;
2905
- }
2906
- return app;
2907
- }),
2897
+ pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
2908
2898
 
2909
2899
  baseScopesApi: baseScopes === 'api',
2910
2900
  baseScopesImap: baseScopes === 'imap' || !baseScopes,
@@ -2961,8 +2951,6 @@ return true;`
2961
2951
  defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
2962
2952
  }
2963
2953
 
2964
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
2965
-
2966
2954
  return h
2967
2955
  .view(
2968
2956
  'config/oauth/new',
@@ -2977,15 +2965,7 @@ return true;`
2977
2965
  providerData,
2978
2966
  defaultRedirectUrl,
2979
2967
 
2980
- pubSubApps:
2981
- pubSubApps &&
2982
- pubSubApps.apps &&
2983
- pubSubApps.apps.map(app => {
2984
- if (app.id === request.payload.pubSubApp) {
2985
- app.selected = true;
2986
- }
2987
- return app;
2988
- }),
2968
+ pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
2989
2969
 
2990
2970
  baseScopesApi: baseScopes === 'api',
2991
2971
  baseScopesImap: baseScopes === 'imap' || !baseScopes,
@@ -3041,8 +3021,6 @@ return true;`
3041
3021
  tenant: appData.authority && !['common', 'organizations', 'consumers'].includes(appData.authority) ? appData.authority : ''
3042
3022
  });
3043
3023
 
3044
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
3045
-
3046
3024
  return h.view(
3047
3025
  'config/oauth/edit',
3048
3026
  {
@@ -3059,15 +3037,7 @@ return true;`
3059
3037
  hasClientSecret: !!appData.clientSecret,
3060
3038
  hasServiceKey: !!appData.serviceKey,
3061
3039
 
3062
- pubSubApps:
3063
- pubSubApps &&
3064
- pubSubApps.apps &&
3065
- pubSubApps.apps.map(app => {
3066
- if (app.id === values.pubSubApp) {
3067
- app.selected = true;
3068
- }
3069
- return app;
3070
- }),
3040
+ pubSubApps: await getPubSubAppsForSelect(values.pubSubApp),
3071
3041
 
3072
3042
  values,
3073
3043
 
@@ -3142,7 +3112,7 @@ return true;`
3142
3112
  throw new Error('Unexpected result');
3143
3113
  }
3144
3114
 
3145
- if (oauth2App && oauth2App.pubsubUpdates && oauth2App.pubsubUpdates.pubSubSubscription) {
3115
+ if (oauth2App && oauth2App.pubsubUpdates && Object.keys(oauth2App.pubsubUpdates).length > 0) {
3146
3116
  await call({ cmd: 'googlePubSub', app: oauth2App.id });
3147
3117
  }
3148
3118
 
@@ -3160,8 +3130,6 @@ return true;`
3160
3130
  defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
3161
3131
  }
3162
3132
 
3163
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
3164
-
3165
3133
  return h.view(
3166
3134
  'config/oauth/edit',
3167
3135
  {
@@ -3177,15 +3145,7 @@ return true;`
3177
3145
  hasClientSecret: !!appData.clientSecret,
3178
3146
  hasServiceKey: !!appData.serviceKey,
3179
3147
 
3180
- pubSubApps:
3181
- pubSubApps &&
3182
- pubSubApps.apps &&
3183
- pubSubApps.apps.map(app => {
3184
- if (app.id === request.payload.pubSubApp) {
3185
- app.selected = true;
3186
- }
3187
- return app;
3188
- }),
3148
+ pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
3189
3149
 
3190
3150
  baseScopesApi: request.payload.baseScopes === 'api',
3191
3151
  baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
@@ -3249,8 +3209,6 @@ return true;`
3249
3209
  defaultRedirectUrl = defaultRedirectUrl.replace(/^http:\/\/127\.0\.0\.1\b/i, 'http://localhost');
3250
3210
  }
3251
3211
 
3252
- let pubSubApps = await oauth2Apps.list(0, 1000, { pubsub: true });
3253
-
3254
3212
  return h
3255
3213
  .view(
3256
3214
  'config/oauth/edit',
@@ -3268,15 +3226,7 @@ return true;`
3268
3226
  hasClientSecret: !!appData.clientSecret,
3269
3227
  hasServiceKey: !!appData.serviceKey,
3270
3228
 
3271
- pubSubApps:
3272
- pubSubApps &&
3273
- pubSubApps.apps &&
3274
- pubSubApps.apps.map(app => {
3275
- if (app.id === request.payload.pubSubApp) {
3276
- app.selected = true;
3277
- }
3278
- return app;
3279
- }),
3229
+ pubSubApps: await getPubSubAppsForSelect(request.payload.pubSubApp),
3280
3230
 
3281
3231
  baseScopesApi: request.payload.baseScopes === 'api',
3282
3232
  baseScopesImap: request.payload.baseScopes === 'imap' || !request.payload.baseScopes,
@@ -4666,6 +4616,11 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
4666
4616
  requestPayload.email = accountData.email;
4667
4617
  }
4668
4618
 
4619
+ // Service providers use client_credentials - no interactive authorization
4620
+ if (SERVICE_ACCOUNT_PROVIDERS.has(oAuth2Client.provider)) {
4621
+ throw Boom.badRequest('Application-only OAuth providers do not support interactive authorization');
4622
+ }
4623
+
4669
4624
  let authorizeUrl = oAuth2Client.generateAuthUrl(requestPayload);
4670
4625
 
4671
4626
  return h.redirect(authorizeUrl);
@@ -5473,7 +5428,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
5473
5428
 
5474
5429
  async failAction(request, h, err) {
5475
5430
  await request.flash({ type: 'danger', message: `Couldn't delete account. Try again.` });
5476
- request.logger.error({ msg: 'Failed to delete delete the account', err });
5431
+ request.logger.error({ msg: 'Failed to delete the account', err });
5477
5432
 
5478
5433
  return h.redirect('/admin/accounts').takeover();
5479
5434
  },