1
0
Fork 0

fix: Handle newsletter unsubscribe

There were a few issues with the newsletter unsubscribe links that we
sent out in the newsletter. They were pointing to the backend for one
thing.

Also updated the frontend pages to handle unsubscribe from both users
and subscribers.
This commit is contained in:
joostdecock 2024-01-02 16:59:14 +01:00
parent f8feb5cf8b
commit 9208fb9a3c
10 changed files with 71 additions and 36 deletions

View file

@ -115,7 +115,9 @@ const send = async (test = true) => {
if (l > 0) {
const body = mustache.render(template, {
...i18n[lang],
unsubscribe: `${backend}newsletter/unsubscribe/${sub.ehash}`,
unsubscribe: `https://freesewing.org${
lang === 'en' ? '/' : '/' + lang + '/'
}newsletter/unsubscribe?x=${sub.ehash}`,
content,
})
console.log(`[${lang}] ${l}/${subs} (${i}) Sending to ${sub.email}`)

View file

@ -25,12 +25,12 @@ SubscribersController.prototype.subscribeConfirm = async (req, res, tools) => {
}
/*
* Unsubscribe confirmation
* Unsubscribe
* See: https://freesewing.dev/reference/backend/api
*/
SubscribersController.prototype.unsubscribeConfirm = async (req, res, tools) => {
SubscribersController.prototype.unsubscribe = async (req, res, tools) => {
const Subscriber = new SubscriberModel(tools)
await Subscriber.unsubscribeConfirm(req)
await Subscriber.unsubscribe(req)
return Subscriber.sendResponse(res)
}

View file

@ -9,6 +9,7 @@ export function SubscriberModel(tools) {
return decorateModel(this, tools, {
name: 'subscriber',
encryptedFields: ['email'],
models: ['user'],
})
}
@ -128,32 +129,48 @@ SubscriberModel.prototype.subscribeConfirm = async function ({ body }) {
}
/*
* Confirms a pending unsubscription
* This is an unauthenticated route
* Unsubscribe a user
* This is an unauthenticated route (has to for newsletter subscribers might not be users)
*
* @param {body} object - The request body
* @returns {SubscriberModal} object - The SubscriberModel
*/
SubscriberModel.prototype.unsubscribeConfirm = async function ({ params }) {
SubscriberModel.prototype.unsubscribe = async function ({ params }) {
/*
* Validate input and load subscription record
* Is ehash set?
*/
await this.verifySubscription(params)
if (!params.ehash) return this.setResponse(400, 'ehashMissing')
const { ehash } = params
/*
* If a status code is already set, do not continue
* Find the subscription record
*/
if (this.response?.status) return this
await this.read({ ehash })
/*
* Remove the record
* If found, remove the record
*/
await this.delete({ id: this.record.id })
if (this.record) {
await this.delete({ id: this.record.id })
return this.setResponse(204)
} else {
/*
* If not, perhaps it's an account ehash rather than subscriber ehash
*/
await this.User.read({ ehash })
if (this.User.record) {
await this.User.update({ newsletter: false })
return this.setResponse(204)
}
}
/*
* Return 204
* Return 404
*/
return this.setResponse(204)
return this.setResponse(404)
}
/*

View file

@ -1675,6 +1675,7 @@ UserModel.prototype.asAccount = function () {
consent: this.record.consent,
control: this.record.control,
createdAt: this.record.createdAt,
ehash: this.record.ehash,
email: this.clear.email,
data,
ihash: this.record.ihash,

View file

@ -16,5 +16,5 @@ export function subscribersRoutes(tools) {
app.put('/subscriber', (req, res) => Subscriber.subscribeConfirm(req, res, tools))
// Unsubscribe from newsletter
app.delete('/subscriber/:id/:ehash', (req, res) => Subscriber.unsubscribeConfirm(req, res, tools))
app.delete('/subscriber/:ehash', (req, res) => Subscriber.unsubscribe(req, res, tools))
}

View file

@ -33,7 +33,7 @@ const NewsletterPage = ({ page }) => {
useEffect(() => {
const newId = getSearchParam('id')
const newEhash = getSearchParam('ehash')
const newEhash = getSearchParam('check')
if (newId !== id) setId(newId)
if (newEhash !== ehash) setEhash(newEhash)
}, [id, ehash])
@ -49,6 +49,7 @@ const NewsletterPage = ({ page }) => {
return (
<PageWrapper {...page} title={false}>
<Hodl />
<pre>{JSON.stringify({ id, ehash })}</pre>
</PageWrapper>
)
@ -63,7 +64,7 @@ const NewsletterPage = ({ page }) => {
<p>{t('newsletter:subscribePs')}</p>
<p>
<PageLink
href={`/newsletter/unsubscribe/${id}/${ehash}`}
href={`/newsletter/unsubscribe?x=${ehash}`}
txt={t('newsletter:unsubscribeLink')}
/>
</p>
@ -85,7 +86,7 @@ const NewsletterPage = ({ page }) => {
<p>
{t('newsletter:faqLead')}:{' '}
<PageLink
href="/docs/faq/newsletter/why-subscribe-multiple-clicks"
href="/docs/about/faq/newsletter/why-subscribe-multiple-clicks"
txt={t('newsletter:subscribeWhy')}
/>
</p>

View file

@ -27,25 +27,22 @@ const NewsletterPage = ({ page }) => {
const { setLoadingStatus } = useContext(LoadingStatusContext)
const backend = useBackend()
const [confirmed, setConfirmed] = useState(false)
const [id, setId] = useState()
const [ehash, setEhash] = useState()
const [done, setDone] = useState(false)
useEffect(() => {
const newId = getSearchParam('id')
const newEhash = getSearchParam('ehash')
if (newId !== id) setId(newId)
const newEhash = getSearchParam('x')
if (newEhash !== ehash) setEhash(newEhash)
}, [id, ehash])
}, [ehash])
const handler = async () => {
setLoadingStatus([true, 'status:contactingBackend'])
await backend.confirmNewsletterUnsubscribe({ id, ehash })
await backend.newsletterUnsubscribe(ehash)
setLoadingStatus([true, 'status:settingsSaved', true, true])
setConfirmed(true)
setDone(true)
}
if (!id || !ehash)
if (!ehash)
return (
<PageWrapper {...page} title={false}>
<Hodl />
@ -55,7 +52,7 @@ const NewsletterPage = ({ page }) => {
return (
<PageWrapper {...page} title={false}>
<div className="max-w-xl">
{confirmed ? (
{done ? (
<>
<h1>{t('newsletter:newsletter')}</h1>
<p>{t('newsletter:thanksDone')}</p>
@ -76,7 +73,7 @@ const NewsletterPage = ({ page }) => {
<p>
{t('newsletter:faqLead')}:{' '}
<PageLink
href="/docs/faq/newsletter/why-unsubscribe-multiple-clicks"
href="/docs/about/faq/newsletter/why-unsubscribe-multiple-clicks"
txt={t('newsletter:unsubscribeWhy')}
/>
</p>

View file

@ -11,8 +11,10 @@ import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { ListInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { PageLink } from 'shared/components/link.mjs'
export const ns = ['account', 'status']
export const ns = ['account', 'status', 'newsletter']
export const NewsletterSettings = ({ welcome = false, bare = false }) => {
// Hooks
@ -89,6 +91,15 @@ export const NewsletterSettings = ({ welcome = false, bare = false }) => {
) : bare ? null : (
<BackToAccountButton />
)}
<Popout tip>
<p>{t('newsletter:subscribePs')}</p>
<p>
<PageLink
href={`/newsletter/unsubscribe?x=${account.ehash}`}
txt={t('newsletter:unsubscribeLink')}
/>
</p>
</Popout>
</div>
)
}

View file

@ -127,7 +127,7 @@ const ConsentLacking = ({ banner, refresh }) => {
export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
const { t } = useTranslation(ns)
const { account, token, admin, stopImpersonating, signOut } = useAccount()
const { account, setAccount, token, admin, stopImpersonating, signOut } = useAccount()
const backend = useBackend()
const [ready, setReady] = useState(false)
@ -151,7 +151,13 @@ export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
}
const verifyUser = async () => {
const result = await backend.ping()
if (!result.success) {
if (result.success) {
// Refresh account in local storage
setAccount({
...account,
...result.data.account,
})
} else {
if (result.data?.error?.error) setError(result.data.error.error)
else signOut()
}

View file

@ -500,10 +500,10 @@ Backend.prototype.confirmNewsletterSubscribe = async function ({ id, ehash }) {
}
/*
* Confirm newsletter unsubscribe
* Newsletter unsubscribe
*/
Backend.prototype.confirmNewsletterUnsubscribe = async function ({ id, ehash }) {
return responseHandler(await api.delete(`/subscriber/${id}/${ehash}`))
Backend.prototype.newsletterUnsubscribe = async function (ehash) {
return responseHandler(await api.delete(`/subscriber/${ehash}`))
}
/*