security improvements
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user