refactor: clean DB schema, remove users/sessions tables
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bc300f54f2
commit
9b6d8f0555
1 changed files with 38 additions and 142 deletions
180
lib/db.ts
180
lib/db.ts
|
|
@ -1,11 +1,10 @@
|
||||||
import Database from "better-sqlite3"
|
import Database from "better-sqlite3"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
// Database file stored in project root
|
|
||||||
const DB_PATH = path.join(process.cwd(), "data", "quotes.db")
|
const DB_PATH = path.join(process.cwd(), "data", "quotes.db")
|
||||||
|
|
||||||
// Ensure data directory exists
|
// Ensure data directory exists
|
||||||
import fs from "fs"
|
|
||||||
const dataDir = path.dirname(DB_PATH)
|
const dataDir = path.dirname(DB_PATH)
|
||||||
if (!fs.existsSync(dataDir)) {
|
if (!fs.existsSync(dataDir)) {
|
||||||
fs.mkdirSync(dataDir, { recursive: true })
|
fs.mkdirSync(dataDir, { recursive: true })
|
||||||
|
|
@ -13,7 +12,10 @@ if (!fs.existsSync(dataDir)) {
|
||||||
|
|
||||||
const db = new Database(DB_PATH)
|
const db = new Database(DB_PATH)
|
||||||
|
|
||||||
// Initialize database schema
|
// Enable WAL mode for better concurrent read performance
|
||||||
|
db.pragma("journal_mode = WAL")
|
||||||
|
|
||||||
|
// Create schema if not exists
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS quotes (
|
CREATE TABLE IF NOT EXISTS quotes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|
@ -21,65 +23,38 @@ db.exec(`
|
||||||
address TEXT,
|
address TEXT,
|
||||||
area REAL NOT NULL,
|
area REAL NOT NULL,
|
||||||
height REAL NOT NULL,
|
height REAL NOT NULL,
|
||||||
include_floor_heating INTEGER NOT NULL DEFAULT 1,
|
include_floor_heating INTEGER DEFAULT 1,
|
||||||
flooring_type TEXT NOT NULL DEFAULT 'STANDARD',
|
flooring_type TEXT DEFAULT 'STANDARD',
|
||||||
customer_name TEXT NOT NULL,
|
customer_name TEXT NOT NULL,
|
||||||
customer_email TEXT NOT NULL,
|
customer_email TEXT NOT NULL,
|
||||||
customer_phone TEXT NOT NULL,
|
customer_phone TEXT NOT NULL,
|
||||||
remarks TEXT,
|
remarks TEXT,
|
||||||
total_excl_vat REAL,
|
total_excl_vat REAL NOT NULL,
|
||||||
total_incl_vat REAL,
|
total_incl_vat REAL NOT NULL,
|
||||||
status TEXT NOT NULL DEFAULT 'new',
|
status TEXT DEFAULT 'new',
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
email_opened_at TEXT
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// Migration: Add status column if it doesn't exist
|
// Ensure quote IDs start at 1000
|
||||||
try {
|
|
||||||
db.exec("ALTER TABLE quotes ADD COLUMN status TEXT NOT NULL DEFAULT 'new'")
|
|
||||||
} catch {
|
|
||||||
// Column already exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration: Add email_opened_at column for tracking
|
|
||||||
try {
|
|
||||||
db.exec("ALTER TABLE quotes ADD COLUMN email_opened_at TEXT")
|
|
||||||
} catch {
|
|
||||||
// Column already exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users table for authentication
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
email TEXT UNIQUE NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
|
|
||||||
// Sessions table for login sessions
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
expires_at TEXT NOT NULL,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
|
|
||||||
// Start IDs at 1000
|
|
||||||
const countResult = db.prepare("SELECT COUNT(*) as count FROM quotes").get() as { count: number }
|
const countResult = db.prepare("SELECT COUNT(*) as count FROM quotes").get() as { count: number }
|
||||||
if (countResult.count === 0) {
|
if (countResult.count === 0) {
|
||||||
db.exec(
|
try {
|
||||||
"INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone) VALUES (999, '0000', 0, 0, 'init', 'init', 'init')"
|
const seqExists = db.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'quotes'").get()
|
||||||
)
|
if (!seqExists) {
|
||||||
db.exec("DELETE FROM quotes WHERE id = 999")
|
db.exec(
|
||||||
// Reset autoincrement to start at 1000
|
"INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone, total_excl_vat, total_incl_vat) VALUES (999, '0000', 0, 0, 'init', 'init', 'init', 0, 0)"
|
||||||
db.exec("UPDATE sqlite_sequence SET seq = 999 WHERE name = 'quotes'")
|
)
|
||||||
|
db.exec("DELETE FROM quotes WHERE id = 999")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// sqlite_sequence may not exist yet on fresh DB — that's fine
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface QuoteInput {
|
export interface QuoteInput {
|
||||||
postalCode: string
|
postalCode: string
|
||||||
address?: string
|
address?: string
|
||||||
|
|
@ -91,8 +66,8 @@ export interface QuoteInput {
|
||||||
customerEmail: string
|
customerEmail: string
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
remarks?: string
|
remarks?: string
|
||||||
totalExclVat?: number
|
totalExclVat: number
|
||||||
totalInclVat?: number
|
totalInclVat: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuoteStatus = "new" | "contacted" | "accepted" | "rejected"
|
export type QuoteStatus = "new" | "contacted" | "accepted" | "rejected"
|
||||||
|
|
@ -104,6 +79,8 @@ export interface StoredQuote extends QuoteInput {
|
||||||
emailOpenedAt: string | null
|
emailOpenedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Queries ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function saveQuote(quote: QuoteInput): { id: number; slug: string } {
|
export function saveQuote(quote: QuoteInput): { id: number; slug: string } {
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO quotes (
|
INSERT INTO quotes (
|
||||||
|
|
@ -123,8 +100,8 @@ export function saveQuote(quote: QuoteInput): { id: number; slug: string } {
|
||||||
quote.customerEmail,
|
quote.customerEmail,
|
||||||
quote.customerPhone,
|
quote.customerPhone,
|
||||||
quote.remarks || null,
|
quote.remarks || null,
|
||||||
quote.totalExclVat || null,
|
quote.totalExclVat,
|
||||||
quote.totalInclVat || null
|
quote.totalInclVat
|
||||||
)
|
)
|
||||||
|
|
||||||
const id = result.lastInsertRowid as number
|
const id = result.lastInsertRowid as number
|
||||||
|
|
@ -140,39 +117,32 @@ export function getQuoteBySlug(slug: string): StoredQuote | null {
|
||||||
const [, postalCode, idStr] = match
|
const [, postalCode, idStr] = match
|
||||||
const id = parseInt(idStr, 10)
|
const id = parseInt(idStr, 10)
|
||||||
|
|
||||||
const stmt = db.prepare("SELECT * FROM quotes WHERE id = ? AND postal_code = ?")
|
const row = db.prepare("SELECT * FROM quotes WHERE id = ? AND postal_code = ?").get(id, postalCode) as any
|
||||||
const row = stmt.get(id, postalCode) as any
|
|
||||||
|
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
|
||||||
return rowToQuote(row)
|
return rowToQuote(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getQuoteById(id: number): StoredQuote | null {
|
export function getQuoteById(id: number): StoredQuote | null {
|
||||||
const stmt = db.prepare("SELECT * FROM quotes WHERE id = ?")
|
const row = db.prepare("SELECT * FROM quotes WHERE id = ?").get(id) as any
|
||||||
const row = stmt.get(id) as any
|
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
return rowToQuote(row)
|
return rowToQuote(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllQuotes(): StoredQuote[] {
|
export function getAllQuotes(): StoredQuote[] {
|
||||||
const stmt = db.prepare("SELECT * FROM quotes ORDER BY id DESC")
|
const rows = db.prepare("SELECT * FROM quotes ORDER BY id DESC").all() as any[]
|
||||||
const rows = stmt.all() as any[]
|
|
||||||
return rows.map(rowToQuote)
|
return rows.map(rowToQuote)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateQuoteStatus(id: number, status: QuoteStatus): boolean {
|
export function updateQuoteStatus(id: number, status: QuoteStatus): boolean {
|
||||||
const stmt = db.prepare("UPDATE quotes SET status = ? WHERE id = ?")
|
const result = db.prepare("UPDATE quotes SET status = ? WHERE id = ?").run(status, id)
|
||||||
const result = stmt.run(status, id)
|
|
||||||
return result.changes > 0
|
return result.changes > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markEmailOpened(id: number): boolean {
|
export function markEmailOpened(id: number): boolean {
|
||||||
// Only update if not already set (first open)
|
const result = db.prepare(
|
||||||
const stmt = db.prepare(
|
|
||||||
"UPDATE quotes SET email_opened_at = ? WHERE id = ? AND email_opened_at IS NULL"
|
"UPDATE quotes SET email_opened_at = ? WHERE id = ? AND email_opened_at IS NULL"
|
||||||
)
|
).run(new Date().toISOString(), id)
|
||||||
const result = stmt.run(new Date().toISOString(), id)
|
|
||||||
return result.changes > 0
|
return result.changes > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,77 +166,3 @@ function rowToQuote(row: any): StoredQuote {
|
||||||
emailOpenedAt: row.email_opened_at || null,
|
emailOpenedAt: row.email_opened_at || null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User management
|
|
||||||
export interface User {
|
|
||||||
id: number
|
|
||||||
email: string
|
|
||||||
name: string
|
|
||||||
createdAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserWithPassword extends User {
|
|
||||||
passwordHash: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createUser(email: string, passwordHash: string, name: string): User {
|
|
||||||
const stmt = db.prepare("INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)")
|
|
||||||
const result = stmt.run(email, passwordHash, name)
|
|
||||||
return {
|
|
||||||
id: result.lastInsertRowid as number,
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserByEmail(email: string): UserWithPassword | null {
|
|
||||||
const stmt = db.prepare("SELECT * FROM users WHERE email = ?")
|
|
||||||
const row = stmt.get(email) as any
|
|
||||||
if (!row) return null
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
email: row.email,
|
|
||||||
name: row.name,
|
|
||||||
passwordHash: row.password_hash,
|
|
||||||
createdAt: row.created_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserById(id: number): User | null {
|
|
||||||
const stmt = db.prepare("SELECT id, email, name, created_at FROM users WHERE id = ?")
|
|
||||||
const row = stmt.get(id) as any
|
|
||||||
if (!row) return null
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
email: row.email,
|
|
||||||
name: row.name,
|
|
||||||
createdAt: row.created_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session management
|
|
||||||
export function createSession(sessionId: string, userId: number, expiresAt: Date): void {
|
|
||||||
const stmt = db.prepare("INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)")
|
|
||||||
stmt.run(sessionId, userId, expiresAt.toISOString())
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSession(sessionId: string): { userId: number; expiresAt: Date } | null {
|
|
||||||
const stmt = db.prepare("SELECT user_id, expires_at FROM sessions WHERE id = ?")
|
|
||||||
const row = stmt.get(sessionId) as any
|
|
||||||
if (!row) return null
|
|
||||||
return {
|
|
||||||
userId: row.user_id,
|
|
||||||
expiresAt: new Date(row.expires_at),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteSession(sessionId: string): void {
|
|
||||||
const stmt = db.prepare("DELETE FROM sessions WHERE id = ?")
|
|
||||||
stmt.run(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cleanExpiredSessions(): void {
|
|
||||||
const stmt = db.prepare("DELETE FROM sessions WHERE expires_at < ?")
|
|
||||||
stmt.run(new Date().toISOString())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue