Parcourir la source

Add OAuth 2.0 email configuration and backend support

Co-authored-by: yuki-takei <1638767+yuki-takei@users.noreply.github.com>
copilot-swe-agent[bot] il y a 3 mois
Parent
commit
7025d35c1a

+ 3 - 0
apps/app/src/interfaces/activity.ts

@@ -89,6 +89,7 @@ const ACTION_ADMIN_APP_SETTINGS_UPDATE = 'ADMIN_APP_SETTING_UPDATE';
 const ACTION_ADMIN_SITE_URL_UPDATE = 'ADMIN_SITE_URL_UPDATE';
 const ACTION_ADMIN_SITE_URL_UPDATE = 'ADMIN_SITE_URL_UPDATE';
 const ACTION_ADMIN_MAIL_SMTP_UPDATE = 'ADMIN_MAIL_SMTP_UPDATE';
 const ACTION_ADMIN_MAIL_SMTP_UPDATE = 'ADMIN_MAIL_SMTP_UPDATE';
 const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
 const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
+const ACTION_ADMIN_MAIL_OAUTH2_UPDATE = 'ADMIN_MAIL_OAUTH2_UPDATE';
 const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT';
 const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT';
 const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE =
 const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE =
   'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
   'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
@@ -281,6 +282,7 @@ export const SupportedAction = {
   ACTION_ADMIN_SITE_URL_UPDATE,
   ACTION_ADMIN_SITE_URL_UPDATE,
   ACTION_ADMIN_MAIL_SMTP_UPDATE,
   ACTION_ADMIN_MAIL_SMTP_UPDATE,
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_SES_UPDATE,
+  ACTION_ADMIN_MAIL_OAUTH2_UPDATE,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
   ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
   ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
@@ -472,6 +474,7 @@ export const LargeActionGroup = {
   ACTION_ADMIN_SITE_URL_UPDATE,
   ACTION_ADMIN_SITE_URL_UPDATE,
   ACTION_ADMIN_MAIL_SMTP_UPDATE,
   ACTION_ADMIN_MAIL_SMTP_UPDATE,
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_SES_UPDATE,
+  ACTION_ADMIN_MAIL_OAUTH2_UPDATE,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
   ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
   ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,

+ 107 - 1
apps/app/src/server/routes/apiv3/app-settings/index.ts

@@ -340,7 +340,7 @@ module.exports = (crowi) => {
         .trim()
         .trim()
         .if((value) => value !== '')
         .if((value) => value !== '')
         .isEmail(),
         .isEmail(),
-      body('transmissionMethod').isIn(['smtp', 'ses']),
+      body('transmissionMethod').isIn(['smtp', 'ses', 'oauth2']),
     ],
     ],
     smtpSetting: [
     smtpSetting: [
       body('smtpHost').trim(),
       body('smtpHost').trim(),
@@ -358,6 +358,15 @@ module.exports = (crowi) => {
         .matches(/^[\da-zA-Z]+$/),
         .matches(/^[\da-zA-Z]+$/),
       body('sesSecretAccessKey').trim(),
       body('sesSecretAccessKey').trim(),
     ],
     ],
+    oauth2Setting: [
+      body('oauth2ClientId').trim(),
+      body('oauth2ClientSecret').trim(),
+      body('oauth2RefreshToken').trim(),
+      body('oauth2User')
+        .trim()
+        .if((value) => value !== '')
+        .isEmail(),
+    ],
     pageBulkExportSettings: [
     pageBulkExportSettings: [
       body('isBulkExportPagesEnabled').isBoolean(),
       body('isBulkExportPagesEnabled').isBoolean(),
       body('bulkExportDownloadExpirationSeconds').isInt(),
       body('bulkExportDownloadExpirationSeconds').isInt(),
@@ -422,6 +431,10 @@ module.exports = (crowi) => {
         smtpPassword: configManager.getConfig('mail:smtpPassword'),
         smtpPassword: configManager.getConfig('mail:smtpPassword'),
         sesAccessKeyId: configManager.getConfig('mail:sesAccessKeyId'),
         sesAccessKeyId: configManager.getConfig('mail:sesAccessKeyId'),
         sesSecretAccessKey: configManager.getConfig('mail:sesSecretAccessKey'),
         sesSecretAccessKey: configManager.getConfig('mail:sesSecretAccessKey'),
+        oauth2ClientId: configManager.getConfig('mail:oauth2ClientId'),
+        oauth2ClientSecret: configManager.getConfig('mail:oauth2ClientSecret'),
+        oauth2RefreshToken: configManager.getConfig('mail:oauth2RefreshToken'),
+        oauth2User: configManager.getConfig('mail:oauth2User'),
 
 
         fileUploadType: configManager.getConfig('app:fileUploadType'),
         fileUploadType: configManager.getConfig('app:fileUploadType'),
         envFileUploadType: configManager.getConfig(
         envFileUploadType: configManager.getConfig(
@@ -759,6 +772,10 @@ module.exports = (crowi) => {
       smtpPassword: configManager.getConfig('mail:smtpPassword'),
       smtpPassword: configManager.getConfig('mail:smtpPassword'),
       sesAccessKeyId: configManager.getConfig('mail:sesAccessKeyId'),
       sesAccessKeyId: configManager.getConfig('mail:sesAccessKeyId'),
       sesSecretAccessKey: configManager.getConfig('mail:sesSecretAccessKey'),
       sesSecretAccessKey: configManager.getConfig('mail:sesSecretAccessKey'),
+      oauth2ClientId: configManager.getConfig('mail:oauth2ClientId'),
+      oauth2ClientSecret: configManager.getConfig('mail:oauth2ClientSecret'),
+      oauth2RefreshToken: configManager.getConfig('mail:oauth2RefreshToken'),
+      oauth2User: configManager.getConfig('mail:oauth2User'),
     };
     };
   };
   };
 
 
@@ -932,6 +949,95 @@ module.exports = (crowi) => {
     },
     },
   );
   );
 
 
+  /**
+   * @swagger
+   *
+   *    /app-settings/oauth2-setting:
+   *      put:
+   *        tags: [AppSettings]
+   *        security:
+   *          - cookieAuth: []
+   *        summary: /app-settings/oauth2-setting
+   *        description: Update OAuth 2.0 setting for email
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  fromAddress:
+   *                    type: string
+   *                    description: e-mail address used as from address
+   *                    example: 'info@growi.org'
+   *                  transmissionMethod:
+   *                    type: string
+   *                    description: transmission method
+   *                    example: 'oauth2'
+   *                  oauth2ClientId:
+   *                    type: string
+   *                    description: OAuth 2.0 Client ID
+   *                  oauth2ClientSecret:
+   *                    type: string
+   *                    description: OAuth 2.0 Client Secret
+   *                  oauth2RefreshToken:
+   *                    type: string
+   *                    description: OAuth 2.0 Refresh Token
+   *                  oauth2User:
+   *                    type: string
+   *                    description: Email address of the authorized account
+   *        responses:
+   *          200:
+   *            description: Succeeded to update OAuth 2.0 setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  type: object
+   *                  properties:
+   *                    mailSettingParams:
+   *                      type: object
+   */
+  router.put(
+    '/oauth2-setting',
+    accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
+    loginRequiredStrictly,
+    adminRequired,
+    addActivity,
+    validator.oauth2Setting,
+    apiV3FormValidator,
+    async (req, res) => {
+      const { mailService } = crowi;
+
+      const requestOAuth2SettingParams = {
+        'mail:from': req.body.fromAddress,
+        'mail:transmissionMethod': req.body.transmissionMethod,
+        'mail:oauth2ClientId': req.body.oauth2ClientId,
+        'mail:oauth2ClientSecret': req.body.oauth2ClientSecret,
+        'mail:oauth2RefreshToken': req.body.oauth2RefreshToken,
+        'mail:oauth2User': req.body.oauth2User,
+      };
+
+      let mailSettingParams: Awaited<ReturnType<typeof updateMailSettinConfig>>;
+      try {
+        mailSettingParams = await updateMailSettinConfig(
+          requestOAuth2SettingParams,
+        );
+      } catch (err) {
+        const msg = 'Error occurred in updating OAuth 2.0 setting';
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(msg, 'update-oauth2-setting-failed'));
+      }
+
+      await mailService.initialize();
+      mailService.publishUpdatedMessage();
+      const parameters = {
+        action: SupportedAction.ACTION_ADMIN_MAIL_OAUTH2_UPDATE,
+      };
+      activityEvent.emit('update', res.locals.activity._id, parameters);
+      return res.apiv3({ mailSettingParams });
+    },
+  );
+
   router.use('/file-upload-setting', require('./file-upload-setting')(crowi));
   router.use('/file-upload-setting', require('./file-upload-setting')(crowi));
 
 
   router.put(
   router.put(

+ 19 - 1
apps/app/src/server/service/config-manager/config-definition.ts

@@ -200,6 +200,10 @@ export const CONFIG_KEYS = [
   'mail:smtpPassword',
   'mail:smtpPassword',
   'mail:sesSecretAccessKey',
   'mail:sesSecretAccessKey',
   'mail:sesAccessKeyId',
   'mail:sesAccessKeyId',
+  'mail:oauth2ClientId',
+  'mail:oauth2ClientSecret',
+  'mail:oauth2RefreshToken',
+  'mail:oauth2User',
 
 
   // Customize Settings
   // Customize Settings
   'customize:isEmailPublishedForNewUser',
   'customize:isEmailPublishedForNewUser',
@@ -920,7 +924,7 @@ export const CONFIG_DEFINITIONS = {
   'mail:from': defineConfig<string | undefined>({
   'mail:from': defineConfig<string | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
-  'mail:transmissionMethod': defineConfig<'smtp' | 'ses' | undefined>({
+  'mail:transmissionMethod': defineConfig<'smtp' | 'ses' | 'oauth2' | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
   'mail:smtpHost': defineConfig<string | undefined>({
   'mail:smtpHost': defineConfig<string | undefined>({
@@ -941,6 +945,20 @@ export const CONFIG_DEFINITIONS = {
   'mail:sesSecretAccessKey': defineConfig<string | undefined>({
   'mail:sesSecretAccessKey': defineConfig<string | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
+  'mail:oauth2ClientId': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'mail:oauth2ClientSecret': defineConfig<string | undefined>({
+    defaultValue: undefined,
+    isSecret: true,
+  }),
+  'mail:oauth2RefreshToken': defineConfig<string | undefined>({
+    defaultValue: undefined,
+    isSecret: true,
+  }),
+  'mail:oauth2User': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
 
 
   // Customize Settings
   // Customize Settings
   'customize:isEmailPublishedForNewUser': defineConfig<boolean>({
   'customize:isEmailPublishedForNewUser': defineConfig<boolean>({

+ 35 - 0
apps/app/src/server/service/mail.ts

@@ -107,6 +107,8 @@ class MailService implements S2sMessageHandlable {
       this.mailer = this.createSMTPClient();
       this.mailer = this.createSMTPClient();
     } else if (transmissionMethod === 'ses') {
     } else if (transmissionMethod === 'ses') {
       this.mailer = this.createSESClient();
       this.mailer = this.createSESClient();
+    } else if (transmissionMethod === 'oauth2') {
+      this.mailer = this.createOAuth2Client();
     } else {
     } else {
       this.mailer = null;
       this.mailer = null;
     }
     }
@@ -183,6 +185,39 @@ class MailService implements S2sMessageHandlable {
     return client;
     return client;
   }
   }
 
 
+  createOAuth2Client(option?) {
+    const { configManager } = this;
+
+    if (!option) {
+      const clientId = configManager.getConfig('mail:oauth2ClientId');
+      const clientSecret = configManager.getConfig('mail:oauth2ClientSecret');
+      const refreshToken = configManager.getConfig('mail:oauth2RefreshToken');
+      const user = configManager.getConfig('mail:oauth2User');
+
+      if (clientId == null || clientSecret == null || refreshToken == null || user == null) {
+        return null;
+      }
+
+      option = {
+        // eslint-disable-line no-param-reassign
+        service: 'gmail',
+        auth: {
+          type: 'OAuth2',
+          user,
+          clientId,
+          clientSecret,
+          refreshToken,
+        },
+      };
+    }
+
+    const client = nodemailer.createTransport(option);
+
+    logger.debug('mailer set up for OAuth2', client);
+
+    return client;
+  }
+
   setupMailConfig(overrideConfig) {
   setupMailConfig(overrideConfig) {
     const c = overrideConfig;
     const c = overrideConfig;