diff --git a/server/api/analysis/index.post.ts b/server/api/analysis/index.post.ts index 579c7be..8426698 100644 --- a/server/api/analysis/index.post.ts +++ b/server/api/analysis/index.post.ts @@ -1,8 +1,9 @@ -import db from '../../utils/db' +import { useDb } from '../../utils/db' import { requireAdmin } from '../../utils/auth' export default defineEventHandler(async (event) => { requireAdmin(event) + const db = useDb() const ideas = db.prepare('SELECT text, category FROM ideas WHERE hidden = 0').all() as any[] if (ideas.length < 3) { diff --git a/server/api/ideas/[id].delete.ts b/server/api/ideas/[id].delete.ts index 1a0aa27..83f180b 100644 --- a/server/api/ideas/[id].delete.ts +++ b/server/api/ideas/[id].delete.ts @@ -1,8 +1,9 @@ -import db from '../../utils/db' +import { useDb } from '../../utils/db' import { requireAdmin } from '../../utils/auth' export default defineEventHandler((event) => { requireAdmin(event) + const db = useDb() const id = getRouterParam(event, 'id') db.prepare('DELETE FROM ideas WHERE id = ?').run(id) return { ok: true } diff --git a/server/api/ideas/[id].patch.ts b/server/api/ideas/[id].patch.ts index c571e67..9466fac 100644 --- a/server/api/ideas/[id].patch.ts +++ b/server/api/ideas/[id].patch.ts @@ -1,8 +1,9 @@ -import db from '../../utils/db' +import { useDb } from '../../utils/db' import { requireAdmin } from '../../utils/auth' export default defineEventHandler(async (event) => { requireAdmin(event) + const db = useDb() const id = getRouterParam(event, 'id') const body = await readBody(event) diff --git a/server/api/ideas/index.get.ts b/server/api/ideas/index.get.ts index 518bca7..bac5a8b 100644 --- a/server/api/ideas/index.get.ts +++ b/server/api/ideas/index.get.ts @@ -1,7 +1,8 @@ -import db from '../../utils/db' +import { useDb } from '../../utils/db' import { isAdmin } from '../../utils/auth' export default defineEventHandler((event) => { + const db = useDb() const admin = isAdmin(event) const rows = admin ? db.prepare('SELECT * FROM ideas ORDER BY created_at DESC').all() diff --git a/server/api/ideas/index.post.ts b/server/api/ideas/index.post.ts index ab96767..85364cd 100644 --- a/server/api/ideas/index.post.ts +++ b/server/api/ideas/index.post.ts @@ -1,6 +1,7 @@ -import db from '../../utils/db' +import { useDb } from '../../utils/db' export default defineEventHandler(async (event) => { + const db = useDb() const { text, category } = await readBody(event) if (!text?.trim()) { throw createError({ statusCode: 400, statusMessage: 'Text is required' }) diff --git a/server/api/votes/[id].post.ts b/server/api/votes/[id].post.ts index 491e11c..4052417 100644 --- a/server/api/votes/[id].post.ts +++ b/server/api/votes/[id].post.ts @@ -1,7 +1,8 @@ import { createHash } from 'crypto' -import db from '../../utils/db' +import { useDb } from '../../utils/db' export default defineEventHandler((event) => { + const db = useDb() const id = getRouterParam(event, 'id') const ip = getRequestIP(event, { xForwardedFor: true }) || 'unknown' const voterHash = createHash('sha256').update(ip + ':' + id).digest('hex') @@ -10,7 +11,6 @@ export default defineEventHandler((event) => { db.prepare('INSERT INTO vote_log (idea_id, voter_hash) VALUES (?, ?)').run(id, voterHash) db.prepare('UPDATE ideas SET votes = votes + 1 WHERE id = ?').run(id) } catch { - // UNIQUE constraint = already voted throw createError({ statusCode: 409, statusMessage: 'Already voted' }) } diff --git a/server/utils/auth.ts b/server/utils/auth.ts index ab4d55d..a66eec7 100644 --- a/server/utils/auth.ts +++ b/server/utils/auth.ts @@ -1,8 +1,9 @@ import { randomBytes, timingSafeEqual } from 'crypto' import type { H3Event } from 'h3' -import db from './db' +import { useDb } from './db' export function createSession(): string { + const db = useDb() const token = randomBytes(32).toString('hex') const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() db.prepare('INSERT INTO sessions (token, expires_at) VALUES (?, ?)').run(token, expires) @@ -10,6 +11,7 @@ export function createSession(): string { } export function validateSession(token: string): boolean { + const db = useDb() const row = db.prepare('SELECT expires_at FROM sessions WHERE token = ?').get(token) as any if (!row) return false if (new Date(row.expires_at) < new Date()) { @@ -20,12 +22,12 @@ export function validateSession(token: string): boolean { } export function destroySession(token: string) { + const db = useDb() db.prepare('DELETE FROM sessions WHERE token = ?').run(token) } export function checkPassword(input: string): boolean { - const config = useRuntimeConfig() - const expected = config.adminPassword as string + const expected = process.env.ADMIN_PASSWORD || 'admin' if (input.length !== expected.length) return false return timingSafeEqual(Buffer.from(input), Buffer.from(expected)) } diff --git a/server/utils/db.ts b/server/utils/db.ts index 84567ab..0444dd4 100644 --- a/server/utils/db.ts +++ b/server/utils/db.ts @@ -2,38 +2,43 @@ import Database from 'better-sqlite3' import { mkdirSync } from 'fs' import { dirname } from 'path' -const config = useRuntimeConfig() -const dbPath = config.dbPath as string +let _db: InstanceType | null = null -mkdirSync(dirname(dbPath), { recursive: true }) +export function useDb() { + if (_db) return _db -const db = new Database(dbPath) -db.pragma('journal_mode = WAL') -db.pragma('foreign_keys = ON') + const dbPath = process.env.DB_PATH || './data/brainstorm.db' -db.exec(` - CREATE TABLE IF NOT EXISTS ideas ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - text TEXT NOT NULL, - category TEXT NOT NULL DEFAULT 'General', - votes INTEGER NOT NULL DEFAULT 0, - hidden INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); + mkdirSync(dirname(dbPath), { recursive: true }) - CREATE TABLE IF NOT EXISTS vote_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - idea_id INTEGER NOT NULL REFERENCES ideas(id) ON DELETE CASCADE, - voter_hash TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE(idea_id, voter_hash) - ); + _db = new Database(dbPath) + _db.pragma('journal_mode = WAL') + _db.pragma('foreign_keys = ON') - CREATE TABLE IF NOT EXISTS sessions ( - token TEXT PRIMARY KEY, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - expires_at TEXT NOT NULL - ); -`) + _db.exec(` + CREATE TABLE IF NOT EXISTS ideas ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL, + category TEXT NOT NULL DEFAULT 'General', + votes INTEGER NOT NULL DEFAULT 0, + hidden INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); -export default db + CREATE TABLE IF NOT EXISTS vote_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + idea_id INTEGER NOT NULL REFERENCES ideas(id) ON DELETE CASCADE, + voter_hash TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(idea_id, voter_hash) + ); + + CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT NOT NULL + ); + `) + + return _db +}