Skip to content

Commit

Permalink
Export BrevoContactsService to enable the application to add own ro…
Browse files Browse the repository at this point in the history
…utes for subscribing to the brevo-contacts (#55)

* add public api route to subscribe to brevo

* export brevo contacts service and implement demo controller for subscribing with REST

* remove prop for enablePublicApiSubscriptionRoute and update changelog

* Update .changeset/cool-walls-drop.md: put empty line between first and second sentence

Co-authored-by: Johannes Obermair <[email protected]>

* use IsValidRedirectUrl instead of Validate(IsValidRedirectUrlConstraint)

* add a comment and adapt changelog to indicate that the application should take care of recaptcha

---------

Co-authored-by: Johannes Obermair <[email protected]>
  • Loading branch information
raphaelblum and johnnyomair authored Jul 23, 2024
1 parent 85af17c commit 31f1241
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 19 deletions.
7 changes: 7 additions & 0 deletions .changeset/cool-walls-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@comet/brevo-api": minor
---

Export `BrevoContactsService` so that it can be used in the application

This allows, for example, adding a custom REST request in the application to subscribe to the newsletter. The application should then add reCAPTCHA before calling the BrevoContactsService to prevent problems with bots.
2 changes: 2 additions & 0 deletions demo/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { Request } from "express";
import { AccessControlService } from "./auth/access-control.service";
import { AuthModule } from "./auth/auth.module";
import { AuthLocalModule } from "./auth/auth-local.module";
import { BrevoContactSubscribeModule } from "./brevo-contact/brevo-contact-subscribe.module";
import { BrevoContactAttributes, BrevoContactFilterAttributes } from "./brevo-contact/dto/brevo-contact-attributes";
import { Config } from "./config/config";
import { ConfigModule } from "./config/config.module";
Expand Down Expand Up @@ -172,6 +173,7 @@ export class AppModule {
},
},
}),
BrevoContactSubscribeModule,
],
};
}
Expand Down
21 changes: 21 additions & 0 deletions demo/api/src/brevo-contact/brevo-contact-subscribe.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { BrevoContactsService, SubscribeResponse } from "@comet/brevo-api";
import { DisableGlobalGuard } from "@comet/cms-api";
import { Body, Controller, Post } from "@nestjs/common";

import { BrevoContactSubscribeInput } from "./dto/brevo-contact-subscribe.input";

@Controller("brevo-contacts")
export class BrevoContactSubscribeController {
constructor(private readonly brevoContactsService: BrevoContactsService) {}

@DisableGlobalGuard()
@Post(`/subscribe`)
async subscribe(@Body() data: BrevoContactSubscribeInput): Promise<SubscribeResponse> {
// Here, the application should add logic to handle reCAPTCHA verification
// This ensures that the request is coming from a human and not a bot

const { scope, ...input } = data;

return this.brevoContactsService.subscribeBrevoContact(input, data.scope);
}
}
8 changes: 8 additions & 0 deletions demo/api/src/brevo-contact/brevo-contact-subscribe.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";

import { BrevoContactSubscribeController } from "./brevo-contact-subscribe.controller";

@Module({
controllers: [BrevoContactSubscribeController],
})
export class BrevoContactSubscribeModule {}
25 changes: 25 additions & 0 deletions demo/api/src/brevo-contact/dto/brevo-contact-subscribe.input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IsValidRedirectURL } from "@comet/brevo-api";
import { Type } from "class-transformer";
import { IsEmail, IsNotEmpty, IsUrl, ValidateNested } from "class-validator";

import { BrevoContactAttributes } from "./brevo-contact-attributes";
import { EmailContactSubscribeScope } from "./brevo-contact-subscribe.scope";

export class BrevoContactSubscribeInput {
@IsEmail()
email: string;

@IsUrl({ require_tld: process.env.NODE_ENV === "production" })
@IsValidRedirectURL()
redirectionUrl: string;

@ValidateNested()
@Type(() => EmailContactSubscribeScope)
@IsNotEmpty()
scope: EmailContactSubscribeScope;

@ValidateNested()
@Type(() => BrevoContactAttributes)
@IsNotEmpty()
attributes: BrevoContactAttributes;
}
11 changes: 11 additions & 0 deletions demo/api/src/brevo-contact/dto/brevo-contact-subscribe.scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsString, MaxLength } from "class-validator";

export class EmailContactSubscribeScope {
@IsString()
@MaxLength(64)
domain: string;

@IsString()
@MaxLength(64)
language: string;
}
1 change: 1 addition & 0 deletions packages/api/src/brevo-contact/brevo-contact.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class BrevoContactModule {
module: BrevoContactModule,
imports: [BrevoApiModule, ConfigModule, MikroOrmModule.forFeature([TargetGroup])],
providers: [BrevoContactsService, BrevoContactResolver, EcgRtrListService, IsValidRedirectURLConstraint],
exports: [BrevoContactsService],
};
}
}
16 changes: 1 addition & 15 deletions packages/api/src/brevo-contact/brevo-contact.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,21 +200,7 @@ export function createBrevoContactResolver({
@Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope))
scope: typeof Scope,
): Promise<SubscribeResponse> {
if ((await this.ecgRtrListService.getContainedEcgRtrListEmails([data.email])).length > 0) {
return SubscribeResponse.ERROR_CONTAINED_IN_ECG_RTR_LIST;
}

const created = await this.brevoContactsService.createDoubleOptInContact({
...data,
scope,
templateId: this.config.brevo.resolveConfig(scope).doubleOptInTemplateId,
});

if (created) {
return SubscribeResponse.SUCCESSFUL;
}

return SubscribeResponse.ERROR_UNKNOWN;
return this.brevoContactsService.subscribeBrevoContact(data, scope);
}
}

Expand Down
32 changes: 30 additions & 2 deletions packages/api/src/brevo-contact/brevo-contacts.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { Injectable } from "@nestjs/common";
import { Inject, Injectable } from "@nestjs/common";

import { BrevoApiContactsService } from "../brevo-api/brevo-api-contact.service";
import { BrevoModuleConfig } from "../config/brevo-module.config";
import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants";
import { TargetGroupsService } from "../target-group/target-groups.service";
import { BrevoContactAttributesInterface, EmailCampaignScopeInterface } from "../types";
import { BrevoContactInterface } from "./dto/brevo-contact.factory";
import { SubscribeInputInterface } from "./dto/subscribe-input.factory";
import { SubscribeResponse } from "./dto/subscribe-response.enum";
import { EcgRtrListService } from "./ecg-rtr-list/ecg-rtr-list.service";

@Injectable()
export class BrevoContactsService {
constructor(private readonly brevoContactsApiService: BrevoApiContactsService, private readonly targetGroupService: TargetGroupsService) {}
constructor(
@Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig,
private readonly brevoContactsApiService: BrevoApiContactsService,
private readonly ecgRtrListService: EcgRtrListService,
private readonly targetGroupService: TargetGroupsService,
) {}

public async createDoubleOptInContact({
email,
Expand Down Expand Up @@ -63,6 +73,24 @@ export class BrevoContactsService {
return targetGroupIds;
}

public async subscribeBrevoContact(data: SubscribeInputInterface, scope: EmailCampaignScopeInterface): Promise<SubscribeResponse> {
if ((await this.ecgRtrListService.getContainedEcgRtrListEmails([data.email])).length > 0) {
return SubscribeResponse.ERROR_CONTAINED_IN_ECG_RTR_LIST;
}

const created = await this.createDoubleOptInContact({
...data,
scope,
templateId: this.config.brevo.resolveConfig(scope).doubleOptInTemplateId,
});

if (created) {
return SubscribeResponse.SUCCESSFUL;
}

return SubscribeResponse.ERROR_UNKNOWN;
}

public async getTargetGroupIdsForExistingContact({
contact,
scope,
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/brevo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class BrevoModule {
}),
ConfigModule.forRoot(config),
],
exports: [TargetGroupModule],
exports: [TargetGroupModule, BrevoContactModule],
};
}
}
1 change: 0 additions & 1 deletion packages/api/src/config/brevo-module.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export interface BrevoModuleConfig {
ecgRtrList: {
apiKey: string;
};

emailCampaigns: {
Scope: Type<EmailCampaignScopeInterface>;
EmailCampaignContentBlock: Block;
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { BrevoContactsService } from "./brevo-contact/brevo-contacts.service";
export { SubscribeResponse } from "./brevo-contact/dto/subscribe-response.enum";
export { IsValidRedirectURL } from "./brevo-contact/validator/redirect-url.validator";
export { BrevoModule } from "./brevo-module";

0 comments on commit 31f1241

Please sign in to comment.