source: Klonkt/src/services/SubscriberService.js@ 8f2f97c

main
Last change on this file since 8f2f97c was 8d32dcf, checked in by roboburr <roboburr@…>, 3 months ago

Show agenda + notify-me (premium feature #8) — last of the 8

  • DB: shows (site_id, date, time, city, venue, country, ticket_url, notes).
  • routes/shows.js (public, premium-gated): GET /shows (upcoming shows + notify form), POST /shows/notify (subscribers source 'notify', double opt-in if SMTP; confirm/unsub via the generic /nieuwsbrief links).
  • routes/admin-shows.js (site admin + premium): GET /admin/shows (list + add form + subscriber count), POST / (add; optional notify email to 'notify' subscribers if SMTP is available), POST /:id/delete.
  • SubscriberService.confirmedFor(siteId, source?) — optional source filter ('notify').
  • views: pages/shows.ejs + pages/admin-shows.ejs. Dashboard 📅 Agenda link.

Notify email requires SMTP (like #1/#2 sending); without SMTP the show is saved
and sign-ups still work. node --check passed.

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

  • Property mode set to 100644
File size: 3.7 KB
RevLine 
[2e247e4]1/**
2 * SubscriberService — nieuwsbrief-abonnees per site (premium feature #1).
3 *
4 * Double opt-in als SMTP er is (status 'pending' → 'confirmed' via confirm-link),
5 * anders single opt-in ('confirmed' meteen). Elke abonnee heeft een token dat zowel
6 * de confirm- als de unsubscribe-link draagt. Hergebruikt door #2 (download-voor-
7 * email) en #8 (notify-me) als gedeelde abonnee-opslag.
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 * Voeg een abonnee toe (of heractiveer een uitgeschreven/bestaande).
26 * @returns {{ok:boolean, status?:string, token?:string, created?:boolean, error?:string}}
27 * status 'pending' → er moet nog bevestigd worden (stuur confirm-mail)
28 * status 'confirmed'→ direct actief (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 // Al actief → niets te doen (idempotent, geen dubbele mail).
40 if (existing.status === 'confirmed') return { ok: true, status: 'confirmed', token: existing.token, created: false };
41 // Pending of uitgeschreven → opnieuw uitnodigen/activeren met een verse 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
[8d32dcf]71/** Bevestigde abonnees (email + token) voor een site — voor het versturen.
72 * Optioneel filteren op bron (bv. 'notify' voor show-aankondigingen). */
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 }
[2e247e4]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.