From b67fbaa1b65936425421a2ac7e5523e3988bde47 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Sat, 30 May 2026 14:50:27 -0500 Subject: [PATCH] feat(signup): welcome email + admin signup notification (slice 1) Every new user now gets a personal welcome email from "Dan at ScreenTinker" , and Dan gets an admin notification, immediately after signup. Fired from all three signup paths (local /register, Google, Microsoft) via a shared helper (services/signupEmails.js) at the new-user branch only, so OAuth logins of existing users don't re-trigger. - Reuses the single Microsoft Graph transport (services/email.js). Adds two optional, backward-compatible params: fromName (custom From display name; address stays support@ so replies route there) and rawSubject (skip the "[ScreenTinker] " prefix for clean subjects "Welcome to ScreenTinker" / "New signup: "). - Idempotency: users.welcome_email_sent_at, stamped after the send block; non-null short-circuits so a user is only emailed once. Paired backfill stamps all pre-existing users with sentinel 1 so a future "IS NULL" sweep can't mistake the legacy base for un-welcomed and blast them. - Production-only: gated on !config.selfHosted so self-host operators never emit mail from our domain or CC Dan. - No retry logic by design (no re-trigger path on existing users); per-email {sent, reason} is logged so a Graph hiccup is visible. Admin notification includes workspace org name, email, UTC + Central timestamp, client IP (CF-aware), CF-IPCountry, and user agent. --- server/db/database.js | 11 ++ server/routes/auth.js | 12 +++ server/services/email.js | 14 ++- server/services/signupEmails.js | 177 ++++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 server/services/signupEmails.js diff --git a/server/db/database.js b/server/db/database.js index 1abd792..902da80 100644 --- a/server/db/database.js +++ b/server/db/database.js @@ -142,6 +142,17 @@ const migrations = [ // playlist_items conversion (migrateAssignmentsToPlaylists) dropped this // column. Column ADD is idempotent via the surrounding try/catch loop. "ALTER TABLE playlist_items ADD COLUMN zone_id TEXT REFERENCES layout_zones(id) ON DELETE SET NULL", + // Slice 1: idempotency guard for the one-time signup welcome/admin emails. + // Non-null = this user has already been handled, so we never double-send. + // New signups are stamped with the real unix-seconds time the send block ran + // (see services/signupEmails.js). The paired backfill below stamps every + // pre-existing user with the sentinel value 1, so that a future "IS NULL" + // sweep/nudge can't mistake the legacy user base for un-welcomed accounts and + // blast all of them. Sentinel 1 (vs a real timestamp) also lets a later + // deliberate campaign tell "backfilled, never emailed" apart from "genuinely + // sent at