Add study user re-engagement reminder emails; v1.1.19
Sends 7/14/30-day inactivity emails to study users who haven't logged in, with one-click HMAC-signed unsubscribe and automatic re-arm on next login. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto'
|
||||
import { createHash, createHmac, randomUUID, timingSafeEqual } from 'node:crypto'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import qrcode from 'qrcode'
|
||||
import {
|
||||
@@ -174,6 +174,7 @@ export function register(app) {
|
||||
|
||||
user.lastLoginAt = new Date().toISOString()
|
||||
user.updatedAt = user.lastLoginAt
|
||||
user.reengagementSentAt = {}
|
||||
queueStudyUsersWrite()
|
||||
|
||||
const sessionToken = createStudySession(user.id)
|
||||
@@ -209,6 +210,7 @@ export function register(app) {
|
||||
function completeLogin(extra = {}) {
|
||||
user.lastLoginAt = new Date().toISOString()
|
||||
user.updatedAt = user.lastLoginAt
|
||||
user.reengagementSentAt = {}
|
||||
queueStudyUsersWrite()
|
||||
const sessionToken = createStudySession(user.id)
|
||||
setStudySessionCookie(res, sessionToken)
|
||||
@@ -359,4 +361,32 @@ export function register(app) {
|
||||
clearStudySessionCookie(res)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
// One-click unsubscribe from re-engagement emails (no login required)
|
||||
app.get('/api/study-auth/unsubscribe-reminders', (req, res) => {
|
||||
const { uid, token } = req.query ?? {}
|
||||
if (typeof uid !== 'string' || typeof token !== 'string') {
|
||||
res.status(400).send('Invalid unsubscribe link.')
|
||||
return
|
||||
}
|
||||
const secret = process.env.RESEND_WEBHOOK_TOKEN ?? 'siteforge'
|
||||
const expected = createHmac('sha256', secret).update(uid).digest('hex')
|
||||
const expectedBuf = Buffer.from(expected, 'hex')
|
||||
const actualBuf = Buffer.from(token.length === expected.length ? token : '', 'hex')
|
||||
let valid = false
|
||||
try { valid = timingSafeEqual(expectedBuf, actualBuf) } catch { valid = false }
|
||||
if (!valid) {
|
||||
res.status(400).send('Invalid or expired unsubscribe link.')
|
||||
return
|
||||
}
|
||||
const user = state.studyUsers.find(u => u.id === uid)
|
||||
if (!user) {
|
||||
res.status(404).send('Account not found.')
|
||||
return
|
||||
}
|
||||
user.studyRemindersEnabled = false
|
||||
user.updatedAt = new Date().toISOString()
|
||||
queueStudyUsersWrite()
|
||||
res.send('<!doctype html><html><head><meta charset="utf-8"><title>Unsubscribed</title><style>body{font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;padding:24px;text-align:center;color:#333}h1{font-size:1.4rem;margin-bottom:12px}p{color:#666;line-height:1.6}</style></head><body><h1>You\'ve been unsubscribed</h1><p>You will no longer receive re-engagement reminder emails. You can re-enable them anytime from your account settings.</p></body></html>')
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user