source: Klonkt/src/services/SubscriberService.js@ 7e9d0ea

main
Last change on this file since 7e9d0ea was 834bcc3, checked in by Robin Genis <roboburr@…>, 3 months ago

i18n: translate Dutch code comments to English across src/

Comments in routes/services/views/config/middleware/assets translated to
English for the public repo. A few dev-facing throw/console message strings
were Englished too. No user-facing UI strings or i18n dictionary values changed
(src/services/i18n.js untouched). Logic unchanged.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 3.7 KB
Line 
1/**
2 * SubscriberService — newsletter subscribers per site (premium feature #1).
3 *
4 * Double opt-in when SMTP is configured (status 'pending' → 'confirmed' via
5 * confirm link), otherwise single opt-in ('confirmed' immediately). Each
6 * subscriber has a token used for both the confirm and unsubscribe links.
7 * Reused by #2 (download-for-email) and #8 (notify-me) as shared subscriber storage.
8 */
9
10import crypto from 'crypto';
11import db from '../config/database.js';
12import { v4 as uuid } from 'uuid';
13
14const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
15
16export function isValidEmail(email) {
17 return typeof email === 'string' && email.length <= 254 && EMAIL_RE.test(email.trim());
18}
19
20function newToken() {
21 return crypto.randomBytes(24).toString('hex');
22}
23
24/**
25 * Add a subscriber (or reactivate an unsubscribed/existing one).
26 * @returns {{ok:boolean, status?:string, token?:string, created?:boolean, error?:string}}
27 * status 'pending' → confirmation still required (send confirm email)
28 * status 'confirmed'→ immediately active (single opt-in)
29 */
30export function addSubscriber(siteId, email, source = 'widget', { doubleOptin = false } = {}) {
31 email = (email || '').trim().toLowerCase();
32 if (!siteId) return { ok: false, error: 'no_site' };
33 if (!isValidEmail(email)) return { ok: false, error: 'invalid_email' };
34
35 const existing = db.prepare('SELECT * FROM subscribers WHERE site_id = ? AND email = ?').get(siteId, email);
36 const status = doubleOptin ? 'pending' : 'confirmed';
37
38 if (existing) {
39 // Already confirmed → nothing to do (idempotent, no duplicate email).
40 if (existing.status === 'confirmed') return { ok: true, status: 'confirmed', token: existing.token, created: false };
41 // Pending or unsubscribed → re-invite/reactivate with a fresh token.
42 const token = newToken();
43 db.prepare("UPDATE subscribers SET status = ?, token = ?, source = ?, confirmed_at = CASE WHEN ? = 'confirmed' THEN CURRENT_TIMESTAMP ELSE NULL END WHERE id = ?")
44 .run(status, token, source, status, existing.id);
45 return { ok: true, status, token, created: false };
46 }
47
48 const token = newToken();
49 db.prepare(
50 "INSERT INTO subscribers (id, site_id, email, status, source, token, confirmed_at) VALUES (?,?,?,?,?,?, CASE WHEN ? = 'confirmed' THEN CURRENT_TIMESTAMP ELSE NULL END)"
51 ).run(uuid(), siteId, email, status, source, token, status);
52 return { ok: true, status, token, created: true };
53}
54
55export function confirm(token) {
56 if (!token) return false;
57 const row = db.prepare('SELECT id FROM subscribers WHERE token = ?').get(token);
58 if (!row) return false;
59 db.prepare("UPDATE subscribers SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP WHERE id = ?").run(row.id);
60 return true;
61}
62
63export function unsubscribe(token) {
64 if (!token) return false;
65 const row = db.prepare('SELECT id FROM subscribers WHERE token = ?').get(token);
66 if (!row) return false;
67 db.prepare("UPDATE subscribers SET status = 'unsub' WHERE id = ?").run(row.id);
68 return true;
69}
70
71/** Confirmed subscribers (email + token) for a site — for sending newsletters.
72 * Optionally filter by source (e.g. 'notify' for show announcements). */
73export function confirmedFor(siteId, source) {
74 if (source) {
75 return db.prepare("SELECT email, token FROM subscribers WHERE site_id = ? AND status = 'confirmed' AND source = ?").all(siteId, source);
76 }
77 return db.prepare("SELECT email, token FROM subscribers WHERE site_id = ? AND status = 'confirmed'").all(siteId);
78}
79
80export function counts(siteId) {
81 const c = (st) => db.prepare('SELECT COUNT(*) AS n FROM subscribers WHERE site_id = ? AND status = ?').get(siteId, st).n;
82 return { confirmed: c('confirmed'), pending: c('pending'), unsub: c('unsub') };
83}
Note: See TracBrowser for help on using the repository browser.