security improvements

This commit is contained in:
2025-10-07 13:39:53 -04:00
parent a4aca9c99d
commit 329becfb08
11 changed files with 287 additions and 35 deletions

View File

@@ -44,6 +44,22 @@ export interface SermonNote {
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')
@@ -102,6 +118,27 @@ export function getDatabase() {
)
`)
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
@@ -249,3 +286,61 @@ 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)
}