Files
Siteforge/server/config.js
T
nmemmertandClaude Sonnet 5.5 5d5c026762
Publish Container / build (push) Successful in 16m20s
Redo /newsletter and /nuggets pages; add Nuggets episode tags with admin editor; v2.37.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 13:49:15 -04:00

193 lines
9.1 KiB
JavaScript

import path from 'node:path'
import { readFileSync } from 'node:fs'
import { execSync } from 'node:child_process'
import { validateAdminPasswordSetup } from './auth.js'
import { ROOT_DIR, DATA_DIR } from './paths.js'
export { ROOT_DIR, DATA_DIR }
export { DEFAULT_SEO, DEFAULT_LEGAL, DEFAULT_REDIRECT_RULES, DEFAULT_PODCAST_FEATURED_LINKS, DEFAULT_PUBLISH_STATE } from './helpers.js'
function readPackageVersion() {
try {
const pkg = JSON.parse(readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'))
return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version.trim() : 'unknown'
} catch {
return 'unknown'
}
}
function readGitCommit() {
// CI injects the full SHA as COMMIT_SHA; fall back to running git locally.
if (typeof process.env.COMMIT_SHA === 'string' && process.env.COMMIT_SHA.trim()) {
return process.env.COMMIT_SHA.trim().slice(0, 7)
}
try {
return execSync('git rev-parse --short HEAD', { cwd: ROOT_DIR, timeout: 2000 }).toString().trim()
} catch {
return 'unknown'
}
}
// APP_VERSION is sourced from package.json.
// GIT_COMMIT is the 7-char short SHA, from COMMIT_SHA env var (set by CI) or git at startup.
export const APP_VERSION = readPackageVersion()
export const GIT_COMMIT = readGitCommit()
export const DATA_FILE = path.join(DATA_DIR, 'admin-content.json')
export const DRAFT_DATA_FILE = path.join(DATA_DIR, 'admin-content-draft.json')
export const HIT_STATS_FILE = path.join(DATA_DIR, 'hit-stats.json')
export const VISITOR_STATS_FILE = path.join(DATA_DIR, 'visitor-stats.json')
export const CONTACT_SUBMISSIONS_FILE = path.join(DATA_DIR, 'contact-submissions.json')
export const QUESTIONS_FILE = path.join(DATA_DIR, 'questions.json')
export const DRAFT_QUESTIONS_FILE = path.join(DATA_DIR, 'questions-draft.json')
export const STUDY_USERS_FILE = path.join(DATA_DIR, 'study-users.json')
export const STUDY_NOTES_FILE = path.join(DATA_DIR, 'study-notes.json') // legacy — kept only for one-time migration
export const STUDY_NOTES_DIR = path.join(DATA_DIR, 'study-notes')
export const STUDY_PROGRESS_DIR = path.join(DATA_DIR, 'study-progress')
export const STUDY_COMMUNITY_FILE = path.join(DATA_DIR, 'study-community.json')
export const REPLY_TEMPLATES_FILE = path.join(DATA_DIR, 'admin-reply-templates.json')
export const REPLY_HISTORY_FILE = path.join(DATA_DIR, 'admin-reply-history.json')
export const BACKUP_DIR = path.join(DATA_DIR, 'backups')
export const UPLOADS_DIR = path.join(DATA_DIR, 'uploads')
export const UPLOADS_META_FILE = path.join(DATA_DIR, 'uploads-meta.json')
export const DOWNLOAD_COUNTS_FILE = path.join(DATA_DIR, 'download-counts.json')
export const STUDY_REMINDERS_FILE = path.join(DATA_DIR, 'study-reminders.json')
export const STUDY_COMMENTS_FILE = path.join(DATA_DIR, 'study-section-comments.json')
export const STUDY_CERTIFICATES_FILE = path.join(DATA_DIR, 'study-certificates.json')
export const EPISODE_SCRIPTS_FILE = path.join(DATA_DIR, 'episode-scripts.json')
export const QR_CODES_FILE = path.join(DATA_DIR, 'qr-codes.json')
export const EPISODE_PLAYS_FILE = path.join(DATA_DIR, 'episode-plays.json')
export const ANALYTICS_EVENTS_FILE = path.join(DATA_DIR, 'analytics-events.json')
export const NUGGETS_UPLOAD_HOSTS_FILE = path.join(DATA_DIR, 'nuggets-upload-hosts.json')
export const NUGGETS_SEAFILE_SEEN_FILE = path.join(DATA_DIR, 'nuggets-seafile-seen.json')
export const NUGGETS_UPLOAD_HISTORY_FILE = path.join(DATA_DIR, 'nuggets-upload-history.json')
export const NUGGETS_PORTAL_ANNOUNCEMENT_FILE = path.join(DATA_DIR, 'nuggets-portal-announcement.json')
export const NUGGETS_PORTAL_GUIDELINES_FILE = path.join(DATA_DIR, 'nuggets-portal-guidelines.json')
export const SEAFILE_NUGGETS_SHARE_URL = process.env.SEAFILE_NUGGETS_SHARE_URL ?? 'https://seafile.necloud.us/d/437ee6b8476d4b7e90a2/'
// Email address for Nuggets upload notifications (defaults to the admin inbox)
export const NUGGETS_NOTIFY_EMAIL = process.env.NUGGETS_NOTIFY_EMAIL ?? process.env.RESEND_TO ?? 'hello@versebyversewithnate.us'
export const SEAFILE_POLL_INTERVAL_MS = 15 * 60 * 1000 // 15 minutes
export const EMAIL_SETTINGS_FILE = path.join(DATA_DIR, 'email-settings.json')
export const AUDIT_LOG_FILE = path.join(DATA_DIR, 'audit-log.json')
export const WEBHOOKS_FILE = path.join(DATA_DIR, 'webhooks.json')
export const STUDY_SESSIONS_FILE = path.join(DATA_DIR, 'study-sessions.json')
export const NEWSLETTER_ISSUES_FILE = path.join(DATA_DIR, 'newsletter-issues.json')
export const NUGGETS_TAGS_FILE = path.join(DATA_DIR, 'nuggets-tags.json')
export const MAX_EPISODE_SCRIPT_LENGTH = 200_000 // ~150k words, well beyond any sermon
// Seafile integration for Nuggets episode uploads
export const SEAFILE_URL = (process.env.SEAFILE_URL ?? '').replace(/\/$/, '')
export const SEAFILE_TOKEN = process.env.SEAFILE_TOKEN ?? ''
export const SEAFILE_NUGGETS_REPO_ID = process.env.SEAFILE_NUGGETS_REPO_ID ?? ''
export const SEAFILE_EPISODE_INFO_DIR = process.env.SEAFILE_EPISODE_INFO_DIR ?? '/Episode Info'
export const SEAFILE_EPISODE_UPLOAD_DIR = process.env.SEAFILE_EPISODE_UPLOAD_DIR ?? '/Episode Upload'
// Cloudflare R2 storage
export const R2_ACCOUNT_ID = process.env.R2_ACCOUNT_ID ?? ''
export const R2_ACCESS_KEY_ID = process.env.R2_ACCESS_KEY_ID ?? ''
export const R2_SECRET_ACCESS_KEY = process.env.R2_SECRET_ACCESS_KEY ?? ''
export const R2_BUCKET = process.env.R2_BUCKET ?? ''
export const R2_PUBLIC_URL = (process.env.R2_PUBLIC_URL ?? '').replace(/\/$/, '')
export const HOST_SESSION_COOKIE = 'vbn_host_session'
export const HOST_SESSION_TTL_MS = 24 * 60 * 60 * 1000
export const DIST_DIR = path.join(ROOT_DIR, 'dist')
export const INDEX_FILE = path.join(DIST_DIR, 'index.html')
export const DIST_IMAGES_DIR = path.join(DIST_DIR, 'images')
export const PUBLIC_IMAGES_DIR = path.join(ROOT_DIR, 'public', 'images')
validateAdminPasswordSetup()
export const TITUS_STUDY_FILE = process.env.TITUS_STUDY_FILE
? path.resolve(ROOT_DIR, process.env.TITUS_STUDY_FILE)
: path.join(ROOT_DIR, 'A_Study_of_Titus.pdf')
export const TITUS_STUDY_DOWNLOAD_NAME = process.env.TITUS_STUDY_DOWNLOAD_NAME ?? 'A_Study_of_Titus.pdf'
// Rate limit / timing constants
export const VISITOR_COOKIE = 'vbn_vid'
export const CONSENT_COOKIE = 'vbn_analytics_consent'
export const MAX_RECENT_VISITS = 1000
export const VISITOR_RETENTION_DAYS_DEFAULT = 180
export const BACKUP_RETENTION_DAYS = 30
export const BACKUP_INTERVAL_MS = 24 * 60 * 60 * 1000
export const MAX_CONTACT_SUBMISSIONS = 5000
export const CONTACT_EMAIL_COOLDOWN_MS = Math.max(10 * 1000, Number(process.env.CONTACT_EMAIL_COOLDOWN_MS ?? 60 * 1000) || 60 * 1000)
export const DOWNLOAD_TOKEN_TTL_MS = 10 * 60 * 1000
export const MAX_QUESTIONS = 1000
export const MAX_STUDY_COMMENTS = 10000
export const MAX_STUDY_COMMENT_LENGTH = 2000
export const MAX_STUDY_USERS = 5000
export const MAX_STUDY_ENROLLMENTS_PER_USER = 100
export const MAX_STUDY_NOTES_PER_USER = 500
export const MAX_STUDY_NOTE_LENGTH = 12000
export const EMAIL_CHANGE_TOKEN_TTL_MS = 24 * 60 * 60 * 1000
export const EMAIL_VERIFY_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000
export const PASSWORD_RESET_TOKEN_TTL_MS = 60 * 60 * 1000
export const STUDY_SESSION_COOKIE = 'vbn_study_session'
export const STUDY_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000
export const STUDY_TOTP_PENDING_TTL_MS = 5 * 60 * 1000
export const EMAIL_OTP_TTL_MS = 10 * 60 * 1000
export const EMAIL_OTP_MAX_ATTEMPTS = 5
export const USE_RESEND_AUTOMATION_WELCOME = process.env.RESEND_AUTOMATION_WELCOME === 'true'
export const DEFAULT_RESEND_FROM = 'Verse by Verse with Nate <hello@versebyversewithnate.us>'
export const DEFAULT_RESEND_TO = 'hello@versebyversewithnate.us'
export const DEFAULT_RESEND_REPLY_TO = 'hello@versebyversewithnate.us'
export const ADMIN_REPLY_FROM = DEFAULT_RESEND_FROM
export const NATE_RESEND_FROM = 'Verse by Verse with Nate <nate@versebyversewithnate.us>'
// Addresses an admin may send a reply from — must stay in sync with the <select> options in src/AdminPage.tsx.
export const ADMIN_REPLY_FROM_OPTIONS = [DEFAULT_RESEND_FROM, NATE_RESEND_FROM]
export const DEFAULT_REPLY_TEMPLATES = [
{
id: 'thanks-for-reaching-out',
label: 'Thank You Reply',
subject: 'Thanks for reaching out to Verse by Verse with Nate',
message: 'Thank you for reaching out.\n\nI appreciate your message and wanted to follow up personally.',
},
{
id: 'question-received',
label: 'Question Received',
subject: 'Your Bible question was received',
message: 'Thank you for sending your Bible question.\n\nI have received it, and I appreciate you taking the time to write in.',
},
{
id: 'testimony-thank-you',
label: 'Testimony Thank You',
subject: 'Thank you for sharing your testimony',
message: 'Thank you for sharing what the Lord is doing in your life.\n\nYour message was an encouragement to read.',
},
]
export const EMPTY_HIT_STATS = {
totalHits: 0,
realHits: 0,
botHits: 0,
firstHitAt: null,
lastHitAt: null,
byPath: {},
byPathReal: {},
byPathBot: {},
byDay: {},
byDayReal: {},
byDayBot: {},
botReasons: {},
}
export const EMPTY_VISITOR_STATS = {
totalVisits: 0,
uniqueVisitors: 0,
returningVisits: 0,
firstVisitAt: null,
lastVisitAt: null,
visitors: {},
ipHashIndex: {},
recentVisits: [],
geoCacheByIp: {},
}