1
0
Fork 0

feat(backend): Add one-click unsubscribe endpoints

This commit is contained in:
joostdecock 2024-04-01 16:57:35 +02:00
parent c44dac26ec
commit 61f111fc8d
6 changed files with 93 additions and 11 deletions

View file

@ -113,11 +113,13 @@ const send = async (test = true) => {
for (let sub of subscribers[lang]) {
if (l > 0) {
const unsubGet = `https://freesewing.org${
lang === 'en' ? '/' : '/' + lang + '/'
}newsletter/unsubscribe?x=${sub.ehash}`
const unsubPost = `https://backend3.freesewing.org/ocunsub/${sub.ehash}`
const body = mustache.render(template, {
...i18n[lang],
unsubscribe: `https://freesewing.org${
lang === 'en' ? '/' : '/' + lang + '/'
}newsletter/unsubscribe?x=${sub.ehash}`,
unsubscribe: unsubGet,
content,
})
console.log(`[${lang}] ${l}/${subs} (${i}) Sending to ${sub.email}`)
@ -141,6 +143,16 @@ const send = async (test = true) => {
Charset: 'utf-8',
Data: i18n[lang].title,
},
Headers: [
{
Name: 'List-Unsubscribe',
Value: unsubPost,
},
{
Name: 'List-Unsubscribe-Post',
Value: 'List-Unsubscribe=One-Click',
},
],
},
},
Destination: {

View file

@ -1,4 +1,7 @@
import { SubscriberModel } from '../models/subscriber.mjs'
// Catch-all page
import { html as ocunsubOk } from '../html/ocunsub-ok.mjs'
import { html as ocunsubKo } from '../html/ocunsub-ko.mjs'
export function SubscribersController() {}
@ -47,12 +50,16 @@ SubscribersController.prototype.confirm = async (req, res, tools) => {
}
/*
* Unsubscribe from the newsletter
* One-Click unsubscribe from the newsletter
* See: https://freesewing.dev/reference/backend/api
*/
SubscribersController.unsubscribe = async (req, res, tools) => {
const Subscriber = new SubscriberModel(tools)
await Subscriber.unsubscribe(req)
SubscribersController.prototype.ocunsub = async (req, res, tools) => {
if (!res.params?.ehash) return res.set('Content-Type', 'text/html').status(200).send(ocunsubKo)
return Subscriber.sendResponse(res)
const Subscriber = new SubscriberModel(tools)
const result = await Subscriber.ocunsub(req)
if (result) return res.set('Content-Type', 'text/html').status(200).send(ocunsubOk)
return res.set('Content-Type', 'text/html').status(200).send(okunsubKo)
}

View file

@ -0,0 +1,9 @@
import { wrapper } from './shared.mjs'
export const html = wrapper({
content: `
<h1><span role="img">🫤</span> FNpe</h1>
<p>Whatever you intended to do, it did not work.</p>
<p><a href="https://freesewing.org/support">Contact support</a></p>
`,
})

View file

@ -0,0 +1,8 @@
import { wrapper } from './shared.mjs'
export const html = wrapper({
content: `
<h1><span role="img">👋</span> Farewell</h1>
<p>You have been unsubscribed.</p>
`,
})

View file

@ -156,9 +156,9 @@ SubscriberModel.prototype.unsubscribe = async function ({ params }) {
return this.setResponse(204)
} else {
/*
* If not, perhaps it's an account ehash rather than subscriber ehash
*/
/*
* 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 })
@ -173,6 +173,43 @@ SubscriberModel.prototype.unsubscribe = async function ({ params }) {
return this.setResponse(404)
}
/*
* One-click 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.ocunsub = async function ({ params }) {
const { ehash } = params
/*
* Find the subscription record
*/
await this.read({ ehash })
/*
* If found, remove the record
*/
if (this.record) {
await this.delete({ id: this.record.id })
return true
} 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 true
}
}
return false
}
/*
* A helper method to validate input and load the subscription record
*

View file

@ -17,4 +17,13 @@ export function subscribersRoutes(tools) {
// Unsubscribe from newsletter
app.delete('/subscriber/:ehash', (req, res) => Subscriber.unsubscribe(req, res, tools))
// One-Click unsubscribe (ocunsub) from newsletter needs to be a POST request.
// See https://datatracker.ietf.org/doc/html/rfc8058
app.post('/ocunsub/:ehash', (req, res) => Subscriber.ocunsub(req, res, tools))
// Just in case somebody lands here with a GET request
app.get('/ocunsub/:ehash', (req, res) =>
res.redirect(`https://freesewing.org/newsletter/unsubscribe?i=${req.params.ehash}`)
)
}