feat(backend): Add one-click unsubscribe endpoints
This commit is contained in:
parent
c44dac26ec
commit
61f111fc8d
6 changed files with 93 additions and 11 deletions
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
9
sites/backend/src/html/ocunsub-ko.mjs
Normal file
9
sites/backend/src/html/ocunsub-ko.mjs
Normal 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>
|
||||
`,
|
||||
})
|
8
sites/backend/src/html/ocunsub-ok.mjs
Normal file
8
sites/backend/src/html/ocunsub-ok.mjs
Normal 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>
|
||||
`,
|
||||
})
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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}`)
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue