Files
dickendock/backend/src/db.js

617 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const Database = require('better-sqlite3');
const bcrypt = require('bcryptjs');
const path = require('path');
const db = new Database(process.env.DB_PATH || '/data/dickendock.db');
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Dashboard-Widgets
CREATE TABLE IF NOT EXISTS quick_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
url TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '🔗',
sort_order INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
text TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL DEFAULT '',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Tool: 3D-Kalkulator
CREATE TABLE IF NOT EXISTS calendar_feeds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
url TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#4ecdc4',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS folder_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
folder_id INTEGER NOT NULL REFERENCES folders(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(folder_id, shared_with)
);
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
originalname TEXT NOT NULL,
mimetype TEXT NOT NULL DEFAULT 'application/octet-stream',
size INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS file_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(file_id, shared_with)
);
CREATE TABLE IF NOT EXISTS admin_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
bemerkung TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'warteliste',
custom_price REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
calculation_id INTEGER REFERENCES calculations(id) ON DELETE SET NULL,
calc_name TEXT NOT NULL,
preis_freundschaft REAL NOT NULL,
preis_normal REAL NOT NULL,
preis_auftrag REAL NOT NULL,
stueckzahl INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS calculations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
gramm REAL NOT NULL,
stunden REAL NOT NULL,
farben INTEGER NOT NULL DEFAULT 1,
materialpreis_pro_gramm REAL NOT NULL DEFAULT 0.01,
stromverbrauch_kw REAL NOT NULL DEFAULT 0.15,
strompreis_pro_kwh REAL NOT NULL DEFAULT 0.38,
druckerpreis REAL NOT NULL DEFAULT 550,
gesamtdruckstunden REAL NOT NULL DEFAULT 5000,
verschleiss_pro_stunde REAL NOT NULL DEFAULT 0.06,
preis_freundschaft REAL NOT NULL,
preis_normal REAL NOT NULL,
preis_auftrag REAL NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
// Standard-Admin beim ersten Start anlegen
// Default admin settings
const settingDefaults = [
['file_max_size_mb', '50'],
['file_max_count', '20'],
['file_allowed_ext', '.pdf,.jpg,.jpeg,.png,.gif,.zip,.txt,.docx,.xlsx,.mp4,.stl,.3mf'],
];
for (const [key, value] of settingDefaults) {
if (!db.prepare('SELECT key FROM admin_settings WHERE key=?').get(key))
db.prepare('INSERT INTO admin_settings (key,value) VALUES (?,?)').run(key, value);
}
if (db.prepare('SELECT COUNT(*) c FROM users').get().c === 0) {
db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)')
.run('admin', bcrypt.hashSync('admin123', 12), 'admin');
console.log('✅ Erster Start: Admin angelegt: admin / admin123 → Bitte Passwort sofort ändern!');
}
// ── Migrationen (werden bei jedem Start geprüft) ─────────────────────────────
const cols = db.prepare("PRAGMA table_info(calculations)").all().map(r => r.name);
if (!cols.includes('image')) db.exec("ALTER TABLE calculations ADD COLUMN image TEXT");
if (!cols.includes('bemerkung')) db.exec("ALTER TABLE calculations ADD COLUMN bemerkung TEXT NOT NULL DEFAULT ''");
// Migrationen orders
const ordCols = db.prepare("PRAGMA table_info(orders)").all().map(r => r.name);
if (!ordCols.includes('bezahlt')) db.exec("ALTER TABLE orders ADD COLUMN bezahlt INTEGER NOT NULL DEFAULT 0");
if (!ordCols.includes('bezahlt_am')) db.exec("ALTER TABLE orders ADD COLUMN bezahlt_am DATETIME");
if (!ordCols.includes('abgeholt')) db.exec("ALTER TABLE orders ADD COLUMN abgeholt INTEGER NOT NULL DEFAULT 0");
if (!ordCols.includes('abgeholt_am'))db.exec("ALTER TABLE orders ADD COLUMN abgeholt_am DATETIME");
// Migrationen order_items
// files migrations
const fileCols = db.prepare("PRAGMA table_info(files)").all().map(r => r.name);
if (!fileCols.includes('folder_id')) db.exec("ALTER TABLE files ADD COLUMN folder_id INTEGER REFERENCES folders(id) ON DELETE SET NULL");
// folder_shares migrations (only if table exists)
const fsTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='folder_shares'").get();
if (fsTableExists) {
const fsCols = db.prepare("PRAGMA table_info(folder_shares)").all().map(r => r.name);
if (!fsCols.includes('created_at')) db.exec("ALTER TABLE folder_shares ADD COLUMN created_at DATETIME DEFAULT NULL");
}
const oiCols = db.prepare("PRAGMA table_info(order_items)").all().map(r => r.name);
if (!oiCols.includes('stunden')) db.exec("ALTER TABLE order_items ADD COLUMN stunden REAL NOT NULL DEFAULT 0");
if (!oiCols.includes('custom_price')) db.exec("ALTER TABLE order_items ADD COLUMN custom_price REAL");
if (!oiCols.includes('status')) db.exec("ALTER TABLE order_items ADD COLUMN status TEXT NOT NULL DEFAULT 'warteliste'");
if (!oiCols.includes('qty_warteliste')) db.exec("ALTER TABLE order_items ADD COLUMN qty_warteliste INTEGER NOT NULL DEFAULT 0");
if (!oiCols.includes('qty_in_arbeit')) db.exec("ALTER TABLE order_items ADD COLUMN qty_in_arbeit INTEGER NOT NULL DEFAULT 0");
if (!oiCols.includes('qty_fertig')) db.exec("ALTER TABLE order_items ADD COLUMN qty_fertig INTEGER NOT NULL DEFAULT 0");
// Bestehende Einträge: alle Stücke auf Warteliste setzen falls noch keine Qtys gesetzt
db.exec("UPDATE order_items SET qty_warteliste=stueckzahl WHERE qty_warteliste=0 AND qty_in_arbeit=0 AND qty_fertig=0");
// Migration: file_shares Passwort und Zugriffszeit
const fShareCols = db.prepare("PRAGMA table_info(file_shares)").all().map(r => r.name);
if (fShareCols.length && !fShareCols.includes('password_hash'))
db.exec("ALTER TABLE file_shares ADD COLUMN password_hash TEXT DEFAULT NULL");
if (fShareCols.length && !fShareCols.includes('accessed_at'))
db.exec("ALTER TABLE file_shares ADD COLUMN accessed_at DATETIME DEFAULT NULL");
// ── Nachrichten-Migrationen ───────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'").get()) {
db.exec(`
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
encrypted_content TEXT NOT NULL,
iv TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
read_by_recipient INTEGER NOT NULL DEFAULT 0
)
`);
}
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='user_keys'").get()) {
db.exec(`
CREATE TABLE user_keys (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
public_key TEXT NOT NULL,
updated_at DATETIME DEFAULT NULL
)
`);
}
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='pushover_settings'").get()) {
db.exec(`
CREATE TABLE pushover_settings (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
user_key TEXT NOT NULL,
app_token TEXT NOT NULL
)
`);
}
// Migration: Soft-Delete-Spalten entfernen (nur Nachrichten übernehmen die noch sichtbar waren)
const msgCols = db.prepare("PRAGMA table_info(messages)").all().map(r => r.name);
if (msgCols.includes('deleted_by_sender')) {
db.exec(`
CREATE TABLE messages_v2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
encrypted_content TEXT NOT NULL,
iv TEXT NOT NULL,
created_at DATETIME DEFAULT NULL,
read_by_recipient INTEGER NOT NULL DEFAULT 0
);
INSERT INTO messages_v2 (id, sender_id, recipient_id, encrypted_content, iv, created_at, read_by_recipient)
SELECT id, sender_id, recipient_id, encrypted_content, iv, created_at,
COALESCE(read_by_recipient, 0)
FROM messages
WHERE deleted_by_sender = 0 AND deleted_by_recipient = 0;
DROP TABLE messages;
ALTER TABLE messages_v2 RENAME TO messages;
`);
console.log('✅ Migration: messages → Hard-Delete (Soft-Delete-Spalten entfernt)');
}
// Falls read_by_recipient noch fehlt (sehr alte Installation)
const msgCols2 = db.prepare("PRAGMA table_info(messages)").all().map(r => r.name);
if (msgCols2.length && !msgCols2.includes('read_by_recipient')) {
db.exec("ALTER TABLE messages ADD COLUMN read_by_recipient INTEGER NOT NULL DEFAULT 0");
}
// User-Preferences (JSON-Blob)
const userCols2 = db.prepare("PRAGMA table_info(users)").all().map(r => r.name);
if (!userCols2.includes('preferences'))
db.exec("ALTER TABLE users ADD COLUMN preferences TEXT DEFAULT NULL");
if (!userCols2.includes('last_active_at'))
db.exec("ALTER TABLE users ADD COLUMN last_active_at DATETIME DEFAULT NULL");
if (!userCols2.includes('hidden'))
db.exec("ALTER TABLE users ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0");
if (!userCols2.includes('chat_active_at'))
db.exec("ALTER TABLE users ADD COLUMN chat_active_at DATETIME DEFAULT NULL");
if (!userCols2.includes('hidden_tools'))
db.exec("ALTER TABLE users ADD COLUMN hidden_tools TEXT NOT NULL DEFAULT '[]'");
// ── Login-Sicherheit ──────────────────────────────────────────────────────────
// Spalten für Account-Lockout in users-Tabelle
const userCols = db.prepare("PRAGMA table_info(users)").all().map(r => r.name);
if (!userCols.includes('failed_attempts'))
db.exec("ALTER TABLE users ADD COLUMN failed_attempts INTEGER NOT NULL DEFAULT 0");
if (!userCols.includes('locked_until'))
db.exec("ALTER TABLE users ADD COLUMN locked_until DATETIME DEFAULT NULL");
if (!userCols.includes('last_failed_at'))
db.exec("ALTER TABLE users ADD COLUMN last_failed_at DATETIME DEFAULT NULL");
// Login-Fehlversuche Log
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='login_attempts'").get()) {
db.exec(`
CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
ip TEXT,
success INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT NULL
)
`);
}
// Default-Einstellungen für Login-Schutz
const loginDefaults = [
['login_max_attempts', '5'],
['login_lockout_minutes', '30'],
];
for (const [k, v] of loginDefaults) {
if (!db.prepare('SELECT value FROM admin_settings WHERE key=?').get(k))
db.prepare('INSERT INTO admin_settings (key, value) VALUES (?, ?)').run(k, v);
}
// ── Ideen-Board ───────────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='board_items'").get()) {
db.exec(`
CREATE TABLE board_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL CHECK(type IN ('roadmap','wish')),
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
promoted_from_wish INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT NULL
)
`);
}
// Migration: promoted_from_wish Spalte
const boardCols = db.prepare("PRAGMA table_info(board_items)").all().map(r => r.name);
if (boardCols.length && !boardCols.includes('promoted_from_wish'))
db.exec("ALTER TABLE board_items ADD COLUMN promoted_from_wish INTEGER NOT NULL DEFAULT 0");
if (boardCols.length && !boardCols.includes('promoted_at'))
db.exec("ALTER TABLE board_items ADD COLUMN promoted_at DATETIME DEFAULT NULL");
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='board_reads'").get()) {
db.exec(`
CREATE TABLE board_reads (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
last_read DATETIME DEFAULT NULL
)
`);
}
// Migration: Pushover retry/expire
const poCols = db.prepare("PRAGMA table_info(pushover_settings)").all().map(r => r.name);
if (poCols.length && !poCols.includes('retry'))
db.exec("ALTER TABLE pushover_settings ADD COLUMN retry INTEGER DEFAULT NULL");
if (poCols.length && !poCols.includes('expire'))
db.exec("ALTER TABLE pushover_settings ADD COLUMN expire INTEGER DEFAULT NULL");
// ── Changelog ─────────────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='changelog'").get()) {
db.exec(`
CREATE TABLE changelog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL DEFAULT '',
build_time TEXT DEFAULT NULL,
created_at DATETIME DEFAULT NULL
)
`);
}
// Migration: build_time Spalte
const clCols = db.prepare("PRAGMA table_info(changelog)").all().map(r => r.name);
if (clCols.length && !clCols.includes('build_time'))
db.exec("ALTER TABLE changelog ADD COLUMN build_time TEXT DEFAULT NULL");
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='changelog_reads'").get()) {
db.exec(`
CREATE TABLE changelog_reads (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
last_read DATETIME DEFAULT NULL
)
`);
}
// ── Push-Zeitplaner ───────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='push_schedules'").get()) {
db.exec(`
CREATE TABLE push_schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
message TEXT NOT NULL,
scheduled_at DATETIME NOT NULL,
sent INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT NULL
)
`);
}
// ── Link-Liste ────────────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='link_list'").get()) {
db.exec(`
CREATE TABLE link_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
url TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '🔗',
description TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT NULL
)
`);
}
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='link_list_shares'").get()) {
db.exec(`
CREATE TABLE link_list_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
link_id INTEGER NOT NULL REFERENCES link_list(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT NULL,
UNIQUE(link_id, shared_with)
)
`);
}
// ── Kalkulator-Shares ─────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='calculation_shares'").get()) {
db.exec(`
CREATE TABLE calculation_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
calc_id INTEGER NOT NULL REFERENCES calculations(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT NULL,
UNIQUE(calc_id, shared_with)
)
`);
}
// ── Code-Schnipsel ────────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='snippets'").get()) {
db.exec(`
CREATE TABLE snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
code TEXT NOT NULL DEFAULT '',
language TEXT NOT NULL DEFAULT 'text',
description TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT NULL,
updated_at DATETIME DEFAULT NULL
);
CREATE TABLE snippet_tags (
snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
PRIMARY KEY (snippet_id, tag)
);
CREATE TABLE snippet_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE,
code TEXT NOT NULL,
language TEXT NOT NULL DEFAULT 'text',
saved_at DATETIME DEFAULT NULL
);
CREATE TABLE snippet_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT NULL,
UNIQUE(snippet_id, shared_with)
)
`);
}
// ── Link-Liste Ordner + Erweiterungen ─────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='link_list_folders'").get()) {
db.exec(`
CREATE TABLE link_list_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '📁',
sort_order INTEGER DEFAULT 0,
in_quickaccess INTEGER DEFAULT 0,
created_at DATETIME DEFAULT NULL
)
`);
}
// link_list Spalten nachrüsten
{
const cols = db.pragma('table_info(link_list)').map(c => c.name);
if (!cols.includes('folder_id')) db.exec('ALTER TABLE link_list ADD COLUMN folder_id INTEGER DEFAULT NULL');
if (!cols.includes('sort_order')) db.exec('ALTER TABLE link_list ADD COLUMN sort_order INTEGER DEFAULT 0');
if (!cols.includes('in_quickaccess')) db.exec('ALTER TABLE link_list ADD COLUMN in_quickaccess INTEGER DEFAULT 0');
}
// ── QR-Codes ──────────────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='qr_codes'").get()) {
db.exec(`
CREATE TABLE qr_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
label TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL,
size INTEGER NOT NULL DEFAULT 256,
fg_color TEXT NOT NULL DEFAULT '#000000',
bg_color TEXT NOT NULL DEFAULT '#ffffff',
margin INTEGER NOT NULL DEFAULT 4,
dot_style TEXT NOT NULL DEFAULT 'square',
corner_style TEXT NOT NULL DEFAULT 'square',
caption TEXT NOT NULL DEFAULT '',
caption_pos TEXT NOT NULL DEFAULT 'bottom',
caption_color TEXT NOT NULL DEFAULT '#000000',
caption_size INTEGER NOT NULL DEFAULT 14,
created_at DATETIME DEFAULT NULL
)
`);
}
// ── Link-Folder Shares ────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='link_folder_shares'").get()) {
db.exec(`
CREATE TABLE link_folder_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
folder_id INTEGER NOT NULL REFERENCES link_list_folders(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT NULL,
UNIQUE(folder_id, shared_with)
)
`);
}
// ── Kalender-Event-Cache für Suche ────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='calendar_event_cache'").get()) {
db.exec(`
CREATE TABLE calendar_event_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
feed_id INTEGER NOT NULL REFERENCES calendar_feeds(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
summary TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
location TEXT NOT NULL DEFAULT '',
start_dt TEXT,
end_dt TEXT,
synced_at DATETIME DEFAULT NULL
)
`);
}
// short_url auf upload_shares
{
const cols = db.pragma('table_info(upload_shares)').map(c => c.name);
if (!cols.includes('short_url'))
db.exec('ALTER TABLE upload_shares ADD COLUMN short_url TEXT DEFAULT NULL');
}
// ── Movie-Favoriten ───────────────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='movie_favorites'").get()) {
db.exec(`
CREATE TABLE movie_favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tmdb_id INTEGER NOT NULL,
title TEXT NOT NULL DEFAULT '',
poster_path TEXT NOT NULL DEFAULT '',
release_date_de TEXT NOT NULL DEFAULT '',
added_at DATETIME DEFAULT NULL,
UNIQUE(user_id, tmdb_id)
)
`);
}
// movie_favorites neue Spalten (Migration)
{
const cols = db.pragma('table_info(movie_favorites)').map(c => c.name);
if (!cols.includes('release_date'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN release_date TEXT NOT NULL DEFAULT ''");
if (!cols.includes('genres'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN genres TEXT NOT NULL DEFAULT '[]'");
if (!cols.includes('fsk'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN fsk TEXT NOT NULL DEFAULT ''");
if (!cols.includes('acknowledged'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN acknowledged INTEGER NOT NULL DEFAULT 0");
if (!cols.includes('acknowledged_at'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN acknowledged_at DATETIME DEFAULT NULL");
if (!cols.includes('user_notified'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN user_notified INTEGER NOT NULL DEFAULT 0");
if (!cols.includes('media_type'))
db.exec("ALTER TABLE movie_favorites ADD COLUMN media_type TEXT NOT NULL DEFAULT 'movie'");
}
// xREL Badge Cache (persistent, überlebt Container-Restarts)
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='xrel_cache'").get()) {
db.exec(`
CREATE TABLE xrel_cache (
cache_key TEXT PRIMARY KEY,
value TEXT NOT NULL,
expires_at INTEGER NOT NULL
)
`);
}
// TMDb-Token Default
if (!db.prepare("SELECT value FROM admin_settings WHERE key='tmdb_token'").get())
db.prepare("INSERT INTO admin_settings (key,value) VALUES ('tmdb_token','')").run();
// Öffentliche Datei/Ordner-Freigabe Links
// CREATE TABLE IF NOT EXISTS sicher bei jedem Start
db.exec(`
CREATE TABLE IF NOT EXISTS public_file_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
folder_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
label TEXT,
password_hash TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
download_count INTEGER NOT NULL DEFAULT 0
)
`);
// 3D-Druck Ausgaben (Haushaltsbuch)
db.exec(`
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
date TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'Sonstiges',
description TEXT NOT NULL,
amount REAL NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
module.exports = db;