Files
Lars Behrends 065a6e657d feat: add world map functionality and admin map management
- Added world map page with interactive marker display
- Implemented admin map management for marker CRUD operations
- Added map layers and markers seed data to database
- Integrated new routes for map functionality
- Updated database configuration for production environment
- Added documentation page route
- Enhanced package.json with required dependencies for map features
2026-01-02 05:08:07 +01:00

2351 lines
90 KiB
JavaScript

const express = require('express');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const passport = require('passport');
const DiscordStrategy = require('passport-discord').Strategy;
const cors = require('cors');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { db, init } = require('./database');
const MapProcessor = require('./map-processor');
const app = express();
const PORT = 3000;
// Env variables (provided via Docker/Environment)
const CLIENT_ID = process.env.DISCORD_CLIENT_ID || 'mock_id';
const CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET || 'mock_secret';
const CALLBACK_URL = process.env.CALLBACK_URL || 'http://localhost:3000/auth/discord/callback';
const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:8000';
const BACKEND_URL = process.env.BACKEND_URL || 'https://vollidioten.ceraticsoft.de';
// CORS configuration - allow multiple origins for development
const corsOrigins = process.env.NODE_ENV === 'production'
? [FRONTEND_URL]
: [FRONTEND_URL, 'http://localhost:3000', 'http://localhost:8000', 'http://127.0.0.1:3000'];
// File upload configuration
const UPLOAD_DIR = path.join(__dirname, 'uploads');
if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, UPLOAD_DIR);
},
filename: (req, file, cb) => {
// Generate unique filename with timestamp
const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(file.originalname);
cb(null, uniqueName);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
},
fileFilter: (req, file, cb) => {
// Allow only image files
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Nur Bilddateien sind erlaubt'), false);
}
}
});
init(); // Initialize DB
// Configure MySQL session store
const sessionStore = new MySQLStore({
host: process.env.DB_HOST || 'db',
port: 3306,
user: process.env.DB_USER || 'obsidian_user',
password: process.env.DB_PASS || 'obsidian_pass',
database: process.env.DB_NAME || 'obsidian_db',
clearExpired: true,
checkExpirationInterval: 900000, // Clean expired sessions every 15 minutes
expiration: 86400000 * 30, // 30 days default
createDatabaseTable: true, // Auto-create sessions table
schema: {
tableName: 'sessions',
columnNames: {
session_id: 'session_id',
expires: 'expires',
data: 'data'
}
}
});
// Middleware
app.use(express.json());
app.use(cors({ origin: corsOrigins, credentials: true }));
app.use(session({
secret: process.env.SESSION_SECRET || 'dev_secret',
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // Set true if using https
maxAge: 24 * 60 * 60 * 1000, // 24 hours default
httpOnly: true,
sameSite: 'lax'
}
}));
app.use(passport.initialize());
app.use(passport.session());
// Passport Setup
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((obj, done) => {
// If user has a linked player, fetch their Minecraft username and admin status from database
if (obj.linkedPlayerUuid) {
db.get("SELECT username, isAdmin FROM players WHERE uuid = ?", [obj.linkedPlayerUuid], (err, row) => {
if (!err && row) {
// Replace Discord username with Minecraft username for easier API usage
obj.username = row.username;
obj.isAdmin = !!row.isAdmin;
}
done(null, obj);
});
} else {
obj.isAdmin = false;
done(null, obj);
}
});
passport.use(new DiscordStrategy({
clientID: CLIENT_ID,
clientSecret: CLIENT_SECRET,
callbackURL: CALLBACK_URL,
scope: ['identify']
},
function(accessToken, refreshToken, profile, cb) {
// In a real app, you might map the Discord ID to a player in the DB here
// For this demo, we just pass the profile through
// We try to find a player who has this discordId linked, or return the raw profile
db.get("SELECT uuid FROM players WHERE discordId = ?", [profile.id], (err, row) => {
const user = {
id: profile.id,
username: profile.username,
discriminator: profile.discriminator,
avatarUrl: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`,
linkedPlayerUuid: row ? row.uuid : null
};
// Hack for demo: If user is "DrKButz" (or you), make them admin of that player
// For testing, we just link the first login to DrKButz if not taken
if (!row && profile.username === 'YourDiscordUsername') { // Replace logic as needed
user.linkedPlayerUuid = '8984c0b5-d912-4462-b189-c864fba4a1af';
}
return cb(null, user);
});
}
));
app.get('/api/status', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: '1.0.0',
database: 'connected'
});
});
// --- ROUTES ---
// Auth
app.get('/auth/discord', (req, res, next) => {
// Check if user wants to be remembered (longer session)
const rememberMe = req.query.remember_me === 'true';
req.session.rememberMe = rememberMe;
// Set session maxAge based on remember me preference
if (rememberMe) {
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
req.session.cookie.expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
} else {
req.session.cookie.maxAge = 24 * 60 * 60 * 1000; // 24 hours
req.session.cookie.expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
}
passport.authenticate('discord')(req, res, next);
});
app.get('/auth/discord/callback', passport.authenticate('discord', {
failureRedirect: FRONTEND_URL + '?error=login_failed'
}), (req, res) => {
// Ensure session is saved with correct maxAge
if (req.session.rememberMe) {
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
req.session.cookie.expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
}
req.session.save(() => {
res.redirect(FRONTEND_URL);
});
});
app.get('/auth/me', (req, res) => {
if (req.isAuthenticated()) {
res.json(req.user);
} else {
res.status(401).json({ error: 'Not authenticated' });
}
});
app.get('/auth/logout', (req, res) => {
req.logout(() => {
res.redirect(FRONTEND_URL);
});
});
// API: Players
app.get('/api/players', (req, res) => {
db.all("SELECT * FROM players", (err, rows) => {
if (err) return res.status(500).json({error: err.message});
// Parse JSON fields
const parsed = rows.map(r => ({
...r,
tags: JSON.parse(r.tags || '[]'),
stats: JSON.parse(r.stats || '{}'),
minecraftStats: r.minecraftStats ? JSON.parse(r.minecraftStats) : undefined,
inventory: JSON.parse(r.inventory || '[]'),
isOnline: !!r.isOnline,
organizationId: r.organizationId || undefined
}));
res.json(parsed);
});
});
// API: Unlinked Players (for Discord account linking)
app.get('/api/unlinked-players', (req, res) => {
db.all("SELECT uuid, username, tags, stats FROM players WHERE discordId IS NULL OR discordId = ''", (err, rows) => {
if (err) return res.status(500).json({error: err.message});
// Parse JSON fields
const parsed = rows.map(r => ({
...r,
tags: JSON.parse(r.tags),
stats: JSON.parse(r.stats)
}));
res.json(parsed);
});
});
app.put('/api/players/:uuid', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user owns this player profile OR if it's an admin creating/updating NPCs
const isOwner = req.user.linkedPlayerUuid === req.params.uuid;
const isAdmin = req.user.isAdmin;
if (!isOwner && !isAdmin) {
return res.status(403).json({error: 'Keine Berechtigung zum Bearbeiten'});
}
const updates = req.body;
const allowedFields = ['storyMarkdown', 'tags', 'organizationId'];
const updateFields = [];
const values = [];
// Handle fields
for (const field of allowedFields) {
if (updates[field] !== undefined) {
if (field === 'tags') {
// Tags are stored as JSON
updateFields.push(`${field} = ?`);
values.push(JSON.stringify(updates[field]));
} else {
updateFields.push(`${field} = ?`);
values.push(updates[field]);
}
}
}
if (updateFields.length === 0) {
return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'});
}
const query = `UPDATE players SET ${updateFields.join(', ')} WHERE uuid = ?`;
values.push(req.params.uuid);
db.run(query, values, function(err) {
if (err) {
console.error('Error updating player:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren'});
}
res.json({success: true, message: 'Spieler erfolgreich aktualisiert'});
});
});
// API: Orgs
app.get('/api/orgs', (req, res) => {
db.all("SELECT * FROM orgs", (err, rows) => {
if (err) return res.status(500).json({error: err.message});
const parsed = rows.map(r => ({
...r,
gallery: JSON.parse(r.gallery || '[]'),
bannerUrl: getImageUrl(r.bannerImageId),
logoUrl: getImageUrl(r.logoImageId),
cityStats: r.cityStats ? JSON.parse(r.cityStats) : undefined
}));
res.json(parsed);
});
});
// Helper function to get full image URL
function getImageUrl(imageId) {
if (!imageId) return null;
return `${BACKEND_URL}/api/images/${imageId}`;
}
// Helper function to resolve gallery image IDs to URLs
function resolveGalleryImages(gallery) {
if (!gallery) return [];
try {
const imageIds = JSON.parse(gallery);
return imageIds.map(id => ({ id, url: getImageUrl(id) }));
} catch (e) {
return [];
}
}
// API: Projects
app.get('/api/projects', (req, res) => {
db.all("SELECT * FROM projects", (err, rows) => {
if (err) return res.status(500).json({error: err.message});
const parsed = rows.map(r => ({
...r,
employees: JSON.parse(r.employees),
shopCatalog: JSON.parse(r.shopCatalog),
gallery: resolveGalleryImages(r.gallery),
bannerUrl: getImageUrl(r.bannerImageId),
logoUrl: getImageUrl(r.logoImageId),
hiring: !!r.hiring
}));
res.json(parsed);
});
});
// Create new project
app.post('/api/projects', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
console.log('Received project creation request');
console.log('Request body:', req.body);
console.log('Authenticated user:', req.user);
const { title, description, category, associatedOrgId, owner } = req.body;
if (!title || !description) {
return res.status(400).json({ error: 'Titel und Beschreibung sind erforderlich' });
}
// Use the owner from request body (Minecraft character name) or fallback to Discord username
const projectOwner = owner || req.user.username;
console.log('Using project owner:', projectOwner);
// Generate unique ID
const projectId = 'proj_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
db.run(`INSERT INTO projects
(id, title, description, category, status, progress, owner, employees, hiring, foundedDate, associatedOrgId, bannerImageId, shopCatalog, gallery)
VALUES (?, ?, ?, ?, 'active', 0, ?, '[]', 0, ?, ?, null, '[]', '[]')`,
[projectId, title, description, category || 'Enterprise', projectOwner,
new Date().toISOString().split('T')[0], associatedOrgId || null],
function(err) {
if (err) {
console.error('Error creating project:', err);
return res.status(500).json({error: 'Fehler beim Erstellen des Projekts'});
}
res.json({
success: true,
projectId: projectId,
message: 'Projekt erfolgreich erstellt'
});
}
);
});
// Update project (with ownership check)
app.put('/api/projects/:id', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const projectId = req.params.id;
const updates = req.body;
// First check ownership
db.get("SELECT owner FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username) {
return res.status(403).json({error: 'Keine Berechtigung zum Bearbeiten'});
}
// Build dynamic update query
const allowedFields = ['title', 'description', 'category', 'status', 'progress', 'hiring', 'bannerUrl'];
const updateFields = [];
const values = [];
for (const field of allowedFields) {
if (updates[field] !== undefined) {
if (field === 'hiring') {
updateFields.push(`${field} = ?`);
values.push(updates[field] ? 1 : 0);
} else {
updateFields.push(`${field} = ?`);
values.push(updates[field]);
}
}
}
if (updateFields.length === 0) {
return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'});
}
const query = `UPDATE projects SET ${updateFields.join(', ')} WHERE id = ?`;
values.push(projectId);
db.run(query, values, function(err) {
if (err) {
console.error('Error updating project:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren'});
}
res.json({success: true, message: 'Projekt erfolgreich aktualisiert'});
});
});
});
// Delete project (with ownership check)
app.delete('/api/projects/:id', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const projectId = req.params.id;
// Check ownership
db.get("SELECT owner FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username) {
return res.status(403).json({error: 'Keine Berechtigung zum Löschen'});
}
db.run("DELETE FROM projects WHERE id = ?", [projectId], function(err) {
if (err) {
console.error('Error deleting project:', err);
return res.status(500).json({error: 'Fehler beim Löschen'});
}
res.json({success: true, message: 'Projekt erfolgreich gelöscht'});
});
});
});
// Link Discord to Player (Admin/Dev helper)
app.post('/api/link-user', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const { playerUuid } = req.body;
db.run("UPDATE players SET discordId = ? WHERE uuid = ?", [req.user.id, playerUuid], (err) => {
if (err) return res.status(500).json({error: err});
res.json({success: true});
});
});
// === NPC MANAGEMENT API (Admin Only) ===
// Create NPC Citizen
app.post('/api/admin/npc-citizen', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { username, tags, role, organizationId, storyMarkdown } = req.body;
if (!username) {
return res.status(400).json({error: 'NPC-Name erforderlich'});
}
// Generate UUID for NPC
const uuid = 'npc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const npcData = {
uuid,
username,
isOnline: 0,
isNpc: 1,
tags: JSON.stringify(tags || []),
stats: JSON.stringify({
playtimeHours: Math.floor(Math.random() * 1000),
level: Math.floor(Math.random() * 50) + 1,
role: role || 'Bürger',
organizationId: organizationId || null
}),
minecraftStats: null,
inventory: JSON.stringify([]),
storyMarkdown: storyMarkdown || `# ${username}\n\nEin NPC-Bürger im Obsidian-Tal.`,
discordId: null
};
db.run(`INSERT INTO players (uuid, username, isOnline, isNpc, tags, stats, minecraftStats, inventory, storyMarkdown, discordId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[npcData.uuid, npcData.username, npcData.isOnline, npcData.isNpc, npcData.tags,
npcData.stats, npcData.minecraftStats, npcData.inventory, npcData.storyMarkdown, npcData.discordId],
function(err) {
if (err) {
console.error('Error creating NPC citizen:', err);
return res.status(500).json({error: 'Fehler beim Erstellen des NPC-Bürgers'});
}
res.json({
success: true,
npc: { ...npcData, tags: JSON.parse(npcData.tags), stats: JSON.parse(npcData.stats) },
message: 'NPC-Bürger erfolgreich erstellt'
});
}
);
});
// Create NPC Company
app.post('/api/admin/npc-company', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { title, description, category, owner, associatedOrgId, shopCatalog } = req.body;
if (!title || !owner) {
return res.status(400).json({error: 'Titel und NPC-Eigentümer erforderlich'});
}
// Generate unique ID
const projectId = 'npc_proj_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
db.run(`INSERT INTO projects
(id, title, description, category, status, progress, owner, employees, hiring, foundedDate, associatedOrgId, bannerImageId, shopCatalog, gallery)
VALUES (?, ?, ?, ?, 'active', ?, ?, '[]', 0, ?, ?, null, ?, '[]')`,
[projectId, title, description || `${title} - Eine NPC-Firma im Obsidian-Tal.`, category || 'Enterprise',
Math.floor(Math.random() * 100), owner, new Date().toISOString().split('T')[0],
associatedOrgId || null, JSON.stringify(shopCatalog || [])],
function(err) {
if (err) {
console.error('Error creating NPC company:', err);
return res.status(500).json({error: 'Fehler beim Erstellen der NPC-Firma'});
}
res.json({
success: true,
projectId: projectId,
message: 'NPC-Firma erfolgreich erstellt'
});
}
);
});
// Upload NPC Company Banner
app.post('/api/admin/npc-companies/:projectId/banner/upload', upload.single('banner'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { projectId } = req.params;
// Verify this is an NPC company
if (!projectId.startsWith('npc_proj_')) {
return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'});
}
// Check if NPC company exists
db.get("SELECT title FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Convert to WebP
const originalPath = path.join(UPLOAD_DIR, req.file.filename);
const webpFilename = imageId + '.webp';
const webpPath = path.join(UPLOAD_DIR, webpFilename);
convertToWebP(originalPath, webpPath).then((webpSize) => {
// Clean up original file
fs.unlinkSync(originalPath);
// Create image record with WebP data
const imageData = {
id: imageId,
filename: webpFilename,
originalName: req.file.originalname,
mimeType: 'image/webp',
size: webpSize,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.title} Banner`,
entityType: 'project',
entityId: projectId,
imageType: 'banner'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating NPC company banner image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Banners'});
}
// Update NPC company banner
db.run("UPDATE projects SET bannerImageId = ? WHERE id = ?", [imageId, projectId], function(err) {
if (err) {
console.error('Error updating NPC company banner:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Banners'});
}
res.json({
success: true,
imageId: imageId,
imageUrl: `${BACKEND_URL}/api/images/${imageId}`,
message: 'Banner erfolgreich hochgeladen'
});
});
});
}).catch((error) => {
console.error('Error converting NPC company banner to WebP:', error);
// Clean up files on error
try {
if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath);
if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath);
} catch (cleanupErr) {
console.error('Error cleaning up files:', cleanupErr);
}
res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'});
});
});
});
// Upload NPC Company Logo
app.post('/api/admin/npc-companies/:projectId/logo/upload', upload.single('logo'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { projectId } = req.params;
// Verify this is an NPC company
if (!projectId.startsWith('npc_proj_')) {
return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'});
}
// Check if NPC company exists
db.get("SELECT title FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Convert to WebP
const originalPath = path.join(UPLOAD_DIR, req.file.filename);
const webpFilename = imageId + '.webp';
const webpPath = path.join(UPLOAD_DIR, webpFilename);
convertToWebP(originalPath, webpPath).then((webpSize) => {
// Clean up original file
fs.unlinkSync(originalPath);
// Create image record with WebP data
const imageData = {
id: imageId,
filename: webpFilename,
originalName: req.file.originalname,
mimeType: 'image/webp',
size: webpSize,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.title} Logo`,
entityType: 'project',
entityId: projectId,
imageType: 'logo'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating NPC company logo image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Logos'});
}
// Update NPC company logo
db.run("UPDATE projects SET logoImageId = ? WHERE id = ?", [imageId, projectId], function(err) {
if (err) {
console.error('Error updating NPC company logo:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Logos'});
}
res.json({
success: true,
imageId: imageId,
logoUrl: `${BACKEND_URL}/api/images/${imageId}`,
message: 'Logo erfolgreich hochgeladen'
});
});
});
}).catch((error) => {
console.error('Error converting NPC company logo to WebP:', error);
// Clean up files on error
try {
if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath);
if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath);
} catch (cleanupErr) {
console.error('Error cleaning up files:', cleanupErr);
}
res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'});
});
});
});
// Upload NPC Company Gallery Image
app.post('/api/admin/npc-companies/:projectId/gallery/upload', upload.single('gallery'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { projectId } = req.params;
// Verify this is an NPC company
if (!projectId.startsWith('npc_proj_')) {
return res.status(400).json({error: 'Nur NPC-Firmen können über diese Route bearbeitet werden'});
}
// Check if NPC company exists
db.get("SELECT title, gallery FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'NPC-Firma nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Convert to WebP
const originalPath = path.join(UPLOAD_DIR, req.file.filename);
const webpFilename = imageId + '.webp';
const webpPath = path.join(UPLOAD_DIR, webpFilename);
convertToWebP(originalPath, webpPath).then((webpSize) => {
// Clean up original file
fs.unlinkSync(originalPath);
// Create image record with WebP data
const imageData = {
id: imageId,
filename: webpFilename,
originalName: req.file.originalname,
mimeType: 'image/webp',
size: webpSize,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.title} Portfolio`,
entityType: 'project',
entityId: projectId,
imageType: 'gallery'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating NPC company gallery image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Bildes'});
}
try {
// Add image ID to gallery array
const gallery = JSON.parse(row.gallery || '[]');
gallery.push(imageId);
db.run("UPDATE projects SET gallery = ? WHERE id = ?", [JSON.stringify(gallery), projectId], function(err) {
if (err) {
console.error('Error updating NPC company gallery:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren der Galerie'});
}
res.json({
success: true,
imageId: imageId,
imageUrl: `${BACKEND_URL}/api/images/${imageId}`,
gallery: gallery,
message: 'Bild erfolgreich zum Portfolio hinzugefügt'
});
});
} catch (e) {
console.error('Error parsing NPC company gallery:', e);
res.status(500).json({error: 'Fehler beim Verarbeiten der Galerie-Daten'});
}
});
}).catch((error) => {
console.error('Error converting NPC company gallery image to WebP:', error);
// Clean up files on error
try {
if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath);
if (fs.existsSync(webpPath)) fs.unlinkSync(webpPath);
} catch (cleanupErr) {
console.error('Error cleaning up files:', cleanupErr);
}
res.status(500).json({error: 'Fehler beim Konvertieren des Bildes'});
});
});
});
// Grant admin rights to a player (Admin only)
app.post('/api/admin/grant-admin/:uuid', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const playerUuid = req.params.uuid;
db.run("UPDATE players SET isAdmin = 1 WHERE uuid = ?", [playerUuid], function(err) {
if (err) {
console.error('Error granting admin rights:', err);
return res.status(500).json({error: 'Fehler beim Vergeben der Admin-Rechte'});
}
if (this.changes === 0) {
return res.status(404).json({error: 'Spieler nicht gefunden'});
}
res.json({success: true, message: 'Admin-Rechte erfolgreich vergeben'});
});
});
// Revoke admin rights from a player (Admin only)
app.post('/api/admin/revoke-admin/:uuid', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const playerUuid = req.params.uuid;
// Prevent revoking own admin rights
if (req.user.linkedPlayerUuid === playerUuid) {
return res.status(400).json({error: 'Sie können Ihre eigenen Admin-Rechte nicht entziehen'});
}
db.run("UPDATE players SET isAdmin = 0 WHERE uuid = ?", [playerUuid], function(err) {
if (err) {
console.error('Error revoking admin rights:', err);
return res.status(500).json({error: 'Fehler beim Entziehen der Admin-Rechte'});
}
if (this.changes === 0) {
return res.status(404).json({error: 'Spieler nicht gefunden'});
}
res.json({success: true, message: 'Admin-Rechte erfolgreich entzogen'});
});
});
// Get all NPCs (for admin overview)
app.get('/api/admin/npcs', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
// First get all players with isNpc flag
db.all("SELECT * FROM players WHERE isNpc = 1", (err, npcPlayers) => {
if (err) return res.status(500).json({error: err.message});
// Get all projects where owner is an NPC (starts with npc_)
db.all("SELECT * FROM projects WHERE owner LIKE 'npc_%'", (err, npcOwnedProjects) => {
if (err) return res.status(500).json({error: err.message});
// Also get all projects with NPC-like IDs (npc_proj_ prefix)
db.all("SELECT * FROM projects WHERE id LIKE 'npc_proj_%'", (err, npcCreatedProjects) => {
if (err) return res.status(500).json({error: err.message});
// Combine and deduplicate projects
const allNpcProjects = [...npcOwnedProjects, ...npcCreatedProjects];
const uniqueProjects = allNpcProjects.filter((project, index, self) =>
index === self.findIndex(p => p.id === project.id)
);
console.log('NPC Debug Info:');
console.log('NPC Players found:', npcPlayers.length);
console.log('NPC Owned Projects:', npcOwnedProjects.length);
console.log('NPC Created Projects:', npcCreatedProjects.length);
console.log('Total Unique Projects:', uniqueProjects.length);
// Parse JSON fields for players
const parsedPlayers = npcPlayers.map(r => ({
...r,
tags: JSON.parse(r.tags || '[]'),
stats: JSON.parse(r.stats || '{}'),
inventory: JSON.parse(r.inventory || '[]'),
isOnline: !!r.isOnline,
isNpc: !!r.isNpc
}));
// Parse JSON fields for projects
const parsedProjects = uniqueProjects.map(r => ({
...r,
employees: JSON.parse(r.employees || '[]'),
shopCatalog: JSON.parse(r.shopCatalog || '[]'),
gallery: JSON.parse(r.gallery || '[]'),
hiring: !!r.hiring
}));
res.json({
citizens: parsedPlayers,
companies: parsedProjects,
debug: {
npcPlayersCount: npcPlayers.length,
npcOwnedProjectsCount: npcOwnedProjects.length,
npcCreatedProjectsCount: npcCreatedProjects.length,
totalUniqueProjects: uniqueProjects.length
}
});
});
});
});
});
// === SHOP MANAGEMENT API ===
// Get shop items for a project
app.get('/api/projects/:projectId/shop', (req, res) => {
const projectId = req.params.projectId;
db.get("SELECT shopCatalog FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
try {
const shopCatalog = JSON.parse(row.shopCatalog || '[]');
res.json(shopCatalog);
} catch (e) {
res.status(500).json({error: 'Fehler beim Laden des Shops'});
}
});
});
// Add shop item
app.post('/api/projects/:projectId/shop', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const projectId = req.params.projectId;
const { name, description, price, currency, stock, type, materialsRequired } = req.body;
// Check ownership
db.get("SELECT owner, shopCatalog FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
try {
const shopCatalog = JSON.parse(row.shopCatalog || '[]');
const newItem = {
id: `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name,
description,
price: parseFloat(price),
currency: currency || 'Gold',
stock: parseInt(stock) || 0,
type: type || 'item',
materialsRequired: materialsRequired || null
};
shopCatalog.push(newItem);
db.run("UPDATE projects SET shopCatalog = ? WHERE id = ?", [JSON.stringify(shopCatalog), projectId], function(err) {
if (err) {
console.error('Error adding shop item:', err);
return res.status(500).json({error: 'Fehler beim Hinzufügen'});
}
res.json({success: true, item: newItem});
});
} catch (e) {
res.status(500).json({error: 'Fehler beim Verarbeiten der Daten'});
}
});
});
// Update shop item
app.put('/api/projects/:projectId/shop/:itemId', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const { projectId, itemId } = req.params;
const updates = req.body;
// Check ownership
db.get("SELECT owner, shopCatalog FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
try {
const shopCatalog = JSON.parse(row.shopCatalog || '[]');
const itemIndex = shopCatalog.findIndex(item => item.id === itemId);
if (itemIndex === -1) {
return res.status(404).json({error: 'Artikel nicht gefunden'});
}
// Update item
shopCatalog[itemIndex] = { ...shopCatalog[itemIndex], ...updates };
db.run("UPDATE projects SET shopCatalog = ? WHERE id = ?", [JSON.stringify(shopCatalog), projectId], function(err) {
if (err) {
console.error('Error updating shop item:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren'});
}
res.json({success: true, item: shopCatalog[itemIndex]});
});
} catch (e) {
res.status(500).json({error: 'Fehler beim Verarbeiten der Daten'});
}
});
});
// Delete shop item
app.delete('/api/projects/:projectId/shop/:itemId', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const { projectId, itemId } = req.params;
// Check ownership
db.get("SELECT owner, shopCatalog FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
try {
const shopCatalog = JSON.parse(row.shopCatalog || '[]');
const filteredCatalog = shopCatalog.filter(item => item.id !== itemId);
if (filteredCatalog.length === shopCatalog.length) {
return res.status(404).json({error: 'Artikel nicht gefunden'});
}
db.run("UPDATE projects SET shopCatalog = ? WHERE id = ?", [JSON.stringify(filteredCatalog), projectId], function(err) {
if (err) {
console.error('Error deleting shop item:', err);
return res.status(500).json({error: 'Fehler beim Löschen'});
}
res.json({success: true});
});
} catch (e) {
res.status(500).json({error: 'Fehler beim Verarbeiten der Daten'});
}
});
});
// === EMPLOYEE MANAGEMENT API ===
// Get project employees
app.get('/api/projects/:projectId/employees', (req, res) => {
const projectId = req.params.projectId;
db.get("SELECT employees FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
try {
const employees = JSON.parse(row.employees || '[]');
res.json(employees);
} catch (e) {
res.status(500).json({error: 'Fehler beim Laden der Mitarbeiter'});
}
});
});
// Add employee
app.post('/api/projects/:projectId/employees', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const projectId = req.params.projectId;
const { employeeName } = req.body;
if (!employeeName) {
return res.status(400).json({error: 'Mitarbeiter-Name erforderlich'});
}
// Check ownership
db.get("SELECT owner, employees FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
try {
const employees = JSON.parse(row.employees || '[]');
if (employees.includes(employeeName)) {
return res.status(400).json({error: 'Mitarbeiter bereits hinzugefügt'});
}
employees.push(employeeName);
db.run("UPDATE projects SET employees = ? WHERE id = ?", [JSON.stringify(employees), projectId], function(err) {
if (err) {
console.error('Error adding employee:', err);
return res.status(500).json({error: 'Fehler beim Hinzufügen'});
}
res.json({success: true, employees});
});
} catch (e) {
res.status(500).json({error: 'Fehler beim Verarbeiten der Daten'});
}
});
});
// Remove employee
app.delete('/api/projects/:projectId/employees/:employeeName', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const { projectId, employeeName } = req.params;
// Check ownership
db.get("SELECT owner, employees FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
try {
const employees = JSON.parse(row.employees || '[]');
const filteredEmployees = employees.filter(emp => emp !== employeeName);
if (filteredEmployees.length === employees.length) {
return res.status(404).json({error: 'Mitarbeiter nicht gefunden'});
}
db.run("UPDATE projects SET employees = ? WHERE id = ?", [JSON.stringify(filteredEmployees), projectId], function(err) {
if (err) {
console.error('Error removing employee:', err);
return res.status(500).json({error: 'Fehler beim Entfernen'});
}
res.json({success: true, employees: filteredEmployees});
});
} catch (e) {
res.status(500).json({error: 'Fehler beim Verarbeiten der Daten'});
}
});
});
// === GALLERY MANAGEMENT API ===
// Get project gallery
app.get('/api/projects/:projectId/gallery', (req, res) => {
const projectId = req.params.projectId;
db.get("SELECT gallery FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
try {
const gallery = JSON.parse(row.gallery || '[]');
res.json(gallery);
} catch (e) {
res.status(500).json({error: 'Fehler beim Laden der Galerie'});
}
});
});
// Add gallery image (for now just URL-based)
app.post('/api/projects/:projectId/gallery', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const projectId = req.params.projectId;
const { imageUrl } = req.body;
if (!imageUrl) {
return res.status(400).json({error: 'Bild-URL erforderlich'});
}
// Check ownership
db.get("SELECT owner, gallery FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
try {
const gallery = JSON.parse(row.gallery || '[]');
gallery.push(imageUrl);
db.run("UPDATE projects SET gallery = ? WHERE id = ?", [JSON.stringify(gallery), projectId], function(err) {
if (err) {
console.error('Error adding gallery image:', err);
return res.status(500).json({error: 'Fehler beim Hinzufügen'});
}
res.json({success: true, gallery});
});
} catch (e) {
res.status(500).json({error: 'Fehler beim Verarbeiten der Daten'});
}
});
});
// Delete gallery image
app.delete('/api/projects/:projectId/gallery/:imageId', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const { projectId, imageId } = req.params;
// Check ownership
db.get("SELECT owner, gallery FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
try {
const gallery = JSON.parse(row.gallery || '[]');
const imageIndex = gallery.indexOf(imageId);
if (imageIndex === -1) {
return res.status(404).json({error: 'Bild nicht in Galerie gefunden'});
}
// Remove from gallery array
gallery.splice(imageIndex, 1);
// Update project gallery
db.run("UPDATE projects SET gallery = ? WHERE id = ?", [JSON.stringify(gallery), projectId], function(err) {
if (err) {
console.error('Error updating gallery:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren der Galerie'});
}
// Delete image record and file
db.get("SELECT filename FROM images WHERE id = ?", [imageId], (err, imageRow) => {
if (err) {
console.error('Error finding image record:', err);
return res.status(500).json({error: 'Fehler beim Löschen des Bildes'});
}
if (imageRow) {
// Delete file from filesystem
const filePath = path.join(UPLOAD_DIR, imageRow.filename);
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (fileErr) {
console.error('Error deleting file:', fileErr);
}
// Delete image record from database
db.run("DELETE FROM images WHERE id = ?", [imageId], function(err) {
if (err) {
console.error('Error deleting image record:', err);
return res.status(500).json({error: 'Fehler beim Löschen des Bildes'});
}
res.json({
success: true,
gallery: gallery,
message: 'Bild erfolgreich gelöscht'
});
});
} else {
// Image record not found, but gallery was updated
res.json({
success: true,
gallery: gallery,
message: 'Bild aus Galerie entfernt'
});
}
});
});
} catch (e) {
res.status(500).json({error: 'Fehler beim Verarbeiten der Galerie-Daten'});
}
});
});
// === IMAGE MANAGEMENT API ===
// Get image by ID
app.get('/api/images/:imageId', (req, res) => {
const { imageId } = req.params;
db.get("SELECT * FROM images WHERE id = ?", [imageId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Bild nicht gefunden'});
// Serve the actual image file
const filePath = path.join(UPLOAD_DIR, row.filename);
if (fs.existsSync(filePath)) {
res.sendFile(filePath);
} else {
res.status(404).json({error: 'Bild-Datei nicht gefunden'});
}
});
});
// Get image metadata by ID
app.get('/api/images/:imageId/meta', (req, res) => {
const { imageId } = req.params;
db.get("SELECT * FROM images WHERE id = ?", [imageId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Bild nicht gefunden'});
res.json({
id: row.id,
filename: row.filename,
originalName: row.originalName,
mimeType: row.mimeType,
size: row.size,
uploadDate: row.uploadDate,
altText: row.altText,
url: `${BACKEND_URL}/api/images/${row.id}`
});
});
});
// Upload banner image
app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
const { projectId } = req.params;
// Check ownership
db.get("SELECT owner FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Create image record
const imageData = {
id: imageId,
filename: req.file.filename,
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size,
uploadDate: new Date().toISOString(),
altText: req.body.altText || null,
entityType: 'project',
entityId: projectId,
imageType: 'banner'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Bildes'});
}
// Update project banner
db.run("UPDATE projects SET bannerImageId = ? WHERE id = ?", [imageId, projectId], function(err) {
if (err) {
console.error('Error updating banner:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Banners'});
}
res.json({
success: true,
imageId: imageId,
imageUrl: `${BACKEND_URL}/api/images/${imageId}`,
message: 'Banner erfolgreich hochgeladen'
});
});
});
});
});
// Upload gallery image
app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
const { projectId } = req.params;
// Check ownership
db.get("SELECT owner, gallery FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Create image record
const imageData = {
id: imageId,
filename: req.file.filename,
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size,
uploadDate: new Date().toISOString(),
altText: req.body.altText || null,
entityType: 'project',
entityId: projectId,
imageType: 'gallery'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Bildes'});
}
try {
// Add image ID to gallery array
const gallery = JSON.parse(row.gallery || '[]');
gallery.push(imageId);
db.run("UPDATE projects SET gallery = ? WHERE id = ?", [JSON.stringify(gallery), projectId], function(err) {
if (err) {
console.error('Error updating gallery:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren der Galerie'});
}
res.json({
success: true,
imageId: imageId,
imageUrl: `${BACKEND_URL}/api/images/${imageId}`,
gallery: gallery,
message: 'Bild erfolgreich zur Galerie hinzugefügt'
});
});
} catch (e) {
console.error('Error parsing gallery:', e);
res.status(500).json({error: 'Fehler beim Verarbeiten der Galerie-Daten'});
}
});
});
});
// Upload logo image
app.post('/api/projects/:projectId/logo/upload', upload.single('logo'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
const { projectId } = req.params;
// Check ownership
db.get("SELECT owner FROM projects WHERE id = ?", [projectId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Projekt nicht gefunden'});
if (row.owner !== req.user.username && req.user.username !== 'admin') {
return res.status(403).json({error: 'Keine Berechtigung'});
}
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Create image record
const imageData = {
id: imageId,
filename: req.file.filename,
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.title} Logo`,
entityType: 'project',
entityId: projectId,
imageType: 'logo'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating logo image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Logos'});
}
// Update project logo
db.run("UPDATE projects SET logoImageId = ? WHERE id = ?", [imageId, projectId], function(err) {
if (err) {
console.error('Error updating logo:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Logos'});
}
res.json({
success: true,
imageId: imageId,
logoUrl: `${BACKEND_URL}/api/images/${imageId}`,
message: 'Logo erfolgreich hochgeladen'
});
});
});
});
});
// === CITY MANAGEMENT API (Admin Only) ===
// Create new city
app.post('/api/admin/cities', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { name, description, mayor, establishedYear, cityStats } = req.body;
if (!name) {
return res.status(400).json({error: 'Stadt-Name erforderlich'});
}
// Generate unique ID
const cityId = 'city_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const cityData = {
id: cityId,
name: name.trim(),
type: 'City',
description: description || `${name} - Eine Stadt im Obsidian-Tal.`,
memberCount: 0,
status: 'active',
mayor: mayor || '',
establishedYear: establishedYear || new Date().getFullYear().toString(),
bannerImageId: null,
logoImageId: null,
gallery: '[]',
cityStats: cityStats ? JSON.stringify(cityStats) : JSON.stringify({
taxRate: 5.0,
biome: 'Ebene',
defenseRating: 5,
government: 'Demokratie',
specialty: 'Handel'
})
};
db.run(`INSERT INTO orgs (id, name, type, description, memberCount, status, mayor, establishedYear, bannerImageId, logoImageId, gallery, cityStats)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[cityData.id, cityData.name, cityData.type, cityData.description, cityData.memberCount, cityData.status,
cityData.mayor, cityData.establishedYear, cityData.bannerImageId, cityData.logoImageId, cityData.gallery, cityData.cityStats],
function(err) {
if (err) {
console.error('Error creating city:', err);
return res.status(500).json({error: 'Fehler beim Erstellen der Stadt'});
}
res.json({
success: true,
cityId: cityId,
message: 'Stadt erfolgreich erstellt'
});
}
);
});
// Update city
app.put('/api/admin/cities/:cityId', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { cityId } = req.params;
const updates = req.body;
// Build dynamic update query
const allowedFields = ['name', 'description', 'mayor', 'establishedYear', 'status', 'cityStats'];
const updateFields = [];
const values = [];
for (const field of allowedFields) {
if (updates[field] !== undefined) {
if (field === 'cityStats') {
updateFields.push(`${field} = ?`);
values.push(JSON.stringify(updates[field]));
} else {
updateFields.push(`${field} = ?`);
values.push(updates[field]);
}
}
}
if (updateFields.length === 0) {
return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'});
}
const query = `UPDATE orgs SET ${updateFields.join(', ')} WHERE id = ? AND type = 'City'`;
values.push(cityId);
db.run(query, values, function(err) {
if (err) {
console.error('Error updating city:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren'});
}
if (this.changes === 0) {
return res.status(404).json({error: 'Stadt nicht gefunden'});
}
res.json({success: true, message: 'Stadt erfolgreich aktualisiert'});
});
});
// Delete city
app.delete('/api/admin/cities/:cityId', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { cityId } = req.params;
db.run("DELETE FROM orgs WHERE id = ? AND type = 'City'", [cityId], function(err) {
if (err) {
console.error('Error deleting city:', err);
return res.status(500).json({error: 'Fehler beim Löschen'});
}
if (this.changes === 0) {
return res.status(404).json({error: 'Stadt nicht gefunden'});
}
res.json({success: true, message: 'Stadt erfolgreich gelöscht'});
});
});
// Upload city banner
app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { cityId } = req.params;
// Check if city exists
db.get("SELECT name FROM orgs WHERE id = ? AND type = 'City'", [cityId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Stadt nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Create image record
const imageData = {
id: imageId,
filename: req.file.filename,
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.name} Banner`,
entityType: 'org',
entityId: cityId,
imageType: 'banner'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating city banner image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Banners'});
}
// Update city banner
db.run("UPDATE orgs SET bannerImageId = ? WHERE id = ? AND type = 'City'", [imageId, cityId], function(err) {
if (err) {
console.error('Error updating city banner:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Banners'});
}
res.json({
success: true,
imageId: imageId,
bannerUrl: `${BACKEND_URL}/api/images/${imageId}`,
message: 'Banner erfolgreich hochgeladen'
});
});
});
});
});
// Upload city logo
app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { cityId } = req.params;
// Check if city exists
db.get("SELECT name FROM orgs WHERE id = ? AND type = 'City'", [cityId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Stadt nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Create image record
const imageData = {
id: imageId,
filename: req.file.filename,
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.name} Logo`,
entityType: 'org',
entityId: cityId,
imageType: 'logo'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating city logo image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Logos'});
}
// Update city logo
db.run("UPDATE orgs SET logoImageId = ? WHERE id = ? AND type = 'City'", [imageId, cityId], function(err) {
if (err) {
console.error('Error updating city logo:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Logos'});
}
res.json({
success: true,
imageId: imageId,
logoUrl: `${BACKEND_URL}/api/images/${imageId}`,
message: 'Logo erfolgreich hochgeladen'
});
});
});
});
});
// Upload city gallery image
app.post('/api/admin/cities/:cityId/gallery/upload', upload.single('gallery'), async (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({error: 'Nicht authentifiziert'});
// Check if user is admin
if (!req.user.isAdmin) {
return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
}
const { cityId } = req.params;
// Check if city exists
db.get("SELECT name, gallery FROM orgs WHERE id = ? AND type = 'City'", [cityId], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Stadt nicht gefunden'});
if (!req.file) {
return res.status(400).json({error: 'Keine Datei hochgeladen'});
}
// Generate unique image ID
const imageId = 'img_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Create image record
const imageData = {
id: imageId,
filename: req.file.filename,
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size,
uploadDate: new Date().toISOString(),
altText: req.body.altText || `${row.name} Galerie`,
entityType: 'org',
entityId: cityId,
imageType: 'gallery'
};
db.run(`INSERT INTO images (id, filename, originalName, mimeType, size, uploadDate, altText, entityType, entityId, imageType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[imageData.id, imageData.filename, imageData.originalName, imageData.mimeType, imageData.size,
imageData.uploadDate, imageData.altText, imageData.entityType, imageData.entityId, imageData.imageType],
function(err) {
if (err) {
console.error('Error creating city gallery image record:', err);
return res.status(500).json({error: 'Fehler beim Speichern des Bildes'});
}
try {
// Add image ID to gallery array
const gallery = JSON.parse(row.gallery || '[]');
gallery.push(imageId);
db.run("UPDATE orgs SET gallery = ? WHERE id = ? AND type = 'City'", [JSON.stringify(gallery), cityId], function(err) {
if (err) {
console.error('Error updating city gallery:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren der Galerie'});
}
res.json({
success: true,
imageId: imageId,
imageUrl: `${BACKEND_URL}/api/images/${imageId}`,
gallery: gallery,
message: 'Bild erfolgreich zur Galerie hinzugefügt'
});
});
} catch (e) {
console.error('Error parsing city gallery:', e);
res.status(500).json({error: 'Fehler beim Verarbeiten der Galerie-Daten'});
}
});
});
});
app.post('/api/data', (req, res) => { const { eventType, player, uuid, timestamp, char, statistics, advancements } = req.body;
if (!uuid || !player) {
return res.status(400).json({ error: 'UUID und Player-Name erforderlich' });
}
// Prepare player data
const playerData = {
uuid,
username: player,
isOnline: eventType === 'JOIN' ? 1 : 0,
isNpc: 0,
isAdmin: 0,
tags: JSON.stringify([]),
stats: JSON.stringify({}),
minecraftStats: JSON.stringify({
char,
statistics,
advancements,
lastSync: timestamp
}),
inventory: JSON.stringify([]),
storyMarkdown: '',
discordId: null
};
// Check if player exists
db.get("SELECT uuid FROM players WHERE uuid = ?", [uuid], (err, row) => {
if (err) {
console.error('Error checking player existence:', err);
return res.status(500).json({ error: 'Datenbankfehler beim Überprüfen des Spielers' });
}
if (row) {
// Update existing player
db.run(`UPDATE players SET username = ?, isOnline = ?, minecraftStats = ? WHERE uuid = ?`,
[playerData.username, playerData.isOnline, playerData.minecraftStats, uuid],
function(err) {
if (err) {
console.error('Error updating player:', err);
return res.status(500).json({ error: 'Fehler beim Aktualisieren des Spielers' });
}
res.json({
success: true,
message: 'Spieler erfolgreich aktualisiert',
action: 'updated',
uuid
});
}
);
} else {
// Insert new player
db.run(`INSERT INTO players (uuid, username, isOnline, isNpc, isAdmin, tags, stats, minecraftStats, inventory, storyMarkdown, discordId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[playerData.uuid, playerData.username, playerData.isOnline, playerData.isNpc, playerData.isAdmin,
playerData.tags, playerData.stats, playerData.minecraftStats, playerData.inventory, playerData.storyMarkdown, playerData.discordId],
function(err) {
if (err) {
console.error('Error inserting player:', err);
return res.status(500).json({ error: 'Fehler beim Erstellen des Spielers' });
}
res.json({
success: true,
message: 'Spieler erfolgreich erstellt',
action: 'created',
uuid
});
}
);
}
});
});
// Helper function to convert images to WebP
function convertToWebP(inputPath, outputPath) {
return new Promise((resolve, reject) => {
const sharp = require('sharp');
sharp(inputPath)
.webp({ quality: 80 })
.toFile(outputPath)
.then(info => {
resolve(info.size);
})
.catch(reject);
});
}
// Initialize Map Processor
const mapProcessor = new MapProcessor();
mapProcessor.init();
// Debug console interface for map processing
// Available when running as standalone script or when required as module
const debugInterface = {
assembleMap: async () => {
try {
console.log('🔄 Starting map assembly...');
const success = await mapProcessor.assembleWorldMap();
if (success) {
console.log('✅ Map assembly completed successfully');
} else {
console.log('❌ Map assembly failed');
}
} catch (error) {
console.error('❌ Error during map assembly:', error.message);
}
},
getTiles: async () => {
try {
const tiles = await mapProcessor.getMapTiles();
console.log(`📋 Found ${tiles.length} map tiles:`);
tiles.forEach((tile, index) => {
if (index < 5) { // Show first 5 tiles
console.log(` ${tile.filename}: X=${tile.x}, Z=${tile.z}`);
}
});
if (tiles.length > 5) {
console.log(` ... and ${tiles.length - 5} more tiles`);
}
} catch (error) {
console.error('❌ Error getting tiles:', error.message);
}
},
getMetadata: async () => {
try {
const metadata = await mapProcessor.getMapMetadata();
console.log('📊 Map Metadata:');
console.log(` Width: ${metadata.width}px`);
console.log(` Height: ${metadata.height}px`);
console.log(` Offset X: ${metadata.offsetX}`);
console.log(` Offset Z: ${metadata.offsetZ}`);
console.log(` Tile Size: ${metadata.tileSize}px`);
console.log(` Last Updated: ${metadata.lastUpdated || 'Never'}`);
} catch (error) {
console.error('❌ Error getting metadata:', error.message);
}
},
testCoords: async (x, z) => {
try {
const coords = await mapProcessor.getCoordinateConversion(x, z);
console.log(`📍 Coordinate Conversion for (${x}, ${z}):`);
console.log(` Pixel X: ${coords.pixelX}`);
console.log(` Pixel Y: ${coords.pixelY}`);
console.log(` Percentage X: ${coords.x.toFixed(2)}%`);
console.log(` Percentage Y: ${coords.y.toFixed(2)}%`);
} catch (error) {
console.error('❌ Error converting coordinates:', error.message);
}
}
};
// Make debug interface available globally
global.mapProcessor = mapProcessor;
global.assembleMap = debugInterface.assembleMap;
global.getTiles = debugInterface.getTiles;
global.getMetadata = debugInterface.getMetadata;
global.testCoords = debugInterface.testCoords;
// Export debug interface for module usage
module.exports = debugInterface;
// If running as standalone script, show console interface
if (require.main === module) {
console.log('🔧 Map Processor Debug Console');
console.log('Available commands:');
console.log(' assembleMap() - Assemble world map');
console.log(' getTiles() - Get all map tiles');
console.log(' getMetadata() - Get map metadata');
console.log(' testCoords(x, z) - Test coordinate conversion');
console.log('');
console.log('💡 Usage examples:');
console.log(' node -e "require(\'./server.js\').assembleMap()"');
console.log(' node -e "require(\'./server.js\').getTiles()"');
console.log(' node -e "require(\'./server.js\').testCoords(0, 0)"');
console.log('');
console.log('🚀 Starting debug session...');
}
// === MAP API ===
// Get world map metadata
app.get('/api/map/metadata', (req, res) => {
mapProcessor.getMapMetadata()
.then(metadata => {
res.json({
width: parseInt(metadata.width) || 0,
height: parseInt(metadata.height) || 0,
offsetX: parseInt(metadata.offsetX) || 0,
offsetZ: parseInt(metadata.offsetZ) || 0,
tileSize: parseInt(metadata.tileSize) || 1024,
lastUpdated: metadata.lastUpdated || null
});
})
.catch(err => {
console.error('Error getting map metadata:', err);
res.status(500).json({error: 'Fehler beim Laden der Karten-Metadaten'});
});
});
// Get assembled world map
app.get('/api/map/world-map', (req, res) => {
const worldMapPath = path.join(__dirname, 'uploads', 'processed', 'world-map.png');
if (fs.existsSync(worldMapPath)) {
res.sendFile(worldMapPath);
} else {
res.status(404).json({error: 'Weltkarte nicht gefunden. Bitte zuerst zusammenstellen.'});
}
});
// Assemble world map from tiles
app.post('/api/map/assemble', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
console.log('🔄 Starting map assembly process...');
// Check if database is ready before assembling
db.get("SELECT 1", [], (err) => {
if (err) {
console.error('❌ Database not ready for map assembly:', err);
return res.status(500).json({
error: 'Datenbank nicht bereit für Karten-Zusammenstellung',
details: err.message,
stack: err.stack
});
}
console.log('✅ Database connection verified');
mapProcessor.assembleWorldMap()
.then(success => {
if (success) {
console.log('✅ Map assembly completed successfully');
res.json({success: true, message: 'Weltkarte erfolgreich zusammengestellt'});
} else {
console.error('❌ Map assembly failed - unknown error');
res.status(500).json({
error: 'Karten-Zusammenstellung fehlgeschlagen',
details: 'Unbekannter Fehler bei der Karten-Zusammenstellung'
});
}
})
.catch(err => {
console.error('❌ Error assembling world map:', err);
res.status(500).json({
error: 'Fehler beim Zusammensetzen der Weltkarte',
details: err.message,
stack: err.stack,
type: err.constructor.name
});
});
});
});
// Get all markers with coordinates
app.get('/api/map/markers', (req, res) => {
mapProcessor.getMarkersWithCoordinates()
.then(markers => {
res.json(markers);
})
.catch(err => {
console.error('Error getting markers:', err);
res.status(500).json({error: 'Fehler beim Laden der Marker'});
});
});
// Get public markers only
app.get('/api/map/markers/public', (req, res) => {
db.all('SELECT * FROM map_markers WHERE is_public = 1', [], async (err, markers) => {
if (err) {
console.error('Error getting public markers:', err);
return res.status(500).json({error: 'Fehler beim Laden der öffentlichen Marker'});
}
const markersWithCoords = await Promise.all(markers.map(async (marker) => {
const coords = await mapProcessor.getCoordinateConversion(marker.x_coord, marker.z_coord);
return {
...marker,
coordinates: coords
};
}));
res.json(markersWithCoords);
});
});
// Create new marker (Admin only)
app.post('/api/map/markers', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
const { name, type, x_coord, z_coord, description, linked_entity_type, linked_entity_id, icon_type, color } = req.body;
if (!name || x_coord === undefined || z_coord === undefined) {
return res.status(400).json({error: 'Name und Koordinaten sind erforderlich'});
}
// Generate unique ID
const markerId = 'marker_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const markerData = {
id: markerId,
name: name.trim(),
type: type || 'poi',
x_coord: parseInt(x_coord),
z_coord: parseInt(z_coord),
description: description || '',
linked_entity_type: linked_entity_type || null,
linked_entity_id: linked_entity_id || null,
icon_type: icon_type || 'flag',
color: color || '#2563eb',
is_public: 1
};
db.run(`INSERT INTO map_markers (id, name, type, x_coord, z_coord, description, linked_entity_type, linked_entity_id, icon_type, color, is_public)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[markerData.id, markerData.name, markerData.type, markerData.x_coord, markerData.z_coord,
markerData.description, markerData.linked_entity_type, markerData.linked_entity_id,
markerData.icon_type, markerData.color, markerData.is_public],
function(err) {
if (err) {
console.error('Error creating marker:', err);
return res.status(500).json({error: 'Fehler beim Erstellen des Markers'});
}
res.json({
success: true,
markerId: markerId,
message: 'Marker erfolgreich erstellt'
});
}
);
});
// Update marker (Admin only)
app.put('/api/map/markers/:markerId', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
const { markerId } = req.params;
const updates = req.body;
// Build dynamic update query
const allowedFields = ['name', 'type', 'x_coord', 'z_coord', 'description', 'linked_entity_type', 'linked_entity_id', 'icon_type', 'color', 'is_public'];
const updateFields = [];
const values = [];
for (const field of allowedFields) {
if (updates[field] !== undefined) {
updateFields.push(`${field} = ?`);
values.push(field.includes('_coord') ? parseInt(updates[field]) : updates[field]);
}
}
if (updateFields.length === 0) {
return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'});
}
const query = `UPDATE map_markers SET ${updateFields.join(', ')} WHERE id = ?`;
values.push(markerId);
db.run(query, values, function(err) {
if (err) {
console.error('Error updating marker:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Markers'});
}
if (this.changes === 0) {
return res.status(404).json({error: 'Marker nicht gefunden'});
}
res.json({success: true, message: 'Marker erfolgreich aktualisiert'});
});
});
// Delete marker (Admin only)
app.delete('/api/map/markers/:markerId', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
const { markerId } = req.params;
db.run("DELETE FROM map_markers WHERE id = ?", [markerId], function(err) {
if (err) {
console.error('Error deleting marker:', err);
return res.status(500).json({error: 'Fehler beim Löschen des Markers'});
}
if (this.changes === 0) {
return res.status(404).json({error: 'Marker nicht gefunden'});
}
res.json({success: true, message: 'Marker erfolgreich gelöscht'});
});
});
// Get map layers
app.get('/api/map/layers', (req, res) => {
db.all('SELECT * FROM map_layers ORDER BY order_index', [], (err, rows) => {
if (err) {
console.error('Error getting map layers:', err);
return res.status(500).json({error: 'Fehler beim Laden der Karten-Layer'});
}
res.json(rows);
});
});
// Update map layer visibility/order (Admin only)
app.put('/api/map/layers/:layerId', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
const { layerId } = req.params;
const { is_active, order_index } = req.body;
const updateFields = [];
const values = [];
if (is_active !== undefined) {
updateFields.push('is_active = ?');
values.push(is_active ? 1 : 0);
}
if (order_index !== undefined) {
updateFields.push('order_index = ?');
values.push(parseInt(order_index));
}
if (updateFields.length === 0) {
return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'});
}
const query = `UPDATE map_layers SET ${updateFields.join(', ')} WHERE id = ?`;
values.push(layerId);
db.run(query, values, function(err) {
if (err) {
console.error('Error updating map layer:', err);
return res.status(500).json({error: 'Fehler beim Aktualisieren des Layers'});
}
if (this.changes === 0) {
return res.status(404).json({error: 'Layer nicht gefunden'});
}
res.json({success: true, message: 'Layer erfolgreich aktualisiert'});
});
});
// Coordinate conversion endpoint
app.get('/api/map/convert-coords', (req, res) => {
const { x, z } = req.query;
if (x === undefined || z === undefined) {
return res.status(400).json({error: 'X und Z Koordinaten sind erforderlich'});
}
mapProcessor.getCoordinateConversion(parseInt(x), parseInt(z))
.then(coords => {
res.json(coords);
})
.catch(err => {
console.error('Error converting coordinates:', err);
res.status(500).json({error: 'Fehler bei der Koordinaten-Konvertierung'});
});
});
// Serve uploaded files statically
app.use('/uploads', express.static(UPLOAD_DIR));
app.listen(PORT, () => {
console.log(`Backend running on http://localhost:${PORT}`);
});