532 lines
24 KiB
JavaScript
532 lines
24 KiB
JavaScript
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");
|
||
|
||
// 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");
|
||
|
||
// ── 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');
|
||
}
|
||
|
||
module.exports = db;
|