Datenbank bereinigt / Gitea-Integration gefixt
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
395598c2b0
Commit
c21be47428
@ -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;
|
||||
|
||||
@ -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
643
backend/routes/coding.js
Normale Datei
@ -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;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren