Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config for unsubscribe page (Depends on: https://github.com/vivid-planet/comet-brevo-module/pull/114) #117

Open
wants to merge 18 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/twelve-games-run.md
Original file line number Diff line number Diff line change
@@ -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,
//...
},
//..
})
```
7 changes: 7 additions & 0 deletions demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ type EmailCampaign implements DocumentInterface {
subject: String!
brevoId: Int
scheduledAt: DateTime
unsubscriptionPageId: String
targetGroups: [TargetGroup!]!
content: EmailCampaignContentBlockData!
scope: EmailCampaignContentScope!
Expand Down Expand Up @@ -507,6 +508,8 @@ type BrevoConfig implements DocumentInterface {
senderName: String!
doubleOptInTemplateId: Int!
folderId: Int!
allowedRedirectionUrl: String!
unsubscriptionPageId: String!
createdAt: DateTime!
scope: EmailCampaignContentScope!
}
Expand Down Expand Up @@ -1034,11 +1037,15 @@ input BrevoConfigInput {
senderName: String!
doubleOptInTemplateId: Int!
folderId: Int!
allowedRedirectionUrl: String!
unsubscriptionPageId: String!
}

input BrevoConfigUpdateInput {
senderMail: String
senderName: String
doubleOptInTemplateId: Int
folderId: Int
allowedRedirectionUrl: String
unsubscriptionPageId: String
}
2 changes: 0 additions & 2 deletions demo/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Expand Down
1 change: 0 additions & 1 deletion demo/api/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
3 changes: 0 additions & 3 deletions demo/api/src/config/environment-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,6 @@ export class EnvironmentVariables {
@IsString()
BREVO_API_KEY: string;

@IsString()
BREVO_ALLOWED_REDIRECT_URL: string;

@IsString()
REDIRECT_URL_FOR_IMPORT: string;

Expand Down
2 changes: 2 additions & 0 deletions packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const brevoConfigFormFragment = gql`
senderName
doubleOptInTemplateId
folderId
allowedRedirectionUrl
unsubscriptionPageId
}
`;

Expand Down
78 changes: 75 additions & 3 deletions packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Loading,
MainContent,
NumberField,
TextField,
Toolbar,
ToolbarActions,
ToolbarFillSpace,
Expand Down Expand Up @@ -50,12 +51,24 @@ type FormValues = {
sender: Option;
doubleOptInTemplate: Option;
folderId: number;
allowedRedirectionUrl: string;
unsubscriptionPageId: 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 (
<FormattedMessage id="cometBrevoModule.brevoConfig.allowedRedirectionUrl.validationError" defaultMessage="Please enter a valid URL." />
);
}
return undefined;
}

export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
const client = useApolloClient();
const formApiRef = useFormApiRef<FormValues>();
Expand Down Expand Up @@ -114,14 +127,19 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
label: `${doubleOptInTemplate?.id}: ${doubleOptInTemplate?.name}`,
}
: undefined,
allowedRedirectionUrl: data?.brevoConfig?.allowedRedirectionUrl ?? "",
folderId: data?.brevoConfig?.folderId ?? 1,
unsubscriptionPageId: data?.brevoConfig?.unsubscriptionPageId ?? "",
};
}, [
data?.brevoConfig?.folderId,
data?.brevoConfig?.allowedRedirectionUrl,

data?.brevoConfig?.doubleOptInTemplateId,
data?.brevoConfig?.senderMail,
data?.brevoConfig?.senderName,
doubleOptInTemplatesData?.doubleOptInTemplates,
data?.brevoConfig?.unsubscriptionPageId,
sendersData?.senders,
]);

Expand Down Expand Up @@ -155,7 +173,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 || !state.unsubscriptionPageId) {
throw new Error("Not all required fields are set");
}

Expand All @@ -164,6 +182,8 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
senderMail: sender?.email,
doubleOptInTemplateId: Number(state.doubleOptInTemplate.value),
folderId: state.folderId ?? 1,
allowedRedirectionUrl: state?.allowedRedirectionUrl ?? "",
unsubscriptionPageId: state.unsubscriptionPageId,
};

if (mode === "edit") {
Expand All @@ -177,7 +197,10 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
} else {
const { data: mutationResponse } = await client.mutate<GQLCreateBrevoConfigMutation, GQLCreateBrevoConfigMutationVariables>({
mutation: createBrevoConfigMutation,
variables: { scope, input: output },
variables: {
scope,
input: output,
},
});
if (!event.navigatingBack) {
const id = mutationResponse?.createBrevoConfig.id;
Expand All @@ -196,6 +219,19 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
return <Loading behavior="fillPageHeight" />;
}

const validateUnsubscriptionPageId = (value: string) => {
const validUnsubscriptionPageId = /^[a-zA-Z0-9]{24}$/;
if (!validUnsubscriptionPageId.test(value)) {
return (
<FormattedMessage
id="cometBrevoModule.brevoConfig.unsubscriptionPageId.validation"
defaultMessage="Must be a 24-digit alphanumeric ID"
/>
);
}
return undefined;
};

return (
<FinalForm<FormValues> apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}>
{({ values }) => {
Expand All @@ -222,7 +258,6 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
fullWidth
required
/>

<Field
component={FinalFormAutocomplete}
getOptionLabel={(option: Option) => option.label}
Expand Down Expand Up @@ -260,6 +295,43 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
fullWidth
required
/>
<TextField
required
fullWidth
name="allowedRedirectionUrl"
label={
<>
<FormattedMessage
id="cometBrevoModule.brevoConfig.allowedRedirectionUrl"
defaultMessage="Allowed redirection URL"
/>
<Tooltip
title={
<FormattedMessage
id="cometBrevoModule.brevoConfig.allowedRedirectionUrl.info"
defaultMessage="Defines the schema of a valid redirection URL that is set when creating or importing contacts."
/>
}
sx={{ marginLeft: "5px" }}
>
<Info />
</Tooltip>
</>
}
validate={validateUrl}
/>
<TextField
fullWidth
name="unsubscriptionPageId"
required
label={
<FormattedMessage
id="cometBrevoModule.brevoConfig.unsubscriptionPageId"
defaultMessage="Unsubscription Page ID"
/>
}
validate={validateUnsubscriptionPageId}
/>
</MainContent>
</>
);
Expand Down
9 changes: 8 additions & 1 deletion packages/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ type TargetGroup implements DocumentInterface {
createdAt: DateTime!
title: String!
isMainList: Boolean!
isTestList: Boolean
isTestList: Boolean!
brevoId: Int!
totalSubscribers: Int!
scope: EmailCampaignContentScope!
Expand Down Expand Up @@ -202,6 +202,7 @@ type EmailCampaign implements DocumentInterface {
createdAt: DateTime!
title: String!
subject: String!
unsubscriptionPageId: String
brevoId: Int
scheduledAt: DateTime
targetGroups: [TargetGroup!]!
Expand Down Expand Up @@ -233,6 +234,8 @@ type BrevoConfig implements DocumentInterface {
senderName: String!
doubleOptInTemplateId: Int!
folderId: Int!
allowedRedirectionUrl: String!
unsubscriptionPageId: String
createdAt: DateTime!
scope: EmailCampaignContentScope!
}
Expand Down Expand Up @@ -451,11 +454,15 @@ input BrevoConfigInput {
senderName: String!
doubleOptInTemplateId: Int!
folderId: Int!
allowedRedirectionUrl: String!
unsubscriptionPageId: String!
}

input BrevoConfigUpdateInput {
senderMail: String
senderName: String
doubleOptInTemplateId: Int
folderId: Int
allowedRedirectionUrl: String
unsubscriptionPageId: String
}
3 changes: 3 additions & 0 deletions packages/api/src/brevo-api/brevo-api-campaigns.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ export class BrevoApiCampaignsService {
htmlContent,
sender,
scheduledAt,
unsubscriptionPageId,
}: {
campaign: EmailCampaignInterface;
htmlContent: string;
sender: { name: string; mail: string };
scheduledAt?: Date;
unsubscriptionPageId?: string;
}): Promise<number> {
try {
const targetGroups = await campaign.targetGroups.loadItems();
Expand All @@ -75,6 +77,7 @@ export class BrevoApiCampaignsService {
recipients: { listIds: targetGroups.map((targetGroup) => targetGroup.brevoId) },
htmlContent,
scheduledAt: scheduledAt?.toISOString(),
unsubscriptionPageId,
};

const data = await this.getCampaignsApi(campaign.scope).createEmailCampaign(emailCampaign);
Expand Down
11 changes: 10 additions & 1 deletion packages/api/src/brevo-config/dto/brevo-config.input.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -22,6 +22,15 @@ export class BrevoConfigInput {
@Field(() => Int)
@IsInt()
folderId: number;

@IsNotEmpty()
@Field()
@IsUrl({ require_tld: process.env.NODE_ENV === "production" })
allowedRedirectionUrl: string;

@IsString()
@Field()
unsubscriptionPageId: string;
}

@InputType()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface BrevoConfigInterface {
senderMail: string;
doubleOptInTemplateId: number;
folderId: number;
allowedRedirectionUrl: string;
unsubscriptionPageId: string;
createdAt: Date;
updatedAt: Date;
scope: EmailCampaignScopeInterface;
Expand Down Expand Up @@ -47,6 +49,14 @@ export class BrevoConfigEntityFactory {
@Field(() => Int)
folderId: number;

@Property({ columnType: "text" })
@Field()
allowedRedirectionUrl: string;

@Property({ columnType: "text" })
@Field(() => String)
unsubscriptionPageId: string;

@Property({
columnType: "timestamp with time zone",
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -29,6 +30,7 @@ export function createBrevoContactImportConsole({ Scope }: { Scope: Type<EmailCa
@Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig,
private readonly brevoContactImportService: BrevoContactImportService,
@InjectRepository("TargetGroup") private readonly targetGroupRepository: EntityRepository<TargetGroupInterface>,
@InjectRepository("BrevoConfig") private readonly brevoConfigRepository: EntityRepository<BrevoConfigInterface>,
) {}

@Command({
Expand Down Expand Up @@ -99,13 +101,13 @@ export function createBrevoContactImportConsole({ Scope }: { Scope: Type<EmailCa
}

async validateRedirectUrl(urlToValidate: string, scope: Type<EmailCampaignScopeInterface>): Promise<boolean> {
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;
}

Expand Down
Loading
Loading