From 7f56060d6b23433c34a8319361aaa0d8b4e7d77f Mon Sep 17 00:00:00 2001 From: nmemmert Date: Thu, 30 Apr 2026 15:47:10 -0400 Subject: [PATCH] Beta: admin assets, resource download forms, and resource page redesign --- data/admin-content-draft.json | 17 +- data/admin-content.json | 17 +- ...iscussion_questions_post-1777565348551.png | Bin 0 -> 71248 bytes data/uploads/titus-series-1777568297943.png | Bin 0 -> 27849 bytes server.js | 530 +++++++------ server/auth.js | 85 +++ server/helpers.js | 412 ++++++++++ src/AdminPage.tsx | 435 +++++++---- src/App.css | 259 ++++++- src/App.tsx | 703 ++++-------------- src/components/ContactForm.tsx | 111 +++ src/components/QASection.tsx | 223 ++++++ src/content.ts | 206 +++++ src/icons.tsx | 31 + 14 files changed, 2000 insertions(+), 1029 deletions(-) create mode 100644 data/uploads/discussion_questions_post-1777565348551.png create mode 100644 data/uploads/titus-series-1777568297943.png create mode 100644 server/auth.js create mode 100644 server/helpers.js create mode 100644 src/components/ContactForm.tsx create mode 100644 src/components/QASection.tsx create mode 100644 src/content.ts create mode 100644 src/icons.tsx diff --git a/data/admin-content-draft.json b/data/admin-content-draft.json index 8dac51b..2d76a58 100644 --- a/data/admin-content-draft.json +++ b/data/admin-content-draft.json @@ -17,17 +17,28 @@ "aboutShowP1": "Verse by Verse with Nate walks through Scripture passage by passage — unpacking the original context, drawing out the meaning, and connecting each verse to how we live today.", "aboutShowP2": "Whether you're in the car, at the gym, or just looking for something to anchor your day, each episode is designed to feed your faith with solid, practical teaching.", "aboutNate": "Nate Emmert is a husband, dad, and lifelong student of the Bible from Lynchburg, Va. He's not a pastor or a professor — just someone who fell in love with digging into Scripture and wanted to bring others along for the journey. He created Verse by Verse to make deep Bible study accessible to anyone, whether you've read the Bible your whole life or you're just getting started. No seminary required. No prior knowledge assumed. Just the Word, unpacked verse by verse.", + "aboutPhotoUrl": "/images/nate-photo.jpeg", + "aboutVerseArtUrl": "/images/hebrews-4-12-verse-art.png", + "contactPhotoUrl": "/images/nate-contact-photo.png", "seriesLabel": "Now Playing", "seriesTitle": "Study of Titus: Sound Doctrine", "seriesDescription": "A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.", - "seriesImageUrl": "/images/titus-cover.png", + "seriesImageUrl": "/uploads/titus-series-1777568297943.png", "seriesListenUrl": "https://open.spotify.com/show/0Gq1TzoJOdReSZ1gYQi8Xl", "studyGuideTitle": "Companion Study Guide", "studyGuideDescription": "Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.", "studyGuideUrl": "https://a.co/d/01sG2tOJ", "shareHeading": "Help one more person hear the Word this week.", "shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.", - "customLinks": [], + "customLinks": [ + { + "id": "molso9ow", + "label": "test", + "url": "/uploads/discussion_questions_post-1777565348551.png", + "imageUrl": "/uploads/discussion_questions_post-1777565348551.png", + "placement": "resources" + } + ], "customBlocks": [], "archivedSeries": [], "redirects": [ @@ -97,5 +108,5 @@ ] } }, - "updatedAt": "2026-04-27T20:24:22.589Z" + "updatedAt": "2026-04-30T18:06:00.013Z" } \ No newline at end of file diff --git a/data/admin-content.json b/data/admin-content.json index ecbb29e..5666ead 100644 --- a/data/admin-content.json +++ b/data/admin-content.json @@ -17,17 +17,28 @@ "aboutShowP1": "Verse by Verse with Nate walks through Scripture passage by passage — unpacking the original context, drawing out the meaning, and connecting each verse to how we live today.", "aboutShowP2": "Whether you're in the car, at the gym, or just looking for something to anchor your day, each episode is designed to feed your faith with solid, practical teaching.", "aboutNate": "Nate Emmert is a husband, dad, and lifelong student of the Bible from Lynchburg, Va. He's not a pastor or a professor — just someone who fell in love with digging into Scripture and wanted to bring others along for the journey. He created Verse by Verse to make deep Bible study accessible to anyone, whether you've read the Bible your whole life or you're just getting started. No seminary required. No prior knowledge assumed. Just the Word, unpacked verse by verse.", + "aboutPhotoUrl": "/images/nate-photo.jpeg", + "aboutVerseArtUrl": "/images/hebrews-4-12-verse-art.png", + "contactPhotoUrl": "/images/nate-contact-photo.png", "seriesLabel": "Now Playing", "seriesTitle": "Study of Titus: Sound Doctrine", "seriesDescription": "A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.", - "seriesImageUrl": "/images/titus-cover.png", + "seriesImageUrl": "/uploads/titus-series-1777568297943.png", "seriesListenUrl": "https://open.spotify.com/show/0Gq1TzoJOdReSZ1gYQi8Xl", "studyGuideTitle": "Companion Study Guide", "studyGuideDescription": "Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.", "studyGuideUrl": "https://a.co/d/01sG2tOJ", "shareHeading": "Help one more person hear the Word this week.", "shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.", - "customLinks": [], + "customLinks": [ + { + "id": "molso9ow", + "label": "test", + "url": "/uploads/discussion_questions_post-1777565348551.png", + "imageUrl": "/uploads/discussion_questions_post-1777565348551.png", + "placement": "resources" + } + ], "customBlocks": [], "archivedSeries": [], "redirects": [ @@ -97,5 +108,5 @@ ] } }, - "updatedAt": "2026-04-27T20:24:19.789Z" + "updatedAt": "2026-04-30T18:06:00.017Z" } \ No newline at end of file diff --git a/data/uploads/discussion_questions_post-1777565348551.png b/data/uploads/discussion_questions_post-1777565348551.png new file mode 100644 index 0000000000000000000000000000000000000000..74e82e5e9f7903c2b67c58ce97ad82d3c56a5c5a GIT binary patch literal 71248 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 yys){t_^{ddjH}> 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

ShsefdxsRKSu1qwf6}?ac^xF)T$}$5t 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�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/g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} - -function splitName(fullName) { - const parts = fullName.trim().split(/\s+/).filter(Boolean) - return { - firstName: parts[0] ?? '', - lastName: parts.slice(1).join(' '), - } -} +import { + sanitizeSiteContent, + escapeHtml, + escapeXml, + buildAbsoluteUrl, + injectSeoIntoHtml, + normalizeAssetBaseName, + inferImageExtensionFromDataUrl, + getClientIp, + hasVisitorConsent, + setConsentCookie, + splitName, + parseCookies, +} from './server/helpers.js' +import { + isAdminPasswordConfigured, + isValidAdminSession, + requireAdminAuth, + setAdminSessionCookie, + clearAdminSessionCookie, + createAdminSession, + deleteAdminSession, + validateAdminPasswordSetup, + isAdminPasswordValid, +} from './server/auth.js' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -31,13 +39,16 @@ const HIT_STATS_FILE = path.join(DATA_DIR, 'hit-stats.json') const VISITOR_STATS_FILE = path.join(DATA_DIR, 'visitor-stats.json') const CONTACT_SUBMISSIONS_FILE = path.join(DATA_DIR, 'contact-submissions.json') const QUESTIONS_FILE = path.join(DATA_DIR, 'questions.json') +const DRAFT_QUESTIONS_FILE = path.join(DATA_DIR, 'questions-draft.json') const CHATBOT_FILE = path.join(DATA_DIR, 'chatbot-content.json') const BACKUP_DIR = path.join(DATA_DIR, 'backups') const UPLOADS_DIR = path.join(DATA_DIR, 'uploads') +const UPLOADS_META_FILE = path.join(DATA_DIR, 'uploads-meta.json') const DIST_DIR = path.join(__dirname, 'dist') const INDEX_FILE = path.join(DIST_DIR, 'index.html') const DIST_IMAGES_DIR = path.join(DIST_DIR, 'images') const PUBLIC_IMAGES_DIR = path.join(__dirname, 'public', 'images') +validateAdminPasswordSetup() const TITUS_STUDY_FILE = process.env.TITUS_STUDY_FILE ? path.resolve(__dirname, process.env.TITUS_STUDY_FILE) : path.join(__dirname, 'A_Study_of_Titus.pdf') @@ -62,6 +73,50 @@ const DEFAULT_REDIRECT_RULES = [ statusCode: 301, }, ] + +function normalizeRedirectPath(value) { + if (typeof value !== 'string') return '' + const trimmed = value.trim() + if (!trimmed) return '' + const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}` + const normalized = withSlash.replace(/\/+/g, '/') + if (normalized === '/') return '' + if (normalized.startsWith('/api/') || normalized.startsWith('/admin')) return '' + return normalized +} + +function sanitizeUrl(value) { + if (typeof value !== 'string') return '' + const trimmed = value.trim() + if (!trimmed) return '' + if (trimmed.startsWith('/')) return trimmed + if (/^https?:\/\//i.test(trimmed)) return trimmed + return '' +} + +function sanitizeRedirectRules(value) { + const source = Array.isArray(value) ? value : [] + const seen = new Set() + const out = [] + + for (const item of source) { + const pathValue = normalizeRedirectPath(item?.path) + const target = sanitizeUrl(item?.target) + const statusCode = Number(item?.statusCode) === 302 ? 302 : 301 + if (!pathValue || !target) continue + if (seen.has(pathValue)) continue + seen.add(pathValue) + out.push({ + id: typeof item?.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + path: pathValue, + target, + statusCode, + }) + } + + return out.length > 0 ? out : DEFAULT_REDIRECT_RULES +} + const DEFAULT_SEO = { title: 'Verse by Verse with Nate', description: 'Verse by Verse with Nate explores Scripture one verse at a time with practical Bible teaching.', @@ -95,130 +150,8 @@ const DEFAULT_PUBLISH_STATE = { let cachedSiteContent = null let cachedDraftSiteContent = null let publishState = { ...DEFAULT_PUBLISH_STATE } - -function sanitizeUrl(value) { - if (typeof value !== 'string') return '' - const trimmed = value.trim() - if (!trimmed) return '' - if (trimmed.startsWith('/')) return trimmed - if (/^https?:\/\//i.test(trimmed)) return trimmed - return '' -} - -function normalizeRedirectPath(value) { - if (typeof value !== 'string') return '' - const trimmed = value.trim() - if (!trimmed) return '' - const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}` - const normalized = withSlash.replace(/\/+/g, '/') - if (normalized === '/') return '' - if (normalized.startsWith('/api/') || normalized.startsWith('/admin')) return '' - return normalized -} - -function normalizeSitemapPath(value) { - if (typeof value !== 'string') return '' - const trimmed = value.trim() - if (!trimmed) return '' - if (trimmed === '/') return '/' - return normalizeRedirectPath(trimmed) -} - -function sanitizeRedirectRules(value) { - const source = Array.isArray(value) ? value : [] - const seen = new Set() - const out = [] - - for (const item of source) { - const pathValue = normalizeRedirectPath(item?.path) - const target = sanitizeUrl(item?.target) - const statusCode = Number(item?.statusCode) === 302 ? 302 : 301 - if (!pathValue || !target) continue - if (seen.has(pathValue)) continue - seen.add(pathValue) - out.push({ - id: typeof item?.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), - path: pathValue, - target, - statusCode, - }) - } - - return out.length > 0 ? out : DEFAULT_REDIRECT_RULES -} - -function sanitizeFeaturedLinks(value) { - const source = Array.isArray(value) ? value : [] - return source - .filter(item => item && typeof item === 'object') - .map(item => { - const discussionQuestions = Array.isArray(item.discussionQuestions) - ? item.discussionQuestions - .filter(question => typeof question === 'string') - .map(question => question.trim()) - .filter(Boolean) - .slice(0, 30) - : [] - - return { - id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), - title: typeof item.title === 'string' ? item.title.trim().slice(0, 140) : '', - episodeNumber: typeof item.episodeNumber === 'string' ? item.episodeNumber.trim().slice(0, 20) : '', - summary: typeof item.summary === 'string' ? item.summary.trim().slice(0, 600) : '', - url: sanitizeUrl(item.url), - embedUrl: sanitizeUrl(item.embedUrl), - showNotes: typeof item.showNotes === 'string' ? item.showNotes.trim().slice(0, 10000) : '', - discussionQuestions, - } - }) - .filter(item => item.title || item.summary || item.url || item.embedUrl || item.showNotes || item.discussionQuestions.length > 0) -} - -function sanitizeSiteContent(siteContent) { - if (!siteContent || typeof siteContent !== 'object' || Array.isArray(siteContent)) return {} - - const seo = siteContent.seo && typeof siteContent.seo === 'object' ? siteContent.seo : {} - const legal = siteContent.legal && typeof siteContent.legal === 'object' ? siteContent.legal : {} - - return { - ...siteContent, - redirects: sanitizeRedirectRules(siteContent.redirects), - podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS), - seo: { - title: typeof seo.title === 'string' && seo.title.trim() ? seo.title.trim().slice(0, 120) : DEFAULT_SEO.title, - description: typeof seo.description === 'string' && seo.description.trim() ? seo.description.trim().slice(0, 240) : DEFAULT_SEO.description, - ogTitle: typeof seo.ogTitle === 'string' && seo.ogTitle.trim() ? seo.ogTitle.trim().slice(0, 120) : DEFAULT_SEO.ogTitle, - ogDescription: typeof seo.ogDescription === 'string' && seo.ogDescription.trim() ? seo.ogDescription.trim().slice(0, 240) : DEFAULT_SEO.ogDescription, - ogImage: sanitizeUrl(seo.ogImage) || DEFAULT_SEO.ogImage, - canonicalUrl: sanitizeUrl(seo.canonicalUrl) || DEFAULT_SEO.canonicalUrl, - robotsPolicy: typeof seo.robotsPolicy === 'string' && seo.robotsPolicy.trim() ? seo.robotsPolicy.trim() : DEFAULT_SEO.robotsPolicy, - sitemapPaths: Array.isArray(seo.sitemapPaths) - ? seo.sitemapPaths - .map(pathItem => normalizeSitemapPath(pathItem)) - .filter(Boolean) - : [...DEFAULT_SEO.sitemapPaths], - }, - legal: { - privacyTitle: typeof legal.privacyTitle === 'string' && legal.privacyTitle.trim() ? legal.privacyTitle.trim().slice(0, 120) : DEFAULT_LEGAL.privacyTitle, - privacyBody: Array.isArray(legal.privacyBody) && legal.privacyBody.length > 0 - ? legal.privacyBody.filter(line => typeof line === 'string').map(line => line.trim()).filter(Boolean).slice(0, 20) - : [...DEFAULT_LEGAL.privacyBody], - termsTitle: typeof legal.termsTitle === 'string' && legal.termsTitle.trim() ? legal.termsTitle.trim().slice(0, 120) : DEFAULT_LEGAL.termsTitle, - termsBody: Array.isArray(legal.termsBody) && legal.termsBody.length > 0 - ? legal.termsBody.filter(line => typeof line === 'string').map(line => line.trim()).filter(Boolean).slice(0, 20) - : [...DEFAULT_LEGAL.termsBody], - }, - } -} - -function escapeXml(value) { - return String(value) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} +let draftQuestions = null +let draftQuestionsWritePromise = Promise.resolve() async function loadSiteContentFile(filePath) { const raw = await readFile(filePath, 'utf8') @@ -252,61 +185,50 @@ async function refreshContentCaches() { } } -function buildAbsoluteUrl(baseUrl, maybeRelativePath) { - const safeBase = typeof baseUrl === 'string' && baseUrl.trim() ? baseUrl.trim() : DEFAULT_SEO.canonicalUrl - const root = safeBase.endsWith('/') ? safeBase.slice(0, -1) : safeBase - if (typeof maybeRelativePath !== 'string' || !maybeRelativePath.trim()) return root - const value = maybeRelativePath.trim() - if (/^https?:\/\//i.test(value)) return value - if (value.startsWith('/')) return `${root}${value}` - return `${root}/${value}` +async function loadDraftQuestionsFromDisk() { + return readFile(DRAFT_QUESTIONS_FILE, 'utf8') + .then(raw => { + const parsed = JSON.parse(raw) + if (Array.isArray(parsed)) { + draftQuestions = parsed.slice(0, MAX_QUESTIONS) + } else if (Array.isArray(parsed?.questions)) { + draftQuestions = parsed.questions.slice(0, MAX_QUESTIONS) + } else { + draftQuestions = null + } + if (typeof parsed?.updatedAt === 'string') { + publishState.draftUpdatedAt = parsed.updatedAt + } + }) + .catch(() => { + draftQuestions = null + }) } -function injectSeoIntoHtml(html, siteContent) { - const seo = siteContent?.seo ?? DEFAULT_SEO - const title = seo.title || DEFAULT_SEO.title - const description = seo.description || DEFAULT_SEO.description - const ogTitle = seo.ogTitle || title - const ogDescription = seo.ogDescription || description - const canonical = buildAbsoluteUrl(seo.canonicalUrl || DEFAULT_SEO.canonicalUrl, '/') - const ogImage = buildAbsoluteUrl(canonical, seo.ogImage || DEFAULT_SEO.ogImage) - const robots = seo.robotsPolicy || DEFAULT_SEO.robotsPolicy - - return html - .replace(/[\s\S]*?<\/title>/i, `<title>${escapeHtml(title)}`) - .replace(//i, ``) - .replace(//i, ``) - .replace(//i, ``) - .replace(//i, ``) - .replace(//i, ``) - .replace(//i, ``) - .replace(//i, ``) - .replace(//i, ``) +function ensureDraftQuestions() { + if (draftQuestions !== null) return + draftQuestions = questions.slice(0, MAX_QUESTIONS) } -function normalizeAssetBaseName(name) { - if (typeof name !== 'string') return `upload-${Date.now()}` - const cleaned = name - .toLowerCase() - .replace(/[^a-z0-9._-]+/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') - return cleaned || `upload-${Date.now()}` +async function readUploadsMetadata() { + try { + const raw = await readFile(UPLOADS_META_FILE, 'utf8') + return JSON.parse(raw) + } catch { + return {} + } } -function inferImageExtensionFromDataUrl(dataUrl) { - if (typeof dataUrl !== 'string') return null - if (dataUrl.startsWith('data:image/png;base64,')) return '.png' - if (dataUrl.startsWith('data:image/jpeg;base64,')) return '.jpg' - if (dataUrl.startsWith('data:image/webp;base64,')) return '.webp' - if (dataUrl.startsWith('data:image/gif;base64,')) return '.gif' - return null +async function writeUploadsMetadata(metadata) { + await mkdir(DATA_DIR, { recursive: true }) + await writeFile(UPLOADS_META_FILE, JSON.stringify(metadata, null, 2), 'utf8') } async function listUploadedAssets() { await mkdir(UPLOADS_DIR, { recursive: true }) const files = await readdir(UPLOADS_DIR) const imageFiles = files.filter(name => /\.(png|jpe?g|webp|gif)$/i.test(name)).sort() + const metadata = await readUploadsMetadata() const withStats = await Promise.all(imageFiles.map(async filename => { const info = await stat(path.join(UPLOADS_DIR, filename)) @@ -315,6 +237,7 @@ async function listUploadedAssets() { url: `/uploads/${filename}`, sizeBytes: info.size, updatedAt: info.mtime.toISOString(), + tags: Array.isArray(metadata[filename]) ? metadata[filename].filter(tag => typeof tag === 'string') : [], } })) @@ -362,13 +285,11 @@ let hitStatsWritePromise = Promise.resolve() const VISITOR_COOKIE = 'vbn_vid' const CONSENT_COOKIE = 'vbn_analytics_consent' -const ADMIN_SESSION_COOKIE = 'vbn_admin_session' const MAX_RECENT_VISITS = 1000 const VISITOR_RETENTION_DAYS_DEFAULT = 180 const BACKUP_RETENTION_DAYS = 30 const BACKUP_INTERVAL_MS = 24 * 60 * 60 * 1000 const ADMIN_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000 -const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD ?? 'change-me-admin-password' const EMPTY_VISITOR_STATS = { totalVisits: 0, @@ -397,47 +318,6 @@ let lastHitStatsWrite = { ok: true, at: null, error: null } let lastBackupStatus = { ok: true, at: null, error: null, file: null } let lastCachePurgeStatus = { ok: true, at: null, error: null } let lastDeployHookStatus = { ok: true, at: null, error: null } -const adminSessions = new Map() - -function sha256(value) { - return createHash('sha256').update(value).digest('hex') -} - -function isAdminPasswordConfigured() { - return ADMIN_PASSWORD !== 'change-me-admin-password' -} - -function isValidAdminSession(req) { - const cookies = parseCookies(req.headers.cookie) - const sessionToken = cookies[ADMIN_SESSION_COOKIE] - if (!sessionToken) return false - - const expiresAt = adminSessions.get(sessionToken) - if (!expiresAt) return false - if (expiresAt <= Date.now()) { - adminSessions.delete(sessionToken) - return false - } - - adminSessions.set(sessionToken, Date.now() + ADMIN_SESSION_TTL_MS) - return true -} - -function setAdminSessionCookie(res, token) { - res.append('Set-Cookie', `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(token)}; Max-Age=${Math.floor(ADMIN_SESSION_TTL_MS / 1000)}; Path=/; HttpOnly; SameSite=Lax`) -} - -function clearAdminSessionCookie(res) { - res.append('Set-Cookie', `${ADMIN_SESSION_COOKIE}=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax`) -} - -function requireAdminAuth(req, res, next) { - if (!isValidAdminSession(req)) { - res.status(401).json({ message: 'Unauthorized' }) - return - } - next() -} function normalizeIp(rawIp) { if (!rawIp) return 'unknown' @@ -459,45 +339,6 @@ function normalizeIp(rawIp) { return ip || 'unknown' } -function getClientIp(req) { - const forwarded = req.headers['x-forwarded-for'] - if (forwarded) { - return normalizeIp(forwarded) - } - return normalizeIp(req.ip) -} - -function parseCookies(cookieHeader) { - if (!cookieHeader) return {} - - return cookieHeader - .split(';') - .map(v => v.trim()) - .filter(Boolean) - .reduce((acc, part) => { - const idx = part.indexOf('=') - if (idx === -1) return acc - const key = part.slice(0, idx).trim() - const value = part.slice(idx + 1).trim() - try { - acc[key] = decodeURIComponent(value) - } catch { - acc[key] = value - } - return acc - }, {}) -} - -function hasVisitorConsent(req) { - const cookies = parseCookies(req.headers.cookie) - return cookies[CONSENT_COOKIE] === 'yes' -} - -function setConsentCookie(res, consent) { - const value = consent ? 'yes' : 'no' - res.append('Set-Cookie', `${CONSENT_COOKIE}=${value}; Max-Age=31536000; Path=/; SameSite=Lax`) -} - function isPrivateOrLocalIp(ip) { return ( ip === '127.0.0.1' @@ -1214,6 +1055,17 @@ app.post('/api/admin-content/publish', requireAdminAuth, async (_req, res) => { cachedSiteContent = source.siteContent publishState.publishedAt = publishedAt + + if (draftQuestions !== null) { + questions = draftQuestions.slice(0, MAX_QUESTIONS) + await mkdir(DATA_DIR, { recursive: true }) + await writeFile( + QUESTIONS_FILE, + JSON.stringify({ questions, updatedAt: publishedAt }, null, 2), + 'utf8', + ) + } + await createBackupSnapshot('post-publish') res.json({ ok: true, publishedAt }) @@ -1254,6 +1106,9 @@ app.post('/api/admin-assets', requireAdminAuth, async (req, res) => { await mkdir(UPLOADS_DIR, { recursive: true }) await writeFile(path.join(UPLOADS_DIR, finalName), buffer) + const metadata = await readUploadsMetadata() + metadata[finalName] = [] + await writeUploadsMetadata(metadata) res.json({ ok: true, asset: { filename: finalName, url: `/uploads/${finalName}` } }) } catch { @@ -1261,6 +1116,31 @@ app.post('/api/admin-assets', requireAdminAuth, async (req, res) => { } }) +app.patch('/api/admin-assets/:filename', requireAdminAuth, async (req, res) => { + try { + const { filename } = req.params + if (typeof filename !== 'string' || filename.includes('/') || filename.includes('..')) { + res.status(400).json({ message: 'Invalid filename.' }) + return + } + + const tags = Array.isArray(req.body?.tags) + ? req.body.tags.filter(tag => typeof tag === 'string').map(tag => tag.trim()).filter(Boolean) + : [] + + const filePath = path.join(UPLOADS_DIR, filename) + await stat(filePath) + + const metadata = await readUploadsMetadata() + metadata[filename] = tags + await writeUploadsMetadata(metadata) + + res.json({ ok: true, tags }) + } catch { + res.status(404).json({ message: 'Asset not found.' }) + } +}) + app.delete('/api/admin-assets/:filename', requireAdminAuth, async (req, res) => { try { const { filename } = req.params @@ -1270,6 +1150,9 @@ app.delete('/api/admin-assets/:filename', requireAdminAuth, async (req, res) => } await unlink(path.join(UPLOADS_DIR, filename)) + const metadata = await readUploadsMetadata() + delete metadata[filename] + await writeUploadsMetadata(metadata) res.json({ ok: true }) } catch { res.status(404).json({ message: 'Asset not found.' }) @@ -1415,6 +1298,23 @@ function queueQuestionsWrite() { }) } +function queueDraftQuestionsWrite() { + if (draftQuestions === null) return + draftQuestionsWritePromise = draftQuestionsWritePromise + .then(async () => { + await mkdir(DATA_DIR, { recursive: true }) + await writeFile( + DRAFT_QUESTIONS_FILE, + JSON.stringify({ questions: draftQuestions, updatedAt: new Date().toISOString() }, null, 2), + 'utf8', + ) + publishState.draftUpdatedAt = new Date().toISOString() + }) + .catch(err => { + console.error('[draft-questions] failed to write draft questions:', err) + }) +} + function loadQuestionsFromDisk() { return readFile(QUESTIONS_FILE, 'utf8') .then(raw => { @@ -1446,13 +1346,12 @@ app.post('/api/admin-auth/login', (req, res) => { return } - if (sha256(password) !== sha256(ADMIN_PASSWORD)) { + if (!isAdminPasswordValid(password)) { res.status(401).json({ message: 'Invalid password.' }) return } - const sessionToken = randomUUID() - adminSessions.set(sessionToken, Date.now() + ADMIN_SESSION_TTL_MS) + const sessionToken = createAdminSession() setAdminSessionCookie(res, sessionToken) res.json({ ok: true }) }) @@ -1460,9 +1359,7 @@ app.post('/api/admin-auth/login', (req, res) => { app.post('/api/admin-auth/logout', (req, res) => { const cookies = parseCookies(req.headers.cookie) const sessionToken = cookies[ADMIN_SESSION_COOKIE] - if (sessionToken) { - adminSessions.delete(sessionToken) - } + deleteAdminSession(sessionToken) clearAdminSessionCookie(res) res.json({ ok: true }) }) @@ -1728,6 +1625,70 @@ app.post('/api/study-downloads/titus', studyDownloadRateLimit, async (req, res) } }) +app.post('/api/resource-download', studyDownloadRateLimit, async (req, res) => { + try { + const { resourceId, firstName, lastName, email, subscribe, _honey } = req.body ?? {} + + if (_honey) { + res.json({ ok: true }) + return + } + + if (!resourceId || typeof resourceId !== 'string') { + res.status(400).json({ message: 'Resource ID is required.' }) + return + } + + const published = await loadSiteContentFile(DATA_FILE) + const resource = Array.isArray(published?.siteContent?.customLinks) + ? published.siteContent.customLinks.find(link => link.id === resourceId && link.placement === 'resources') + : undefined + + if (!resource || typeof resource.url !== 'string' || !resource.url.trim()) { + res.status(400).json({ message: 'Resource not found.' }) + return + } + + if (!firstName || typeof firstName !== 'string' || firstName.trim().length < 1 || firstName.trim().length > 100) { + res.status(400).json({ message: 'First name is required.' }) + return + } + + if (!lastName || typeof lastName !== 'string' || lastName.trim().length < 1 || lastName.trim().length > 100) { + res.status(400).json({ message: 'Last name is required.' }) + return + } + + if (!email || typeof email !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { + res.status(400).json({ message: 'A valid email address is required.' }) + return + } + + const trimmedFirstName = firstName.trim() + const trimmedLastName = lastName.trim() + const trimmedName = `${trimmedFirstName} ${trimmedLastName}`.trim() + const trimmedEmail = email.trim() + const wantsSubscribe = subscribe !== false + + addContactSubmission({ + name: trimmedName, + email: trimmedEmail, + message: `Requested resource download: ${resource.label ?? resource.url}`, + messageType: 'general', + subscribe: wantsSubscribe, + }) + + if (wantsSubscribe) { + await syncContactToResend(trimmedName, trimmedEmail) + } + + res.json({ ok: true, downloadUrl: resource.url.trim() }) + } catch (err) { + console.error('[resource-download] request error:', err) + res.status(500).json({ message: 'Failed to process your request. Please try again.' }) + } +}) + app.get('/api/study-downloads/titus/file', async (req, res) => { const token = typeof req.query?.token === 'string' ? req.query.token : '' if (!token || !consumeTitusDownloadToken(token)) { @@ -1808,6 +1769,11 @@ app.post('/api/contact', contactRateLimit, async (req, res) => { } questions.unshift(question) questions = questions.slice(0, MAX_QUESTIONS) + if (draftQuestions !== null) { + draftQuestions.unshift(question) + draftQuestions = draftQuestions.slice(0, MAX_QUESTIONS) + queueDraftQuestionsWrite() + } queueQuestionsWrite() } const resend = new Resend(process.env.RESEND_API_KEY) @@ -1874,7 +1840,7 @@ app.post('/api/contact', contactRateLimit, async (req, res) => { // Get all questions (for admin) app.get('/api/admin-questions', requireAdminAuth, (_req, res) => { - res.json({ questions }) + res.json({ questions: draftQuestions ?? questions }) }) // Get only approved public questions (for homepage) @@ -1893,7 +1859,8 @@ app.post('/api/admin-questions/:id/answer', requireAdminAuth, (req, res) => { return } - const question = questions.find(q => q.id === id) + ensureDraftQuestions() + const question = draftQuestions.find(q => q.id === id) if (!question) { res.status(404).json({ message: 'Question not found.' }) return @@ -1901,7 +1868,7 @@ app.post('/api/admin-questions/:id/answer', requireAdminAuth, (req, res) => { question.answer = answer.trim() question.answeredAt = new Date().toISOString() - queueQuestionsWrite() + queueDraftQuestionsWrite() res.json({ ok: true, question }) }) @@ -1911,7 +1878,8 @@ app.post('/api/admin-questions/:id/approve', requireAdminAuth, (req, res) => { const { id } = req.params const { approved } = req.body ?? {} - const question = questions.find(q => q.id === id) + ensureDraftQuestions() + const question = draftQuestions.find(q => q.id === id) if (!question) { res.status(404).json({ message: 'Question not found.' }) return @@ -1919,7 +1887,7 @@ app.post('/api/admin-questions/:id/approve', requireAdminAuth, (req, res) => { question.isApproved = approved === true question.approvedAt = approved === true ? new Date().toISOString() : null - queueQuestionsWrite() + queueDraftQuestionsWrite() res.json({ ok: true, question }) }) @@ -1927,15 +1895,16 @@ app.post('/api/admin-questions/:id/approve', requireAdminAuth, (req, res) => { // Delete a question (admin) app.delete('/api/admin-questions/:id', requireAdminAuth, (req, res) => { const { id } = req.params - const index = questions.findIndex(q => q.id === id) + ensureDraftQuestions() + const index = draftQuestions.findIndex(q => q.id === id) if (index === -1) { res.status(404).json({ message: 'Question not found.' }) return } - questions.splice(index, 1) - queueQuestionsWrite() + draftQuestions.splice(index, 1) + queueDraftQuestionsWrite() res.json({ ok: true }) }) @@ -2091,6 +2060,7 @@ Promise.all([ loadVisitorStatsFromDisk(), loadContactSubmissionsFromDisk(), loadQuestionsFromDisk(), + loadDraftQuestionsFromDisk(), loadChatbotFromDisk(), refreshContentCaches(), ]) diff --git a/server/auth.js b/server/auth.js new file mode 100644 index 0000000..b1f8225 --- /dev/null +++ b/server/auth.js @@ -0,0 +1,85 @@ +import { createHash, randomUUID } from 'node:crypto' +import { parseCookies } from './helpers.js' + +const ADMIN_SESSION_COOKIE = 'vbn_admin_session' +const ADMIN_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000 +const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD +const adminSessions = new Map() + +function cookieFlags() { + return process.env.NODE_ENV === 'production' ? '; Secure' : '' +} + +export function sha256(value) { + return createHash('sha256').update(String(value)).digest('hex') +} + +export function isAdminPasswordConfigured() { + return Boolean(ADMIN_PASSWORD) +} + +export function validateAdminPasswordSetup() { + if (!isAdminPasswordConfigured() && process.env.NODE_ENV === 'production') { + throw new Error('ADMIN_PASSWORD is required in production.') + } + + if (!isAdminPasswordConfigured()) { + console.warn('ADMIN_PASSWORD is not configured; admin routes will remain disabled until the environment is configured.') + } +} + +export function isAdminPasswordValid(password) { + if (!isAdminPasswordConfigured()) return false + return sha256(password) === sha256(ADMIN_PASSWORD) +} + +export function createAdminSession() { + const token = randomUUID() + adminSessions.set(token, Date.now() + ADMIN_SESSION_TTL_MS) + return token +} + +export function deleteAdminSession(token) { + if (token) { + adminSessions.delete(token) + } +} + +export function isValidAdminSession(req) { + if (!isAdminPasswordConfigured()) return false + + const cookies = parseCookies(req.headers.cookie) + const sessionToken = cookies[ADMIN_SESSION_COOKIE] + if (!sessionToken) return false + + const expiresAt = adminSessions.get(sessionToken) + if (!expiresAt || expiresAt <= Date.now()) { + adminSessions.delete(sessionToken) + return false + } + + adminSessions.set(sessionToken, Date.now() + ADMIN_SESSION_TTL_MS) + return true +} + +export function setAdminSessionCookie(res, token) { + res.append( + 'Set-Cookie', + `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(token)}; Max-Age=${Math.floor(ADMIN_SESSION_TTL_MS / 1000)}; Path=/; HttpOnly; SameSite=Lax${cookieFlags()}`, + ) +} + +export function clearAdminSessionCookie(res) { + res.append( + 'Set-Cookie', + `${ADMIN_SESSION_COOKIE}=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax${cookieFlags()}`, + ) +} + +export function requireAdminAuth(req, res, next) { + if (!isValidAdminSession(req)) { + res.status(401).json({ message: 'Unauthorized' }) + return + } + next() +} diff --git a/server/helpers.js b/server/helpers.js new file mode 100644 index 0000000..aad6fdf --- /dev/null +++ b/server/helpers.js @@ -0,0 +1,412 @@ +import { randomUUID } from 'node:crypto' + +export const DEFAULT_REDIRECT_RULES = [ + { + id: 'spotify', + path: '/spotify', + target: 'https://open.spotify.com/show/0Gq1TzoJOdReSZ1gYQi8Xl', + statusCode: 301, + }, + { + id: 'apple', + path: '/apple', + target: 'https://podcasts.apple.com/search?term=Verse+by+Verse+with+Nate', + statusCode: 301, + }, + { + id: 'amazon', + path: '/amazon', + target: 'https://music.amazon.com/podcasts/202322bf-db86-4e7d-9a6b-4db7cbccbccf/verse-by-verse-with-nate', + statusCode: 301, + }, +] + +export const DEFAULT_SEO = { + title: 'Verse by Verse with Nate', + description: 'Verse by Verse with Nate explores Scripture one verse at a time with practical Bible teaching.', + ogTitle: 'Verse by Verse with Nate', + ogDescription: 'A Journey Through Scripture - verse by verse, nugget by nugget.', + ogImage: '/images/podcast-art.jpeg', + canonicalUrl: 'https://versebyversewithnate.us/', + robotsPolicy: 'index,follow', + sitemapPaths: ['/', '/start-here', '/questions', '/privacy', '/terms'], +} + +export const DEFAULT_LEGAL = { + privacyTitle: 'Privacy Policy', + privacyBody: [ + 'We respect your privacy and collect limited data to operate and improve this site.', + 'If you consent to analytics cookies, we may store masked IP-based location signals and returning visitor activity.', + 'Contact form details are used only to respond to your message and ministry communication requests.', + ], + termsTitle: 'Terms', + termsBody: [ + 'Content on this site is for informational and ministry purposes.', + 'External links are provided for convenience and are subject to third-party policies.', + 'By using this site, you agree to lawful use and respectful communication.', + ], +} + +export const DEFAULT_PODCAST_FEATURED_LINKS = [] +export const DEFAULT_PUBLISH_STATE = { + draftUpdatedAt: null, + publishedAt: null, +} + +export function escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +export function splitName(fullName) { + const parts = String(fullName).trim().split(/\s+/).filter(Boolean) + return { + firstName: parts[0] ?? '', + lastName: parts.slice(1).join(' '), + } +} + +export function sanitizeUrl(value) { + if (typeof value !== 'string') return '' + const trimmed = value.trim() + if (!trimmed) return '' + if (trimmed.startsWith('/')) return trimmed + if (/^https?:\/\//i.test(trimmed)) return trimmed + return '' +} + +export function normalizeRedirectPath(value) { + if (typeof value !== 'string') return '' + const trimmed = value.trim() + if (!trimmed) return '' + const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}` + const normalized = withSlash.replace(/\/+/g, '/') + if (normalized === '/') return '' + if (normalized.startsWith('/api/') || normalized.startsWith('/admin')) return '' + return normalized +} + +export function normalizeSitemapPath(value) { + if (typeof value !== 'string') return '' + const trimmed = value.trim() + if (!trimmed) return '' + if (trimmed === '/') return '/' + return normalizeRedirectPath(trimmed) +} + +export function sanitizeRedirectRules(value) { + const source = Array.isArray(value) ? value : [] + const seen = new Set() + const out = [] + + for (const item of source) { + const pathValue = normalizeRedirectPath(item?.path) + const target = sanitizeUrl(item?.target) + const statusCode = Number(item?.statusCode) === 302 ? 302 : 301 + if (!pathValue || !target) continue + if (seen.has(pathValue)) continue + seen.add(pathValue) + out.push({ + id: typeof item?.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + path: pathValue, + target, + statusCode, + }) + } + + return out.length > 0 ? out : DEFAULT_REDIRECT_RULES +} + +export function sanitizeFeaturedLinks(value) { + const source = Array.isArray(value) ? value : [] + return source + .filter(item => item && typeof item === 'object') + .map(item => { + const discussionQuestions = Array.isArray(item.discussionQuestions) + ? item.discussionQuestions + .filter(question => typeof question === 'string') + .map(question => question.trim()) + .filter(Boolean) + .slice(0, 30) + : [] + + return { + id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + title: typeof item.title === 'string' ? item.title.trim().slice(0, 140) : '', + episodeNumber: typeof item.episodeNumber === 'string' ? item.episodeNumber.trim().slice(0, 20) : '', + summary: typeof item.summary === 'string' ? item.summary.trim().slice(0, 600) : '', + url: sanitizeUrl(item.url), + embedUrl: sanitizeUrl(item.embedUrl), + showNotes: typeof item.showNotes === 'string' ? item.showNotes.trim().slice(0, 10000) : '', + discussionQuestions, + } + }) + .filter( + item => + item.title || + item.summary || + item.url || + item.embedUrl || + item.showNotes || + item.discussionQuestions.length > 0, + ) +} + +function sanitizeCustomLinks(value) { + const source = Array.isArray(value) ? value : [] + return source + .filter(item => item && typeof item === 'object') + .map(item => { + const placement = + item?.placement === 'platforms' || item?.placement === 'footer' || item?.placement === 'resources' + ? item.placement + : 'footer' + + return { + id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + label: typeof item.label === 'string' ? item.label.trim().slice(0, 140) : '', + url: sanitizeUrl(item.url), + imageUrl: sanitizeUrl(item.imageUrl), + placement, + } + }) + .filter(item => item.label && item.url) +} + +function sanitizeCustomBlocks(value) { + const source = Array.isArray(value) ? value : [] + return source + .filter(item => item && typeof item === 'object') + .map(item => ({ + id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + heading: typeof item.heading === 'string' ? item.heading.trim().slice(0, 140) : '', + body: typeof item.body === 'string' ? item.body.trim().slice(0, 4000) : '', + })) + .filter(item => item.heading || item.body) +} + +function sanitizeArchivedSeriesResourceLinks(value) { + const source = Array.isArray(value) ? value : [] + return source + .filter(item => item && typeof item === 'object') + .map(item => ({ + id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + label: typeof item.label === 'string' ? item.label.trim().slice(0, 120) : '', + url: sanitizeUrl(item.url), + })) + .filter(item => item.label && item.url) +} + +function sanitizeArchivedSeriesNotes(value) { + const source = Array.isArray(value) ? value : [] + return source + .filter(item => item && typeof item === 'object') + .map(item => ({ + id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + heading: typeof item.heading === 'string' ? item.heading.trim().slice(0, 140) : '', + body: typeof item.body === 'string' ? item.body.trim().slice(0, 4000) : '', + })) + .filter(item => item.heading || item.body) +} + +function sanitizeArchivedSeries(value) { + const source = Array.isArray(value) ? value : [] + return source + .filter(item => item && typeof item === 'object') + .map(item => ({ + id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + label: typeof item.label === 'string' ? item.label.trim().slice(0, 140) : '', + title: typeof item.title === 'string' ? item.title.trim().slice(0, 200) : '', + description: typeof item.description === 'string' ? item.description.trim().slice(0, 1000) : '', + imageUrl: sanitizeUrl(item.imageUrl), + listenUrl: sanitizeUrl(item.listenUrl), + studyGuideTitle: typeof item.studyGuideTitle === 'string' ? item.studyGuideTitle.trim().slice(0, 140) : '', + studyGuideDescription: typeof item.studyGuideDescription === 'string' ? item.studyGuideDescription.trim().slice(0, 4000) : '', + studyGuideUrl: sanitizeUrl(item.studyGuideUrl), + resourceLinks: sanitizeArchivedSeriesResourceLinks(item.resourceLinks), + notes: sanitizeArchivedSeriesNotes(item.notes), + })) + .filter( + item => + item.title || + item.description || + item.resourceLinks.length > 0 || + item.notes.length > 0, + ) +} + +export function sanitizeSiteContent(siteContent) { + if (!siteContent || typeof siteContent !== 'object' || Array.isArray(siteContent)) return {} + + const seo = siteContent.seo && typeof siteContent.seo === 'object' ? siteContent.seo : {} + const legal = siteContent.legal && typeof siteContent.legal === 'object' ? siteContent.legal : {} + + return { + ...siteContent, + customLinks: sanitizeCustomLinks(siteContent.customLinks), + customBlocks: sanitizeCustomBlocks(siteContent.customBlocks), + archivedSeries: sanitizeArchivedSeries(siteContent.archivedSeries), + redirects: sanitizeRedirectRules(siteContent.redirects), + podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS), + seo: { + title: typeof seo.title === 'string' && seo.title.trim() ? seo.title.trim().slice(0, 120) : DEFAULT_SEO.title, + description: typeof seo.description === 'string' && seo.description.trim() ? seo.description.trim().slice(0, 240) : DEFAULT_SEO.description, + ogTitle: typeof seo.ogTitle === 'string' && seo.ogTitle.trim() ? seo.ogTitle.trim().slice(0, 120) : DEFAULT_SEO.ogTitle, + ogDescription: typeof seo.ogDescription === 'string' && seo.ogDescription.trim() ? seo.ogDescription.trim().slice(0, 240) : DEFAULT_SEO.ogDescription, + ogImage: sanitizeUrl(seo.ogImage) || DEFAULT_SEO.ogImage, + canonicalUrl: sanitizeUrl(seo.canonicalUrl) || DEFAULT_SEO.canonicalUrl, + robotsPolicy: typeof seo.robotsPolicy === 'string' && seo.robotsPolicy.trim() ? seo.robotsPolicy.trim() : DEFAULT_SEO.robotsPolicy, + sitemapPaths: Array.isArray(seo.sitemapPaths) + ? seo.sitemapPaths.map(pathItem => normalizeSitemapPath(pathItem)).filter(Boolean) + : [...DEFAULT_SEO.sitemapPaths], + }, + legal: { + privacyTitle: typeof legal.privacyTitle === 'string' && legal.privacyTitle.trim() ? legal.privacyTitle.trim().slice(0, 120) : DEFAULT_LEGAL.privacyTitle, + privacyBody: Array.isArray(legal.privacyBody) && legal.privacyBody.length > 0 + ? legal.privacyBody.filter(line => typeof line === 'string').map(line => line.trim()).filter(Boolean).slice(0, 20) + : [...DEFAULT_LEGAL.privacyBody], + termsTitle: typeof legal.termsTitle === 'string' && legal.termsTitle.trim() ? legal.termsTitle.trim().slice(0, 120) : DEFAULT_LEGAL.termsTitle, + termsBody: Array.isArray(legal.termsBody) && legal.termsBody.length > 0 + ? legal.termsBody.filter(line => typeof line === 'string').map(line => line.trim()).filter(Boolean).slice(0, 20) + : [...DEFAULT_LEGAL.termsBody], + }, + } +} + +export function escapeXml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +export function buildAbsoluteUrl(baseUrl, maybeRelativePath) { + const safeBase = typeof baseUrl === 'string' && baseUrl.trim() ? baseUrl.trim() : DEFAULT_SEO.canonicalUrl + const root = safeBase.endsWith('/') ? safeBase.slice(0, -1) : safeBase + if (typeof maybeRelativePath !== 'string' || !maybeRelativePath.trim()) return root + const value = maybeRelativePath.trim() + if (/^https?:\/\//i.test(value)) return value + if (value.startsWith('/')) return `${root}${value}` + return `${root}/${value}` +} + +export function injectSeoIntoHtml(html, siteContent) { + const seo = siteContent?.seo ?? DEFAULT_SEO + const title = seo.title || DEFAULT_SEO.title + const description = seo.description || DEFAULT_SEO.description + const ogTitle = seo.ogTitle || title + const ogDescription = seo.ogDescription || description + const canonical = buildAbsoluteUrl(seo.canonicalUrl || DEFAULT_SEO.canonicalUrl, '/') + const ogImage = buildAbsoluteUrl(canonical, seo.ogImage || DEFAULT_SEO.ogImage) + const robots = seo.robotsPolicy || DEFAULT_SEO.robotsPolicy + + return html + .replace(/[\s\S]*?<\/title>/i, `<title>${escapeHtml(title)}`) + .replace(//i, ``) + .replace(//i, ``) + .replace(//i, ``) + .replace(//i, ``) + .replace(//i, ``) + .replace(//i, ``) + .replace(//i, ``) + .replace(//i, ``) +} + +export function normalizeAssetBaseName(name) { + if (typeof name !== 'string') return `upload-${Date.now()}` + const cleaned = name + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + return cleaned || `upload-${Date.now()}` +} + +export function inferImageExtensionFromDataUrl(dataUrl) { + if (typeof dataUrl !== 'string') return null + if (dataUrl.startsWith('data:image/png;base64,')) return '.png' + if (dataUrl.startsWith('data:image/jpeg;base64,')) return '.jpg' + if (dataUrl.startsWith('data:image/webp;base64,')) return '.webp' + if (dataUrl.startsWith('data:image/gif;base64,')) return '.gif' + return null +} + +export function normalizeIp(rawIp) { + if (!rawIp) return 'unknown' + + let ip = String(rawIp).trim() + + if (ip.includes(',')) { + ip = ip.split(',')[0].trim() + } + + if (ip.startsWith('::ffff:')) { + ip = ip.slice(7) + } + + if (ip === '::1') { + ip = '127.0.0.1' + } + + return ip || 'unknown' +} + +export function getClientIp(req) { + const forwarded = req.headers['x-forwarded-for'] + if (forwarded) { + return normalizeIp(forwarded) + } + return normalizeIp(req.ip) +} + +export function parseCookies(cookieHeader) { + if (!cookieHeader) return {} + + return cookieHeader + .split(';') + .map(v => v.trim()) + .filter(Boolean) + .reduce((acc, part) => { + const idx = part.indexOf('=') + if (idx === -1) return acc + const key = part.slice(0, idx).trim() + const value = part.slice(idx + 1).trim() + try { + acc[key] = decodeURIComponent(value) + } catch { + acc[key] = value + } + return acc + }, {}) +} + +export function hasVisitorConsent(req) { + const cookies = parseCookies(req.headers.cookie) + return cookies['vbn_analytics_consent'] === 'yes' +} + +export function setConsentCookie(res, consent) { + const value = consent ? 'yes' : 'no' + const secureFlag = process.env.NODE_ENV === 'production' ? '; Secure' : '' + res.append('Set-Cookie', `vbn_analytics_consent=${value}; Max-Age=31536000; Path=/; SameSite=Lax${secureFlag}`) +} + +export function isPrivateOrLocalIp(ip) { + return ( + ip === '127.0.0.1' || + ip === 'localhost' || + ip.startsWith('10.') || + ip.startsWith('192.168.') || + /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip) || + ip.startsWith('fc') || + ip.startsWith('fd') || + ip.startsWith('fe80:') || + ip === 'unknown' + ) +} diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 762b033..55f116c 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react' import type { ChangeEvent } from 'react' import { Link } from 'react-router-dom' -import type { SiteContent, CustomLink, CustomBlock, ArchivedSeries, ArchivedSeriesResourceLink, ArchivedSeriesNote, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings } from './App' -import { DEFAULTS } from './App' +import type { SiteContent, CustomLink, CustomBlock, ArchivedSeries, ArchivedSeriesResourceLink, ArchivedSeriesNote, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings } from './content' +import { DEFAULTS } from './content' interface Props { content: SiteContent @@ -66,6 +66,7 @@ interface AdminAsset { url: string sizeBytes: number updatedAt: string + tags?: string[] } interface PublishState { @@ -94,7 +95,7 @@ interface Question { } type StringField = Exclude -type MainContentSection = 'hero' | 'start-here' | 'about' | 'series' | 'share' +type MainContentSection = 'hero' | 'start-here' | 'about' | 'contact' | 'series' | 'share' const MAIN_CONTENT_SECTIONS: Array<{ id: MainContentSection; title: string; description: string }> = [ { @@ -110,7 +111,12 @@ const MAIN_CONTENT_SECTIONS: Array<{ id: MainContentSection; title: string; desc { id: 'about', title: 'About Section', - description: 'Manage the main show description and Nate bio content.', + description: 'Manage the main show description, Nate bio, and about images.', + }, + { + id: 'contact', + title: 'Contact Section', + description: 'Manage the contact page profile image and contact copy.', }, { id: 'series', @@ -178,6 +184,9 @@ const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; sect { key: 'aboutShowP1', label: 'About Show — Paragraph 1', multiline: true, section: 'about' }, { key: 'aboutShowP2', label: 'About Show — Paragraph 2', multiline: true, section: 'about' }, { key: 'aboutNate', label: 'About Nate', multiline: true, section: 'about' }, + { key: 'aboutPhotoUrl', label: 'About Section Photo URL', section: 'about' }, + { key: 'aboutVerseArtUrl', label: 'About Verse Art Image URL', section: 'about' }, + { key: 'contactPhotoUrl', label: 'Contact Profile Photo URL', section: 'contact' }, { key: 'seriesLabel', label: 'Series Label (e.g. "Now Playing")', section: 'series' }, { key: 'seriesTitle', label: 'Series Title', section: 'series' }, { key: 'seriesDescription', label: 'Series Description', multiline: true, section: 'series' }, @@ -195,8 +204,8 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { const [lastSavedSnapshot, setLastSavedSnapshot] = useState(() => JSON.stringify(content)) const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle') const [errorMsg, setErrorMsg] = useState('') - const [adminTab, setAdminTab] = useState<'content' | 'episodes' | 'settings' | 'publish' | 'analytics' | 'questions' | 'brand'>('content') - const [contentTab, setContentTab] = useState<'main' | 'custom'>('main') + const [adminTab, setAdminTab] = useState<'content' | 'episodes' | 'settings' | 'analytics' | 'questions' | 'brand' | 'assets'>('content') + const [contentTab, setContentTab] = useState<'main' | 'resources' | 'custom'>('main') const [stats, setStats] = useState(null) const [statsStatus, setStatsStatus] = useState<'loading' | 'ready' | 'error'>('loading') const [maintenanceMsg, setMaintenanceMsg] = useState('') @@ -206,6 +215,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { const [selectedBackupPreview, setSelectedBackupPreview] = useState(null) const [publishState, setPublishState] = useState({ draftUpdatedAt: null, publishedAt: null }) const [assets, setAssets] = useState([]) + const [assetTagEdits, setAssetTagEdits] = useState>({}) const [opsStatus, setOpsStatus] = useState(null) const [assetUploadPending, setAssetUploadPending] = useState(false) @@ -274,13 +284,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { }) .catch(() => {}) - fetch('/api/admin-assets') - .then(r => (r.ok ? r.json() : Promise.reject(new Error('Failed to load assets')))) - .then(data => { - const list = Array.isArray((data as { assets?: unknown }).assets) ? (data as { assets: AdminAsset[] }).assets : [] - setAssets(list) - }) - .catch(() => {}) + void reloadAssets() fetch('/api/admin-ops/status') .then(r => (r.ok ? r.json() : Promise.reject(new Error('Failed to load operations status')))) @@ -346,7 +350,12 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { const r = await fetch('/api/admin-assets') if (!r.ok) throw new Error('Could not refresh assets') const data = await r.json() as { assets?: AdminAsset[] } - setAssets(Array.isArray(data.assets) ? data.assets : []) + const incomingAssets = Array.isArray(data.assets) ? data.assets : [] + setAssets(incomingAssets) + setAssetTagEdits(incomingAssets.reduce>((memo, asset) => { + memo[asset.filename] = (asset.tags ?? []).join(', ') + return memo + }, {})) } async function reloadOpsStatus() { @@ -371,6 +380,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { } const data = await res.json() as { updatedAt?: string } setPublishState(prev => ({ ...prev, draftUpdatedAt: data.updatedAt ?? new Date().toISOString() })) + setLastSavedSnapshot(JSON.stringify(form)) setStatus('saved') setTimeout(() => setStatus('idle'), 3500) } catch (err) { @@ -564,6 +574,30 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { } } + function handleAssetTagChange(filename: string, value: string) { + setAssetTagEdits(prev => ({ ...prev, [filename]: value })) + } + + async function handleSaveAssetTags(filename: string) { + const tagsText = assetTagEdits[filename] ?? '' + const tags = tagsText.split(',').map(tag => tag.trim()).filter(Boolean) + try { + const res = await fetch(`/api/admin-assets/${encodeURIComponent(filename)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tags }), + }) + if (!res.ok) { + const data = await res.json().catch(() => ({})) + throw new Error((data as { message?: string }).message ?? 'Save failed') + } + await reloadAssets() + setOpsMsg('Asset tags saved.') + } catch (err) { + setOpsMsg(err instanceof Error ? err.message : 'Save failed.') + } + } + async function handlePurgeCache() { try { const res = await fetch('/api/admin-ops/purge-cache', { method: 'POST' }) @@ -701,18 +735,43 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { setForm(f => ({ ...f, [key]: value })) } + function renderImageAssetSelector(value: string | null | undefined, onChange: (value: string) => void, fieldId: string) { + const assetOptions = [...assets.map(asset => ({ label: asset.filename, value: asset.url }))] + const currentValue = value?.trim() ?? '' + + if (currentValue && !assetOptions.some(item => item.value === currentValue)) { + assetOptions.unshift({ label: `Current image (${currentValue})`, value: currentValue }) + } + + if (assetOptions.length === 0) return null + + return ( +

+ + +
+ ) + } + function confirmLeaveUnsavedChanges() { if (!isDirty) return true return confirm('You have unsaved changes. Leave this section without saving?') } - function handleAdminTabChange(nextTab: 'content' | 'episodes' | 'settings' | 'publish' | 'analytics' | 'questions' | 'brand') { + function handleAdminTabChange(nextTab: 'content' | 'episodes' | 'settings' | 'analytics' | 'questions' | 'brand' | 'assets') { if (nextTab === adminTab) return if (!confirmLeaveUnsavedChanges()) return setAdminTab(nextTab) } - function handleContentTabChange(nextTab: 'main' | 'custom') { + function handleContentTabChange(nextTab: 'main' | 'resources' | 'custom') { if (nextTab === contentTab) return if (!confirmLeaveUnsavedChanges()) return setContentTab(nextTab) @@ -723,12 +782,22 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { ...f, customLinks: [ ...(f.customLinks ?? []), - { id: Date.now().toString(36), label: '', url: '', placement: 'platforms' as const }, + { id: Date.now().toString(36), label: '', url: '', imageUrl: '', placement: 'platforms' as const }, ], })) } - function updateLink(id: string, field: keyof CustomLink, value: string) { + function addResource() { + setForm(f => ({ + ...f, + customLinks: [ + ...(f.customLinks ?? []), + { id: Date.now().toString(36), label: '', url: '', imageUrl: '', placement: 'resources' as const }, + ], + })) + } + + function updateLink(id: string, field: keyof CustomLink, value: string | string[]) { setForm(f => ({ ...f, customLinks: (f.customLinks ?? []).map(l => l.id === id ? { ...l, [field]: value } : l), @@ -985,29 +1054,6 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { setContentTab('custom') } - async function handleSave() { - setStatus('saving') - setErrorMsg('') - try { - const res = await fetch('/api/admin-content', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ siteContent: form }), - }) - if (!res.ok) { - const data = await res.json().catch(() => ({})) - throw new Error((data as { message?: string }).message ?? 'Save failed') - } - onSave(form) - setLastSavedSnapshot(JSON.stringify(form)) - setStatus('saved') - setTimeout(() => setStatus('idle'), 3500) - } catch (err) { - setErrorMsg(err instanceof Error ? err.message : 'Unknown error') - setStatus('error') - } - } - function handleReset() { if (confirm('Reset all fields to defaults?')) { setForm(DEFAULTS) @@ -1147,11 +1193,11 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { + + + + {adminTab === 'brand' && (
@@ -1225,64 +1290,6 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
)} - {adminTab === 'publish' && ( -
-
-

Publishing Workflow

-

Save drafts, preview before release, and publish when ready.

-
- -
-
-

Draft Updated

-

{formatDate(publishState.draftUpdatedAt)}

-
-
-

Last Published

-

{formatDate(publishState.publishedAt)}

-
-
- -
- - - -
- -
-

Deployment and Cache

-

Trigger deployment hooks and cache purge hooks.

-
-
-
-

Build Commit

-

{opsStatus?.buildCommit ?? 'Not available'}

-
-
-

Build Number

-

{opsStatus?.buildNumber ?? 'Not available'}

-
-
-

Deployed At

-

{formatDate(opsStatus?.deployedAt ?? null)}

-
-
-

Cache Purge

-

{opsStatus?.cachePurge.ok ? 'Healthy' : 'Needs setup'}

-

{formatDate(opsStatus?.cachePurge.at ?? null)}

-
-
-
- - - -
- - {opsMsg &&

{opsMsg}

} - {status === 'error' &&

✗ {errorMsg}

} - {status === 'saved' &&

✓ Draft action completed.

} -
- )} {adminTab === 'episodes' && (
@@ -1330,16 +1337,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { ))} -
- -
+ {opsMsg &&

{opsMsg}

} {status === 'saved' &&

✓ Changes saved.

} {status === 'error' &&

✗ {errorMsg}

}
@@ -1373,6 +1371,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
updateSeoField('ogImage', e.target.value)} /> + {renderImageAssetSelector(form.seo?.ogImage, value => updateSeoField('ogImage', value), 'seo-og-image-asset')}
@@ -1457,28 +1456,39 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
-
+ {opsMsg &&

{opsMsg}

} + {status === 'saved' &&

✓ Changes saved.

} + {status === 'error' &&

✗ {errorMsg}

} + + )} + + {adminTab === 'assets' && ( +
+

Asset Manager

-

Upload and reuse hosted images from this domain.

+

Upload, tag, and reuse hosted images from this domain.

+
+ {assets.length === 0 ? (

No uploaded assets yet.

) : (
- +
+ - + @@ -1486,9 +1496,20 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { + - + ))} @@ -1496,19 +1517,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { )} -
- -
{opsMsg &&

{opsMsg}

} - {status === 'saved' &&

✓ Changes saved.

} - {status === 'error' &&

✗ {errorMsg}

} )} {adminTab === 'analytics' && ( @@ -1731,6 +1740,31 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { +
+

Deployment & Cache Status

+

Current deployment metadata and cache purge health for the live app.

+
+ +
+
+

Build Commit

+

{opsStatus?.buildCommit ?? 'Not available'}

+
+
+

Build Number

+

{opsStatus?.buildNumber ?? 'Not available'}

+
+
+

Deployed At

+

{formatDate(opsStatus?.deployedAt ?? null)}

+
+
+

Cache Purge

+

{opsStatus?.cachePurge.ok ? 'Healthy' : 'Needs setup'}

+

{formatDate(opsStatus?.cachePurge.at ?? null)}

+
+
+
@@ -1738,6 +1772,12 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
+
+ + + +
+
handleChange(key, e.target.value)} - /> + <> + handleChange(key, e.target.value)} + /> + {(key.includes('ImageUrl') || key.includes('PhotoUrl')) && renderImageAssetSelector(form[key] as string, value => handleChange(key, value), `field-${key}-asset`)} + )}
))} @@ -1852,12 +1904,84 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { )} + {contentTab === 'resources' && ( + <> +
+
+

Resource Links

+

{(form.customLinks ?? []).filter(link => link.placement === 'resources').length}

+
+
+ +
+

More Resources

+

Manage additional resources shown on the More Resources page.

+
+ {(form.customLinks ?? []).filter(link => link.placement === 'resources').length === 0 && ( +

No resources yet.

+ )} + {(form.customLinks ?? []).filter(link => link.placement === 'resources').map(link => ( +
+
+
+ + updateLink(link.id, 'label', e.target.value)} + /> +
+
+ + updateLink(link.id, 'url', e.target.value)} + /> +
+
+ + updateLink(link.id, 'imageUrl', e.target.value)} + /> + {renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `resource-image-${link.id}-asset`)} +
+
+ + updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} + /> +
+
+ +
+ ))} + + + )} + {contentTab === 'custom' && ( <>

Custom Links

-

{(form.customLinks ?? []).length}

+

{(form.customLinks ?? []).filter(link => link.placement !== 'resources').length}

Custom Blocks

@@ -1871,12 +1995,12 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {

Custom Links

-

Add links to show in the platform buttons row, footer, or a dedicated "More Resources" section.

+

Add links to show in the platform buttons row or footer navigation.

- {(form.customLinks ?? []).length === 0 && ( + {(form.customLinks ?? []).filter(link => link.placement !== 'resources').length === 0 && (

No custom links yet.

)} - {(form.customLinks ?? []).map(link => ( + {(form.customLinks ?? []).filter(link => link.placement !== 'resources').map(link => (
@@ -1899,6 +2023,27 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { onChange={e => updateLink(link.id, 'url', e.target.value)} />
+
+ + updateLink(link.id, 'imageUrl', e.target.value)} + /> + {renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `link-image-${link.id}-asset`)} +
+
+ + updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} + /> +
handleSearch(e.target.value)} - /> - -
-
- - {filteredQuestions.length === 0 ? ( -
-

No matching questions found. Submit your question

-
- ) : ( - <> -
- {pagedQuestions.map(question => ( -
-
toggleExpanded(question.id)} - role="button" - tabIndex={0} - aria-expanded={!!expanded[question.id]} - aria-label={question.question} - onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && toggleExpanded(question.id)} - > -
- Q -

{question.question}

- {expanded[question.id] ? '▲' : '▼'} -
-
- A -

{question.answer}

- {getRelatedQuestions(question).length > 0 && ( -
-

Related questions

-
- {getRelatedQuestions(question).map(related => ( - - ))} -
-
- )} -

— Answered by Nate

-
-
-
- ))} -
- - {totalPages > 1 && ( -
- - - {page + 1} / {totalPages} - - -
- )} - - )} - - )} -
- - ) -} - -function ContactForm() { - const navigate = useNavigate() - const [fields, setFields] = useState({ firstName: '', lastName: '', email: '', message: '', messageType: 'question' }) - const [subscribe, setSubscribe] = useState(true) - const [honey, setHoney] = useState('') - const [status, setStatus] = useState<'idle' | 'submitting' | 'error'>('idle') - const [errorMsg, setErrorMsg] = useState('') - - function handleChange(e: React.ChangeEvent) { - setFields(f => ({ ...f, [e.target.name]: e.target.value })) - } - - function handleSelectChange(e: React.ChangeEvent) { - setFields(f => ({ ...f, [e.target.name]: e.target.value })) - } - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setStatus('submitting') - setErrorMsg('') - try { - const res = await fetch('/api/contact', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...fields, subscribe, _honey: honey }), - }) - if (!res.ok) { - const data = await res.json().catch(() => ({})) - setErrorMsg((data as { message?: string }).message ?? 'Something went wrong. Please try again.') - setStatus('error') - return - } - navigate('/thanks') - } catch { - setErrorMsg('Could not connect. Please try again later.') - setStatus('error') - } - } - - return ( -
- setHoney(e.target.value)} - /> - - - - -
Preview URLTags Size UpdatedActionActions
{asset.filename} {asset.url} + handleAssetTagChange(asset.filename, e.target.value)} + placeholder="comma separated tags" + /> + {(asset.sizeBytes / 1024).toFixed(1)} KB {formatDate(asset.updatedAt)} + + +