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

@ -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;