import Database from 'better-sqlite3' import { join } from 'path' import bcrypt from 'bcrypt' let db: Database.Database | null = null 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_at?: string } export interface User { id?: number username: string password: string email?: string first_name?: string last_name?: string is_admin: number } 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 expires_at: string created_at?: string } export interface RateLimit { id?: number identifier: string endpoint: string attempts: number reset_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, archived INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) 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 ) `) 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) ) `) db.exec(` CREATE TABLE IF NOT EXISTS sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, token TEXT UNIQUE NOT NULL, username TEXT NOT NULL, expires_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) 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) ) `) // Insert default admin user from environment variables with hashed password const config = useRuntimeConfig() const adminUsername = config.adminUsername const adminPassword = config.adminPassword const userExists = db.prepare('SELECT COUNT(*) as count FROM users WHERE username = ?').get(adminUsername) as { count: number } if (userExists.count === 0) { // Hash the password before storing const saltRounds = 10 const hashedPassword = bcrypt.hashSync(adminPassword, saltRounds) db.prepare('INSERT INTO users (username, password, is_admin) VALUES (?, ?, 1)').run(adminUsername, hashedPassword) } } 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 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) 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 ) } 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 FROM users ORDER BY username').all() as Omit[] } 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) } 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) { const db = getDatabase() return db.prepare('INSERT INTO sessions (token, username, expires_at) VALUES (?, ?, ?)') .run(token, username, 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() } // 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) }