diff --git a/packages/frontend-2/components/auth/RegisterPanel.vue b/packages/frontend-2/components/auth/RegisterPanel.vue
index e0831dcc5..d82dd5a22 100644
--- a/packages/frontend-2/components/auth/RegisterPanel.vue
+++ b/packages/frontend-2/components/auth/RegisterPanel.vue
@@ -63,6 +63,10 @@ graphql(`
}
`)
+const newsletterConsent = ref(false)
+
+provide('newsletterconsent', newsletterConsent)
+
const { result } = useQuery(loginServerInfoQuery)
const { appId, challenge, inviteToken } = useLoginOrRegisterUtils()
diff --git a/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue b/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue
index 885324cbf..c1fef56d7 100644
--- a/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue
+++ b/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue
@@ -43,6 +43,19 @@
:class="`mt-2 overflow-hidden ${pwdFocused ? 'h-8' : 'h-0'} transition-[height]`"
/>
Sign up
+
+
+
+
>('newsletterconsent')
+
const pwdFocused = ref(false)
const finalLoginRoute = computed(() => {
@@ -116,7 +131,8 @@ const onSubmit = handleSubmit(async (fullUser) => {
await signUpWithEmail({
user,
challenge: props.challenge,
- inviteToken: inviteToken.value
+ inviteToken: inviteToken.value,
+ newsletter: newsletterConsent?.value
})
} catch (e) {
triggerNotification({
diff --git a/packages/frontend-2/components/auth/third-party/LoginBlock.vue b/packages/frontend-2/components/auth/third-party/LoginBlock.vue
index d9500ac90..927f951d9 100644
--- a/packages/frontend-2/components/auth/third-party/LoginBlock.vue
+++ b/packages/frontend-2/components/auth/third-party/LoginBlock.vue
@@ -50,6 +50,8 @@ const {
const mixpanel = useMixpanel()
const { inviteToken } = useAuthManager()
+const newsletterConsent = inject[>('newsletterconsent')
+
const NuxtLink = resolveComponent('NuxtLink')
const GoogleButton = resolveComponent('AuthThirdPartyLoginButtonGoogle')
const MicrosoftButton = resolveComponent('AuthThirdPartyLoginButtonMicrosoft')
@@ -68,6 +70,11 @@ const buildAuthUrl = (strat: StrategyType) => {
url.searchParams.set('token', inviteToken.value)
}
+ if (newsletterConsent?.value) {
+ url.searchParams.set('newsletter', 'true')
+ }
+
+ console.log(url)
return url.toString()
}
diff --git a/packages/frontend-2/lib/auth/composables/auth.ts b/packages/frontend-2/lib/auth/composables/auth.ts
index 6ea70ea61..d19a9c530 100644
--- a/packages/frontend-2/lib/auth/composables/auth.ts
+++ b/packages/frontend-2/lib/auth/composables/auth.ts
@@ -239,14 +239,16 @@ export const useAuthManager = () => {
}
challenge: string
inviteToken?: string
+ newsletter?: boolean
}) => {
- const { user, challenge, inviteToken } = params
+ const { user, challenge, inviteToken, newsletter } = params
const { accessCode } = await registerAndGetAccessCode({
apiOrigin,
challenge,
user,
- inviteToken
+ inviteToken,
+ newsletter
})
// eslint-disable-next-line camelcase
diff --git a/packages/frontend-2/lib/auth/services/auth.ts b/packages/frontend-2/lib/auth/services/auth.ts
index 16db249c3..921a310e9 100644
--- a/packages/frontend-2/lib/auth/services/auth.ts
+++ b/packages/frontend-2/lib/auth/services/auth.ts
@@ -27,6 +27,7 @@ type RegisterParams = {
apiOrigin: string
challenge: string
inviteToken?: string
+ newsletter?: boolean
user: {
email: string
password: string
@@ -85,7 +86,7 @@ export async function getAccessCode(params: LoginParams) {
}
export async function registerAndGetAccessCode(params: RegisterParams) {
- const { apiOrigin, challenge, user, inviteToken } = params
+ const { apiOrigin, challenge, user, inviteToken, newsletter } = params
if (!user.email || !user.password || !user.name) {
throw new InvalidRegisterParametersError(
"Can't register without a valid email, password and name!"
@@ -98,6 +99,10 @@ export async function registerAndGetAccessCode(params: RegisterParams) {
registerUrl.searchParams.append('token', inviteToken)
}
+ if (newsletter) {
+ registerUrl.searchParams.append('newsletter', 'true')
+ }
+
const res = await fetch(registerUrl, {
method: 'POST',
headers: {
diff --git a/packages/server/modules/auth/services/mailchimp.ts b/packages/server/modules/auth/services/mailchimp.ts
new file mode 100644
index 000000000..9dda9fa96
--- /dev/null
+++ b/packages/server/modules/auth/services/mailchimp.ts
@@ -0,0 +1,52 @@
+/* eslint-disable camelcase */
+import mailchimp from '@mailchimp/mailchimp_marketing'
+import { logger } from '@/logging/logging'
+import { md5 } from '@/modules/shared/helpers/cryptoHelper'
+import {
+ getMailchimpConfig,
+ getMailchimpStatus
+} from '@/modules/shared/helpers/envHelper'
+import { getUserById } from '@/modules/core/services/users'
+
+async function addToMailchimpAudience(userId: string) {
+ // Do not do anything (inc. logging) if we do not explicitely enable it
+ if (!getMailchimpStatus()) return
+
+ // Note: fails here should not block registration at any cost
+ try {
+ const config = getMailchimpConfig() // Note: throws an error if not configured
+
+ mailchimp.setConfig({
+ apiKey: config.apiKey,
+ server: config.serverPrefix
+ })
+
+ const user = await getUserById({ userId })
+
+ if (!user) {
+ throw new Error(
+ 'Could not register user for newsletter - no db user record found.'
+ )
+ }
+
+ const [first, second] = user.name.split(' ')
+ const subscriberHash = md5(user.email.toLowerCase())
+
+ // NOTE: using setListMember (NOT addListMember) to prevent errors for previously
+ // registered members.
+ await mailchimp.lists.setListMember(config.listId, subscriberHash, {
+ status_if_new: 'subscribed',
+ email_address: user.email,
+ merge_fields: {
+ EMAIL: user.email,
+ FNAME: first,
+ LNAME: second,
+ FULLNAME: user.name // NOTE: this field needs to be set in the audience merge fields
+ }
+ })
+ } catch (e) {
+ logger.warn(e, 'Failed to register user to newsletter.')
+ }
+}
+
+export { addToMailchimpAudience }
diff --git a/packages/server/modules/auth/strategies.js b/packages/server/modules/auth/strategies.js
index e3e688531..47195b4c7 100644
--- a/packages/server/modules/auth/strategies.js
+++ b/packages/server/modules/auth/strategies.js
@@ -11,9 +11,10 @@ const { isSSLServer, getRedisUrl } = require('@/modules/shared/helpers/envHelper
const { authLogger } = require('@/logging/logging')
const { createRedisClient } = require('@/modules/shared/redis/redis')
const { mixpanel, resolveMixpanelUserId } = require('@/modules/shared/utils/mixpanel')
-
+const { addToMailchimpAudience } = require('./services/mailchimp')
/**
* TODO: Get rid of session entirely, we don't use it for the app and it's not really necessary for the auth flow, so it only complicates things
+ * NOTE: it does seem used!
*/
module.exports = async (app) => {
@@ -49,6 +50,11 @@ module.exports = async (app) => {
req.session.token = token
}
+ const newsletterConsent = req.query.newsletter || null
+ if (newsletterConsent) {
+ req.session.newsletterConsent = true
+ }
+
next()
}
@@ -64,6 +70,9 @@ module.exports = async (app) => {
challenge: req.session.challenge
})
+ let newsletterConsent = false
+ if (req.session.newsletterConsent) newsletterConsent = true // NOTE: it's only set if it's true
+
if (req.session) req.session.destroy()
// Resolve redirect URL
@@ -83,6 +92,10 @@ module.exports = async (app) => {
}
}
+ if (newsletterConsent) {
+ await addToMailchimpAudience(req.user.id)
+ }
+
const redirectUrl = urlObj.toString()
return res.redirect(redirectUrl)
diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts
index a3c6982c0..727dfda5a 100644
--- a/packages/server/modules/shared/helpers/envHelper.ts
+++ b/packages/server/modules/shared/helpers/envHelper.ts
@@ -81,6 +81,26 @@ export function getOidcName() {
return process.env.OIDC_NAME
}
+export function getMailchimpStatus() {
+ return [true, 'true'].includes(process.env.MAILCHIMP_ENABLED || false)
+}
+
+export function getMailchimpConfig() {
+ if (
+ !process.env.MAILCHIMP_API_KEY ||
+ !process.env.MAILCHIMP_SERVER_PREFIX ||
+ !process.env.MAILCHIMP_LIST_ID
+ ) {
+ throw new MisconfiguredEnvironmentError('Mailchimp is not configured')
+ }
+
+ return {
+ apiKey: process.env.MAILCHIMP_API_KEY,
+ serverPrefix: process.env.MAILCHIMP_SERVER_PREFIX,
+ listId: process.env.MAILCHIMP_LIST_ID
+ }
+}
+
/**
* Get app base url / canonical url / origin
* TODO: Go over all getBaseUrl() usages and move them to getXOrigin() instead
diff --git a/packages/server/package.json b/packages/server/package.json
index d5fc5f7f5..0015a2e7b 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -36,9 +36,11 @@
"@aws-sdk/lib-storage": "^3.100.0",
"@godaddy/terminus": "^4.9.0",
"@graphql-tools/schema": "^9.0.4",
+ "@mailchimp/mailchimp_marketing": "^3.0.80",
"@sentry/node": "^6.17.9",
"@sentry/tracing": "^6.17.9",
"@speckle/shared": "workspace:^",
+ "@types/mailchimp__mailchimp_marketing": "^3.0.9",
"@types/pino-http": "^5.8.1",
"@types/uuid": "^9.0.0",
"apollo-server-express": "^3.10.2",
diff --git a/utils/helm/speckle-server/templates/server/deployment.yml b/utils/helm/speckle-server/templates/server/deployment.yml
index 20fde29f6..bd94b6e64 100644
--- a/utils/helm/speckle-server/templates/server/deployment.yml
+++ b/utils/helm/speckle-server/templates/server/deployment.yml
@@ -276,6 +276,27 @@ spec:
- name: EMAIL_FROM
value: "{{ .Values.server.email.from }}"
{{- end }}
+
+ # *** Newsletter ***
+ {{- if (default false (.Values.server.mailchimp).enabled) }}
+ - name: MAILCHIMP_ENABLED
+ value: {{ default false (.Values.server.mailchimp).enabled }}
+ - name: MAILCHIMP_API_KEY
+ valueFrom:
+ secretKeyRef:
+ name: {{ default .Values.secretName ((.Values.server.mailchimp).apikey).secretName }}
+ key: {{ default "mailchimp_apikey" ((.Values.server.mailchimp).apikey).secretKey }}
+ - name: MAILCHIMP_SERVER_PREFIX
+ valueFrom:
+ secretKeyRef:
+ name: {{ default .Values.secretName ((.Values.server.mailchimp).serverprefix).secretName }}
+ key: {{ default "mailchimp_serverprefix" ((.Values.server.mailchimp).serverprefix).secretKey }}
+ - name: MAILCHIMP_LIST_ID
+ valueFrom:
+ secretKeyRef:
+ name: {{ default .Values.secretName ((.Values.server.mailchimp).listid).secretName }}
+ key: {{ default "mailchimp_listid" ((.Values.server.mailchimp).listid).secretKey }}
+ {{- end }}
# *** Tracking / Tracing ***
- name: SENTRY_DSN
diff --git a/yarn.lock b/yarn.lock
index e1a57c2f3..a5d18fe64 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9344,6 +9344,16 @@ __metadata:
languageName: node
linkType: hard
+"@mailchimp/mailchimp_marketing@npm:^3.0.80":
+ version: 3.0.80
+ resolution: "@mailchimp/mailchimp_marketing@npm:3.0.80"
+ dependencies:
+ dotenv: ^8.2.0
+ superagent: 3.8.1
+ checksum: a43f69334766bf02fccf4f6076fa38b7e6a53d9d99485547e1e11d23f9188349ac2831a329f15bb8e31be46cca5e22275a120d0d108b80212ed92ad12df709e4
+ languageName: node
+ linkType: hard
+
"@mapbox/node-pre-gyp@npm:^1.0.0":
version: 1.0.9
resolution: "@mapbox/node-pre-gyp@npm:1.0.9"
@@ -11237,6 +11247,7 @@ __metadata:
"@graphql-codegen/typescript-operations": ^2.5.2
"@graphql-codegen/typescript-resolvers": 2.7.2
"@graphql-tools/schema": ^9.0.4
+ "@mailchimp/mailchimp_marketing": ^3.0.80
"@sentry/node": ^6.17.9
"@sentry/tracing": ^6.17.9
"@speckle/objectloader": "workspace:^"
@@ -11251,6 +11262,7 @@ __metadata:
"@types/ejs": ^3.1.1
"@types/express": ^4.17.13
"@types/lodash": ^4.14.180
+ "@types/mailchimp__mailchimp_marketing": ^3.0.9
"@types/mjml": ^4.7.0
"@types/mocha": ^10.0.0
"@types/mock-require": ^2.0.1
@@ -15321,6 +15333,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/mailchimp__mailchimp_marketing@npm:^3.0.9":
+ version: 3.0.9
+ resolution: "@types/mailchimp__mailchimp_marketing@npm:3.0.9"
+ checksum: 3bf413367a77a331fd87552096ce42f23baee3f34309c91766d26095bc686fa541aa1ce5d72061fd66e9e2b8bb3589542b4709e04043c1ca54e28e71e4480934
+ languageName: node
+ linkType: hard
+
"@types/mdx@npm:^2.0.0":
version: 2.0.3
resolution: "@types/mdx@npm:2.0.3"
@@ -25534,7 +25553,7 @@ __metadata:
languageName: node
linkType: hard
-"formidable@npm:^1.2.0":
+"formidable@npm:^1.1.1, formidable@npm:^1.2.0":
version: 1.2.6
resolution: "formidable@npm:1.2.6"
checksum: 2b68ed07ba88302b9c63f8eda94f19a460cef6017bfda48348f09f41d2a36660c9353137991618e0e4c3db115b41e4b8f6fa63bc973b7a7c91dec66acdd02a56
@@ -40206,6 +40225,24 @@ __metadata:
languageName: node
linkType: hard
+"superagent@npm:3.8.1":
+ version: 3.8.1
+ resolution: "superagent@npm:3.8.1"
+ dependencies:
+ component-emitter: ^1.2.0
+ cookiejar: ^2.1.0
+ debug: ^3.1.0
+ extend: ^3.0.0
+ form-data: ^2.3.1
+ formidable: ^1.1.1
+ methods: ^1.1.1
+ mime: ^1.4.1
+ qs: ^6.5.1
+ readable-stream: ^2.0.5
+ checksum: 42895e220fb5aab303edeef7ec4d9c38fef31638d18254fe57329366e7a74624dffe20acd682a9a27df4f62fe4acb953b33d16be9611f184284815c2492d35cf
+ languageName: node
+ linkType: hard
+
"superagent@npm:^3.7.0, superagent@npm:^3.8.3":
version: 3.8.3
resolution: "superagent@npm:3.8.3"
]