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

Add sign up webhook #173

Merged
merged 1 commit into from
Apr 8, 2023
Merged
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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ CLICKHOUSE_USER=poeticmetric
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# MAIL BLUSTER
MAIL_BLUSTER_API_KEY=

# MAILPIT
MAILPIT_BASE_URL=https://mailpit.dev.poeticmetric.com

Expand All @@ -20,9 +23,9 @@ DEBUG=false
FRONTEND_BASE_URL=https://dev.poeticmetric.com
FRONTEND_USE_POLLING=false
HOSTED=true
NODE_RED_BASE_URL=
REMOTE_DEBUG=false
REST_API_BASE_URL=https://api.dev.poeticmetric.com
WEBHOOK_URL=
WORKER_COUNT=1

# POSTGRES
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ jobs:
BASE_URL=${{ vars.FRONTEND_BASE_URL }}
GOOGLE_CLIENT_ID=${{ vars.GOOGLE_CLIENT_ID }}
HOSTED=true
NODE_RED_BASE_URL=${{ secrets.NODE_RED_BASE_URL }}
ROBOTS_TXT_ALLOW=${{ vars.ROBOTS_TXT_ALLOW }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
Expand Down
3 changes: 2 additions & 1 deletion backend/pkg/env/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const (
GoogleClientSecret = "GOOGLE_CLIENT_SECRET"
Hosted = "HOSTED"
Instance = "INSTANCE"
NodeRedBaseUrl = "NODE_RED_BASE_URL"
MailBlusterApiKey = "MAIL_BLUSTER_API_KEY"
PostgresDatabase = "POSTGRES_DATABASE"
PostgresHost = "POSTGRES_HOST"
PostgresPassword = "POSTGRES_PASSWORD"
Expand All @@ -48,6 +48,7 @@ const (
SmtpUser = "SMTP_USER"
StripeSecretKey = "STRIPE_SECRET_KEY"
StripeWebhookSigningSecret = "STRIPE_WEBHOOK_SIGNING_SECRET"
WebhookUrl = "WEBHOOK_URL"
WorkerCount = "WORKER_COUNT"
WorkerQueues = "WORKER_QUEUES"
)
Expand Down
78 changes: 78 additions & 0 deletions backend/pkg/mailbluster/add_lead.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package mailbluster

import (
"bytes"
"encoding/json"
"io"
"net/http"

v "github.com/RussellLuo/validating/v3"
"github.com/go-errors/errors"

"github.com/th0th/poeticmetric/backend/pkg/depot"
"github.com/th0th/poeticmetric/backend/pkg/env"
"github.com/th0th/poeticmetric/backend/pkg/validator"
)

type AddLeadPayload struct {
Email *string `json:"email"`
}

func AddLead(dp *depot.Depot, payload *AddLeadPayload) error {
err := validateAddLeadPayload(payload)
if err != nil {
return err
}

reqBody := map[string]any{
"email": payload.Email,
"overrideExisting": true,
"subscribed": true,
}

reqBodyJson, err := json.Marshal(reqBody)
if err != nil {
return errors.Wrap(err, 0)
}

req, err := http.NewRequest(http.MethodPost, "https://api.mailbluster.com/api/leads", bytes.NewBuffer(reqBodyJson))
if err != nil {
return errors.Wrap(err, 0)
}

req.Header.Set("content-type", "application/json")
req.Header.Set("authorization", env.Get(env.MailBlusterApiKey))

res, err := dp.HttpClient().Do(req)
if err != nil {
return errors.Wrap(err, 0)
}

defer res.Body.Close()

if res.StatusCode != http.StatusCreated {
resBodyBytes, _ := io.ReadAll(res.Body)

return errors.Errorf("received an unexpected response from MailBluster: (%d) %s", res.StatusCode, string(resBodyBytes))
}

return nil
}

func validateAddLeadPayload(payload *AddLeadPayload) error {
errs := v.Validate(v.Schema{
v.F("email", payload.Email): v.All(
v.Nonzero[*string]().Msg("This field is required."),

v.Is[*string](func(s *string) bool {
return validator.Email(*s)
}).Msg("Please provide a valid e-mail address."),
),
})

if len(errs) > 0 {
return errs
}

return nil
}
1 change: 1 addition & 0 deletions backend/pkg/restapi/root/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ func Add(app *fiber.App) {
app.Get("/", index)
app.Get("/bootstrap-status", bootstrapStatus)
app.Get("/pm.js", tracker)
app.Post("/newsletter-subscription", createNewsletterSubscription)
}
26 changes: 26 additions & 0 deletions backend/pkg/restapi/root/subscribe_to_newsletter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package root

import (
"github.com/gofiber/fiber/v2"

"github.com/th0th/poeticmetric/backend/pkg/mailbluster"
dm "github.com/th0th/poeticmetric/backend/pkg/restapi/middleware/depot"
)

func createNewsletterSubscription(c *fiber.Ctx) error {
dp := dm.Get(c)

payload := &mailbluster.AddLeadPayload{}

err := c.BodyParser(payload)
if err != nil {
return err
}

err = mailbluster.AddLead(dp, payload)
if err != nil {
return err
}

return c.SendStatus(fiber.StatusCreated)
}
6 changes: 1 addition & 5 deletions backend/pkg/service/organization/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package organization

import (
"fmt"
"log"
"time"

v "github.com/RussellLuo/validating/v3"
Expand Down Expand Up @@ -131,13 +130,10 @@ func Delete(dp *depot.Depot, id uint64, payload *DeletionPayload) error {
return err
}

err = worker.SendWebhook(dp, &worker.SendWebhookPayload{
worker.SendWebhook(dp, &worker.SendWebhookPayload{
Event: worker.SendWebhookEventOrganizationDeleted,
Data: *organizationDeletion,
})
if err != nil {
log.Print(err)
}

return nil
}
Expand Down
17 changes: 13 additions & 4 deletions backend/pkg/service/userself/sign_up.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ func SignUp(dp *depot.Depot, payload *SignUpPayload) (*UserSelf, error) {
Password: passwordHash,
}

err = dp.WithPostgresTransaction(func(dp2 *depot.Depot) error {
modelOrganization := &model.Organization{
Name: *payload.OrganizationName,
}
modelOrganization := &model.Organization{
Name: *payload.OrganizationName,
}

err = dp.WithPostgresTransaction(func(dp2 *depot.Depot) error {
err2 := dp2.Postgres().
Create(modelOrganization).
Error
Expand Down Expand Up @@ -90,6 +90,15 @@ func SignUp(dp *depot.Depot, payload *SignUpPayload) (*UserSelf, error) {
return nil, err
}

worker.SendWebhook(dp, &worker.SendWebhookPayload{
Event: worker.SendWebhookEventUserSignedUp,
Data: map[string]any{
"organizationName": modelOrganization.Name,
"userEmail": modelUser.Email,
"userName": modelUser.Name,
},
})

return userSelf, nil
}

Expand Down
2 changes: 1 addition & 1 deletion backend/pkg/validator/non_disposable_email.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func nonDisposableEmail(dp *depot.Depot, v string) bool {
var err error

req, err := http.NewRequest(http.MethodGet, "https://isemaildisposable.webgazer.iox", nil)
req, err := http.NewRequest(http.MethodGet, "https://isemaildisposable.webgazer.io", nil)
if err != nil {
panic(err)
}
Expand Down
25 changes: 19 additions & 6 deletions backend/pkg/worker/send_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"log"
"strings"

"github.com/getsentry/sentry-go"

"github.com/th0th/poeticmetric/backend/pkg/depot"
"github.com/th0th/poeticmetric/backend/pkg/depot/rabbitmq"
"github.com/th0th/poeticmetric/backend/pkg/env"
Expand All @@ -15,21 +17,32 @@ const SendWebhookQueue rabbitmq.QueueName = "sendWebhook"

const (
SendWebhookEventOrganizationDeleted Event = "organization.deleted"
SendWebhookEventUserSignedUp Event = "user.signed_up"
)

type Event string

type SendWebhookPayload struct {
Event Event
Data any
Event Event
}

func SendWebhook(dp *depot.Depot, payload *SendWebhookPayload) error {
if env.Get(env.NodeRedBaseUrl) == "" {
return nil
func SendWebhook(dp *depot.Depot, payload *SendWebhookPayload) {
if env.Get(env.WebhookUrl) == "" {
return
}

return publish(dp, SendWebhookQueue, payload)
err := publish(dp, SendWebhookQueue, payload)
if err != nil {
sentry.WithScope(func(scope *sentry.Scope) {
scope.SetContext("payload", sentry.Context{
"Data": payload.Data,
"Event": payload.Event,
})

sentry.CaptureException(err)
})
}
}

func sendWebhook(dp *depot.Depot, b []byte) error {
Expand Down Expand Up @@ -70,7 +83,7 @@ func sendWebhook(dp *depot.Depot, b []byte) error {
return err
}

_, err = dp.HttpClient().Post(env.Get(env.NodeRedBaseUrl)+"/webhook", "application/json", bytes.NewBuffer(bodyByteSlice))
_, err = dp.HttpClient().Post(env.Get(env.WebhookUrl), "application/json", bytes.NewBuffer(bodyByteSlice))
if err != nil {
return err
}
Expand Down
13 changes: 7 additions & 6 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ x-backend-common: &backend-common
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
HOSTED: ${HOSTED}
NODE_RED_BASE_URL: ${NODE_RED_BASE_URL}
MAIL_BLUSTER_API_KEY: ${MAIL_BLUSTER_API_KEY}
POSTGRES_DATABASE: ${POSTGRES_DATABASE}
POSTGRES_HOST: ${POSTGRES_HOST}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Expand All @@ -40,6 +40,7 @@ x-backend-common: &backend-common
SMTP_USER: ${SMTP_USER}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
STRIPE_WEBHOOK_SIGNING_SECRET: ${STRIPE_WEBHOOK_SIGNING_SECRET}
WEBHOOK_URL: ${WEBHOOK_URL}
WORKER_COUNT: ${WORKER_COUNT}
restart: unless-stopped
volumes:
Expand Down Expand Up @@ -81,10 +82,10 @@ services:
FRONTEND_USE_POLLING: ${FRONTEND_USE_POLLING}
NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
NEXT_PUBLIC_HOSTED: ${HOSTED}
NEXT_PUBLIC_NODE_RED_BASE_URL: ${NODE_RED_BASE_URL}
NEXT_PUBLIC_SENTRY_DSN: ${SENTRY_DSN}
NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT}
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUBLISHABLE_KEY}
NEXT_PUBLIC_WEBHOOK_URL: ${WEBHOOK_URL}
NEXT_TELEMETRY_DISABLED: "1"
REST_API_BASE_URL: ${REST_API_BASE_URL}
VIRTUAL_HOST: dev.poeticmetric.com
Expand Down Expand Up @@ -169,16 +170,16 @@ services:
<<: *backend-common-environment
INSTANCE: rest-api
VIRTUAL_HOST: api.dev.poeticmetric.com
# ports:
# - "127.0.0.1:2345:2345"
# ports:
# - "127.0.0.1:2345:2345"

scheduler:
<<: *backend-common
environment:
<<: *backend-common-environment
INSTANCE: scheduler
# ports:
# - "127.0.0.1:2345:2345"
# ports:
# - "127.0.0.1:2345:2345"

stripe-webhook:
environment:
Expand Down
2 changes: 0 additions & 2 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ RUN npm install --global [email protected]
ARG BASE_URL
ARG GOOGLE_CLIENT_ID
ARG HOSTED=false
ARG NODE_RED_BASE_URL
ARG ROBOTS_TXT_ALLOW=false
ARG SENTRY_AUTH_TOKEN
ARG SENTRY_DSN
Expand Down Expand Up @@ -49,7 +48,6 @@ ENV NEXT_TELEMETRY_DISABLED=1
ENV BASE_URL=${BASE_URL}
ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
ENV NEXT_PUBLIC_HOSTED=${HOSTED}
ENV NEXT_PUBLIC_NODE_RED_BASE_URL=${NODE_RED_BASE_URL}
ENV NEXT_PUBLIC_SENTRY_DSN=${SENTRY_DSN}
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
Expand Down
8 changes: 2 additions & 6 deletions frontend/components/BlogPage/MailListForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import classNames from "classnames";
import React, { useCallback, useContext } from "react";
import { Button, Container, ContainerProps, Form } from "react-bootstrap";
import { ToastsContext } from "../../../contexts";
import { api } from "../../../helpers";
import { useForm } from "../../../hooks";
import styles from "./MailListForm.module.scss";

Expand All @@ -22,12 +23,7 @@ export function MailListForm({ className, ...props }: MailListFormProps) {
const handleSubmit = useCallback<React.FormEventHandler<HTMLFormElement>>(async (event) => {
event.preventDefault();

const response = await fetch(`${process.env.NEXT_PUBLIC_NODE_RED_BASE_URL}/mail-list`, {
body: JSON.stringify(values),
headers: { "Content-Type": "application/json" },
method: "POST",
});

const response = await api.post("/newsletter-subscription", values);
const responseJson = await response.json();

if (response.ok) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/PrivacyPolicy/privacy-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ We retain the personal information for 1 year from the date we collect it from y

- Amazon SES (Transactional e-mails)

- MailBluster (Marketing communications)
- MailBluster (Newsletter subscriptions)

- Stripe (Payment service provider)

Expand Down Expand Up @@ -198,7 +198,7 @@ We retain the personal information for 10 years from the date we collect it from

- Twilio (Phone Call Alerting)

- MailBluster ( Marketing Communications)
- MailBluster (Newsletter subscriptions)

- Stripe (Payment Service Provider)

Expand Down