Dateien
TaskMate/backend/database.js
Server Deploy 48c917eb28 Wissensdatenbank: Markdown, FTS5-Suche, Sanitizing, UX
- Markdown-Rendering fuer Notizen (fett, kursiv, Ueberschriften, Listen, Code, Links)
- HTML-Sanitizing im Frontend und Backend (XSS-Schutz)
- FTS5 Volltextindex fuer schnelle Suche mit Ranking
- Kategorie-Loeschung zeigt Anzahl betroffener Eintraege

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:38:43 +01:00

913 Zeilen
31 KiB
JavaScript

/**
* 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
};