/** * 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 NOT NULL, 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 CASCADE, 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'); } // 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 ) `); // 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 ) `); // 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); `); logger.info('Datenbank-Tabellen erstellt'); } /** * Standard-Benutzer erstellen */ async function createDefaultUsers() { const existingUsers = db.prepare('SELECT COUNT(*) as count FROM users').get(); 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: 'admin', password: 'Kx9#mP2$vL7@nQ4!wR', displayName: 'Administrator', 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 };