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

@@ -1,29 +1,58 @@
import { H3Event } from 'h3'
import crypto from 'crypto'
export function setAuthCookie(event: H3Event, username: string) {
setCookie(event, 'auth', username, {
// Generate a secure random session token
export function generateSessionToken(): string {
return crypto.randomBytes(32).toString('hex')
}
export function setAuthCookie(event: H3Event, sessionToken: string) {
setCookie(event, 'session_token', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/'
maxAge: 60 * 60 * 24, // 24 hours (shorter than before for better security)
path: '/',
sameSite: 'lax'
})
}
export function getAuthCookie(event: H3Event) {
return getCookie(event, 'auth')
export function getAuthCookie(event: H3Event): string | undefined {
return getCookie(event, 'session_token')
}
// Async version that validates session and returns username
export async function getAuthUsername(event: H3Event): Promise<string | null> {
return await getSessionUsername(event)
}
export function clearAuthCookie(event: H3Event) {
deleteCookie(event, 'auth')
deleteCookie(event, 'session_token')
}
export async function getSessionUsername(event: H3Event): Promise<string | null> {
const token = getAuthCookie(event)
if (!token) {
return null
}
const { getSessionByToken, deleteSession } = await import('./database')
const session = getSessionByToken(token)
if (!session) {
clearAuthCookie(event)
return null
}
return session.username
}
export function isAuthenticated(event: H3Event): boolean {
const auth = getAuthCookie(event)
return !!auth
const token = getAuthCookie(event)
return !!token
}
export async function getAuthUser(event: H3Event) {
const username = getAuthCookie(event)
const username = await getSessionUsername(event)
if (!username) {
return null
}

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)
}