/** * TASKMATE - Datenbank * ==================== * SQLite-Datenbank mit better-sqlite3 */ const Database = require('better-sqlite3'); const path = require('path'); const bcrypt = require('bcryptjs'); const logger = require('./utils/logger'); // Datenbank-Pfad const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data', 'taskmate.db'); let db = null; /** * Datenbank initialisieren */ async function initialize() { try { // Datenbank öffnen/erstellen db = new Database(DB_PATH); // WAL-Modus für bessere Performance db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); // Tabellen erstellen createTables(); // Standard-Benutzer erstellen (falls nicht vorhanden) await createDefaultUsers(); logger.info('Datenbank initialisiert'); return db; } catch (error) { logger.error('Datenbank-Fehler:', error); throw error; } } /** * Tabellen erstellen */ function createTables() { // Benutzer db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, display_name TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#00D4FF', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME, failed_attempts INTEGER DEFAULT 0, locked_until DATETIME ) `); // Login-Audit db.exec(` CREATE TABLE IF NOT EXISTS login_audit ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, ip_address TEXT, success INTEGER NOT NULL, user_agent TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ) `); // Projekte db.exec(` CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, archived INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Spalten db.exec(` CREATE TABLE IF NOT EXISTS columns ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, name TEXT NOT NULL, position INTEGER NOT NULL, color TEXT, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE ) `); // Labels db.exec(` CREATE TABLE IF NOT EXISTS labels ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, name TEXT NOT NULL, color TEXT NOT NULL, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE ) `); // Aufgaben db.exec(` CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, column_id INTEGER, title TEXT NOT NULL, description TEXT, priority TEXT DEFAULT 'medium', start_date DATE, due_date DATE, assigned_to INTEGER, time_estimate_min INTEGER, depends_on INTEGER, position INTEGER NOT NULL, archived INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE SET NULL, FOREIGN KEY (assigned_to) REFERENCES users(id), FOREIGN KEY (depends_on) REFERENCES tasks(id) ON DELETE SET NULL, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Migration: Add start_date column if it doesn't exist const columns = db.prepare("PRAGMA table_info(tasks)").all(); const hasStartDate = columns.some(col => col.name === 'start_date'); if (!hasStartDate) { db.exec('ALTER TABLE tasks ADD COLUMN start_date DATE'); logger.info('Migration: start_date Spalte zu tasks hinzugefuegt'); } // Migration: Add role and permissions columns to users const userColumns = db.prepare("PRAGMA table_info(users)").all(); const hasRole = userColumns.some(col => col.name === 'role'); if (!hasRole) { db.exec("ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'"); logger.info('Migration: role Spalte zu users hinzugefuegt'); } const hasPermissions = userColumns.some(col => col.name === 'permissions'); if (!hasPermissions) { db.exec("ALTER TABLE users ADD COLUMN permissions TEXT DEFAULT '[]'"); logger.info('Migration: permissions Spalte zu users hinzugefuegt'); } // Migration: Add email column to users const hasEmail = userColumns.some(col => col.name === 'email'); if (!hasEmail) { db.exec("ALTER TABLE users ADD COLUMN email TEXT"); logger.info('Migration: email Spalte zu users hinzugefuegt'); } // Migration: Add repositories_base_path column to users const hasRepoBasePath = userColumns.some(col => col.name === 'repositories_base_path'); if (!hasRepoBasePath) { db.exec("ALTER TABLE users ADD COLUMN repositories_base_path TEXT"); logger.info('Migration: repositories_base_path Spalte zu users hinzugefuegt'); } // Migration: Add custom_initials column to users const hasCustomInitials = userColumns.some(col => col.name === 'custom_initials'); if (!hasCustomInitials) { db.exec("ALTER TABLE users ADD COLUMN custom_initials TEXT"); logger.info('Migration: custom_initials Spalte zu users hinzugefuegt'); } // Migration: Add initials column and prepare email const hasInitials = userColumns.some(col => col.name === 'initials'); if (!hasInitials && userColumns.some(col => col.name === 'username')) { logger.info('Migration: Füge initials Spalte hinzu und bereite E-Mail vor'); // Zuerst Daten vorbereiten const users = db.prepare('SELECT id, username, email, custom_initials FROM users').all(); for (const user of users) { // Stelle sicher dass jeder Benutzer eine E-Mail hat if (!user.email || user.email === '') { if (user.username === 'admin') { // Admin bekommt eine spezielle E-Mail db.prepare('UPDATE users SET email = ? WHERE id = ?').run('admin@taskmate.local', user.id); } else if (user.username.includes('@')) { // Username enthält bereits E-Mail (wie bei bestehenden Benutzern) db.prepare('UPDATE users SET email = ? WHERE id = ?').run(user.username, user.id); } } // Initialen setzen (aus custom_initials oder generieren) if (!user.custom_initials || user.custom_initials === '') { let initials = 'XX'; if (user.username === 'admin') { initials = 'AD'; } else if (user.email || user.username.includes('@')) { // Generiere Initialen aus E-Mail const emailPart = (user.email || user.username).split('@')[0]; if (emailPart.includes('_')) { const parts = emailPart.split('_'); initials = (parts[0][0] + parts[1][0]).toUpperCase(); } else if (emailPart.includes('.')) { const parts = emailPart.split('.'); initials = (parts[0][0] + parts[1][0]).toUpperCase(); } else { initials = emailPart.substring(0, 2).toUpperCase(); } } db.prepare('UPDATE users SET custom_initials = ? WHERE id = ?').run(initials, user.id); } } // Neue initials Spalte hinzufügen db.exec("ALTER TABLE users ADD COLUMN initials TEXT"); // Daten von custom_initials nach initials kopieren db.exec("UPDATE users SET initials = custom_initials"); logger.info('Migration: initials Spalte hinzugefügt und E-Mail-Daten vorbereitet'); } // Proposals (Vorschlaege) db.exec(` CREATE TABLE IF NOT EXISTS proposals ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, created_by INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, approved INTEGER DEFAULT 0, approved_by INTEGER, approved_at DATETIME, FOREIGN KEY (created_by) REFERENCES users(id), FOREIGN KEY (approved_by) REFERENCES users(id) ) `); // Proposal Votes db.exec(` CREATE TABLE IF NOT EXISTS proposal_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, proposal_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ) `); // Unique constraint for proposal votes (one vote per user per proposal) db.exec(` CREATE UNIQUE INDEX IF NOT EXISTS idx_proposal_votes_unique ON proposal_votes(proposal_id, user_id) `); // Index for proposal votes db.exec(` CREATE INDEX IF NOT EXISTS idx_proposal_votes_proposal ON proposal_votes(proposal_id) `); // Migration: Add archived, task_id, and project_id columns to proposals const proposalColumns = db.prepare("PRAGMA table_info(proposals)").all(); const hasProposalArchived = proposalColumns.some(col => col.name === 'archived'); if (!hasProposalArchived) { db.exec('ALTER TABLE proposals ADD COLUMN archived INTEGER DEFAULT 0'); logger.info('Migration: archived Spalte zu proposals hinzugefuegt'); } const hasProposalTaskId = proposalColumns.some(col => col.name === 'task_id'); if (!hasProposalTaskId) { db.exec('ALTER TABLE proposals ADD COLUMN task_id INTEGER REFERENCES tasks(id) ON DELETE SET NULL'); logger.info('Migration: task_id Spalte zu proposals hinzugefuegt'); } const hasProposalProjectId = proposalColumns.some(col => col.name === 'project_id'); if (!hasProposalProjectId) { db.exec('ALTER TABLE proposals ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE'); logger.info('Migration: project_id Spalte zu proposals hinzugefuegt'); } // Migration: Add filter_category column to columns const columnColumns = db.prepare("PRAGMA table_info(columns)").all(); const hasFilterCategory = columnColumns.some(col => col.name === 'filter_category'); if (!hasFilterCategory) { db.exec("ALTER TABLE columns ADD COLUMN filter_category TEXT DEFAULT 'in_progress'"); logger.info('Migration: filter_category Spalte zu columns hinzugefuegt'); // Set default values for existing columns based on position const projects = db.prepare('SELECT id FROM projects').all(); for (const project of projects) { const cols = db.prepare('SELECT id, position FROM columns WHERE project_id = ? ORDER BY position').all(project.id); if (cols.length > 0) { // First column = open db.prepare("UPDATE columns SET filter_category = 'open' WHERE id = ?").run(cols[0].id); // Last column = completed if (cols.length > 1) { db.prepare("UPDATE columns SET filter_category = 'completed' WHERE id = ?").run(cols[cols.length - 1].id); } } } logger.info('Migration: Standard-Filterkategorien fuer bestehende Spalten gesetzt'); } // Task-Labels (Verknüpfung) db.exec(` CREATE TABLE IF NOT EXISTS task_labels ( task_id INTEGER NOT NULL, label_id INTEGER NOT NULL, PRIMARY KEY (task_id, label_id), FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE ) `); // Task-Assignees (Mehrfachzuweisung von Mitarbeitern) db.exec(` CREATE TABLE IF NOT EXISTS task_assignees ( task_id INTEGER NOT NULL, user_id INTEGER NOT NULL, PRIMARY KEY (task_id, user_id), FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) `); // Unteraufgaben db.exec(` CREATE TABLE IF NOT EXISTS subtasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, title TEXT NOT NULL, completed INTEGER DEFAULT 0, position INTEGER NOT NULL, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE ) `); // Kommentare db.exec(` CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, user_id INTEGER NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ) `); // Anhänge db.exec(` CREATE TABLE IF NOT EXISTS attachments ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, filename TEXT NOT NULL, original_name TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, uploaded_by INTEGER, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (uploaded_by) REFERENCES users(id) ) `); // Links db.exec(` CREATE TABLE IF NOT EXISTS links ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, title TEXT, url TEXT NOT NULL, created_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Aufgaben-Vorlagen db.exec(` CREATE TABLE IF NOT EXISTS task_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, name TEXT NOT NULL, title_template TEXT, description TEXT, priority TEXT, labels TEXT, subtasks TEXT, time_estimate_min INTEGER, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE ) `); // Historie db.exec(` CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, user_id INTEGER NOT NULL, action TEXT NOT NULL, field_changed TEXT, old_value TEXT, new_value TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ) `); // Einstellungen db.exec(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT ) `); // Refresh Tokens für sichere Token-Rotation db.exec(` CREATE TABLE IF NOT EXISTS refresh_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, token TEXT NOT NULL UNIQUE, expires_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_used DATETIME, user_agent TEXT, ip_address TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) `); // Index für Token-Lookup db.exec(`CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON refresh_tokens(token)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires ON refresh_tokens(expires_at)`); // Anwendungen (Git-Repositories pro Projekt) db.exec(` CREATE TABLE IF NOT EXISTS applications ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL UNIQUE, local_path TEXT NOT NULL, gitea_repo_url TEXT, gitea_repo_owner TEXT, gitea_repo_name TEXT, default_branch TEXT DEFAULT 'main', last_sync DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Benachrichtigungen (Inbox) db.exec(` CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, title TEXT NOT NULL, message TEXT, task_id INTEGER, project_id INTEGER, proposal_id INTEGER, actor_id INTEGER, is_read INTEGER DEFAULT 0, is_persistent INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE, FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL ) `); // Erinnerungen db.exec(` CREATE TABLE IF NOT EXISTS reminders ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, reminder_date DATE NOT NULL, reminder_time TIME DEFAULT '09:00', color TEXT DEFAULT '#F59E0B', advance_days TEXT DEFAULT '1', repeat_type TEXT DEFAULT 'none', repeat_interval INTEGER DEFAULT 1, is_active INTEGER DEFAULT 1, created_by INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Erinnerungs-Benachrichtigungen (für Tracking welche bereits gesendet wurden) db.exec(` CREATE TABLE IF NOT EXISTS reminder_notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, reminder_id INTEGER NOT NULL, notification_date DATE NOT NULL, sent INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE CASCADE, UNIQUE(reminder_id, notification_date) ) `); // Wissensmanagement - Kategorien db.exec(` CREATE TABLE IF NOT EXISTS knowledge_categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, color TEXT DEFAULT '#3B82F6', icon TEXT, position INTEGER NOT NULL DEFAULT 0, created_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Wissensmanagement - Einträge db.exec(` CREATE TABLE IF NOT EXISTS knowledge_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, title TEXT NOT NULL, url TEXT, notes TEXT, position INTEGER NOT NULL DEFAULT 0, created_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES knowledge_categories(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Wissensmanagement - FTS5 Volltextsuche try { db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5( title, notes, content=knowledge_entries, content_rowid=id ) `); // Trigger: INSERT synchronisieren db.exec(` CREATE TRIGGER IF NOT EXISTS knowledge_fts_insert AFTER INSERT ON knowledge_entries BEGIN INSERT INTO knowledge_fts(rowid, title, notes) VALUES (NEW.id, NEW.title, NEW.notes); END `); // Trigger: DELETE synchronisieren db.exec(` CREATE TRIGGER IF NOT EXISTS knowledge_fts_delete AFTER DELETE ON knowledge_entries BEGIN INSERT INTO knowledge_fts(knowledge_fts, rowid, title, notes) VALUES('delete', OLD.id, OLD.title, OLD.notes); END `); // Trigger: UPDATE synchronisieren db.exec(` CREATE TRIGGER IF NOT EXISTS knowledge_fts_update AFTER UPDATE ON knowledge_entries BEGIN INSERT INTO knowledge_fts(knowledge_fts, rowid, title, notes) VALUES('delete', OLD.id, OLD.title, OLD.notes); INSERT INTO knowledge_fts(rowid, title, notes) VALUES (NEW.id, NEW.title, NEW.notes); END `); // Initiales Befuellen der FTS-Tabelle db.exec(`INSERT INTO knowledge_fts(knowledge_fts) VALUES('rebuild')`); logger.info('Knowledge FTS5 Volltextindex erstellt/aktualisiert'); } catch (ftsError) { logger.warn('FTS5 konnte nicht erstellt werden (evtl. nicht unterstuetzt):', ftsError.message); } // Wissensmanagement - Anhänge db.exec(` CREATE TABLE IF NOT EXISTS knowledge_attachments ( id INTEGER PRIMARY KEY AUTOINCREMENT, entry_id INTEGER NOT NULL, filename TEXT NOT NULL, original_name TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, uploaded_by INTEGER, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (entry_id) REFERENCES knowledge_entries(id) ON DELETE CASCADE, FOREIGN KEY (uploaded_by) REFERENCES users(id) ) `); // Coding-Verzeichnisse (projektübergreifend) db.exec(` CREATE TABLE IF NOT EXISTS coding_directories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, local_path TEXT NOT NULL UNIQUE, description TEXT, color TEXT DEFAULT '#4F46E5', gitea_repo_url TEXT, gitea_repo_owner TEXT, gitea_repo_name TEXT, default_branch TEXT DEFAULT 'main', last_sync DATETIME, position INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Migration: Add claude_instructions column to coding_directories const codingDirColumns = db.prepare("PRAGMA table_info(coding_directories)").all(); const hasClaudeInstructions = codingDirColumns.some(col => col.name === 'claude_instructions'); if (!hasClaudeInstructions) { db.exec('ALTER TABLE coding_directories ADD COLUMN claude_instructions TEXT'); logger.info('Migration: claude_instructions Spalte zu coding_directories hinzugefuegt'); } // Coding Verbrauchsdaten db.exec(` CREATE TABLE IF NOT EXISTS coding_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, directory_id INTEGER NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, cpu_percent REAL, memory_mb REAL, disk_read_mb REAL, disk_write_mb REAL, network_recv_mb REAL, network_sent_mb REAL, process_count INTEGER, FOREIGN KEY (directory_id) REFERENCES coding_directories(id) ON DELETE CASCADE ) `); // Neues optimiertes Kontakt-Schema db.exec(` CREATE TABLE IF NOT EXISTS contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL CHECK(type IN ('person', 'company')), -- Gemeinsame Felder display_name TEXT NOT NULL, status TEXT DEFAULT 'active' CHECK(status IN ('active', 'inactive', 'archived')), tags TEXT, notes TEXT, avatar_url TEXT, -- Person-spezifische Felder salutation TEXT, first_name TEXT, last_name TEXT, position TEXT, department TEXT, parent_company_id INTEGER, -- Firma-spezifische Felder company_name TEXT, company_type TEXT, industry TEXT, website TEXT, -- Meta project_id INTEGER NOT NULL, created_by INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (parent_company_id) REFERENCES contacts(id) ON DELETE SET NULL, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Kontaktdetails (E-Mails, Telefone, Adressen) db.exec(` CREATE TABLE IF NOT EXISTS contact_details ( id INTEGER PRIMARY KEY AUTOINCREMENT, contact_id INTEGER NOT NULL, type TEXT NOT NULL CHECK(type IN ('email', 'phone', 'mobile', 'fax', 'address', 'social')), label TEXT DEFAULT 'Arbeit', value TEXT NOT NULL, is_primary BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE ) `); // Kontakt-Kategorien db.exec(` CREATE TABLE IF NOT EXISTS contact_categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, color TEXT DEFAULT '#6B7280', icon TEXT, project_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE ) `); // Kontakt-Kategorie Zuordnungen db.exec(` CREATE TABLE IF NOT EXISTS contact_to_categories ( contact_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (contact_id, category_id), FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES contact_categories(id) ON DELETE CASCADE ) `); // Interaktionen/Historie db.exec(` CREATE TABLE IF NOT EXISTS contact_interactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, contact_id INTEGER NOT NULL, type TEXT NOT NULL CHECK(type IN ('call', 'email', 'meeting', 'note', 'task')), subject TEXT, content TEXT, interaction_date DATETIME DEFAULT CURRENT_TIMESTAMP, created_by INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Trigger für updated_at auf Kontakte-Tabelle db.exec(` CREATE TRIGGER IF NOT EXISTS update_contacts_timestamp AFTER UPDATE ON contacts BEGIN UPDATE contacts SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END `); // Indizes für Performance db.exec(` CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id); CREATE INDEX IF NOT EXISTS idx_tasks_column ON tasks(column_id); CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to); CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date); CREATE INDEX IF NOT EXISTS idx_subtasks_task ON subtasks(task_id); CREATE INDEX IF NOT EXISTS idx_comments_task ON comments(task_id); CREATE INDEX IF NOT EXISTS idx_history_task ON history(task_id); CREATE INDEX IF NOT EXISTS idx_attachments_task ON attachments(task_id); CREATE INDEX IF NOT EXISTS idx_links_task ON links(task_id); CREATE INDEX IF NOT EXISTS idx_task_labels_task ON task_labels(task_id); CREATE INDEX IF NOT EXISTS idx_task_labels_label ON task_labels(label_id); CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id); CREATE INDEX IF NOT EXISTS idx_notifications_user_read ON notifications(user_id, is_read); CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at); CREATE INDEX IF NOT EXISTS idx_applications_project ON applications(project_id); CREATE INDEX IF NOT EXISTS idx_knowledge_entries_category ON knowledge_entries(category_id); CREATE INDEX IF NOT EXISTS idx_knowledge_attachments_entry ON knowledge_attachments(entry_id); CREATE INDEX IF NOT EXISTS idx_coding_directories_position ON coding_directories(position); CREATE INDEX IF NOT EXISTS idx_coding_usage_directory ON coding_usage(directory_id); CREATE INDEX IF NOT EXISTS idx_coding_usage_timestamp ON coding_usage(timestamp); -- Kontakt-Indizes für Performance CREATE INDEX IF NOT EXISTS idx_contacts_type ON contacts(type); CREATE INDEX IF NOT EXISTS idx_contacts_status ON contacts(status); CREATE INDEX IF NOT EXISTS idx_contacts_project ON contacts(project_id); CREATE INDEX IF NOT EXISTS idx_contacts_parent ON contacts(parent_company_id); CREATE INDEX IF NOT EXISTS idx_contacts_display_name ON contacts(display_name); CREATE INDEX IF NOT EXISTS idx_details_contact ON contact_details(contact_id); CREATE INDEX IF NOT EXISTS idx_details_type ON contact_details(type); CREATE INDEX IF NOT EXISTS idx_interactions_contact ON contact_interactions(contact_id); CREATE INDEX IF NOT EXISTS idx_interactions_date ON contact_interactions(interaction_date); `); logger.info('Datenbank-Tabellen erstellt'); } /** * Standard-Benutzer erstellen und Admin-Passwort korrigieren */ async function createDefaultUsers() { const existingUsers = db.prepare('SELECT COUNT(*) as count FROM users').get(); // Admin-Passwort korrigieren (falls aus .env verschieden) const adminExists = db.prepare('SELECT id, password_hash FROM users WHERE email = ? AND role = ?').get('admin@taskmate.local', 'admin'); if (adminExists) { const correctAdminPassword = process.env.ADMIN_PASSWORD || 'admin123'; // Prüfen ob das Passwort bereits korrekt ist const isCorrect = await bcrypt.compare(correctAdminPassword, adminExists.password_hash); if (!isCorrect) { logger.info('Admin-Passwort wird aus .env aktualisiert'); const correctHash = await bcrypt.hash(correctAdminPassword, 12); db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(correctHash, adminExists.id); logger.info('Admin-Passwort erfolgreich aktualisiert'); } else { logger.info('Admin-Passwort bereits korrekt'); } } if (existingUsers.count === 0) { // Benutzer aus Umgebungsvariablen const user1 = { username: process.env.USER1_USERNAME || 'user1', password: process.env.USER1_PASSWORD || 'changeme123', displayName: process.env.USER1_DISPLAYNAME || 'Benutzer 1', color: process.env.USER1_COLOR || '#00D4FF' }; const user2 = { username: process.env.USER2_USERNAME || 'user2', password: process.env.USER2_PASSWORD || 'changeme456', displayName: process.env.USER2_DISPLAYNAME || 'Benutzer 2', color: process.env.USER2_COLOR || '#FF9500' }; const insertUser = db.prepare(` INSERT INTO users (username, password_hash, display_name, color, role, permissions) VALUES (?, ?, ?, ?, ?, ?) `); // Admin-Benutzer const adminUser = { username: process.env.ADMIN_USERNAME || 'admin', password: process.env.ADMIN_PASSWORD || 'admin123', displayName: process.env.ADMIN_DISPLAYNAME || 'Administrator', color: process.env.ADMIN_COLOR || '#8B5CF6' }; // Passwoerter hashen und Benutzer erstellen const hash1 = await bcrypt.hash(user1.password, 12); const hash2 = await bcrypt.hash(user2.password, 12); const hashAdmin = await bcrypt.hash(adminUser.password, 12); insertUser.run(user1.username, hash1, user1.displayName, user1.color, 'user', '[]'); insertUser.run(user2.username, hash2, user2.displayName, user2.color, 'user', '[]'); insertUser.run(adminUser.username, hashAdmin, adminUser.displayName, adminUser.color, 'admin', '[]'); logger.info('Standard-Benutzer und Admin erstellt'); // Standard-Projekt erstellen const projectResult = db.prepare(` INSERT INTO projects (name, description, created_by) VALUES (?, ?, ?) `).run('Mein erstes Projekt', 'Willkommen bei TaskMate!', 1); const projectId = projectResult.lastInsertRowid; // Standard-Spalten erstellen const insertColumn = db.prepare(` INSERT INTO columns (project_id, name, position, color) VALUES (?, ?, ?, ?) `); insertColumn.run(projectId, 'Offen', 0, null); insertColumn.run(projectId, 'In Arbeit', 1, null); insertColumn.run(projectId, 'Erledigt', 2, null); // Standard-Labels erstellen const insertLabel = db.prepare(` INSERT INTO labels (project_id, name, color) VALUES (?, ?, ?) `); insertLabel.run(projectId, 'Bug', '#DC2626'); insertLabel.run(projectId, 'Feature', '#059669'); insertLabel.run(projectId, 'Dokumentation', '#3182CE'); logger.info('Standard-Projekt mit Spalten und Labels erstellt'); } } /** * Datenbank-Instanz abrufen */ function getDb() { if (!db) { throw new Error('Datenbank nicht initialisiert'); } return db; } /** * Datenbank schließen */ function close() { if (db) { db.close(); logger.info('Datenbank geschlossen'); } } module.exports = { initialize, getDb, close };