Files
TaskMate/backend/database.js
Claude Project Manager ab1e5be9a9 Initial commit
2025-12-28 21:36:45 +00:00

537 Zeilen
17 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 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: '!1Data123',
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
};