Merge pull request #1634 from specklesystems/dim/fe2/newsletter-registration

feat(server): opt-in audience subscribing on registration
This commit is contained in:
Dimitrie Stefanescu
2023-06-23 15:00:17 +01:00
committed by GitHub
11 changed files with 185 additions and 6 deletions
@@ -63,6 +63,10 @@ graphql(`
}
`)
const newsletterConsent = ref(false)
provide('newsletterconsent', newsletterConsent)
const { result } = useQuery(loginServerInfoQuery)
const { appId, challenge, inviteToken } = useLoginOrRegisterUtils()
@@ -43,6 +43,19 @@
:class="`mt-2 overflow-hidden ${pwdFocused ? 'h-8' : 'h-0'} transition-[height]`"
/>
<FormButton submit full-width class="mt-4" :disabled="loading">Sign up</FormButton>
<div
class="mt-3 text-xs flex items-center justify-center text-foreground-2 space-x-2"
>
<!--
Note the newsletter consent box is here because i got very confused re layout of the panel
and didn't figure out a better way to put it where i needed it to be
-->
<FormCheckbox
v-model="newsletterConsent"
name="test"
label="I want to receive tips and tricks on how to use Speckle"
/>
</div>
<div
v-if="serverInfo.termsOfService"
class="mt-2 text-xs text-foreground-2 text-center linkify-tos"
@@ -99,6 +112,8 @@ const nameRules = [isRequired]
const { signUpWithEmail, inviteToken } = useAuthManager()
const { triggerNotification } = useGlobalToast()
const newsletterConsent = inject<Ref<boolean>>('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({
@@ -50,6 +50,8 @@ const {
const mixpanel = useMixpanel()
const { inviteToken } = useAuthManager()
const newsletterConsent = inject<Ref<boolean>>('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()
}
@@ -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
@@ -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: {
@@ -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 }
+14 -1
View File
@@ -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)
@@ -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
+2
View File
@@ -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",
@@ -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
+38 -1
View File
@@ -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"