import path from 'node:path' import { fileURLToPath } from 'node:url' import { validateAdminPasswordSetup } from './auth.js' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) // server/config.js is inside server/, so the project root is one level up export const ROOT_DIR = path.resolve(__dirname, '..') const DEFAULT_DATA_DIR = path.join(ROOT_DIR, 'data') const configuredDataDir = typeof process.env.SITEFORGE_DATA_DIR === 'string' ? process.env.SITEFORGE_DATA_DIR.trim() : '' export const DATA_DIR = configuredDataDir ? (path.isAbsolute(configuredDataDir) ? configuredDataDir : path.resolve(ROOT_DIR, configuredDataDir)) : DEFAULT_DATA_DIR 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 PODCAST_CHECKLIST_FILE = path.join(DATA_DIR, 'podcast-checklist.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 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 ADMIN_SESSION_TTL_MS = 7 * 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_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 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 ' 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 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 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_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 DEFAULT_PODCAST_CHECKLIST_TASKS = [ { id: 'verify_script', label: 'Verify Script', phase: 'pre' }, { id: 'read_script', label: 'Read Script', phase: 'pre' }, { id: 'record', label: 'Record', phase: 'pre' }, { id: 'mix', label: 'Mix', phase: 'pre' }, { id: 'edit', label: 'Edit', phase: 'pre' }, { id: 'video_script', label: 'Run Video Conversion Script', phase: 'pre' }, { id: 'post_spotify', label: 'Post on Spotify', phase: 'pre' }, { id: 'update_website', label: 'Update Website', phase: 'post' }, { id: 'send_email', label: 'Send Email', phase: 'post' }, ] 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: {}, } // ── Podcast checklist helpers (no state dependency) ──────────────────────── function buildChecklistEpisode(series, number) { const tasks = {} for (const task of DEFAULT_PODCAST_CHECKLIST_TASKS) { tasks[task.id] = false } return { id: `${series.toLowerCase()}-${number}`, series, episodeNumber: number, title: '', datePublished: '', expanded: false, tasks, } } export function buildDefaultPodcastChecklist() { const titusEpisodes = [11, 12, 13, 14, 15].map(number => buildChecklistEpisode('Titus', number)) const colossiansEpisodes = Array.from({ length: 27 }, (_, index) => buildChecklistEpisode('Colossians', index + 1)) return { tasks: DEFAULT_PODCAST_CHECKLIST_TASKS, episodes: [...titusEpisodes, ...colossiansEpisodes], } }