Files
nlcc-itinerary/server/utils/database.ts
Joshua Ryder 0ff37e8999 feat: Implement automatic sermon archiving based on dates
Add intelligent auto-archiving system that automatically moves sermons to the "Previous Sermons" list when they are 1 day past their most recent date.

Features:
- Auto-archive logic that checks both primary and additional sermon dates
- Finds the most recent date across all dates for a sermon
- Archives sermon 1 day after the most recent date has passed
- Manual trigger via "Run Auto-Archive Now" button on admin page
- Automatic daily execution via scheduled cleanup task
- Clear admin UI with explanatory text and status messages
- Manual archive/unarchive functionality preserved

Implementation:
- Added getMostRecentSermonDate() helper to find latest date from primary and additional dates
- Added autoArchiveOldSermons() function to database utils
- Created /api/sermons/auto-archive endpoint for manual triggering
- Integrated into daily cleanup plugin schedule
- Updated admin UI with auto-archive button and status indicators
- Added unarchiveSermon() function for completeness

The system runs automatically every 24 hours and can be manually triggered by admins. Sermons are moved to the previous sermons dropdown on the home page exactly 1 day after their final presentation date, ensuring the main page always shows current and upcoming content while preserving access to past sermons.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 09:53:17 -05:00

735 lines
24 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Database from 'better-sqlite3'
import { join } from 'path'
import bcrypt from 'bcrypt'
import crypto from 'crypto'
let db: Database.Database | null = null
/**
* Generate a cryptographically secure random string
* @param length Length of the string to generate
* @param charset Character set to use (default: alphanumeric + symbols)
*/
function generateSecurePassword(length: number = 16): string {
const lowercase = 'abcdefghijklmnopqrstuvwxyz'
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const numbers = '0123456789'
const symbols = '!@#$%^&*-_=+'
const allChars = lowercase + uppercase + numbers + symbols
let password = ''
// Ensure at least one character from each category
password += lowercase[crypto.randomInt(lowercase.length)]
password += uppercase[crypto.randomInt(uppercase.length)]
password += numbers[crypto.randomInt(numbers.length)]
password += symbols[crypto.randomInt(symbols.length)]
// Fill the rest randomly
for (let i = password.length; i < length; i++) {
password += allChars[crypto.randomInt(allChars.length)]
}
// Shuffle the password to avoid predictable patterns
return password.split('').sort(() => crypto.randomInt(3) - 1).join('')
}
/**
* Generate a cryptographically secure AUTH_SECRET
*/
function generateAuthSecret(): string {
return crypto.randomBytes(32).toString('hex')
}
/**
* Initialize or retrieve AUTH_SECRET from database
* Falls back to environment variable if provided
*/
function initializeAuthSecret(): string {
const db = getDatabase()
const config = useRuntimeConfig()
// Check if AUTH_SECRET is already in database
const existingSecret = getSetting('auth_secret')
if (existingSecret) {
return existingSecret.value
}
// Check if provided via environment (for advanced users)
const envSecret = config.authSecret
if (envSecret && envSecret !== 'change-this-secret-in-production' && envSecret !== '') {
// Store the env-provided secret to database for consistency
setSetting('auth_secret', envSecret)
console.log(' Using AUTH_SECRET from environment variable')
return envSecret
}
// Generate new secure secret
const newSecret = generateAuthSecret()
setSetting('auth_secret', newSecret)
console.log('\n' + '='.repeat(80))
console.log('🔐 GENERATED NEW AUTH_SECRET')
console.log('='.repeat(80))
console.log('A new authentication secret has been generated and stored in the database.')
console.log('This secret is used for session management and will persist across restarts.')
console.log('='.repeat(80) + '\n')
return newSecret
}
/**
* Initialize default admin user if no admin exists
* Generates secure random password and logs it on first creation
*/
function initializeAdminUser(): void {
const db = getDatabase()
const config = useRuntimeConfig()
// Check if ANY admin user exists
const adminExists = db.prepare('SELECT COUNT(*) as count FROM users WHERE is_admin = 1').get() as { count: number }
if (adminExists.count > 0) {
// Admin already exists, nothing to do
return
}
// Determine admin username (allow override via env)
const adminUsername = config.adminUsername || 'admin'
// Check if environment provides a password (for backward compatibility or custom setups)
let adminPassword = config.adminPassword
let passwordWasGenerated = false
if (!adminPassword || adminPassword === 'admin123' || adminPassword === 'Admin123!') {
// Generate secure random password
adminPassword = generateSecurePassword(20)
passwordWasGenerated = true
}
// Hash the password
const saltRounds = 10
const hashedPassword = bcrypt.hashSync(adminPassword, saltRounds)
// Create admin user
db.prepare('INSERT INTO users (username, password, is_admin) VALUES (?, ?, 1)')
.run(adminUsername, hashedPassword)
// Log credentials prominently
console.log('\n' + '='.repeat(80))
console.log('🎉 INITIAL ADMIN ACCOUNT CREATED')
console.log('='.repeat(80))
console.log(`Username: ${adminUsername}`)
console.log(`Password: ${adminPassword}`)
console.log('='.repeat(80))
console.log('⚠️ IMPORTANT: Save these credentials immediately!')
console.log('This password will not be shown again.')
if (passwordWasGenerated) {
console.log('This password was randomly generated for security.')
}
console.log('You can change it after logging in via the Profile page.')
console.log('='.repeat(80) + '\n')
}
export interface Sermon {
id?: number
slug: string
title: string
date: string
dates?: string
bible_references: string
personal_appliance: string
pastors_challenge: string
worship_songs?: string
created_by?: string
created_at?: string
}
export interface User {
id?: number
username: string
password: string
email?: string
first_name?: string
last_name?: string
is_admin: number
failed_login_attempts?: number
locked_until?: string | null
}
export interface PasswordResetCode {
id?: number
email: string
code: string
expires_at: string
created_at?: string
}
export interface SermonNote {
id?: number
user_id: number
sermon_id: number
notes: string
created_at?: string
updated_at?: string
}
export interface Session {
id?: number
token: string
username: string
csrf_token?: string
expires_at: string
created_at?: string
}
export interface RateLimit {
id?: number
identifier: string
endpoint: string
attempts: number
reset_at: string
}
export interface Setting {
id?: number
key: string
value: string
updated_at?: string
}
export function getDatabase() {
if (!db) {
const dbPath = join(process.cwd(), 'data', 'sermons.db')
db = new Database(dbPath)
// Create tables if they don't exist
db.exec(`
CREATE TABLE IF NOT EXISTS sermons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
date TEXT NOT NULL,
dates TEXT,
bible_references TEXT NOT NULL,
personal_appliance TEXT NOT NULL,
pastors_challenge TEXT NOT NULL,
worship_songs TEXT,
created_by TEXT,
archived INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// Add created_by column if it doesn't exist (migration)
try {
db.exec(`ALTER TABLE sermons ADD COLUMN created_by TEXT`)
} catch (e) {
// Column already exists, ignore error
}
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT,
first_name TEXT,
last_name TEXT,
is_admin INTEGER DEFAULT 0,
failed_login_attempts INTEGER DEFAULT 0,
locked_until DATETIME
)
`)
// Add account lockout columns if they don't exist (migration)
try {
db.exec(`ALTER TABLE users ADD COLUMN failed_login_attempts INTEGER DEFAULT 0`)
} catch (e) {
// Column already exists, ignore error
}
try {
db.exec(`ALTER TABLE users ADD COLUMN locked_until DATETIME`)
} catch (e) {
// Column already exists, ignore error
}
db.exec(`
CREATE TABLE IF NOT EXISTS password_reset_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
code TEXT NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
db.exec(`
CREATE TABLE IF NOT EXISTS sermon_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
sermon_id INTEGER NOT NULL,
notes TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (sermon_id) REFERENCES sermons(id) ON DELETE CASCADE,
UNIQUE(user_id, sermon_id)
)
`)
// Create indexes for foreign keys to optimize queries
// SQLite doesn't automatically index foreign keys
db.exec(`CREATE INDEX IF NOT EXISTS idx_sermon_notes_user_id ON sermon_notes(user_id)`)
db.exec(`CREATE INDEX IF NOT EXISTS idx_sermon_notes_sermon_id ON sermon_notes(sermon_id)`)
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT UNIQUE NOT NULL,
username TEXT NOT NULL,
csrf_token TEXT,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// Add csrf_token column if it doesn't exist (migration)
try {
db.exec(`ALTER TABLE sessions ADD COLUMN csrf_token TEXT`)
} catch (e) {
// Column already exists, ignore error
}
db.exec(`
CREATE TABLE IF NOT EXISTS rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
identifier TEXT NOT NULL,
endpoint TEXT NOT NULL,
attempts INTEGER DEFAULT 1,
reset_at DATETIME NOT NULL,
UNIQUE(identifier, endpoint)
)
`)
db.exec(`
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// Create additional performance indexes for frequently queried columns
// Sermons: date is used for sorting and filtering
db.exec(`CREATE INDEX IF NOT EXISTS idx_sermons_date ON sermons(date DESC)`)
db.exec(`CREATE INDEX IF NOT EXISTS idx_sermons_archived ON sermons(archived, date DESC)`)
// Users: email lookups for password reset
db.exec(`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`)
// Sessions: token lookups for authentication
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token)`)
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at)`)
// Rate limits: identifier+endpoint lookups
db.exec(`CREATE INDEX IF NOT EXISTS idx_rate_limits_identifier_endpoint ON rate_limits(identifier, endpoint)`)
db.exec(`CREATE INDEX IF NOT EXISTS idx_rate_limits_reset_at ON rate_limits(reset_at)`)
// Password reset codes: email+code lookups
db.exec(`CREATE INDEX IF NOT EXISTS idx_password_reset_codes_email ON password_reset_codes(email)`)
db.exec(`CREATE INDEX IF NOT EXISTS idx_password_reset_codes_expires_at ON password_reset_codes(expires_at)`)
// Initialize AUTH_SECRET (generate if needed)
initializeAuthSecret()
// Initialize admin user (create if no admin exists)
initializeAdminUser()
}
return db
}
export function getAllSermons(limit?: number, includeArchived: boolean = false) {
const db = getDatabase()
const whereClause = includeArchived ? '' : 'WHERE archived = 0'
if (limit) {
return db.prepare(`SELECT * FROM sermons ${whereClause} ORDER BY date DESC LIMIT ?`).all(limit) as Sermon[]
}
return db.prepare(`SELECT * FROM sermons ${whereClause} ORDER BY date DESC`).all() as Sermon[]
}
export function getArchivedSermons() {
const db = getDatabase()
return db.prepare('SELECT * FROM sermons WHERE archived = 1 ORDER BY date DESC').all() as Sermon[]
}
export function archiveSermon(id: number) {
const db = getDatabase()
return db.prepare('UPDATE sermons SET archived = 1 WHERE id = ?').run(id)
}
export function unarchiveSermon(id: number) {
const db = getDatabase()
return db.prepare('UPDATE sermons SET archived = 0 WHERE id = ?').run(id)
}
// Get the most recent date from a sermon (checking both primary date and additional dates)
function getMostRecentSermonDate(sermon: any): Date {
const dates: string[] = [sermon.date]
// Add additional dates if they exist
if (sermon.dates) {
try {
const additionalDates = JSON.parse(sermon.dates)
dates.push(...additionalDates)
} catch {
// If parsing fails, just use primary date
}
}
// Convert all dates to Date objects and find the most recent
const dateTimes = dates.map(dateStr => {
const date = new Date(dateStr + 'T00:00:00')
return date.getTime()
})
return new Date(Math.max(...dateTimes))
}
export function autoArchiveOldSermons(): { archivedCount: number } {
const db = getDatabase()
// Get all non-archived sermons
const sermons = db.prepare('SELECT * FROM sermons WHERE archived = 0').all() as any[]
const now = new Date()
now.setHours(0, 0, 0, 0) // Start of today
let archivedCount = 0
for (const sermon of sermons) {
const mostRecentDate = getMostRecentSermonDate(sermon)
// Calculate days difference
const oneDayAfterSermon = new Date(mostRecentDate)
oneDayAfterSermon.setDate(oneDayAfterSermon.getDate() + 1)
// If it's been at least 1 day since the most recent sermon date, archive it
if (now >= oneDayAfterSermon) {
archiveSermon(sermon.id!)
archivedCount++
}
}
return { archivedCount }
}
export function getSermonBySlug(slug: string) {
const db = getDatabase()
return db.prepare('SELECT * FROM sermons WHERE slug = ?').get(slug) as Sermon | undefined
}
export function createSermon(sermon: Sermon) {
const db = getDatabase()
const stmt = db.prepare(`
INSERT INTO sermons (slug, title, date, dates, bible_references, personal_appliance, pastors_challenge, worship_songs, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
return stmt.run(
sermon.slug,
sermon.title,
sermon.date,
sermon.dates || null,
sermon.bible_references,
sermon.personal_appliance,
sermon.pastors_challenge,
sermon.worship_songs || null,
sermon.created_by || null
)
}
export function getUserByUsername(username: string) {
const db = getDatabase()
return db.prepare('SELECT * FROM users WHERE username = ?').get(username) as User | undefined
}
export function createUser(username: string, password: string, email?: string, firstName?: string, lastName?: string) {
const db = getDatabase()
const saltRounds = 10
const hashedPassword = bcrypt.hashSync(password, saltRounds)
return db.prepare('INSERT INTO users (username, password, email, first_name, last_name, is_admin) VALUES (?, ?, ?, ?, ?, 0)')
.run(username, hashedPassword, email || null, firstName || null, lastName || null)
}
export function getUserByEmail(email: string) {
const db = getDatabase()
return db.prepare('SELECT * FROM users WHERE email = ?').get(email) as User | undefined
}
export function createPasswordResetCode(email: string, code: string, expiresAt: string) {
const db = getDatabase()
// Delete any existing codes for this email
db.prepare('DELETE FROM password_reset_codes WHERE email = ?').run(email)
return db.prepare('INSERT INTO password_reset_codes (email, code, expires_at) VALUES (?, ?, ?)')
.run(email, code, expiresAt)
}
export function getPasswordResetCode(email: string, code: string) {
const db = getDatabase()
return db.prepare("SELECT * FROM password_reset_codes WHERE email = ? AND code = ? AND expires_at > datetime('now')")
.get(email, code) as PasswordResetCode | undefined
}
export function deletePasswordResetCode(email: string) {
const db = getDatabase()
return db.prepare('DELETE FROM password_reset_codes WHERE email = ?').run(email)
}
export function resetPasswordByEmail(email: string, newPassword: string) {
const db = getDatabase()
const saltRounds = 10
const hashedPassword = bcrypt.hashSync(newPassword, saltRounds)
const result = db.prepare('UPDATE users SET password = ? WHERE LOWER(email) = LOWER(?)').run(hashedPassword, email)
return result
}
export function getAllUsers() {
const db = getDatabase()
return db.prepare('SELECT id, username, email, first_name, last_name, is_admin, failed_login_attempts, locked_until FROM users ORDER BY username').all() as Omit<User, 'password'>[]
}
export function deleteUser(id: number) {
const db = getDatabase()
return db.prepare('DELETE FROM users WHERE id = ?').run(id)
}
export function updateUserRole(id: number, isAdmin: number) {
const db = getDatabase()
return db.prepare('UPDATE users SET is_admin = ? WHERE id = ?').run(isAdmin, id)
}
export function resetUserPassword(id: number, newPassword: string) {
const db = getDatabase()
const saltRounds = 10
const hashedPassword = bcrypt.hashSync(newPassword, saltRounds)
return db.prepare('UPDATE users SET password = ? WHERE id = ?').run(hashedPassword, id)
}
/**
* Account Lockout Functions
* Provides protection against distributed brute force attacks
*/
/**
* Check if account is currently locked
* Returns true if account is locked, false if unlocked or lock has expired
*/
export function isAccountLocked(username: string): boolean {
const db = getDatabase()
const user = db.prepare("SELECT locked_until FROM users WHERE username = ? AND locked_until > datetime('now')")
.get(username) as { locked_until: string } | undefined
return !!user
}
/**
* Increment failed login attempts for an account
* Locks account if threshold (10 attempts) is reached
*/
export function incrementFailedAttempts(username: string): void {
const db = getDatabase()
// Get current attempt count
const user = db.prepare('SELECT failed_login_attempts FROM users WHERE username = ?')
.get(username) as { failed_login_attempts: number } | undefined
if (!user) return
const newAttempts = (user.failed_login_attempts || 0) + 1
const maxAttempts = 5 // Aligned with IP rate limiting
if (newAttempts >= maxAttempts) {
// Lock account for 15 minutes (aligned with IP rate limiting)
const lockUntil = new Date(Date.now() + 15 * 60 * 1000).toISOString()
db.prepare('UPDATE users SET failed_login_attempts = ?, locked_until = ? WHERE username = ?')
.run(newAttempts, lockUntil, username)
console.log(`[ACCOUNT LOCKED] User ${username} locked after ${newAttempts} failed attempts until ${lockUntil}`)
} else {
// Just increment counter
db.prepare('UPDATE users SET failed_login_attempts = ? WHERE username = ?')
.run(newAttempts, username)
console.log(`[ACCOUNT SECURITY] Failed login attempt #${newAttempts} for user: ${username}`)
}
}
/**
* Reset failed login attempts on successful login
*/
export function resetFailedAttempts(username: string): void {
const db = getDatabase()
db.prepare('UPDATE users SET failed_login_attempts = 0, locked_until = NULL WHERE username = ?')
.run(username)
}
/**
* Admin function to manually unlock an account
*/
export function unlockAccount(userId: number): void {
const db = getDatabase()
db.prepare('UPDATE users SET failed_login_attempts = 0, locked_until = NULL WHERE id = ?')
.run(userId)
}
export function getSermonNote(userId: number, sermonId: number) {
const db = getDatabase()
return db.prepare('SELECT * FROM sermon_notes WHERE user_id = ? AND sermon_id = ?').get(userId, sermonId) as SermonNote | undefined
}
export function saveSermonNote(userId: number, sermonId: number, notes: string) {
const db = getDatabase()
const existing = getSermonNote(userId, sermonId)
if (existing) {
return db.prepare('UPDATE sermon_notes SET notes = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND sermon_id = ?').run(notes, userId, sermonId)
} else {
return db.prepare('INSERT INTO sermon_notes (user_id, sermon_id, notes) VALUES (?, ?, ?)').run(userId, sermonId, notes)
}
}
export function deleteSermonNote(userId: number, sermonId: number) {
const db = getDatabase()
return db.prepare('DELETE FROM sermon_notes WHERE user_id = ? AND sermon_id = ?').run(userId, sermonId)
}
// Session management functions
export function createSession(token: string, username: string, expiresAt: string, csrfToken?: string) {
const db = getDatabase()
return db.prepare('INSERT INTO sessions (token, username, csrf_token, expires_at) VALUES (?, ?, ?, ?)')
.run(token, username, csrfToken || null, expiresAt)
}
export function getSessionByToken(token: string) {
const db = getDatabase()
return db.prepare("SELECT * FROM sessions WHERE token = ? AND expires_at > datetime('now')")
.get(token) as Session | undefined
}
export function deleteSession(token: string) {
const db = getDatabase()
return db.prepare('DELETE FROM sessions WHERE token = ?').run(token)
}
export function deleteExpiredSessions() {
const db = getDatabase()
return db.prepare("DELETE FROM sessions WHERE expires_at <= datetime('now')").run()
}
/**
* Delete all sessions for a specific user
* Used when password changes or security events require session invalidation
*/
export function deleteAllUserSessions(username: string) {
const db = getDatabase()
return db.prepare('DELETE FROM sessions WHERE username = ?').run(username)
}
/**
* Delete all sessions except the current one
* Used when user wants to logout other devices but keep current session
*/
export function deleteOtherUserSessions(username: string, currentToken: string) {
const db = getDatabase()
return db.prepare('DELETE FROM sessions WHERE username = ? AND token != ?').run(username, currentToken)
}
// Rate limiting functions
export function checkRateLimit(identifier: string, endpoint: string, maxAttempts: number, windowMinutes: number): boolean {
const db = getDatabase()
// Clean up expired rate limit records
db.prepare("DELETE FROM rate_limits WHERE reset_at <= datetime('now')").run()
const existing = db.prepare('SELECT * FROM rate_limits WHERE identifier = ? AND endpoint = ?')
.get(identifier, endpoint) as RateLimit | undefined
if (!existing) {
// First attempt - create new record
const resetAt = new Date(Date.now() + windowMinutes * 60 * 1000).toISOString()
db.prepare('INSERT INTO rate_limits (identifier, endpoint, attempts, reset_at) VALUES (?, ?, 1, ?)')
.run(identifier, endpoint, resetAt)
return true
}
if (existing.attempts >= maxAttempts) {
// Rate limit exceeded
return false
}
// Increment attempts
db.prepare('UPDATE rate_limits SET attempts = attempts + 1 WHERE identifier = ? AND endpoint = ?')
.run(identifier, endpoint)
return true
}
export function resetRateLimit(identifier: string, endpoint: string) {
const db = getDatabase()
return db.prepare('DELETE FROM rate_limits WHERE identifier = ? AND endpoint = ?')
.run(identifier, endpoint)
}
/**
* Clear all rate limits for a specific endpoint
* Used when admin performs actions that should clear all rate limiting (e.g., unlocking accounts)
*/
export function clearAllRateLimitsForEndpoint(endpoint: string) {
const db = getDatabase()
const result = db.prepare('DELETE FROM rate_limits WHERE endpoint = ?').run(endpoint)
console.log(`[RATE LIMIT CLEARED] Cleared ${result.changes} rate limit entries for endpoint: ${endpoint}`)
return result
}
// Settings management functions
export function getSetting(key: string) {
const db = getDatabase()
return db.prepare('SELECT * FROM settings WHERE key = ?').get(key) as Setting | undefined
}
export function setSetting(key: string, value: string) {
const db = getDatabase()
const existing = getSetting(key)
if (existing) {
return db.prepare('UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?').run(value, key)
} else {
return db.prepare('INSERT INTO settings (key, value) VALUES (?, ?)').run(key, value)
}
}
/**
* Get the AUTH_SECRET from database
* This should always exist after database initialization
*/
export function getAuthSecret(): string {
const secret = getSetting('auth_secret')
if (!secret) {
throw new Error('AUTH_SECRET not found in database. Database may not be properly initialized.')
}
return secret.value
}
// Sermon retention policy functions
export function deleteOldSermons(retentionDays: number) {
const db = getDatabase()
// Calculate the cutoff date based on retention days
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - retentionDays)
const cutoffDateStr = cutoffDate.toISOString().split('T')[0] // Format as YYYY-MM-DD
// Delete sermons older than the cutoff date
return db.prepare('DELETE FROM sermons WHERE date < ?').run(cutoffDateStr)
}