Three features from this session, full server suite green (535/535).
TOTP 2FA (#100) — backend shipped without a UI; add it:
- Login: mfa_required -> 6-digit challenge (recovery codes accepted) -> /totp/verify.
- Settings > Account: enable (QR + confirm -> recovery codes once), regenerate,
disable; SSO accounts see "managed by your identity provider".
- /totp/setup returns a server-rendered qr_data_url (bundled qrcode dep). keyuri
folds the request Host into the issuer so multi-instance accounts are
distinguishable in the authenticator app.
Email verification on signup — hosted HARD-block / self-host SOFT-nudge:
- email_verified column; existing users asked on first login (SSO + platform
admins grandfathered); single-use 24h tokens (SHA-256 hashed).
- Gate engages only when email is configured (never locks out a no-mail instance).
GET /verify-email + POST /resend-verification (generic, no account enumeration).
- Client: "confirm your email" flow + resend, verified/error toasts, self-host
banner; onAuthSuccess refuses a tokenless response (defensive).
Tizen SSSP URL-Launcher install — Fusion-style one-URL native install:
- Server hosts /tizen/sssp_config.xml (dynamic <size>, always matches the served
.wgt) + /tizen/ScreenTinker.wgt + a human landing. lib/wgt-cache.js resolves the
signed .wgt (/data mount wins, mirroring the APK).
- build-wgt.sh also emits a static sssp_config.xml for CDN hosting.
- Retail panels require a Samsung Partner cert; dev-mode is SDB self-signed only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin signup-notify recipient was hardcoded to
support@screentinker.com and shipped in the open-source code. Combined
with the opt-out SELF_HOSTED gate, any self-hoster who configured their
own Graph credentials but forgot SELF_HOSTED=true would fire their
users' signup PII (email, IP, country) into our support inbox.
Source the recipient from ADMIN_NOTIFY_EMAIL instead, defaulting to
null. When unset, the admin notification is skipped entirely and logged
("[SIGNUP-EMAIL] admin notify skipped (ADMIN_NOTIFY_EMAIL unset)"); the
user's welcome email is unaffected. Hosted prod sets the env var so its
notifications continue; self-hosters send nothing to us by default, and
the .com address no longer ships in code.
Document ADMIN_NOTIFY_EMAIL (and the related mail/self-host vars) in a
new .env.example.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Admin signup notifications were going to dw5304@gmail.com. Route them
to the monitored support@screentinker.com queue instead, so signups
land in the shared inbox rather than a personal account.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every new user now gets a personal welcome email from
"Dan at ScreenTinker" <support@screentinker.com>, 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: <email>").
- 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.