feat: config loading and sqlite schema
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
export interface Config {
|
||||
dataDir: string
|
||||
artworkDir: string
|
||||
@@ -5,3 +9,30 @@ export interface Config {
|
||||
port: number
|
||||
sessionSecret: string
|
||||
}
|
||||
|
||||
export function loadConfig(env: Record<string, string | undefined> = process.env): Config {
|
||||
const dataDir = env.DATA_DIR ?? path.resolve('data')
|
||||
mkdirSync(dataDir, { recursive: true })
|
||||
const artworkDir = path.join(dataDir, 'artwork-cache')
|
||||
mkdirSync(artworkDir, { recursive: true })
|
||||
return {
|
||||
dataDir,
|
||||
artworkDir,
|
||||
dbPath: path.join(dataDir, 'record-shop.db'),
|
||||
port: Number(env.PORT ?? 3000),
|
||||
sessionSecret: getOrCreateSecret(dataDir),
|
||||
}
|
||||
}
|
||||
|
||||
function getOrCreateSecret(dataDir: string): string {
|
||||
const secretPath = path.join(dataDir, 'session-secret')
|
||||
try {
|
||||
const existing = readFileSync(secretPath, 'utf8').trim()
|
||||
if (existing) return existing
|
||||
} catch {
|
||||
// first boot — create below
|
||||
}
|
||||
const secret = crypto.randomBytes(32).toString('hex')
|
||||
writeFileSync(secretPath, secret, { mode: 0o600 })
|
||||
return secret
|
||||
}
|
||||
|
||||
@@ -6,5 +6,75 @@ export function openDatabase(path: string): DB {
|
||||
const db = new Database(path)
|
||||
db.pragma('journal_mode = WAL')
|
||||
db.pragma('foreign_keys = ON')
|
||||
migrate(db)
|
||||
return db
|
||||
}
|
||||
|
||||
export function migrate(db: DB): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS app_meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
discogs_token TEXT,
|
||||
subsonic_url TEXT,
|
||||
subsonic_username TEXT,
|
||||
subsonic_password TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collection_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
discogs_release_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
artist TEXT NOT NULL,
|
||||
year INTEGER,
|
||||
formats TEXT NOT NULL DEFAULT '[]',
|
||||
genres TEXT NOT NULL DEFAULT '[]',
|
||||
labels TEXT NOT NULL DEFAULT '[]',
|
||||
tracklist TEXT NOT NULL DEFAULT '[]',
|
||||
catno TEXT,
|
||||
country TEXT,
|
||||
cover_url TEXT,
|
||||
local_artwork_path TEXT,
|
||||
barcodes TEXT NOT NULL DEFAULT '[]',
|
||||
rip_override INTEGER,
|
||||
date_added TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, discogs_release_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS digital_albums (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subsonic_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
artist TEXT NOT NULL,
|
||||
UNIQUE (user_id, subsonic_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS match_links (
|
||||
user_id INTEGER NOT NULL,
|
||||
item_id INTEGER NOT NULL REFERENCES collection_items(id) ON DELETE CASCADE,
|
||||
album_id INTEGER NOT NULL REFERENCES digital_albums(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (user_id, item_id)
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user