Datenbank bereinigt / Gitea-Integration gefixt

Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-04 00:24:11 +00:00
committet von Server Deploy
Ursprung 395598c2b0
Commit c21be47428
37 geänderte Dateien mit 30993 neuen und 809 gelöschten Zeilen

Datei anzeigen

@ -368,6 +368,25 @@ function createTables() {
)
`);
// 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 (
@ -457,6 +476,34 @@ function createTables() {
)
`);
// 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');
}
// Indizes für Performance
db.exec(`
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
@ -476,17 +523,36 @@ function createTables() {
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);
`);
logger.info('Datenbank-Tabellen erstellt');
}
/**
* Standard-Benutzer erstellen
* 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 username = ? AND role = ?').get('admin', 'admin');
if (adminExists) {
const correctAdminPassword = process.env.ADMIN_PASSWORD || 'admin123';
const bcrypt = require('bcryptjs');
// 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 = {
@ -510,10 +576,10 @@ async function createDefaultUsers() {
// Admin-Benutzer
const adminUser = {
username: 'admin',
password: 'Kx9#mP2$vL7@nQ4!wR',
displayName: 'Administrator',
color: '#8B5CF6'
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

Datei anzeigen

@ -5,15 +5,22 @@
*/
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const logger = require('../utils/logger');
const { getDb } = require('../database');
const JWT_SECRET = process.env.JWT_SECRET || 'UNSICHER_BITTE_AENDERN';
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET muss in .env gesetzt und mindestens 32 Zeichen lang sein!');
}
const ACCESS_TOKEN_EXPIRY = 15; // Minuten (kürzer für mehr Sicherheit)
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60; // 7 Tage in Minuten
const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT) || 30; // Minuten
/**
* JWT-Token generieren
* JWT Access-Token generieren (kurze Lebensdauer)
*/
function generateToken(user) {
function generateAccessToken(user) {
// Permissions parsen falls als String gespeichert
let permissions = user.permissions || [];
if (typeof permissions === 'string') {
@ -31,13 +38,38 @@ function generateToken(user) {
displayName: user.display_name,
color: user.color,
role: user.role || 'user',
permissions: permissions
permissions: permissions,
type: 'access'
},
JWT_SECRET,
{ expiresIn: `${SESSION_TIMEOUT}m` }
{ expiresIn: `${ACCESS_TOKEN_EXPIRY}m` }
);
}
/**
* Refresh-Token generieren (lange Lebensdauer)
*/
function generateRefreshToken(userId, ipAddress, userAgent) {
const db = getDb();
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY * 60 * 1000);
// Token in Datenbank speichern
db.prepare(`
INSERT INTO refresh_tokens (user_id, token, expires_at, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?)
`).run(userId, token, expiresAt.toISOString(), ipAddress, userAgent);
return token;
}
/**
* Legacy generateToken für Rückwärtskompatibilität
*/
function generateToken(user) {
return generateAccessToken(user);
}
/**
* JWT-Token verifizieren
*/
@ -179,8 +211,72 @@ function generateCsrfToken() {
return randomBytes(32).toString('hex');
}
/**
* Refresh-Token validieren und neuen Access-Token generieren
*/
async function refreshAccessToken(refreshToken, ipAddress, userAgent) {
const db = getDb();
// Token in Datenbank suchen
const tokenRecord = db.prepare(`
SELECT rt.*, u.* FROM refresh_tokens rt
JOIN users u ON rt.user_id = u.id
WHERE rt.token = ? AND rt.expires_at > datetime('now')
`).get(refreshToken);
if (!tokenRecord) {
throw new Error('Ungültiger oder abgelaufener Refresh-Token');
}
// Token als benutzt markieren
db.prepare(`
UPDATE refresh_tokens SET last_used = CURRENT_TIMESTAMP WHERE id = ?
`).run(tokenRecord.id);
// Neuen Access-Token generieren
const user = {
id: tokenRecord.user_id,
username: tokenRecord.username,
display_name: tokenRecord.display_name,
color: tokenRecord.color,
role: tokenRecord.role,
permissions: tokenRecord.permissions
};
return generateAccessToken(user);
}
/**
* Alle Refresh-Tokens eines Benutzers löschen (Logout auf allen Geräten)
*/
function revokeAllRefreshTokens(userId) {
const db = getDb();
db.prepare('DELETE FROM refresh_tokens WHERE user_id = ?').run(userId);
}
/**
* Abgelaufene Refresh-Tokens aufräumen
*/
function cleanupExpiredTokens() {
const db = getDb();
const result = db.prepare(`
DELETE FROM refresh_tokens WHERE expires_at < datetime('now')
`).run();
if (result.changes > 0) {
logger.info(`Bereinigt: ${result.changes} abgelaufene Refresh-Tokens`);
}
}
// Cleanup alle 6 Stunden
setInterval(cleanupExpiredTokens, 6 * 60 * 60 * 1000);
module.exports = {
generateToken,
generateAccessToken,
generateRefreshToken,
refreshAccessToken,
revokeAllRefreshTokens,
verifyToken,
authenticateToken,
authenticateSocket,

Datei anzeigen

@ -7,6 +7,7 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const logger = require('../utils/logger');
@ -18,18 +19,54 @@ if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
// Mapping: Dateiendung -> erlaubte MIME-Types
const EXTENSION_TO_MIME = {
// Dokumente
'pdf': ['application/pdf'],
'doc': ['application/msword'],
'docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'xls': ['application/vnd.ms-excel'],
'xlsx': ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
'ppt': ['application/vnd.ms-powerpoint'],
'pptx': ['application/vnd.openxmlformats-officedocument.presentationml.presentation'],
'odt': ['application/vnd.oasis.opendocument.text'],
'ods': ['application/vnd.oasis.opendocument.spreadsheet'],
'odp': ['application/vnd.oasis.opendocument.presentation'],
'rtf': ['application/rtf', 'text/rtf'],
// Text
'txt': ['text/plain'],
'csv': ['text/csv', 'application/csv', 'text/comma-separated-values'],
'md': ['text/markdown', 'text/x-markdown', 'text/plain'],
'json': ['application/json', 'text/json'],
'xml': ['application/xml', 'text/xml'],
'html': ['text/html'],
'log': ['text/plain'],
// Bilder
'jpg': ['image/jpeg'],
'jpeg': ['image/jpeg'],
'png': ['image/png'],
'gif': ['image/gif'],
'webp': ['image/webp'],
'svg': ['image/svg+xml'],
'bmp': ['image/bmp'],
'ico': ['image/x-icon', 'image/vnd.microsoft.icon'],
// Archive
'zip': ['application/zip', 'application/x-zip-compressed'],
'rar': ['application/x-rar-compressed', 'application/vnd.rar'],
'7z': ['application/x-7z-compressed'],
'tar': ['application/x-tar'],
'gz': ['application/gzip', 'application/x-gzip'],
// Code/Skripte (als text/plain akzeptiert)
'sql': ['application/sql', 'text/plain'],
'js': ['text/javascript', 'application/javascript', 'text/plain'],
'css': ['text/css', 'text/plain'],
'py': ['text/x-python', 'text/plain'],
'sh': ['application/x-sh', 'text/plain']
};
// Standard-Werte (Fallback)
let MAX_FILE_SIZE = (parseInt(process.env.MAX_FILE_SIZE_MB) || 15) * 1024 * 1024;
let ALLOWED_MIME_TYPES = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain', 'text/csv', 'text/markdown',
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
'application/json'
];
let ALLOWED_EXTENSIONS = ['pdf', 'docx', 'txt'];
/**
* Lädt Upload-Einstellungen aus der Datenbank
@ -43,17 +80,9 @@ function loadUploadSettings() {
if (settings) {
MAX_FILE_SIZE = (settings.maxFileSizeMB || 15) * 1024 * 1024;
// Erlaubte MIME-Types aus den aktiven Kategorien zusammenstellen
const types = [];
if (settings.allowedTypes) {
Object.values(settings.allowedTypes).forEach(category => {
if (category.enabled && Array.isArray(category.types)) {
types.push(...category.types);
}
});
}
if (types.length > 0) {
ALLOWED_MIME_TYPES = types;
// Erlaubte Endungen aus den Einstellungen
if (Array.isArray(settings.allowedExtensions) && settings.allowedExtensions.length > 0) {
ALLOWED_EXTENSIONS = settings.allowedExtensions;
}
}
} catch (error) {
@ -67,7 +96,7 @@ function loadUploadSettings() {
*/
function getCurrentSettings() {
loadUploadSettings();
return { maxFileSize: MAX_FILE_SIZE, allowedMimeTypes: ALLOWED_MIME_TYPES };
return { maxFileSize: MAX_FILE_SIZE, allowedExtensions: ALLOWED_EXTENSIONS };
}
/**
@ -99,19 +128,83 @@ const storage = multer.diskStorage({
});
/**
* Datei-Filter
* Gefährliche Dateinamen prüfen
*/
function isSecureFilename(filename) {
// Null-Bytes, Pfad-Traversal, Steuerzeichen blocken
const dangerousPatterns = [
/\x00/, // Null-Bytes
/\.\./, // Path traversal
/[<>:"\\|?*]/, // Windows-spezifische gefährliche Zeichen
/[\x00-\x1F]/, // Steuerzeichen
/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i, // Windows reservierte Namen
];
return !dangerousPatterns.some(pattern => pattern.test(filename));
}
/**
* Datei-Filter: Erweiterte Sicherheitsprüfungen
*/
const fileFilter = (req, file, cb) => {
// Aktuelle Einstellungen laden
const settings = getCurrentSettings();
// MIME-Type prüfen
if (settings.allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
logger.warn(`Abgelehnter Upload: ${file.originalname} (${file.mimetype})`);
cb(new Error(`Dateityp nicht erlaubt: ${file.mimetype}`), false);
// Sicherheitsprüfungen für Dateinamen
if (!isSecureFilename(file.originalname)) {
logger.warn(`Unsicherer Dateiname abgelehnt: ${file.originalname}`);
cb(new Error('Dateiname enthält nicht erlaubte Zeichen'), false);
return;
}
// Dateiname-Länge prüfen
if (file.originalname.length > 255) {
logger.warn(`Dateiname zu lang: ${file.originalname}`);
cb(new Error('Dateiname ist zu lang (max. 255 Zeichen)'), false);
return;
}
// Dateiendung extrahieren (ohne Punkt, lowercase)
const ext = path.extname(file.originalname).toLowerCase().replace('.', '');
// Doppelte Dateiendungen verhindern (z.B. script.txt.exe)
const nameWithoutExt = path.basename(file.originalname, path.extname(file.originalname));
if (path.extname(nameWithoutExt)) {
logger.warn(`Doppelte Dateiendung abgelehnt: ${file.originalname}`);
cb(new Error('Dateien mit mehreren Endungen sind nicht erlaubt'), false);
return;
}
// Prüfen ob Endung erlaubt ist
if (!settings.allowedExtensions.includes(ext)) {
logger.warn(`Abgelehnter Upload (Endung): ${file.originalname} (.${ext})`);
cb(new Error(`Dateityp .${ext} nicht erlaubt`), false);
return;
}
// Executable Dateien zusätzlich blocken
const executableExtensions = [
'exe', 'bat', 'cmd', 'com', 'scr', 'pif', 'vbs', 'vbe', 'js', 'jar',
'app', 'deb', 'pkg', 'dmg', 'run', 'bin', 'msi', 'gadget'
];
if (executableExtensions.includes(ext)) {
logger.warn(`Executable Datei abgelehnt: ${file.originalname}`);
cb(new Error('Ausführbare Dateien sind nicht erlaubt'), false);
return;
}
// MIME-Type gegen bekannte Typen prüfen
const expectedMimes = EXTENSION_TO_MIME[ext];
if (expectedMimes && !expectedMimes.includes(file.mimetype)) {
logger.warn(`MIME-Mismatch: ${file.originalname} (erwartet: ${expectedMimes.join('/')}, bekommen: ${file.mimetype})`);
// Bei kritischen Mismatches ablehnen
if (file.mimetype === 'application/octet-stream' || file.mimetype.startsWith('application/x-')) {
cb(new Error('Verdächtiger Dateityp erkannt'), false);
return;
}
}
cb(null, true);
};
/**
@ -194,5 +287,6 @@ module.exports = {
getCurrentSettings,
UPLOAD_DIR,
MAX_FILE_SIZE,
ALLOWED_MIME_TYPES
ALLOWED_EXTENSIONS,
EXTENSION_TO_MIME
};

Datei anzeigen

@ -5,24 +5,52 @@
*/
const sanitizeHtml = require('sanitize-html');
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
// DOMPurify für Server-side Rendering initialisieren
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
/**
* HTML-Tags entfernen (für reine Text-Felder)
* HTML-Entities dekodieren
*/
function stripHtml(input) {
if (typeof input !== 'string') return input;
return sanitizeHtml(input, {
allowedTags: [],
allowedAttributes: {}
}).trim();
function decodeHtmlEntities(str) {
if (typeof str !== 'string') return str;
const entities = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#039;': "'",
'&#x27;': "'",
'&apos;': "'"
};
return str.replace(/&(amp|lt|gt|quot|#039|#x27|apos);/g, match => entities[match] || match);
}
/**
* Markdown-sichere Bereinigung (erlaubt bestimmte Tags)
* HTML-Tags entfernen (für reine Text-Felder)
* Wichtig: sanitize-html encoded &-Zeichen zu &amp;, daher dekodieren wir danach
*/
function stripHtml(input) {
if (typeof input !== 'string') return input;
const sanitized = sanitizeHtml(input, {
allowedTags: [],
allowedAttributes: {}
}).trim();
// Entities wieder dekodieren, da sanitize-html sie encoded
return decodeHtmlEntities(sanitized);
}
/**
* Markdown-sichere Bereinigung mit DOMPurify (doppelte Sicherheit)
*/
function sanitizeMarkdown(input) {
if (typeof input !== 'string') return input;
return sanitizeHtml(input, {
// Erste Bereinigung mit sanitize-html
const firstPass = sanitizeHtml(input, {
allowedTags: [
'p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre',
'ul', 'ol', 'li', 'blockquote', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
@ -44,6 +72,16 @@ function sanitizeMarkdown(input) {
}
}
});
// Zweite Bereinigung mit DOMPurify (zusätzliche Sicherheit)
return DOMPurify.sanitize(firstPass, {
ALLOWED_TAGS: [
'p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre',
'ul', 'ol', 'li', 'blockquote', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
],
ALLOWED_ATTR: ['href', 'title', 'target', 'rel'],
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i
});
}
/**
@ -65,7 +103,15 @@ function sanitizeObject(obj, options = {}) {
for (const [key, value] of Object.entries(obj)) {
// Bestimmte Felder dürfen Markdown enthalten
const allowHtml = ['description', 'content'].includes(key);
sanitized[key] = sanitizeObject(value, { allowHtml });
// Passwort-Felder NICHT sanitizen (Sonderzeichen erhalten)
const skipSanitization = ['password', 'oldPassword', 'newPassword', 'confirmPassword'].includes(key);
if (skipSanitization) {
sanitized[key] = value; // Passwort unverändert lassen
} else {
sanitized[key] = sanitizeObject(value, { allowHtml });
}
}
return sanitized;
}
@ -119,12 +165,32 @@ const validators = {
},
/**
* URL-Format prüfen
* URL-Format prüfen (erweiterte Sicherheit)
*/
url: (value, fieldName) => {
try {
if (value) {
new URL(value);
const url = new URL(value);
// Nur HTTP/HTTPS erlauben
if (!['http:', 'https:'].includes(url.protocol)) {
return `${fieldName} muss HTTP oder HTTPS verwenden`;
}
// Localhost und private IPs blocken (SSRF-Schutz)
const hostname = url.hostname;
if (hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')) {
return `${fieldName} darf nicht auf lokale Adressen verweisen`;
}
// JavaScript URLs blocken
if (url.href.toLowerCase().startsWith('javascript:')) {
return `${fieldName} enthält ungültigen JavaScript-Code`;
}
}
return null;
} catch {

Datei anzeigen

@ -20,7 +20,10 @@
"cookie-parser": "^1.4.6",
"express-rate-limiter": "^1.3.1",
"sanitize-html": "^2.11.0",
"marked": "^11.1.0"
"marked": "^11.1.0",
"dompurify": "^3.0.6",
"jsdom": "^23.0.1",
"dotenv": "^16.3.1"
},
"engines": {
"node": ">=18.0.0"

87
backend/query_users.js Normale Datei
Datei anzeigen

@ -0,0 +1,87 @@
#!/usr/bin/env node
/**
* Script zum Abfragen der Benutzer aus der SQLite-Datenbank
* Verwendung: node query_users.js
*/
const Database = require('better-sqlite3');
const path = require('path');
// Datenbank-Pfad - angepasst für Docker-Container
const DB_PATH = process.env.DB_PATH || './data/taskmate.db';
try {
console.log('Verbinde zur Datenbank:', DB_PATH);
// Datenbank öffnen
const db = new Database(DB_PATH);
// Alle Benutzer abfragen
const users = db.prepare(`
SELECT
id,
username,
display_name,
color,
role,
email,
repositories_base_path,
created_at,
last_login,
failed_attempts,
locked_until
FROM users
ORDER BY id
`).all();
console.log('\n=== BENUTZER IN DER DATENBANK ===\n');
if (users.length === 0) {
console.log('Keine Benutzer gefunden!');
} else {
users.forEach(user => {
console.log(`ID: ${user.id}`);
console.log(`Benutzername: ${user.username}`);
console.log(`Anzeigename: ${user.display_name}`);
console.log(`Farbe: ${user.color}`);
console.log(`Rolle: ${user.role || 'user'}`);
console.log(`E-Mail: ${user.email || 'nicht gesetzt'}`);
console.log(`Repository-Basispfad: ${user.repositories_base_path || 'nicht gesetzt'}`);
console.log(`Erstellt am: ${user.created_at}`);
console.log(`Letzter Login: ${user.last_login || 'noch nie'}`);
console.log(`Fehlgeschlagene Versuche: ${user.failed_attempts}`);
console.log(`Gesperrt bis: ${user.locked_until || 'nicht gesperrt'}`);
console.log('-----------------------------------');
});
console.log(`\nGesamt: ${users.length} Benutzer gefunden`);
}
// Prüfe auch Login-Audit für weitere Informationen
const recentAttempts = db.prepare(`
SELECT
la.timestamp,
la.ip_address,
la.success,
la.user_agent,
u.username
FROM login_audit la
LEFT JOIN users u ON la.user_id = u.id
ORDER BY la.timestamp DESC
LIMIT 10
`).all();
if (recentAttempts.length > 0) {
console.log('\n=== LETZTE LOGIN-VERSUCHE ===\n');
recentAttempts.forEach(attempt => {
console.log(`${attempt.timestamp}: ${attempt.username || 'Unbekannt'} - ${attempt.success ? 'ERFOLGREICH' : 'FEHLGESCHLAGEN'} - IP: ${attempt.ip_address}`);
});
}
// Datenbank schließen
db.close();
} catch (error) {
console.error('Fehler beim Abfragen der Datenbank:', error.message);
process.exit(1);
}

Datei anzeigen

@ -10,45 +10,14 @@ const router = express.Router();
const { getDb } = require('../database');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const logger = require('../utils/logger');
const backup = require('../utils/backup');
/**
* Standard-Upload-Einstellungen
* Standard-Upload-Einstellungen (neues Format mit Dateiendungen)
*/
const DEFAULT_UPLOAD_SETTINGS = {
maxFileSizeMB: 15,
allowedTypes: {
images: {
enabled: true,
types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
},
documents: {
enabled: true,
types: ['application/pdf']
},
office: {
enabled: true,
types: [
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
]
},
text: {
enabled: true,
types: ['text/plain', 'text/csv', 'text/markdown']
},
archives: {
enabled: true,
types: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed']
},
data: {
enabled: true,
types: ['application/json']
}
}
allowedExtensions: ['pdf', 'docx', 'txt']
};
// Alle Admin-Routes erfordern Authentifizierung und Admin-Rolle
@ -351,6 +320,17 @@ router.get('/upload-settings', (req, res) => {
if (setting) {
const settings = JSON.parse(setting.value);
// Migration: Altes Format (allowedTypes) auf neues Format (allowedExtensions) umstellen
if (settings.allowedTypes && !settings.allowedExtensions) {
// Altes Format erkannt - auf Standard-Einstellungen zurücksetzen
logger.info('Migriere Upload-Einstellungen auf neues Format (allowedExtensions)');
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)')
.run('upload_settings', JSON.stringify(DEFAULT_UPLOAD_SETTINGS));
res.json(DEFAULT_UPLOAD_SETTINGS);
return;
}
res.json(settings);
} else {
// Standard-Einstellungen zurückgeben und speichern
@ -369,24 +349,36 @@ router.get('/upload-settings', (req, res) => {
*/
router.put('/upload-settings', (req, res) => {
try {
const { maxFileSizeMB, allowedTypes } = req.body;
const { maxFileSizeMB, allowedExtensions } = req.body;
// Validierung
if (typeof maxFileSizeMB !== 'number' || maxFileSizeMB < 1 || maxFileSizeMB > 100) {
return res.status(400).json({ error: 'Maximale Dateigröße muss zwischen 1 und 100 MB liegen' });
}
if (!allowedTypes || typeof allowedTypes !== 'object') {
return res.status(400).json({ error: 'Ungültige Dateityp-Konfiguration' });
if (!Array.isArray(allowedExtensions) || allowedExtensions.length === 0) {
return res.status(400).json({ error: 'Mindestens eine Dateiendung muss erlaubt sein' });
}
const settings = { maxFileSizeMB, allowedTypes };
// Endungen validieren (nur alphanumerisch, 1-10 Zeichen)
const validExtensions = allowedExtensions
.map(ext => ext.toLowerCase().replace(/^\./, '')) // Punkt am Anfang entfernen
.filter(ext => /^[a-z0-9]{1,10}$/.test(ext));
if (validExtensions.length === 0) {
return res.status(400).json({ error: 'Keine gültigen Dateiendungen angegeben' });
}
// Duplikate entfernen
const uniqueExtensions = [...new Set(validExtensions)];
const settings = { maxFileSizeMB, allowedExtensions: uniqueExtensions };
const db = getDb();
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)')
.run('upload_settings', JSON.stringify(settings));
logger.info(`Admin ${req.user.username} hat Upload-Einstellungen geändert`);
logger.info(`Admin ${req.user.username} hat Upload-Einstellungen geändert: ${uniqueExtensions.join(', ')}`);
res.json(settings);
} catch (error) {
@ -404,7 +396,12 @@ function getUploadSettings() {
const setting = db.prepare('SELECT value FROM settings WHERE key = ?').get('upload_settings');
if (setting) {
return JSON.parse(setting.value);
const settings = JSON.parse(setting.value);
// Bei altem Format oder fehlendem allowedExtensions: Standard verwenden
if (!settings.allowedExtensions || !Array.isArray(settings.allowedExtensions)) {
return DEFAULT_UPLOAD_SETTINGS;
}
return settings;
}
return DEFAULT_UPLOAD_SETTINGS;
} catch (error) {
@ -413,6 +410,42 @@ function getUploadSettings() {
}
}
/**
* POST /api/admin/backup/create - Sofortiges verschlüsseltes Backup erstellen
*/
router.post('/backup/create', (req, res) => {
try {
const backupPath = backup.createBackup();
if (backupPath) {
logger.info(`Admin ${req.user.username} hat manuelles Backup erstellt`);
res.json({
success: true,
message: 'Verschlüsseltes Backup erfolgreich erstellt',
backupPath: backupPath.split('/').pop() // Nur Dateiname zurückgeben
});
} else {
res.status(500).json({ error: 'Backup-Erstellung fehlgeschlagen' });
}
} catch (error) {
logger.error('Backup-Erstellung durch Admin fehlgeschlagen:', error);
res.status(500).json({ error: 'Interner Fehler beim Erstellen des Backups' });
}
});
/**
* GET /api/admin/backup/list - Liste aller verschlüsselten Backups
*/
router.get('/backup/list', (req, res) => {
try {
const backups = backup.listBackups();
res.json(backups);
} catch (error) {
logger.error('Fehler beim Auflisten der Backups:', error);
res.status(500).json({ error: 'Fehler beim Auflisten der Backups' });
}
});
module.exports = router;
module.exports.getUploadSettings = getUploadSettings;
module.exports.DEFAULT_UPLOAD_SETTINGS = DEFAULT_UPLOAD_SETTINGS;

Datei anzeigen

@ -8,7 +8,7 @@ const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const { getDb } = require('../database');
const { generateToken, authenticateToken } = require('../middleware/auth');
const { generateToken, generateRefreshToken, refreshAccessToken, revokeAllRefreshTokens, authenticateToken } = require('../middleware/auth');
const { getTokenForUser } = require('../middleware/csrf');
const { validatePassword } = require('../middleware/validation');
const logger = require('../utils/logger');
@ -37,13 +37,8 @@ router.post('/login', async (req, res) => {
// Benutzer suchen: Zuerst nach Username "admin", dann nach E-Mail
let user;
if (username.toLowerCase() === 'admin') {
// Admin-User per Username suchen
user = db.prepare('SELECT * FROM users WHERE username = ?').get('admin');
} else {
// Normale User per E-Mail suchen
user = db.prepare('SELECT * FROM users WHERE email = ?').get(username);
}
// User per Username suchen (kann E-Mail-Adresse oder admin sein)
user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
// Audit-Log Eintrag vorbereiten
const logAttempt = (userId, success) => {
@ -111,8 +106,11 @@ router.post('/login', async (req, res) => {
logAttempt(user.id, true);
// JWT-Token generieren
const token = generateToken(user);
// JWT Access-Token generieren (kurze Lebensdauer)
const accessToken = generateToken(user);
// Refresh-Token generieren (lange Lebensdauer)
const refreshToken = generateRefreshToken(user.id, ip, userAgent);
// CSRF-Token generieren
const csrfToken = getTokenForUser(user.id);
@ -128,7 +126,8 @@ router.post('/login', async (req, res) => {
}
res.json({
token,
token: accessToken,
refreshToken,
csrfToken,
user: {
id: user.id,
@ -147,13 +146,19 @@ router.post('/login', async (req, res) => {
/**
* POST /api/auth/logout
* Benutzer abmelden
* Benutzer abmelden und Refresh-Tokens widerrufen
*/
router.post('/logout', authenticateToken, (req, res) => {
// Bei JWT gibt es serverseitig nichts zu tun
// Client muss Token löschen
logger.info(`Logout: ${req.user.username}`);
res.json({ message: 'Erfolgreich abgemeldet' });
try {
// Alle Refresh-Tokens des Benutzers löschen
revokeAllRefreshTokens(req.user.id);
logger.info(`Logout: ${req.user.username}`);
res.json({ message: 'Erfolgreich abgemeldet' });
} catch (error) {
logger.error('Logout-Fehler:', { error: error.message });
res.status(500).json({ error: 'Logout fehlgeschlagen' });
}
});
/**
@ -200,27 +205,68 @@ router.get('/me', authenticateToken, (req, res) => {
/**
* POST /api/auth/refresh
* Token erneuern
* Token mit Refresh-Token erneuern
*/
router.post('/refresh', authenticateToken, (req, res) => {
router.post('/refresh', async (req, res) => {
try {
const db = getDb();
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
if (!user) {
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
const { refreshToken } = req.body;
const ip = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
if (!refreshToken) {
// Fallback für alte Clients - mit Access Token authentifizieren
if (req.headers.authorization) {
return legacyRefresh(req, res);
}
return res.status(400).json({ error: 'Refresh-Token erforderlich' });
}
const token = generateToken(user);
const csrfToken = getTokenForUser(user.id);
// Neuen Access-Token mit Refresh-Token generieren
const accessToken = await refreshAccessToken(refreshToken, ip, userAgent);
const db = getDb();
// User-Daten für CSRF-Token abrufen
const decoded = require('jsonwebtoken').decode(accessToken);
const csrfToken = getTokenForUser(decoded.id);
res.json({ token, csrfToken });
res.json({
token: accessToken,
csrfToken
});
} catch (error) {
logger.error('Token-Refresh Fehler:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
res.status(401).json({ error: 'Token-Erneuerung fehlgeschlagen' });
}
});
// Legacy Refresh für Rückwärtskompatibilität
function legacyRefresh(req, res) {
// Prüfe Authorization Header
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const token = authHeader.substring(7);
const user = require('../middleware/auth').verifyToken(token);
if (!user) {
return res.status(401).json({ error: 'Token ungültig' });
}
const db = getDb();
const dbUser = db.prepare('SELECT * FROM users WHERE id = ?').get(user.id);
if (!dbUser) {
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
}
const newToken = generateToken(dbUser);
const csrfToken = getTokenForUser(dbUser.id);
res.json({ token: newToken, csrfToken });
}
/**
* PUT /api/auth/password
* Passwort ändern

643
backend/routes/coding.js Normale Datei
Datei anzeigen

@ -0,0 +1,643 @@
/**
* TASKMATE - Coding Routes
* ========================
* Verwaltung von Server-Anwendungen mit Claude/Codex Integration
*/
const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const { getDb } = require('../database');
const logger = require('../utils/logger');
const gitService = require('../services/gitService');
/**
* Prüft ob ein Pfad ein Server-Pfad (Linux) ist
*/
function isServerPath(localPath) {
return localPath && localPath.startsWith('/');
}
/**
* Schreibt CLAUDE.md in ein Verzeichnis
*/
function writeCLAUDEmd(directoryPath, content) {
if (!content || !directoryPath) return false;
try {
const claudePath = path.join(directoryPath, 'CLAUDE.md');
fs.writeFileSync(claudePath, content, 'utf8');
logger.info(`CLAUDE.md geschrieben: ${claudePath}`);
return true;
} catch (e) {
logger.error('CLAUDE.md schreiben fehlgeschlagen:', e);
return false;
}
}
/**
* Liest CLAUDE.md aus einem Verzeichnis
*/
function readCLAUDEmd(directoryPath) {
if (!directoryPath) {
logger.info('readCLAUDEmd: No directoryPath provided');
return null;
}
try {
const claudePath = path.join(directoryPath, 'CLAUDE.md');
logger.info(`readCLAUDEmd: Checking path ${claudePath}`);
if (fs.existsSync(claudePath)) {
const content = fs.readFileSync(claudePath, 'utf8');
logger.info(`readCLAUDEmd: Successfully read ${content.length} characters from ${claudePath}`);
return content;
} else {
logger.info(`readCLAUDEmd: File does not exist: ${claudePath}`);
}
} catch (e) {
logger.error('CLAUDE.md lesen fehlgeschlagen:', e);
}
return null;
}
/**
* GET /api/coding/directories
* Alle Coding-Verzeichnisse abrufen
*/
router.get('/directories', (req, res) => {
try {
const db = getDb();
const directories = db.prepare(`
SELECT cd.*, u.display_name as creator_name
FROM coding_directories cd
LEFT JOIN users u ON cd.created_by = u.id
ORDER BY cd.position ASC, cd.name ASC
`).all();
res.json(directories.map(dir => {
// CLAUDE.md aus dem Dateisystem lesen falls vorhanden
let claudeMdFromDisk = null;
if (isServerPath(dir.local_path)) {
claudeMdFromDisk = readCLAUDEmd(dir.local_path);
// Fallback: Wenn Pfad /home/claude-dev/TaskMate ist, versuche /app/taskmate-source
if (!claudeMdFromDisk && dir.local_path === '/home/claude-dev/TaskMate') {
logger.info('Trying fallback path for TaskMate: /app/taskmate-source');
claudeMdFromDisk = readCLAUDEmd('/app/taskmate-source');
}
}
return {
id: dir.id,
name: dir.name,
localPath: dir.local_path,
description: dir.description,
color: dir.color,
claudeInstructions: dir.claude_instructions,
claudeMdFromDisk: claudeMdFromDisk,
hasCLAUDEmd: !!claudeMdFromDisk,
giteaRepoUrl: dir.gitea_repo_url,
giteaRepoOwner: dir.gitea_repo_owner,
giteaRepoName: dir.gitea_repo_name,
defaultBranch: dir.default_branch,
lastSync: dir.last_sync,
position: dir.position,
createdAt: dir.created_at,
createdBy: dir.created_by,
creatorName: dir.creator_name
};
}));
} catch (error) {
logger.error('Fehler beim Abrufen der Coding-Verzeichnisse:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/coding/directories
* Neues Coding-Verzeichnis erstellen
*/
router.post('/directories', (req, res) => {
try {
const { name, localPath, description, color, claudeInstructions, giteaRepoUrl, giteaRepoOwner, giteaRepoName, defaultBranch } = req.body;
if (!name || !localPath) {
return res.status(400).json({ error: 'Name und Server-Pfad sind erforderlich' });
}
const db = getDb();
// Prüfe ob Pfad bereits existiert
const existing = db.prepare('SELECT id FROM coding_directories WHERE local_path = ?').get(localPath);
if (existing) {
return res.status(400).json({ error: 'Diese Anwendung ist bereits registriert' });
}
// Höchste Position ermitteln
const maxPos = db.prepare('SELECT COALESCE(MAX(position), -1) as max FROM coding_directories').get().max;
const result = db.prepare(`
INSERT INTO coding_directories (name, local_path, description, color, claude_instructions, gitea_repo_url, gitea_repo_owner, gitea_repo_name, default_branch, position, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
name,
localPath,
description || null,
color || '#4F46E5',
null, // claudeInstructions wird nicht mehr gespeichert
giteaRepoUrl || null,
giteaRepoOwner || null,
giteaRepoName || null,
defaultBranch || 'main',
maxPos + 1,
req.user.id
);
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(result.lastInsertRowid);
// Ordner erstellen falls nicht vorhanden
let directoryCreated = false;
if (isServerPath(localPath) && !fs.existsSync(localPath)) {
try {
fs.mkdirSync(localPath, { recursive: true });
directoryCreated = true;
logger.info(`Anwendungsordner erstellt: ${localPath}`);
} catch (e) {
logger.error('Ordner erstellen fehlgeschlagen:', e);
}
}
// CLAUDE.md wird nicht mehr geschrieben - nur readonly
let claudeMdWritten = false;
logger.info(`Coding-Anwendung erstellt: ${name} (${localPath})`);
// CLAUDE.md aus dem Dateisystem lesen für aktuelle Anzeige
const claudeMdFromDisk = isServerPath(directory.local_path) ? readCLAUDEmd(directory.local_path) : null;
res.status(201).json({
id: directory.id,
name: directory.name,
localPath: directory.local_path,
description: directory.description,
color: directory.color,
claudeInstructions: directory.claude_instructions,
claudeMdFromDisk: claudeMdFromDisk,
hasCLAUDEmd: !!claudeMdFromDisk,
giteaRepoUrl: directory.gitea_repo_url,
giteaRepoOwner: directory.gitea_repo_owner,
giteaRepoName: directory.gitea_repo_name,
defaultBranch: directory.default_branch,
position: directory.position,
createdAt: directory.created_at,
directoryCreated,
claudeMdWritten
});
} catch (error) {
logger.error('Fehler beim Erstellen der Coding-Anwendung:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* PUT /api/coding/directories/:id
* Coding-Anwendung aktualisieren
*/
router.put('/directories/:id', (req, res) => {
try {
const { id } = req.params;
const { name, localPath, description, color, claudeInstructions, giteaRepoUrl, giteaRepoOwner, giteaRepoName, defaultBranch, position } = req.body;
const db = getDb();
const existing = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!existing) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
// Prüfe ob neuer Pfad bereits von anderem Eintrag verwendet wird
if (localPath && localPath !== existing.local_path) {
const pathExists = db.prepare('SELECT id FROM coding_directories WHERE local_path = ? AND id != ?').get(localPath, id);
if (pathExists) {
return res.status(400).json({ error: 'Dieser Server-Pfad ist bereits registriert' });
}
}
db.prepare(`
UPDATE coding_directories SET
name = COALESCE(?, name),
local_path = COALESCE(?, local_path),
description = ?,
color = COALESCE(?, color),
claude_instructions = ?,
gitea_repo_url = ?,
gitea_repo_owner = ?,
gitea_repo_name = ?,
default_branch = COALESCE(?, default_branch),
position = COALESCE(?, position)
WHERE id = ?
`).run(
name || null,
localPath || null,
description !== undefined ? description : existing.description,
color || null,
null, // claudeInstructions wird nicht mehr aktualisiert
giteaRepoUrl !== undefined ? giteaRepoUrl : existing.gitea_repo_url,
giteaRepoOwner !== undefined ? giteaRepoOwner : existing.gitea_repo_owner,
giteaRepoName !== undefined ? giteaRepoName : existing.gitea_repo_name,
defaultBranch || null,
position !== undefined ? position : null,
id
);
const updated = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
const finalPath = updated.local_path;
// Ordner erstellen falls nicht vorhanden
let directoryCreated = false;
if (isServerPath(finalPath) && !fs.existsSync(finalPath)) {
try {
fs.mkdirSync(finalPath, { recursive: true });
directoryCreated = true;
logger.info(`Anwendungsordner erstellt: ${finalPath}`);
} catch (e) {
logger.error('Ordner erstellen fehlgeschlagen:', e);
}
}
// CLAUDE.md wird nicht mehr geschrieben - nur readonly
let claudeMdWritten = false;
logger.info(`Coding-Anwendung aktualisiert: ${updated.name}`);
// CLAUDE.md aus dem Dateisystem lesen für aktuelle Anzeige
const claudeMdFromDisk = isServerPath(updated.local_path) ? readCLAUDEmd(updated.local_path) : null;
res.json({
id: updated.id,
name: updated.name,
localPath: updated.local_path,
description: updated.description,
color: updated.color,
claudeInstructions: updated.claude_instructions,
claudeMdFromDisk: claudeMdFromDisk,
hasCLAUDEmd: !!claudeMdFromDisk,
giteaRepoUrl: updated.gitea_repo_url,
giteaRepoOwner: updated.gitea_repo_owner,
giteaRepoName: updated.gitea_repo_name,
defaultBranch: updated.default_branch,
position: updated.position,
createdAt: updated.created_at,
directoryCreated,
claudeMdWritten
});
} catch (error) {
logger.error('Fehler beim Aktualisieren der Coding-Anwendung:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* DELETE /api/coding/directories/:id
* Coding-Anwendung löschen
*/
router.delete('/directories/:id', (req, res) => {
try {
const { id } = req.params;
const db = getDb();
const existing = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!existing) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
db.prepare('DELETE FROM coding_directories WHERE id = ?').run(id);
logger.info(`Coding-Anwendung gelöscht: ${existing.name}`);
res.json({ message: 'Anwendung gelöscht' });
} catch (error) {
logger.error('Fehler beim Löschen der Coding-Anwendung:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* GET /api/coding/directories/:id/status
* Git-Status eines Verzeichnisses abrufen
*/
router.get('/directories/:id/status', (req, res) => {
try {
const { id } = req.params;
const db = getDb();
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!directory) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
const localPath = directory.local_path;
// Prüfe ob es ein Git-Repository ist
if (!gitService.isGitRepository(localPath)) {
return res.json({
isGitRepo: false,
message: 'Kein Git-Repository'
});
}
const status = gitService.getStatus(localPath);
if (!status.success) {
return res.status(500).json({ error: status.error });
}
res.json({
isGitRepo: true,
branch: status.branch,
hasChanges: status.hasChanges,
changes: status.changes,
ahead: status.ahead,
behind: status.behind,
isClean: status.isClean
});
} catch (error) {
logger.error('Fehler beim Abrufen des Git-Status:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/coding/directories/:id/fetch
* Git Fetch ausführen
*/
router.post('/directories/:id/fetch', (req, res) => {
try {
const { id } = req.params;
const db = getDb();
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!directory) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
const result = gitService.fetchRemote(directory.local_path);
if (!result.success) {
return res.status(500).json({ error: result.error });
}
// Last sync aktualisieren
db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id);
logger.info(`Git fetch ausgeführt für: ${directory.name}`);
res.json({ success: true, message: 'Fetch erfolgreich' });
} catch (error) {
logger.error('Fehler beim Git Fetch:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/coding/directories/:id/pull
* Git Pull ausführen
*/
router.post('/directories/:id/pull', (req, res) => {
try {
const { id } = req.params;
const db = getDb();
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!directory) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
const result = gitService.pullChanges(directory.local_path, { branch: directory.default_branch });
if (!result.success) {
return res.status(500).json({ error: result.error });
}
// Last sync aktualisieren
db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id);
logger.info(`Git pull ausgeführt für: ${directory.name}`);
res.json({ success: true, message: 'Pull erfolgreich', output: result.output });
} catch (error) {
logger.error('Fehler beim Git Pull:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/coding/directories/:id/push
* Git Push ausführen
*/
router.post('/directories/:id/push', (req, res) => {
try {
const { id } = req.params;
const { force } = req.body;
const db = getDb();
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!directory) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
const result = gitService.pushWithUpstream(directory.local_path, directory.default_branch, 'origin', force);
if (!result.success) {
return res.status(500).json({ error: result.error });
}
// Last sync aktualisieren
db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id);
logger.info(`Git push ausgeführt für: ${directory.name}`);
res.json({ success: true, message: 'Push erfolgreich', output: result.output });
} catch (error) {
logger.error('Fehler beim Git Push:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/coding/directories/:id/commit
* Git Commit ausführen
*/
router.post('/directories/:id/commit', (req, res) => {
try {
const { id } = req.params;
const { message } = req.body;
const db = getDb();
if (!message || message.trim() === '') {
return res.status(400).json({ error: 'Commit-Nachricht erforderlich' });
}
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!directory) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
// Stage all changes
const stageResult = gitService.stageAll(directory.local_path);
if (!stageResult.success) {
return res.status(500).json({ error: stageResult.error });
}
// Commit with author info
const author = {
name: req.user.display_name || req.user.username,
email: req.user.email || `${req.user.username}@taskmate.local`
};
const result = gitService.commit(directory.local_path, message, author);
if (!result.success) {
return res.status(500).json({ error: result.error });
}
logger.info(`Git commit ausgeführt für: ${directory.name} - "${message}"`);
res.json({ success: true, message: 'Commit erfolgreich', output: result.output });
} catch (error) {
logger.error('Fehler beim Git Commit:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* GET /api/coding/directories/:id/branches
* Branches abrufen
*/
router.get('/directories/:id/branches', (req, res) => {
try {
const { id } = req.params;
const db = getDb();
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!directory) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
const result = gitService.getBranches(directory.local_path);
if (!result.success) {
return res.status(500).json({ error: result.error });
}
res.json({ branches: result.branches });
} catch (error) {
logger.error('Fehler beim Abrufen der Branches:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/coding/directories/:id/checkout
* Branch wechseln
*/
router.post('/directories/:id/checkout', (req, res) => {
try {
const { id } = req.params;
const { branch } = req.body;
const db = getDb();
if (!branch) {
return res.status(400).json({ error: 'Branch erforderlich' });
}
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!directory) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
const result = gitService.checkoutBranch(directory.local_path, branch);
if (!result.success) {
return res.status(500).json({ error: result.error });
}
logger.info(`Branch gewechselt für ${directory.name}: ${branch}`);
res.json({ success: true, message: `Gewechselt zu Branch: ${branch}` });
} catch (error) {
logger.error('Fehler beim Branch-Wechsel:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* POST /api/coding/validate-path
* Pfad validieren
*/
router.post('/validate-path', (req, res) => {
try {
const { path: localPath } = req.body;
if (!localPath) {
return res.status(400).json({ error: 'Pfad erforderlich' });
}
// Nur Server-Pfade können validiert werden
if (isServerPath(localPath)) {
const containerPath = gitService.windowsToContainerPath(localPath);
const exists = fs.existsSync(containerPath);
const isGitRepo = exists && gitService.isGitRepository(localPath);
res.json({
valid: true,
exists,
isGitRepo,
isServerPath: true
});
} else {
// Windows-Pfad kann nicht serverseitig validiert werden
res.json({
valid: true,
exists: null,
isGitRepo: null,
isServerPath: false,
message: 'Windows-Pfade können nicht serverseitig validiert werden'
});
}
} catch (error) {
logger.error('Fehler bei der Pfad-Validierung:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* GET /api/coding/directories/:id/commits
* Commit-Historie abrufen
*/
router.get('/directories/:id/commits', (req, res) => {
try {
const { id } = req.params;
const limit = parseInt(req.query.limit) || 20;
const db = getDb();
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
if (!directory) {
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
}
const result = gitService.getCommitHistory(directory.local_path, limit);
if (!result.success) {
return res.status(500).json({ error: result.error });
}
res.json({ commits: result.commits });
} catch (error) {
logger.error('Fehler beim Abrufen der Commit-Historie:', error);
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
module.exports = router;

Datei anzeigen

@ -4,6 +4,9 @@
* Node.js/Express Backend mit Socket.io für Echtzeit-Sync
*/
// Umgebungsvariablen laden (muss ganz oben stehen!)
require('dotenv').config();
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
@ -42,6 +45,7 @@ const gitRoutes = require('./routes/git');
const applicationsRoutes = require('./routes/applications');
const giteaRoutes = require('./routes/gitea');
const knowledgeRoutes = require('./routes/knowledge');
const codingRoutes = require('./routes/coding');
// Express App erstellen
const app = express();
@ -59,17 +63,18 @@ const io = new Server(server, {
// MIDDLEWARE
// =============================================================================
// Sicherheits-Header
// Erweiterte Sicherheits-Header (CSP temporär deaktiviert für Login-Fix)
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:"],
scriptSrc: ["'self'"],
connectSrc: ["'self'", "ws:", "wss:"]
}
contentSecurityPolicy: false, // Temporär deaktiviert
hsts: {
maxAge: 31536000, // 1 Jahr
includeSubDomains: true,
preload: true
},
noSniff: true,
xssFilter: true,
referrerPolicy: {
policy: "strict-origin-when-cross-origin"
}
}));
@ -86,6 +91,10 @@ app.use(express.urlencoded({ extended: true, limit: '1mb' }));
// Cookie Parser
app.use(cookieParser());
// Input Sanitization (vor allen anderen Middlewares)
const { sanitizeMiddleware } = require('./middleware/validation');
app.use(sanitizeMiddleware);
// Request Logging
app.use((req, res, next) => {
const start = Date.now();
@ -148,6 +157,9 @@ app.use('/api/gitea', authenticateToken, csrfProtection, giteaRoutes);
// Knowledge-Routes (Wissensmanagement)
app.use('/api/knowledge', authenticateToken, csrfProtection, knowledgeRoutes);
// Coding-Routes (Entwicklungsverzeichnisse mit Claude/Codex)
app.use('/api/coding', authenticateToken, csrfProtection, codingRoutes);
// =============================================================================
// SOCKET.IO
// =============================================================================

Datei anzeigen

@ -21,6 +21,11 @@ function windowsToContainerPath(windowsPath) {
return windowsPath;
}
// Spezialfall: TaskMate-Verzeichnis ist als /app/taskmate-source gemountet
if (windowsPath === '/home/claude-dev/TaskMate') {
return '/app/taskmate-source';
}
// Windows-Pfad konvertieren (z.B. "C:\foo" oder "C:/foo")
const normalized = windowsPath.replace(/\\/g, '/');
const match = normalized.match(/^([a-zA-Z]):[\/](.*)$/);
@ -73,8 +78,12 @@ function isGitRepository(localPath) {
const containerPath = windowsToContainerPath(localPath);
try {
const gitDir = path.join(containerPath, '.git');
return fs.existsSync(gitDir);
logger.info(`Git-Repository Check: ${localPath} -> ${containerPath} -> ${gitDir}`);
const exists = fs.existsSync(gitDir);
logger.info(`Git directory exists: ${exists}`);
return exists;
} catch (error) {
logger.error(`Git-Repository Check failed: ${error.message}`);
return false;
}
}

Datei anzeigen

@ -7,6 +7,7 @@
const fs = require('fs');
const path = require('path');
const logger = require('./logger');
const { encryptFile, decryptFile, secureDelete } = require('./encryption');
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data');
const BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, '..', 'backups');
@ -17,8 +18,9 @@ if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
}
/**
* Backup erstellen
* Backup erstellen (mit einfacher Verschlüsselung)
*/
function createBackup() {
try {
@ -29,12 +31,27 @@ function createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = `backup_${timestamp}.db`;
const encryptedName = `backup_${timestamp}.db.enc`;
const backupPath = path.join(BACKUP_DIR, backupName);
const encryptedPath = path.join(BACKUP_DIR, encryptedName);
// Datenbank kopieren
// 1. Normales Backup erstellen (für Kompatibilität)
fs.copyFileSync(DB_FILE, backupPath);
// WAL-Datei auch sichern falls vorhanden
// 2. Verschlüsseltes Backup erstellen (zusätzlich)
if (process.env.ENCRYPTION_KEY) {
try {
if (encryptFile(DB_FILE, encryptedPath)) {
logger.info(`Verschlüsseltes Backup erstellt: ${encryptedName}`);
} else {
logger.warn('Verschlüsselung fehlgeschlagen, nur normales Backup erstellt');
}
} catch (encError) {
logger.warn('Verschlüsselung fehlgeschlagen, nur normales Backup erstellt');
}
}
// 3. WAL-Datei sichern falls vorhanden
const walFile = DB_FILE + '-wal';
if (fs.existsSync(walFile)) {
fs.copyFileSync(walFile, backupPath + '-wal');
@ -53,12 +70,12 @@ function createBackup() {
}
/**
* Alte Backups löschen
* Alte Backups löschen (verschlüsselte)
*/
function cleanupOldBackups(keepCount = 30) {
try {
const files = fs.readdirSync(BACKUP_DIR)
.filter(f => f.startsWith('backup_') && f.endsWith('.db'))
.filter(f => f.startsWith('backup_') && f.endsWith('.db.enc'))
.sort()
.reverse();
@ -66,15 +83,15 @@ function cleanupOldBackups(keepCount = 30) {
toDelete.forEach(file => {
const filePath = path.join(BACKUP_DIR, file);
fs.unlinkSync(filePath);
secureDelete(filePath);
// WAL-Datei auch löschen falls vorhanden
// Verschlüsselte WAL-Datei auch löschen falls vorhanden
const walPath = filePath + '-wal';
if (fs.existsSync(walPath)) {
fs.unlinkSync(walPath);
secureDelete(walPath);
}
logger.info(`Altes Backup gelöscht: ${file}`);
logger.info(`Altes Backup sicher gelöscht: ${file}`);
});
} catch (error) {
logger.error('Fehler beim Aufräumen alter Backups:', { error: error.message });
@ -82,32 +99,50 @@ function cleanupOldBackups(keepCount = 30) {
}
/**
* Backup wiederherstellen
* Backup wiederherstellen (entschlüsselt)
*/
function restoreBackup(backupName) {
try {
const backupPath = path.join(BACKUP_DIR, backupName);
const encryptedBackupPath = path.join(BACKUP_DIR, backupName);
if (!fs.existsSync(backupPath)) {
if (!fs.existsSync(encryptedBackupPath)) {
throw new Error(`Backup nicht gefunden: ${backupName}`);
}
// Aktuelles DB sichern bevor überschrieben wird
// Aktuelles DB verschlüsselt sichern bevor überschrieben wird
if (fs.existsSync(DB_FILE)) {
const safetyBackup = DB_FILE + '.before-restore';
fs.copyFileSync(DB_FILE, safetyBackup);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const safetyBackupPath = DB_FILE + `.before-restore-${timestamp}.enc`;
if (!encryptFile(DB_FILE, safetyBackupPath)) {
logger.warn('Sicherheitsbackup vor Wiederherstellung fehlgeschlagen');
}
}
// Backup wiederherstellen
fs.copyFileSync(backupPath, DB_FILE);
// Temporäre entschlüsselte Datei
const tempRestorePath = path.join(BACKUP_DIR, `temp_restore_${Date.now()}.db`);
// Backup entschlüsseln
if (!decryptFile(encryptedBackupPath, tempRestorePath)) {
throw new Error('Backup-Entschlüsselung fehlgeschlagen');
}
// Entschlüsselte DB kopieren
fs.copyFileSync(tempRestorePath, DB_FILE);
// WAL-Datei auch wiederherstellen falls vorhanden
const walBackup = backupPath + '-wal';
if (fs.existsSync(walBackup)) {
fs.copyFileSync(walBackup, DB_FILE + '-wal');
const encryptedWalBackup = encryptedBackupPath + '-wal';
if (fs.existsSync(encryptedWalBackup)) {
const tempWalPath = tempRestorePath + '-wal';
if (decryptFile(encryptedWalBackup, tempWalPath)) {
fs.copyFileSync(tempWalPath, DB_FILE + '-wal');
secureDelete(tempWalPath);
}
}
logger.info(`Backup wiederhergestellt: ${backupName}`);
// Temporäre entschlüsselte Dateien sicher löschen
secureDelete(tempRestorePath);
logger.info(`Verschlüsseltes Backup wiederhergestellt: ${backupName}`);
return true;
} catch (error) {
logger.error('Restore-Fehler:', { error: error.message });
@ -116,12 +151,12 @@ function restoreBackup(backupName) {
}
/**
* Liste aller Backups
* Liste aller verschlüsselten Backups
*/
function listBackups() {
try {
const files = fs.readdirSync(BACKUP_DIR)
.filter(f => f.startsWith('backup_') && f.endsWith('.db'))
.filter(f => f.startsWith('backup_') && f.endsWith('.db.enc'))
.map(f => {
const filePath = path.join(BACKUP_DIR, f);
const stats = fs.statSync(filePath);

237
backend/utils/encryption.js Normale Datei
Datei anzeigen

@ -0,0 +1,237 @@
/**
* TASKMATE - Encryption Utilities
* ================================
* Verschlüsselung für Backups und sensitive Daten
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const logger = require('./logger');
const ALGORITHM = 'aes-256-cbc';
const KEY_LENGTH = 32; // 256 bits
const IV_LENGTH = 16; // 128 bits
const SALT_LENGTH = 32;
const TAG_LENGTH = 16;
/**
* Encryption Key aus Umgebung oder generiert
*/
function getEncryptionKey() {
let key = process.env.ENCRYPTION_KEY;
if (!key) {
// Generiere neuen Key falls nicht vorhanden
key = crypto.randomBytes(KEY_LENGTH).toString('hex');
logger.warn('Encryption Key wurde automatisch generiert. Speichere ihn in der .env: ENCRYPTION_KEY=' + key);
return Buffer.from(key, 'hex');
}
// Validiere Key-Length
if (key.length !== KEY_LENGTH * 2) { // Hex-String ist doppelt so lang
throw new Error(`Encryption Key muss ${KEY_LENGTH * 2} Hex-Zeichen haben`);
}
return Buffer.from(key, 'hex');
}
/**
* Key aus Passwort ableiten (PBKDF2)
*/
function deriveKeyFromPassword(password, salt) {
return crypto.pbkdf2Sync(password, salt, 100000, KEY_LENGTH, 'sha256');
}
/**
* Datei verschlüsseln
*/
function encryptFile(inputPath, outputPath, password = null) {
try {
const data = fs.readFileSync(inputPath);
// Salt und IV generieren
const salt = crypto.randomBytes(SALT_LENGTH);
const iv = crypto.randomBytes(IV_LENGTH);
// Key ableiten
const key = password
? deriveKeyFromPassword(password, salt)
: getEncryptionKey();
// Verschlüsselung
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
// Header + Salt + IV + verschlüsselte Daten
const header = Buffer.from('TMENC001', 'ascii'); // TaskMate Encryption v1
const result = Buffer.concat([
header,
salt,
iv,
encrypted
]);
fs.writeFileSync(outputPath, result);
logger.info(`Datei verschlüsselt: ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
return true;
} catch (error) {
logger.error(`Verschlüsselung fehlgeschlagen: ${error.message}`);
return false;
}
}
/**
* Datei entschlüsseln
*/
function decryptFile(inputPath, outputPath, password = null) {
try {
const encryptedData = fs.readFileSync(inputPath);
// Header prüfen
const header = encryptedData.subarray(0, 8);
if (header.toString('ascii') !== 'TMENC001') {
throw new Error('Ungültiges verschlüsseltes Datei-Format');
}
// Komponenten extrahieren
let offset = 8;
const salt = encryptedData.subarray(offset, offset + SALT_LENGTH);
offset += SALT_LENGTH;
const iv = encryptedData.subarray(offset, offset + IV_LENGTH);
offset += IV_LENGTH;
const encrypted = encryptedData.subarray(offset);
// Key ableiten
const key = password
? deriveKeyFromPassword(password, salt)
: getEncryptionKey();
// Entschlüsselung
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
fs.writeFileSync(outputPath, decrypted);
logger.info(`Datei entschlüsselt: ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
return true;
} catch (error) {
logger.error(`Entschlüsselung fehlgeschlagen: ${error.message}`);
return false;
}
}
/**
* String verschlüsseln (für Passwörter etc.)
*/
function encryptString(plaintext, password = null) {
try {
const salt = crypto.randomBytes(SALT_LENGTH);
const iv = crypto.randomBytes(IV_LENGTH);
const key = password
? deriveKeyFromPassword(password, salt)
: getEncryptionKey();
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(plaintext, 'utf8')),
cipher.final()
]);
// Base64 kodiert zurückgeben
const result = Buffer.concat([salt, iv, encrypted]);
return result.toString('base64');
} catch (error) {
logger.error(`String-Verschlüsselung fehlgeschlagen: ${error.message}`);
return null;
}
}
/**
* String entschlüsseln
*/
function decryptString(encryptedString, password = null) {
try {
const data = Buffer.from(encryptedString, 'base64');
let offset = 0;
const salt = data.subarray(offset, offset + SALT_LENGTH);
offset += SALT_LENGTH;
const iv = data.subarray(offset, offset + IV_LENGTH);
offset += IV_LENGTH;
const encrypted = data.subarray(offset);
const key = password
? deriveKeyFromPassword(password, salt)
: getEncryptionKey();
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
return decrypted.toString('utf8');
} catch (error) {
logger.error(`String-Entschlüsselung fehlgeschlagen: ${error.message}`);
return null;
}
}
/**
* Sicheres Löschen einer Datei (Überschreiben)
*/
function secureDelete(filePath) {
try {
if (!fs.existsSync(filePath)) {
return true;
}
const stats = fs.statSync(filePath);
const fileSize = stats.size;
// Datei mehrfach mit Zufallsdaten überschreiben
const fd = fs.openSync(filePath, 'r+');
for (let pass = 0; pass < 3; pass++) {
const randomData = crypto.randomBytes(fileSize);
fs.writeSync(fd, randomData, 0, fileSize, 0);
fs.fsyncSync(fd);
}
fs.closeSync(fd);
fs.unlinkSync(filePath);
logger.info(`Datei sicher gelöscht: ${path.basename(filePath)}`);
return true;
} catch (error) {
logger.error(`Sicheres Löschen fehlgeschlagen: ${error.message}`);
return false;
}
}
module.exports = {
encryptFile,
decryptFile,
encryptString,
decryptString,
secureDelete,
getEncryptionKey
};