Commit Graph

26 Commits

Author SHA1 Message Date
nmemmert 87090c1c19 Fix: mark approve handler async for await sendResendEmailWithRetry; v1.0.13
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-22 12:59:41 -04:00
nmemmert d74b7523d3 Add notify-on-answer email and save-as-image for Q&A; v1.0.10
- Contact form: notifyOnAnswer checkbox (shown for Bible Questions only)
- Contact route: stores notifyOnAnswer flag on question record
- Email: buildQuestionAnsweredEmailTemplate for branded notification
- Questions route: sends notification email on approve when notifyOnAnswer + email + answer are set
- Q&A section: Canvas API "Save as image" button generates 1080x1080 PNG quote card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-22 12:51:38 -04:00
nmemmert 172e4a9358 Add version numbering (v1.0.0)
- package.json: bump to 1.0.0
- server/config.js: export APP_VERSION (from package.json) and GIT_COMMIT
  (from COMMIT_SHA env var set by CI, or git rev-parse --short HEAD fallback)
- GET /api/version: new public endpoint returning { version, commit }
- GET /api/admin-auth/status: includes version and commit in response
- AdminPage sidebar: displays version string (e.g. "v1.0.0 (1d43875)")
  below the Log Out button, styled as muted metadata text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 07:55:52 -04:00
nmemmert 1d43875e5a Fix 11 bugs: restore crash, draft leak, email failures, memory leaks
Critical fixes:
- sanitizeLoadedHitStats/VisitorStats: restore full state shape so a
  snapshot restore no longer crashes hit-counting middleware (missing
  byPathReal, byPathBot, byDayReal, byDayBot, botReasons, ipHashIndex)
- /questions/share/🆔 read state.questions only, not draft questions
- inbound-email: validate date with Number.isFinite before toISOString
- study-reminders: wrap each send in try/catch so one failure doesn't
  block remaining users; persist sent-markers after each success

Security:
- getClientIp: use req.ip (trust-proxy-resolved) instead of raw
  x-forwarded-for header to prevent IP spoofing
- env-snapshot.env: delete immediately after backup tar stream ends
  so secrets don't linger on disk between exports

Correctness / UX:
- contact form: email failures no longer 500 the user after the
  submission is already saved; log and fall through instead
- study-account profile: cap data URI avatar at 6 MB
- admin enrollment PATCH: validate slug against study catalog
- signup: return 503 at MAX_STUDY_USERS instead of silently dropping
  oldest accounts

Memory leaks:
- contactHits, downloadHits Maps: prune stale entries at 5000 entries
- resendEmailSubmissionIndex: trim to 2000 entries (oldest first)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 07:48:54 -04:00
nmemmert f5e98826be Include env settings snapshot in full data backup
Docker deployments have no .env file — settings live in container env
vars — so the export now writes env-snapshot.env (dotenv format) into
the data dir from a whitelist of portable keys (admin password, Resend
keys, webhook URLs, etc.) before archiving. Machine-specific vars
(PORT, NODE_ENV, SITEFORGE_DATA_DIR, ALLOW_INSECURE_COOKIES, build
metadata) are excluded. Values are not applied automatically on
restore; the file documents what to set on the new server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:12:51 -04:00
nmemmert f4fd177421 Add full /data backup export/import for server migration
- New admin endpoints: GET /api/admin-backup/export streams the entire
  data directory as tar.gz (after flushing queued writes); POST
  /api/admin-backup/import validates the archive, snapshots current
  data, replaces the folder, and reloads all in-memory state.
- Admin UI: "Full Data Backup" section with download and restore-from-
  file controls in the Analytics panel.
- Fix admin TOTP secret path to honor SITEFORGE_DATA_DIR (moved base
  path resolution to server/paths.js to avoid a circular import), so
  2FA for admin and study users survives backup/restore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 12:08:23 -04:00
nmemmert 17c9cbbc8b Fix inbound-email data loss, MIME truncation, and header injection
- server/data.js: preserve source/htmlBody/inboundTo/messageId across
  server restarts (sanitizeLoadedContactSubmissions was silently
  dropping them on reload from disk)
- cloudflare/email-worker.js: rewrite MIME parsing to split on the
  actual boundary marker instead of any literal "--", unfold
  multi-line headers, and correctly recombine multi-byte UTF-8 in
  quoted-printable decoding
- server/routes/inbound-email.js: validate Message-ID against RFC 5322
  grammar before storing/using it, and compare the webhook secret with
  timingSafeEqual to match the rest of the codebase's auth checks
- server/routes/contact.js: re-validate messageId at the point it's
  injected into outgoing In-Reply-To/References headers; move the
  allowed reply-from addresses into a shared config constant
- src/AdminPage.tsx: 30s inbox poll now syncs field updates (e.g.
  archived) on already-loaded submissions instead of only appending
  new ones; consolidate the duplicated from-address list
- .claude/launch.json: add a vite dev server preview config used to
  verify these changes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 08:21:50 -04:00
nmemmert fbb9a24f32 Add per-reply from-address selector and inbox address badges
- Reply composer shows a From dropdown (hello@ or nate@), defaulting
  to whichever address the inbound email was sent to
- Server validates the chosen address against an allowlist before sending
- Inbox list shows colored address badges: blue for hello@, purple for nate@

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 12:14:34 -04:00
nmemmert 77a4f9300f Thread admin replies to original email via In-Reply-To/References headers
When replying to an inbound email that has a messageId, the outgoing
Resend payload now includes In-Reply-To and References headers so the
reply threads correctly in Gmail and other email clients.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 11:36:11 -04:00
nmemmert 118314d0d7 Render inbound email HTML body in admin inbox
Worker now extracts both text/plain and text/html MIME parts and sends
htmlBody in the webhook payload. Server stores it on the submission.
Admin inbox renders htmlBody in a sandboxed iframe when present, falling
back to plain text with whitespace preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 11:32:52 -04:00
nmemmert 4b411f763f Add inbound email capture via Cloudflare Email Worker
- Cloudflare Email Worker forwards hello@ and nate@ to Gmail and POSTs
  parsed MIME email to /api/inbound-email as admin inbox entries
- New server route authenticates via shared secret and deduplicates by
  Message-ID before storing inbound emails as contact submissions
- Admin inbox shows "email" badge for inbound messages; reply composer
  opens blank with auto-subject "Re: [original]" and signature preview
- Documents setup steps in cloudflare/email-worker.js and .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 11:23:27 -04:00
nmemmert 2eb98b66d0 Add Finished Books feature with episode playlists
- New FinishedBook type in content.ts + finishedBooks[] field on SiteContent
- RSS parser now extracts itunes:season into each episode object
- /finished grid page and /finished/:id playlist page (filters episodes by season)
- Episodes nav link replaced with dropdown: Current Series / Finished Books
- Finished Books link added to footer
- Admin: "End Current Series & Start New Book" wizard on Current Series tab
- Admin: Finished Books management tab (add/edit/remove entries)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 10:22:37 -04:00
nmemmert e0ed798741 easter egg 2026-06-18 10:03:32 -04:00
nmemmert 9174331d2d Bug fixes (real breakage):
Newsletter nudge — was calling /api/study-account/profile (wrong endpoint, ignored subscription). Now correctly calls /api/study-account/preferences with PATCH.
Security:

Email regex — replaced the permissive [^\s@]+@[^\s@]+ pattern with a proper RFC-compliant regex in contact.js and downloads.js
Avatar magic bytes — server now checks actual PNG/JPEG/GIF/WEBP header bytes, not just the data URL prefix
Certificate rate limit — public /api/public/certificate/:token now has a 30 req/15min limiter
Session absolute TTL — admin sessions now have a 30-day hard cap; a stolen token can no longer be kept alive indefinitely by passive reads
Account lockout — 5 failed logins locks a study account for 1 hour
CSP headers — Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy headers added globally
Data integrity:

Cascade delete — deleting a study account now also removes their certificates, community posts, comments, and progress file
UX / reliability:

Escape key on modals — all 3 modal groups (study index, notes, account) now close on Escape
Display name min-length — empty spaces-only names rejected; if provided, must be ≥2 chars
Note save rate limit — 30 saves/minute per user max
Analytics fetch timeout — 5s AbortController so a hanging server doesn't block the browser indefinitely
Email validation on signup — frontend catches bad email formats before hitting the server
Cleanup:

Deduplicated download forms — StudyDownloadForm and ResourceDownloadForm now share a single DownloadForm base; both are now thin wrappers
2026-06-18 09:35:44 -04:00
nmemmert 31426fccba Add comprehensive visitor engagement tracking and scroll-to-top on navigation
Tracks scroll depth (25/50/75/90%), time on page, UTM parameters, outbound
link clicks, search queries, audio pause/completion/listen time, and 404s.
Logged-in study users are now tied to their visitor record and surfaced in
the admin recent visits table. Scroll position resets on every route change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:06:21 -04:00
nmemmert 8450d81e7f Add per-episode play tracking with bar chart in admin analytics
- POST /api/analytics/play records a play event (title + date) on first
  play per player mount, skipping admin sessions
- Plays persisted to data/episode-plays.json with total + byDay buckets
- Admin stats response includes episodePlays sorted by total plays
- AnalyticsPanel shows horizontal bar chart + summary cards for all
  episodes dynamically — new episodes appear automatically as played

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:39:35 -04:00
nmemmert 54bc7b28da Fix RSS feed URL to use correct Anchor s/ path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:29:09 -04:00
nmemmert 1010cb1f33 Update study reminders timestamp and exclude admin sessions from pageview analytics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:24:48 -04:00
nmemmert a1d5889711 Replace Spotify embeds with branded custom audio player backed by RSS
- Add /api/episode-audio route returning MP3 URLs from Anchor RSS feed
- Replace Spotify URL text inputs in admin with RSS episode dropdowns
- New EpisodeAudioPlayer component with podcast art, show name, progress
  bar, and Spotify icon link — full and compact sizes
- Backward-compatible: legacy Spotify embed URLs still render as iframes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:23:19 -04:00
nmemmert 48c2f81d00 Add QR code tracker to admin panel
Each QR code gets a /qr/<slug> redirect that logs scans (IP, user agent,
timestamp) to disk. The admin QR Codes view lets you add, edit, enable/disable,
and delete codes, with per-code scan history inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 09:33:35 -04:00
nmemmert c4645e3475 Add cross-site improvements across three phases
Phase 1 — Quick wins:
- Image lazy-loading on series/resource cards
- Newsletter signup added to Episodes page (before highlights)
- Per-route meta tags via usePageMeta hook (title, og:title, og:description)
- Breadcrumbs on study index, section, and notes pages
- SVG completion checkmark badges on study section list
- Analytics time-range filter (7d / 30d / 90d) in admin panel

Phase 2 — Medium features:
- Related episodes on archived series detail pages
- Resource library two-tier filter (type + tag chips)
- Global search (Fuse.js) moved below sticky header as full-width bar
- Q&A anonymous upvoting with localStorage dedup + admin pin/unpin
- Study enrollment funnel tracking (firstVisitAt, firstCompletionAt) with funnel chart in analytics

Phase 3 — Larger features:
- Study section comments (auto-approve for enrolled users, admin moderation panel)
- Study completion certificate (canvas render, PNG download, shareable public URL)
- Episode script full-text search (mammoth docx extraction, server-side search, admin upload UI)
- Reflection questions renamed from Discussion Questions; quiz answers can be shared to section discussion
- Public certificate route at /certificate/:token with og meta tags
- Comment moderation panel added to admin under Manage > Study Comments

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 09:10:29 -04:00
nmemmert 3e1b1e0fae Enrolment issue 2026-06-08 14:45:04 -04:00
nmemmert 95b7a3f211 Add in /salvation 2026-06-08 10:24:37 -04:00
nmemmert cb78b6522d All green. You're good to build and deploy. Here's a summary of everything done since your last commit:
---

**🐛 Bugs fixed**
- Study hub/lesson pages turned gold — CSS merge error with `.study-index-page` fixed
- Lesson announcements not saving — `announcement` field missing from `sanitizeColossiansStudySections` whitelist
- 2FA email method blocked with "not configured" — guard checked `totpSecret` instead of `twoFaMethod`
- `STUDY_REMINDERS_FILE` constant never defined — server would crash on first reminder write
- `sanitizeStudyUsers` stripped all 2FA fields — data lost on every restart
- `scheduleStudyReminders` missing from refactored modules

** Features added**
- Commentary supports paragraph formatting (double newline = new paragraph)
- Lesson announcement banner — full-width gold stripe at top of lesson page
- "Take the Quiz" renamed to "Discussion Questions" throughout
- Students must complete checkpoint before unlocking next lesson (lesson list + Next Lesson button both gate on it)
- No checkpoint questions → simple Mark as Completed button as fallback
- Truth For Life syndication widget on homepage
- Newsletter welcome email updated to your new HTML template

**🔧 Maintenance**
- `server.js` refactored from 6,013 lines into 12 focused modules
2026-06-04 09:17:34 -04:00
nmemmert 07519b4f48 auth fix 2026-06-03 16:14:49 -04:00
nmemmert 346ecb217c refractor server.js 2026-06-03 15:08:14 -04:00