diff --git a/.changeset/twelve-games-run.md b/.changeset/twelve-games-run.md new file mode 100644 index 00000000..846d2bbb --- /dev/null +++ b/.changeset/twelve-games-run.md @@ -0,0 +1,17 @@ +--- +"@comet/brevo-api": major +"@comet/brevo-admin": minor +--- + +Add a brevo configuration field for `allowedRedirectionUrl` +Env vars containing this information can be removed and must be removed from the brevo module configuration. + +```diff +BrevoModule.register({ + brevo: { +- allowedRedirectionUrl: config.brevo.allowedRedirectionUrl, + //... + }, + //.. +}) +``` diff --git a/demo/api/schema.gql b/demo/api/schema.gql index e2ae8814..81908efb 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -507,6 +507,7 @@ type BrevoConfig implements DocumentInterface { senderName: String! doubleOptInTemplateId: Int! folderId: Int! + allowedRedirectionUrl: String! createdAt: DateTime! scope: EmailCampaignContentScope! } @@ -1034,6 +1035,7 @@ input BrevoConfigInput { senderName: String! doubleOptInTemplateId: Int! folderId: Int! + allowedRedirectionUrl: String! } input BrevoConfigUpdateInput { @@ -1041,4 +1043,5 @@ input BrevoConfigUpdateInput { senderName: String doubleOptInTemplateId: Int folderId: Int + allowedRedirectionUrl: String } diff --git a/demo/api/src/app.module.ts b/demo/api/src/app.module.ts index 91d86107..adbef47e 100644 --- a/demo/api/src/app.module.ts +++ b/demo/api/src/app.module.ts @@ -148,13 +148,11 @@ export class AppModule { if (scope.domain === "main") { return { apiKey: config.brevo.apiKey, - allowedRedirectUrl: config.brevo.allowedRedirectUrl, redirectUrlForImport: config.brevo.redirectUrlForImport, }; } else { return { apiKey: config.brevo.apiKey, - allowedRedirectUrl: config.brevo.allowedRedirectUrl, redirectUrlForImport: config.brevo.redirectUrlForImport, }; } diff --git a/demo/api/src/config/config.ts b/demo/api/src/config/config.ts index 3de2d653..264b4093 100644 --- a/demo/api/src/config/config.ts +++ b/demo/api/src/config/config.ts @@ -57,7 +57,6 @@ export function createConfig(processEnv: NodeJS.ProcessEnv) { }, brevo: { apiKey: envVars.BREVO_API_KEY, - allowedRedirectUrl: envVars.BREVO_ALLOWED_REDIRECT_URL, redirectUrlForImport: envVars.REDIRECT_URL_FOR_IMPORT, }, campaign: { diff --git a/demo/api/src/config/environment-variables.ts b/demo/api/src/config/environment-variables.ts index 96182425..46e55232 100644 --- a/demo/api/src/config/environment-variables.ts +++ b/demo/api/src/config/environment-variables.ts @@ -115,9 +115,6 @@ export class EnvironmentVariables { @IsString() BREVO_API_KEY: string; - @IsString() - BREVO_ALLOWED_REDIRECT_URL: string; - @IsString() REDIRECT_URL_FOR_IMPORT: string; diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts b/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts index 81e7523c..80b28b0e 100644 --- a/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts +++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts @@ -6,6 +6,7 @@ export const brevoConfigFormFragment = gql` senderName doubleOptInTemplateId folderId + allowedRedirectionUrl } `; diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx index a719cacb..6804e50f 100644 --- a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx +++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx @@ -8,6 +8,7 @@ import { Loading, MainContent, NumberField, + TextField, Toolbar, ToolbarActions, ToolbarFillSpace, @@ -50,12 +51,23 @@ type FormValues = { sender: Option; doubleOptInTemplate: Option; folderId: number; + allowedRedirectionUrl: string; }; interface FormProps { scope: ContentScopeInterface; } +function validateUrl(value: string): React.ReactNode | undefined { + const urlPattern = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d{1,5})?(\/[^\s]*)?$/; + if (!urlPattern.test(value)) { + return ( + + ); + } + return undefined; +} + export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { const client = useApolloClient(); const formApiRef = useFormApiRef(); @@ -114,10 +126,13 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { label: `${doubleOptInTemplate?.id}: ${doubleOptInTemplate?.name}`, } : undefined, + allowedRedirectionUrl: data?.brevoConfig?.allowedRedirectionUrl ?? "", folderId: data?.brevoConfig?.folderId ?? 1, }; }, [ data?.brevoConfig?.folderId, + data?.brevoConfig?.allowedRedirectionUrl, + data?.brevoConfig?.doubleOptInTemplateId, data?.brevoConfig?.senderMail, data?.brevoConfig?.senderName, @@ -155,7 +170,7 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { const sender = sendersData?.senders?.find((s) => s.email === state.sender.value); - if (!sender || !state.doubleOptInTemplate) { + if (!sender || !state.doubleOptInTemplate || !state.allowedRedirectionUrl) { throw new Error("Not all required fields are set"); } @@ -164,6 +179,7 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { senderMail: sender?.email, doubleOptInTemplateId: Number(state.doubleOptInTemplate.value), folderId: state.folderId ?? 1, + allowedRedirectionUrl: state?.allowedRedirectionUrl ?? "", }; if (mode === "edit") { @@ -177,7 +193,10 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { } else { const { data: mutationResponse } = await client.mutate({ mutation: createBrevoConfigMutation, - variables: { scope, input: output }, + variables: { + scope, + input: output, + }, }); if (!event.navigatingBack) { const id = mutationResponse?.createBrevoConfig.id; @@ -222,7 +241,6 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { fullWidth required /> - option.label} @@ -260,6 +278,31 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { fullWidth required /> + + + + } + sx={{ marginLeft: "5px" }} + > + + + + } + validate={validateUrl} + /> ); diff --git a/packages/api/schema.gql b/packages/api/schema.gql index 18493b03..d4e98867 100644 --- a/packages/api/schema.gql +++ b/packages/api/schema.gql @@ -1,12 +1,12 @@ type User { - id: String! - name: String! - email: String! + id: String! + name: String! + email: String! } type CurrentUserPermission { - permission: String! - contentScopes: [JSONObject!]! + permission: String! + contentScopes: [JSONObject!]! } """ @@ -15,175 +15,153 @@ The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404]( scalar JSONObject @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") type Dependency { - rootId: String! - rootGraphqlObjectType: String! - rootColumnName: String! - jsonPath: String! - visible: Boolean! - targetGraphqlObjectType: String! - targetId: String! - name: String - secondaryInformation: String + rootId: String! + rootGraphqlObjectType: String! + rootColumnName: String! + jsonPath: String! + visible: Boolean! + targetGraphqlObjectType: String! + targetId: String! + name: String + secondaryInformation: String } type ImageCropArea { - focalPoint: FocalPoint! - width: Float - height: Float - x: Float - y: Float + focalPoint: FocalPoint! + width: Float + height: Float + x: Float + y: Float } enum FocalPoint { - SMART - CENTER - NORTHWEST - NORTHEAST - SOUTHWEST - SOUTHEAST + SMART + CENTER + NORTHWEST + NORTHEAST + SOUTHWEST + SOUTHEAST } type BrevoApiEmailTemplateSender { - id: String - subject: String - email: String! + id: String + subject: String + email: String! } type BrevoApiEmailTemplate { - id: ID! - name: String! - subject: String! - isActive: Boolean! - testSent: Boolean! - replyTo: String! - toField: String! - tag: String! - htmlContent: String! - createdAt: String! - modifiedAt: String! - sender: BrevoApiEmailTemplateSender! + id: ID! + name: String! + subject: String! + isActive: Boolean! + testSent: Boolean! + replyTo: String! + toField: String! + tag: String! + htmlContent: String! + createdAt: String! + modifiedAt: String! + sender: BrevoApiEmailTemplateSender! } type BrevoApiSender { - id: ID! - name: String! - email: String! - active: Boolean! - ips: [BrevoIp!] + id: ID! + name: String! + email: String! + active: Boolean! + ips: [BrevoIp!] } type BrevoIp { - ip: String! - domain: String! - weight: Int! + ip: String! + domain: String! + weight: Int! } type CsvImportInformation { - created: Int! - updated: Int! - failed: Int! - failedColumns: [[String!]!] - errorMessage: String + created: Int! + updated: Int! + failed: Int! + failedColumns: [[String!]!] + errorMessage: String } type BrevoApiCampaignStatistics { - """ - Number of unique clicks for the campaign - """ - uniqueClicks: Int! - - """ - Number of total clicks for the campaign - """ - clickers: Int! - - """ - Number of complaints (Spam reports) for the campaign - """ - complaints: Int! - - """ - Number of delivered emails for the campaign - """ - delivered: Int! - - """ - Number of sent emails for the campaign - """ - sent: Int! - - """ - Number of softbounce for the campaign - """ - softBounces: Int! - - """ - Number of hardbounces for the campaign - """ - hardBounces: Int! - - """ - Number of unique openings for the campaign - """ - uniqueViews: Int! - - """ - Number of unique openings for the campaign - """ - trackableViews: Int! - - """ - Rate of recipients without any privacy protection option enabled in their email client, applied to all delivered emails - """ - estimatedViews: Int! - - """ - Number of unsubscription for the campaign - """ - unsubscriptions: Int! - - """ - Number of openings for the campaign - """ - viewed: Int! + """Number of unique clicks for the campaign""" + uniqueClicks: Int! + + """Number of total clicks for the campaign""" + clickers: Int! + + """Number of complaints (Spam reports) for the campaign""" + complaints: Int! + + """Number of delivered emails for the campaign""" + delivered: Int! + + """Number of sent emails for the campaign""" + sent: Int! + + """Number of softbounce for the campaign""" + softBounces: Int! + + """Number of hardbounces for the campaign""" + hardBounces: Int! + + """Number of unique openings for the campaign""" + uniqueViews: Int! + + """Number of unique openings for the campaign""" + trackableViews: Int! + + """ + Rate of recipients without any privacy protection option enabled in their email client, applied to all delivered emails + """ + estimatedViews: Int! + + """Number of unsubscription for the campaign""" + unsubscriptions: Int! + + """Number of openings for the campaign""" + viewed: Int! } type EmailCampaignContentScope { - thisScopeHasNoFields____: String + thisScopeHasNoFields____: String } type BrevoContact { - id: Int! - createdAt: String! - modifiedAt: String! - email: String! - emailBlacklisted: Boolean! - smsBlacklisted: Boolean! - listIds: [Int!]! - listUnsubscribed: [Int!]! + id: Int! + createdAt: String! + modifiedAt: String! + email: String! + emailBlacklisted: Boolean! + smsBlacklisted: Boolean! + listIds: [Int!]! + listUnsubscribed: [Int!]! } type PaginatedBrevoContacts { - nodes: [BrevoContact!]! - totalCount: Int! + nodes: [BrevoContact!]! + totalCount: Int! } type TargetGroup implements DocumentInterface { - id: ID! - updatedAt: DateTime! - createdAt: DateTime! - title: String! - isMainList: Boolean! - isTestList: Boolean - brevoId: Int! - totalSubscribers: Int! - scope: EmailCampaignContentScope! - assignedContactsTargetGroupBrevoId: Int + id: ID! + updatedAt: DateTime! + createdAt: DateTime! + title: String! + isMainList: Boolean! + isTestList: Boolean! + brevoId: Int! + totalSubscribers: Int! + scope: EmailCampaignContentScope! + assignedContactsTargetGroupBrevoId: Int } interface DocumentInterface { - id: ID! - updatedAt: DateTime! + id: ID! + updatedAt: DateTime! } """ @@ -192,270 +170,243 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date scalar DateTime type PaginatedTargetGroups { - nodes: [TargetGroup!]! - totalCount: Int! + nodes: [TargetGroup!]! + totalCount: Int! } type EmailCampaign implements DocumentInterface { - id: ID! - updatedAt: DateTime! - createdAt: DateTime! - title: String! - subject: String! - brevoId: Int - scheduledAt: DateTime - targetGroups: [TargetGroup!]! - content: EmailCampaignContentBlockData! - scope: EmailCampaignContentScope! - sendingState: SendingState! -} - -""" -EmailCampaignContent root block data -""" + id: ID! + updatedAt: DateTime! + createdAt: DateTime! + title: String! + subject: String! + brevoId: Int + scheduledAt: DateTime + targetGroups: [TargetGroup!]! + content: EmailCampaignContentBlockData! + scope: EmailCampaignContentScope! + sendingState: SendingState! +} + +"""EmailCampaignContent root block data""" scalar EmailCampaignContentBlockData @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") enum SendingState { - DRAFT - SENT - SCHEDULED + DRAFT + SENT + SCHEDULED } type PaginatedEmailCampaigns { - nodes: [EmailCampaign!]! - totalCount: Int! + nodes: [EmailCampaign!]! + totalCount: Int! } type BrevoConfig implements DocumentInterface { - id: ID! - updatedAt: DateTime! - senderMail: String! - senderName: String! - doubleOptInTemplateId: Int! - folderId: Int! - createdAt: DateTime! - scope: EmailCampaignContentScope! + id: ID! + updatedAt: DateTime! + senderMail: String! + senderName: String! + doubleOptInTemplateId: Int! + folderId: Int! + allowedRedirectionUrl: String! + createdAt: DateTime! + scope: EmailCampaignContentScope! } input EmailCampaignContentScopeInput { - thisScopeHasNoFields____: String + thisScopeHasNoFields____: String } type Query { - brevoContact(id: Int!, scope: EmailCampaignContentScopeInput!): BrevoContact! - brevoContacts( - targetGroupId: ID - email: String - scope: EmailCampaignContentScopeInput! - offset: Int! = 0 - limit: Int! = 25 - ): PaginatedBrevoContacts! - brevoTestContacts( - targetGroupId: ID - email: String - scope: EmailCampaignContentScopeInput! - offset: Int! = 0 - limit: Int! = 25 - ): PaginatedBrevoContacts! - manuallyAssignedBrevoContacts(offset: Int! = 0, limit: Int! = 25, targetGroupId: ID!, email: String): PaginatedBrevoContacts! - targetGroup(id: ID!): TargetGroup! - targetGroups( - scope: EmailCampaignContentScopeInput! - search: String - filter: TargetGroupFilter - sort: [TargetGroupSort!] - offset: Int! = 0 - limit: Int! = 25 - ): PaginatedTargetGroups! - emailCampaign(id: ID!): EmailCampaign! - emailCampaigns( - scope: EmailCampaignContentScopeInput! - search: String - filter: EmailCampaignFilter - sort: [EmailCampaignSort!] - offset: Int! = 0 - limit: Int! = 25 - ): PaginatedEmailCampaigns! - emailCampaignStatistics(id: ID!): BrevoApiCampaignStatistics - senders(scope: EmailCampaignContentScopeInput!): [BrevoApiSender!] - doubleOptInTemplates: [BrevoApiEmailTemplate!] - brevoConfig(scope: EmailCampaignContentScopeInput!): BrevoConfig + brevoContact(id: Int!, scope: EmailCampaignContentScopeInput!): BrevoContact! + brevoContacts(targetGroupId: ID, email: String, scope: EmailCampaignContentScopeInput!, offset: Int! = 0, limit: Int! = 25): PaginatedBrevoContacts! + brevoTestContacts(targetGroupId: ID, email: String, scope: EmailCampaignContentScopeInput!, offset: Int! = 0, limit: Int! = 25): PaginatedBrevoContacts! + manuallyAssignedBrevoContacts(offset: Int! = 0, limit: Int! = 25, targetGroupId: ID!, email: String): PaginatedBrevoContacts! + targetGroup(id: ID!): TargetGroup! + targetGroups(scope: EmailCampaignContentScopeInput!, search: String, filter: TargetGroupFilter, sort: [TargetGroupSort!], offset: Int! = 0, limit: Int! = 25): PaginatedTargetGroups! + emailCampaign(id: ID!): EmailCampaign! + emailCampaigns(scope: EmailCampaignContentScopeInput!, search: String, filter: EmailCampaignFilter, sort: [EmailCampaignSort!], offset: Int! = 0, limit: Int! = 25): PaginatedEmailCampaigns! + emailCampaignStatistics(id: ID!): BrevoApiCampaignStatistics + senders(scope: EmailCampaignContentScopeInput!): [BrevoApiSender!] + doubleOptInTemplates: [BrevoApiEmailTemplate!] + brevoConfig(scope: EmailCampaignContentScopeInput!): BrevoConfig } input TargetGroupFilter { - createdAt: DateTimeFilter - updatedAt: DateTimeFilter - title: StringFilter - isTestList: BooleanFilter - and: [TargetGroupFilter!] - or: [TargetGroupFilter!] + createdAt: DateTimeFilter + updatedAt: DateTimeFilter + title: StringFilter + isTestList: BooleanFilter + and: [TargetGroupFilter!] + or: [TargetGroupFilter!] } input DateTimeFilter { - equal: DateTime - lowerThan: DateTime - greaterThan: DateTime - lowerThanEqual: DateTime - greaterThanEqual: DateTime - notEqual: DateTime + equal: DateTime + lowerThan: DateTime + greaterThan: DateTime + lowerThanEqual: DateTime + greaterThanEqual: DateTime + notEqual: DateTime } input StringFilter { - contains: String - startsWith: String - endsWith: String - equal: String - notEqual: String + contains: String + startsWith: String + endsWith: String + equal: String + notEqual: String } input BooleanFilter { - equal: Boolean + equal: Boolean } input TargetGroupSort { - field: TargetGroupSortField! - direction: SortDirection! = ASC + field: TargetGroupSortField! + direction: SortDirection! = ASC } enum TargetGroupSortField { - createdAt - updatedAt - title + createdAt + updatedAt + title } enum SortDirection { - ASC - DESC + ASC + DESC } input EmailCampaignFilter { - createdAt: DateTimeFilter - updatedAt: DateTimeFilter - title: StringFilter - subject: StringFilter - scheduledAt: DateTimeFilter - and: [EmailCampaignFilter!] - or: [EmailCampaignFilter!] + createdAt: DateTimeFilter + updatedAt: DateTimeFilter + title: StringFilter + subject: StringFilter + scheduledAt: DateTimeFilter + and: [EmailCampaignFilter!] + or: [EmailCampaignFilter!] } input EmailCampaignSort { - field: EmailCampaignSortField! - direction: SortDirection! = ASC + field: EmailCampaignSortField! + direction: SortDirection! = ASC } enum EmailCampaignSortField { - createdAt - updatedAt - title - subject - scheduledAt + createdAt + updatedAt + title + subject + scheduledAt } type Mutation { - updateBrevoContact(id: Int!, scope: EmailCampaignContentScopeInput!, input: BrevoContactUpdateInput!): BrevoContact! - createBrevoContact(scope: EmailCampaignContentScopeInput!, input: BrevoContactInput!): SubscribeResponse! - createBrevoTestContact(scope: EmailCampaignContentScopeInput!, input: BrevoTestContactInput!): SubscribeResponse! - deleteBrevoContact(id: Int!, scope: EmailCampaignContentScopeInput!): Boolean! - deleteBrevoTestContact(id: Int!, scope: EmailCampaignContentScopeInput!): Boolean! - subscribeBrevoContact(input: SubscribeInput!, scope: EmailCampaignContentScopeInput!): SubscribeResponse! - createTargetGroup(scope: EmailCampaignContentScopeInput!, input: TargetGroupInput!): TargetGroup! - addBrevoContactsToTargetGroup(id: ID!, input: AddBrevoContactsInput!): Boolean! - removeBrevoContactFromTargetGroup(id: ID!, input: RemoveBrevoContactInput!): Boolean! - updateTargetGroup(id: ID!, input: TargetGroupUpdateInput!, lastUpdatedAt: DateTime): TargetGroup! - deleteTargetGroup(id: ID!): Boolean! - createEmailCampaign(scope: EmailCampaignContentScopeInput!, input: EmailCampaignInput!): EmailCampaign! - updateEmailCampaign(id: ID!, input: EmailCampaignUpdateInput!, lastUpdatedAt: DateTime): EmailCampaign! - deleteEmailCampaign(id: ID!): Boolean! - sendEmailCampaignNow(id: ID!): Boolean! - sendEmailCampaignToTestEmails(id: ID!, data: SendTestEmailCampaignArgs!): Boolean! - startBrevoContactImport(fileId: ID!, targetGroupIds: [ID!], scope: EmailCampaignContentScopeInput!): CsvImportInformation! - createBrevoConfig(scope: EmailCampaignContentScopeInput!, input: BrevoConfigInput!): BrevoConfig! - updateBrevoConfig(id: ID!, input: BrevoConfigUpdateInput!, lastUpdatedAt: DateTime): BrevoConfig! + updateBrevoContact(id: Int!, scope: EmailCampaignContentScopeInput!, input: BrevoContactUpdateInput!): BrevoContact! + createBrevoContact(scope: EmailCampaignContentScopeInput!, input: BrevoContactInput!): SubscribeResponse! + createBrevoTestContact(scope: EmailCampaignContentScopeInput!, input: BrevoTestContactInput!): SubscribeResponse! + deleteBrevoContact(id: Int!, scope: EmailCampaignContentScopeInput!): Boolean! + deleteBrevoTestContact(id: Int!, scope: EmailCampaignContentScopeInput!): Boolean! + subscribeBrevoContact(input: SubscribeInput!, scope: EmailCampaignContentScopeInput!): SubscribeResponse! + createTargetGroup(scope: EmailCampaignContentScopeInput!, input: TargetGroupInput!): TargetGroup! + addBrevoContactsToTargetGroup(id: ID!, input: AddBrevoContactsInput!): Boolean! + removeBrevoContactFromTargetGroup(id: ID!, input: RemoveBrevoContactInput!): Boolean! + updateTargetGroup(id: ID!, input: TargetGroupUpdateInput!, lastUpdatedAt: DateTime): TargetGroup! + deleteTargetGroup(id: ID!): Boolean! + createEmailCampaign(scope: EmailCampaignContentScopeInput!, input: EmailCampaignInput!): EmailCampaign! + updateEmailCampaign(id: ID!, input: EmailCampaignUpdateInput!, lastUpdatedAt: DateTime): EmailCampaign! + deleteEmailCampaign(id: ID!): Boolean! + sendEmailCampaignNow(id: ID!): Boolean! + sendEmailCampaignToTestEmails(id: ID!, data: SendTestEmailCampaignArgs!): Boolean! + startBrevoContactImport(fileId: ID!, targetGroupIds: [ID!], scope: EmailCampaignContentScopeInput!): CsvImportInformation! + createBrevoConfig(scope: EmailCampaignContentScopeInput!, input: BrevoConfigInput!): BrevoConfig! + updateBrevoConfig(id: ID!, input: BrevoConfigUpdateInput!, lastUpdatedAt: DateTime): BrevoConfig! } input BrevoContactUpdateInput { - blocked: Boolean! + blocked: Boolean! } enum SubscribeResponse { - SUCCESSFUL - ERROR_UNKNOWN - ERROR_CONTAINED_IN_ECG_RTR_LIST + SUCCESSFUL + ERROR_UNKNOWN + ERROR_CONTAINED_IN_ECG_RTR_LIST } input BrevoContactInput { - email: String! - blocked: Boolean! - redirectionUrl: String! + email: String! + blocked: Boolean! + redirectionUrl: String! } input BrevoTestContactInput { - email: String! - blocked: Boolean! + email: String! + blocked: Boolean! } input SubscribeInput { - email: String! - redirectionUrl: String! + email: String! + redirectionUrl: String! } input TargetGroupInput { - title: String! - filters: BrevoContactFilterAttributesInput + title: String! + filters: BrevoContactFilterAttributesInput } input BrevoContactFilterAttributesInput { - thisFilterHasNoFields____: [String!] + thisFilterHasNoFields____: [String!] } input AddBrevoContactsInput { - brevoContactIds: [Int!]! + brevoContactIds: [Int!]! } input RemoveBrevoContactInput { - brevoContactId: Int! + brevoContactId: Int! } input TargetGroupUpdateInput { - title: String - filters: BrevoContactFilterAttributesInput + title: String + filters: BrevoContactFilterAttributesInput } input EmailCampaignInput { - title: String! - subject: String! - scheduledAt: DateTime - targetGroups: [ID!]! - content: EmailCampaignContentBlockInput! + title: String! + subject: String! + scheduledAt: DateTime + targetGroups: [ID!]! + content: EmailCampaignContentBlockInput! } -""" -EmailCampaignContent root block input -""" +"""EmailCampaignContent root block input""" scalar EmailCampaignContentBlockInput @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") input EmailCampaignUpdateInput { - title: String - subject: String - scheduledAt: DateTime - targetGroups: [ID!] - content: EmailCampaignContentBlockInput + title: String + subject: String + scheduledAt: DateTime + targetGroups: [ID!] + content: EmailCampaignContentBlockInput } input SendTestEmailCampaignArgs { - emails: [String!]! + emails: [String!]! } input BrevoConfigInput { - senderMail: String! - senderName: String! - doubleOptInTemplateId: Int! - folderId: Int! + senderMail: String! + senderName: String! + doubleOptInTemplateId: Int! + folderId: Int! + allowedRedirectionUrl: String! } input BrevoConfigUpdateInput { - senderMail: String - senderName: String - doubleOptInTemplateId: Int - folderId: Int + senderMail: String + senderName: String + doubleOptInTemplateId: Int + folderId: Int + allowedRedirectionUrl: String } diff --git a/packages/api/src/brevo-config/dto/brevo-config.input.ts b/packages/api/src/brevo-config/dto/brevo-config.input.ts index e3ed1657..955dab4f 100644 --- a/packages/api/src/brevo-config/dto/brevo-config.input.ts +++ b/packages/api/src/brevo-config/dto/brevo-config.input.ts @@ -1,6 +1,6 @@ import { PartialType } from "@comet/cms-api"; import { Field, InputType, Int } from "@nestjs/graphql"; -import { IsEmail, IsInt, IsNotEmpty, IsString } from "class-validator"; +import { IsEmail, IsInt, IsNotEmpty, IsString, IsUrl } from "class-validator"; @InputType() export class BrevoConfigInput { @@ -22,6 +22,11 @@ export class BrevoConfigInput { @Field(() => Int) @IsInt() folderId: number; + + @IsNotEmpty() + @Field() + @IsUrl({ require_tld: process.env.NODE_ENV === "production" }) + allowedRedirectionUrl: string; } @InputType() diff --git a/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts b/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts index 12d59036..c295388d 100644 --- a/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts +++ b/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts @@ -13,6 +13,7 @@ export interface BrevoConfigInterface { senderMail: string; doubleOptInTemplateId: number; folderId: number; + allowedRedirectionUrl: string; createdAt: Date; updatedAt: Date; scope: EmailCampaignScopeInterface; @@ -47,6 +48,10 @@ export class BrevoConfigEntityFactory { @Field(() => Int) folderId: number; + @Property({ columnType: "text" }) + @Field() + allowedRedirectionUrl: string; + @Property({ columnType: "timestamp with time zone", }) diff --git a/packages/api/src/brevo-contact/brevo-contact-import.console.ts b/packages/api/src/brevo-contact/brevo-contact-import.console.ts index 3b3829b5..74cab0db 100644 --- a/packages/api/src/brevo-contact/brevo-contact-import.console.ts +++ b/packages/api/src/brevo-contact/brevo-contact-import.console.ts @@ -5,6 +5,7 @@ import { isUUID, validateSync } from "class-validator"; import { InvalidOptionArgumentError } from "commander"; import * as fs from "fs"; import { Command, Console } from "nestjs-console"; +import { BrevoConfigInterface } from "src/brevo-config/entities/brevo-config-entity.factory"; import { BrevoContactImportService } from "../brevo-contact/brevo-contact-import.service"; import { BrevoModuleConfig } from "../config/brevo-module.config"; @@ -29,6 +30,7 @@ export function createBrevoContactImportConsole({ Scope }: { Scope: Type, + @InjectRepository("BrevoConfig") private readonly brevoConfigRepository: EntityRepository, ) {} @Command({ @@ -99,13 +101,13 @@ export function createBrevoContactImportConsole({ Scope }: { Scope: Type): Promise { - const configForScope = this.config.brevo.resolveConfig(scope); + const configForScope = await this.brevoConfigRepository.findOneOrFail({ scope }); if (!configForScope) { throw Error("Scope does not exist"); } - if (urlToValidate?.startsWith(configForScope.allowedRedirectUrl)) { + if (urlToValidate?.startsWith(configForScope.allowedRedirectionUrl)) { return true; } diff --git a/packages/api/src/brevo-contact/validator/redirect-url.validator.ts b/packages/api/src/brevo-contact/validator/redirect-url.validator.ts index 9acf9363..1f20cb92 100644 --- a/packages/api/src/brevo-contact/validator/redirect-url.validator.ts +++ b/packages/api/src/brevo-contact/validator/redirect-url.validator.ts @@ -1,5 +1,8 @@ +import { EntityRepository } from "@mikro-orm/core"; +import { InjectRepository } from "@mikro-orm/nestjs"; import { Inject, Injectable } from "@nestjs/common"; import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator"; +import { BrevoConfigInterface } from "src/brevo-config/entities/brevo-config-entity.factory"; import { EmailCampaignScopeInterface } from "src/types"; import { BrevoModuleConfig } from "../../config/brevo-module.config"; @@ -21,17 +24,20 @@ export const IsValidRedirectURL = (scope: EmailCampaignScopeInterface, validatio @ValidatorConstraint({ name: "IsValidRedirectURL", async: true }) @Injectable() export class IsValidRedirectURLConstraint implements ValidatorConstraintInterface { - constructor(@Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig) {} + constructor( + @Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig, + @InjectRepository("BrevoConfig") private readonly brevoConfigRepository: EntityRepository, + ) {} async validate(urlToValidate: string, args: ValidationArguments): Promise { const [scope] = args.constraints; - const configForScope = this.config.brevo.resolveConfig(scope); + const configForScope = await this.brevoConfigRepository.findOneOrFail({ scope }); if (!configForScope) { throw Error("Scope does not exist"); } - if (urlToValidate?.startsWith(configForScope.allowedRedirectUrl)) { + if (urlToValidate?.startsWith(configForScope.allowedRedirectionUrl)) { return true; } diff --git a/packages/api/src/config/brevo-module.config.ts b/packages/api/src/config/brevo-module.config.ts index d75144f3..8b1efc10 100644 --- a/packages/api/src/config/brevo-module.config.ts +++ b/packages/api/src/config/brevo-module.config.ts @@ -10,7 +10,6 @@ export interface BrevoModuleConfig { brevo: { resolveConfig: (scope: EmailCampaignScopeInterface) => { apiKey: string; - allowedRedirectUrl: string; redirectUrlForImport: string; }; BrevoContactAttributes?: Type; diff --git a/packages/api/src/mikro-orm/migrations/Migration20241022144400.ts b/packages/api/src/mikro-orm/migrations/Migration20241022144400.ts new file mode 100644 index 00000000..47bad202 --- /dev/null +++ b/packages/api/src/mikro-orm/migrations/Migration20241022144400.ts @@ -0,0 +1,7 @@ +import { Migration } from "@mikro-orm/migrations"; + +export class Migration20241022144400 extends Migration { + async up(): Promise { + this.addSql('alter table "BrevoConfig" add column "allowedRedirectionUrl" text;'); + } +} diff --git a/packages/api/src/mikro-orm/migrations/migrations.ts b/packages/api/src/mikro-orm/migrations/migrations.ts index 0527a326..ebb9a181 100644 --- a/packages/api/src/mikro-orm/migrations/migrations.ts +++ b/packages/api/src/mikro-orm/migrations/migrations.ts @@ -11,6 +11,7 @@ import { Migration20240819214939 } from "./Migration20240819214939"; import { Migration20240830112400 } from "./Migration20240830112400"; import { Migration20241016123307 } from "./Migration20241016123307"; import { Migration20241018110515 } from "./Migration20241018110515"; +import { Migration20241022144400 } from "./Migration20241022144400"; import { Migration20241119101706 } from "./Migration20241119101706"; export const migrationsList: MigrationObject[] = [ @@ -26,4 +27,5 @@ export const migrationsList: MigrationObject[] = [ { name: "Migration20241016123307", class: Migration20241016123307 }, { name: "Migration20241018110515", class: Migration20241018110515 }, { name: "Migration20241119101706", class: Migration20241119101706 }, + { name: "Migration20241022144400", class: Migration20241022144400 }, ];