102 lines
3.4 KiB
Go
102 lines
3.4 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// Open opens (or creates) the SQLite database and runs schema migrations.
|
|
func Open(path string) (*sql.DB, error) {
|
|
db, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_foreign_keys=on")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open db: %w", err)
|
|
}
|
|
|
|
if err := db.Ping(); err != nil {
|
|
return nil, fmt.Errorf("ping db: %w", err)
|
|
}
|
|
|
|
if err := runSchema(db); err != nil {
|
|
return nil, fmt.Errorf("schema: %w", err)
|
|
}
|
|
|
|
slog.Info("database ready", "path", path)
|
|
return db, nil
|
|
}
|
|
|
|
// PurgeExpired deletes expired sessions and used/expired password reset tokens.
|
|
func PurgeExpired(database *sql.DB) error {
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
if _, err := database.Exec(`DELETE FROM sessions WHERE expires_at < ?`, now); err != nil {
|
|
return fmt.Errorf("purge sessions: %w", err)
|
|
}
|
|
if _, err := database.Exec(`DELETE FROM password_resets WHERE expires_at < ? OR used = 1`, now); err != nil {
|
|
return fmt.Errorf("purge password_resets: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runSchema(db *sql.DB) error {
|
|
stmts := []string{
|
|
`CREATE TABLE IF NOT EXISTS customers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
first_name TEXT NOT NULL DEFAULT '',
|
|
last_name TEXT NOT NULL DEFAULT '',
|
|
stripe_customer_id TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS sessions (
|
|
token TEXT PRIMARY KEY,
|
|
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
|
stripe_subscription_id TEXT NOT NULL UNIQUE,
|
|
stripe_price_id TEXT NOT NULL DEFAULT '',
|
|
plan_name TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
current_period_end TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS invoices (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
|
stripe_invoice_id TEXT NOT NULL UNIQUE,
|
|
amount_cents INTEGER NOT NULL DEFAULT 0,
|
|
currency TEXT NOT NULL DEFAULT 'usd',
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
invoice_pdf_url TEXT NOT NULL DEFAULT '',
|
|
period_start TEXT NOT NULL DEFAULT '',
|
|
period_end TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS password_resets (
|
|
token TEXT PRIMARY KEY,
|
|
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
|
expires_at TEXT NOT NULL,
|
|
used INTEGER NOT NULL DEFAULT 0
|
|
)`,
|
|
}
|
|
|
|
for _, s := range stmts {
|
|
if _, err := db.Exec(s); err != nil {
|
|
return fmt.Errorf("exec schema: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|