diff --git a/App/FeatureSet/Notification/API/SMTPConfig.ts b/App/FeatureSet/Notification/API/SMTPConfig.ts index a12fd8336f..763f6ab4e6 100644 --- a/App/FeatureSet/Notification/API/SMTPConfig.ts +++ b/App/FeatureSet/Notification/API/SMTPConfig.ts @@ -104,7 +104,9 @@ router.post( clientSecret: config.clientSecret, tokenUrl: config.tokenUrl ? URL.fromString(config.tokenUrl) : undefined, scope: config.scope, - oauthProviderType: config.oauthProviderType as OAuthProviderType | undefined, + oauthProviderType: config.oauthProviderType as + | OAuthProviderType + | undefined, }; try { diff --git a/App/FeatureSet/Notification/Services/MailService.ts b/App/FeatureSet/Notification/Services/MailService.ts index f952568e20..b596c45cbb 100755 --- a/App/FeatureSet/Notification/Services/MailService.ts +++ b/App/FeatureSet/Notification/Services/MailService.ts @@ -84,8 +84,10 @@ class TransporterPool { emailServer: EmailServer, options: { timeout?: number | undefined }, ): Promise { - // For OAuth, we need to create a new transporter each time to get fresh tokens - // The access token has a limited lifetime and needs to be refreshed + /* + * For OAuth, we need to create a new transporter each time to get fresh tokens + * The access token has a limited lifetime and needs to be refreshed + */ if (emailServer.authType === SMTPAuthenticationType.OAuth) { return await this.createOAuthTransporter(emailServer, options); } @@ -136,15 +138,18 @@ class TransporterPool { ); } - // Get the access token using the generic OAuth service - // Provider type determines which grant flow to use (Client Credentials vs JWT Bearer) + /* + * Get the access token using the generic OAuth service + * Provider type determines which grant flow to use (Client Credentials vs JWT Bearer) + */ const accessToken: string = await SMTPOAuthService.getAccessToken({ clientId: emailServer.clientId, clientSecret: emailServer.clientSecret, tokenUrl: emailServer.tokenUrl, scope: emailServer.scope, username: emailServer.username, // Required for JWT Bearer (user to impersonate) - providerType: emailServer.oauthProviderType || OAuthProviderType.ClientCredentials, + providerType: + emailServer.oauthProviderType || OAuthProviderType.ClientCredentials, }); logger.debug("Creating OAuth transporter for SMTP"); diff --git a/App/FeatureSet/Notification/Services/SMTPOAuthService.ts b/App/FeatureSet/Notification/Services/SMTPOAuthService.ts index 82f545a353..bfe1b2246c 100644 --- a/App/FeatureSet/Notification/Services/SMTPOAuthService.ts +++ b/App/FeatureSet/Notification/Services/SMTPOAuthService.ts @@ -80,11 +80,10 @@ export default class SMTPOAuthService { // Try to get cached token from Redis try { - const cachedToken: CachedToken | null = - (await GlobalCache.getJSONObject( - this.TOKEN_CACHE_NAMESPACE, - cacheKey, - )) as CachedToken | null; + const cachedToken: CachedToken | null = (await GlobalCache.getJSONObject( + this.TOKEN_CACHE_NAMESPACE, + cacheKey, + )) as CachedToken | null; if (cachedToken && cachedToken.expiresAt > Date.now()) { logger.debug("Using cached OAuth token from Redis"); diff --git a/Common/Models/DatabaseModels/ProjectSmtpConfig.ts b/Common/Models/DatabaseModels/ProjectSmtpConfig.ts index 2851c9cde0..d10cd96ee6 100755 --- a/Common/Models/DatabaseModels/ProjectSmtpConfig.ts +++ b/Common/Models/DatabaseModels/ProjectSmtpConfig.ts @@ -689,7 +689,8 @@ export default class ProjectSmtpConfig extends BaseModel { title: "OAuth Token URL", description: "The OAuth token endpoint URL. For Microsoft 365: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token. For Google: https://oauth2.googleapis.com/token", - example: "https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/token", + example: + "https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/token", }) @Column({ nullable: true, diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1767896933148-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1767896933148-MigrationName.ts index ea7875a969..5fbe6eda02 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1767896933148-MigrationName.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1767896933148-MigrationName.ts @@ -1,22 +1,41 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class MigrationName1767896933148 implements MigrationInterface { - name = 'MigrationName1767896933148' + name = "MigrationName1767896933148"; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" ADD "authType" character varying(100) NOT NULL DEFAULT 'Username and Password'`); - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" ADD "clientId" character varying(100)`); - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" ADD "clientSecret" character varying(500)`); - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" ADD "tokenUrl" text`); - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" ADD "scope" character varying(500)`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "scope"`); - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "tokenUrl"`); - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "clientSecret"`); - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "clientId"`); - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "authType"`); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" ADD "authType" character varying(100) NOT NULL DEFAULT 'Username and Password'`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" ADD "clientId" character varying(100)`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" ADD "clientSecret" character varying(500)`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" ADD "tokenUrl" text`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" ADD "scope" character varying(500)`, + ); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "scope"`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "tokenUrl"`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "clientSecret"`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "clientId"`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "authType"`, + ); + } } diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1768216593272-IncreaseClientSecretLength.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1768216593272-IncreaseClientSecretLength.ts index 295d26ce4e..732a93226e 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1768216593272-IncreaseClientSecretLength.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1768216593272-IncreaseClientSecretLength.ts @@ -1,17 +1,24 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class IncreaseClientSecretLength1768216593272 implements MigrationInterface { - name = 'IncreaseClientSecretLength1768216593272' +export class IncreaseClientSecretLength1768216593272 + implements MigrationInterface +{ + name = "IncreaseClientSecretLength1768216593272"; - public async up(queryRunner: QueryRunner): Promise { - // Change clientSecret from varchar(500) to text to support longer OAuth secrets - // (e.g., Google service account private keys which are ~1700+ characters) - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "clientSecret" TYPE text`); - } - - public async down(queryRunner: QueryRunner): Promise { - // Revert back to varchar(500) - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "clientSecret" TYPE character varying(500)`); - } + public async up(queryRunner: QueryRunner): Promise { + /* + * Change clientSecret from varchar(500) to text to support longer OAuth secrets + * (e.g., Google service account private keys which are ~1700+ characters) + */ + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "clientSecret" TYPE text`, + ); + } + public async down(queryRunner: QueryRunner): Promise { + // Revert back to varchar(500) + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "clientSecret" TYPE character varying(500)`, + ); + } } diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1768217403078-AddOAuthProviderType.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1768217403078-AddOAuthProviderType.ts index b8c83420cf..babcbed999 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1768217403078-AddOAuthProviderType.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1768217403078-AddOAuthProviderType.ts @@ -1,16 +1,21 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class AddOAuthProviderType1768217403078 implements MigrationInterface { - name = 'AddOAuthProviderType1768217403078' + name = "AddOAuthProviderType1768217403078"; - public async up(queryRunner: QueryRunner): Promise { - // Add oauthProviderType column to ProjectSMTPConfig table - // Values: 'Client Credentials' (for Microsoft 365, etc.) or 'JWT Bearer' (for Google Workspace) - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" ADD "oauthProviderType" character varying(100)`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "oauthProviderType"`); - } + public async up(queryRunner: QueryRunner): Promise { + /* + * Add oauthProviderType column to ProjectSMTPConfig table + * Values: 'Client Credentials' (for Microsoft 365, etc.) or 'JWT Bearer' (for Google Workspace) + */ + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" ADD "oauthProviderType" character varying(100)`, + ); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "oauthProviderType"`, + ); + } } diff --git a/Common/UI/Components/Icon/Icon.tsx b/Common/UI/Components/Icon/Icon.tsx index 9b74b59344..8d99051607 100644 --- a/Common/UI/Components/Icon/Icon.tsx +++ b/Common/UI/Components/Icon/Icon.tsx @@ -1355,11 +1355,7 @@ const Icon: FunctionComponent = ({ strokeLinejoin="round" d="M2 12h3l2-4 3 8 3-6 2 4h7" /> - + , ); } else if (icon === IconProp.Waterfall) { diff --git a/Dashboard/src/Components/CustomSMTP/CustomSMTPTable.tsx b/Dashboard/src/Components/CustomSMTP/CustomSMTPTable.tsx index e9fef251a7..78ed8d6a24 100644 --- a/Dashboard/src/Components/CustomSMTP/CustomSMTPTable.tsx +++ b/Dashboard/src/Components/CustomSMTP/CustomSMTPTable.tsx @@ -163,7 +163,8 @@ const CustomSMTPTable: FunctionComponent = (): ReactElement => { title: "Use SSL / TLS", stepId: "server-info", fieldType: FormFieldSchemaType.Toggle, - description: "Enable secure email communication. Recommended for most providers.", + description: + "Enable secure email communication. Recommended for most providers.", }, { field: { @@ -172,8 +173,9 @@ const CustomSMTPTable: FunctionComponent = (): ReactElement => { title: "Authentication Type", stepId: "authentication", fieldType: FormFieldSchemaType.Dropdown, - dropdownOptions: - DropdownUtil.getDropdownOptionsFromEnum(SMTPAuthenticationType), + dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum( + SMTPAuthenticationType, + ), required: true, defaultValue: SMTPAuthenticationType.UsernamePassword, description: @@ -206,8 +208,8 @@ const CustomSMTPTable: FunctionComponent = (): ReactElement => { disableSpellCheck: true, showIf: (values: FormValues): boolean => { return ( - values["authType"] === SMTPAuthenticationType.UsernamePassword || - !values["authType"] + values["authType"] === + SMTPAuthenticationType.UsernamePassword || !values["authType"] ); }, }, @@ -268,7 +270,8 @@ const CustomSMTPTable: FunctionComponent = (): ReactElement => { stepId: "oauth-info", fieldType: FormFieldSchemaType.URL, required: true, - placeholder: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token", + placeholder: + "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token", description: "The OAuth token endpoint URL. For Microsoft 365: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token. For Google: https://oauth2.googleapis.com/token", disableSpellCheck: true,