feat: Add DatabaseManager and LinkPlayer components, implement authentication and linking logic

- Created DatabaseManager component for managing database access via phpMyAdmin.
- Developed LinkPlayer component to link Discord accounts with game characters, including user authentication and error handling.
- Added mock data files for players, organizations, and projects to handle backend unavailability.
- Implemented AuthService for managing user authentication and session checks.
- Created DatabaseService to fetch and manage player, organization, and project data with fallback to mock data.
- Added HTML page for handling authentication unavailability.
- Developed a test script for validating Docker setup and required files.
This commit is contained in:
Lars Behrends
2025-12-28 16:46:04 +01:00
parent 6abdffe22a
commit d3d7ec46e6
40 changed files with 5967 additions and 102 deletions

226
backend/database.js Normal file
View File

@@ -0,0 +1,226 @@
const mysql = require('mysql2');
// Database Config from Env
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASS || '',
database: process.env.DB_NAME || 'obsidian_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
};
let pool;
// Mock Data for Seeding
const SEED_PLAYERS = [
{
uuid: '80301bff-74df-4579-bcfc-082ac8d26b5b',
username: 'kaiwastoshort',
isOnline: 1,
isNpc: 0,
isAdmin: 0,
tags: JSON.stringify(['#Bürger', '#Händler']),
stats: JSON.stringify({ playtimeHours: 482, level: 45, role: 'Bürger', organizationId: 'org-3' }),
inventory: JSON.stringify([]),
storyMarkdown: '# Der Bauplan von V\n\n> "Stein erinnert sich..."'
},
{
uuid: '8984c0b5-d912-4462-b189-c864fba4a1af',
username: 'DrKButz',
isOnline: 0,
isNpc: 0,
isAdmin: 1, // DrKButz is admin for testing
tags: JSON.stringify(['#Bauunternehmer']),
stats: JSON.stringify({ playtimeHours: 120, level: 12, role: 'Unternehmer', organizationId: 'org-4' }),
inventory: JSON.stringify([]),
storyMarkdown: '# Forschungslogbuch:\n\nSpezialisiert auf...'
}
];
const SEED_ORGS = [
{
id: 'org-3',
name: 'Provisorium Null',
type: 'City',
description: 'Die erste Siedlung...',
memberCount: 6,
status: 'active',
mayor: '',
establishedYear: 'Day 0',
bannerUrl: 'images/screenshots/2025-12-28_01.11.10.png',
gallery: JSON.stringify(['images/screenshots/2025-12-28_01.12.07.png']),
cityStats: JSON.stringify({ taxRate: 3.5, biome: 'Ebene', defenseRating: 8, government: 'Feudal', specialty: 'Handel' })
},
{
id: 'org-4',
name: 'Sakura',
type: 'City',
description: 'Eine dunkle, biolumineszente Hafenstadt...',
memberCount: 2,
status: 'active',
mayor: 'Kampfzwerk',
establishedYear: 'Ära 2',
bannerUrl: 'images/screenshots/2025-12-28_01.11.32.png',
gallery: JSON.stringify([]),
cityStats: JSON.stringify({ taxRate: 15.0, biome: 'Deep Dark', defenseRating: 4, government: 'Syndikat', specialty: 'Schmuggel' })
}
];
const SEED_PROJECTS = [
{
id: 'ven-1',
title: 'DrkButz Architektur',
description: 'Führendes Architekturbüro...',
category: 'Enterprise',
status: 'active',
progress: 85,
owner: 'DrKButz',
employees: JSON.stringify([]),
hiring: 0,
foundedDate: 'Zyklus 12',
associatedOrgId: 'org-4',
bannerUrl: 'images/screenshots/2025-12-28_01.11.49.png',
shopCatalog: JSON.stringify([])
}
];
// Retry connection logic for Docker
function init() {
const tryConnect = () => {
console.log("Attempting to connect to MySQL...");
const tempPool = mysql.createPool(dbConfig);
tempPool.getConnection((err, connection) => {
if (err) {
console.error("Database connection failed. Retrying in 5s...", err.code);
setTimeout(tryConnect, 5000);
} else {
console.log("MySQL Connected!");
pool = tempPool;
connection.release();
setupTables();
}
});
};
tryConnect();
}
function setupTables() {
const queries = [
`CREATE TABLE IF NOT EXISTS players (
uuid VARCHAR(36) PRIMARY KEY,
username VARCHAR(255),
isOnline TINYINT,
isNpc TINYINT DEFAULT 0,
isAdmin TINYINT DEFAULT 0,
tags JSON,
stats JSON,
inventory JSON,
storyMarkdown TEXT,
discordId VARCHAR(255)
)`,
`CREATE TABLE IF NOT EXISTS orgs (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255),
type VARCHAR(50),
description TEXT,
memberCount INTEGER,
status VARCHAR(50),
mayor VARCHAR(255),
establishedYear VARCHAR(100),
bannerUrl VARCHAR(255),
gallery JSON,
cityStats JSON
)`,
`CREATE TABLE IF NOT EXISTS projects (
id VARCHAR(50) PRIMARY KEY,
title VARCHAR(255),
description TEXT,
category VARCHAR(50),
status VARCHAR(50),
progress INTEGER,
owner VARCHAR(255),
employees JSON,
hiring TINYINT,
foundedDate VARCHAR(100),
associatedOrgId VARCHAR(50),
bannerUrl VARCHAR(255),
shopCatalog JSON,
gallery JSON
)`
];
queries.forEach(q => {
pool.query(q, (err) => {
if (err) console.error("Error creating table:", err);
});
});
seedData();
}
function seedData() {
// Check if players exist
pool.query("SELECT COUNT(*) as count FROM players", (err, rows) => {
if (!err && rows[0].count === 0) {
console.log("Seeding Players...");
SEED_PLAYERS.forEach(p => {
pool.query("INSERT INTO players VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[p.uuid, p.username, p.isOnline, p.isNpc, p.isAdmin, p.tags, p.stats, p.inventory, p.storyMarkdown, null]);
});
}
});
pool.query("SELECT COUNT(*) as count FROM orgs", (err, rows) => {
if (!err && rows[0].count === 0) {
console.log("Seeding Orgs...");
SEED_ORGS.forEach(o => {
pool.query("INSERT INTO orgs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[o.id, o.name, o.type, o.description, o.memberCount, o.status, o.mayor, o.establishedYear, o.bannerUrl, o.gallery, o.cityStats]);
});
}
});
pool.query("SELECT COUNT(*) as count FROM projects", (err, rows) => {
if (!err && rows[0].count === 0) {
console.log("Seeding Projects...");
SEED_PROJECTS.forEach(p => {
pool.query("INSERT INTO projects VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[p.id, p.title, p.description, p.category, p.status, p.progress, p.owner, p.employees, p.hiring, p.foundedDate, p.associatedOrgId, p.bannerUrl, p.shopCatalog, '[]']);
});
}
});
}
// Wrapper to mimic SQLite API for easy migration in server.js
const dbWrapper = {
get: (sql, params, cb) => {
if (!pool) return cb(new Error("DB not ready"));
pool.query(sql, params, (err, rows) => {
if (err) return cb(err);
cb(null, rows[0]);
});
},
all: (sql, params, cb) => {
// Handle case where params is omitted and cb is second parameter
if (typeof params === 'function') {
cb = params;
params = [];
}
if (!pool) return cb(new Error("DB not ready"));
pool.query(sql, params || [], (err, rows) => {
cb(err, rows);
});
},
run: (sql, params, cb) => {
if (!pool) return cb(new Error("DB not ready"));
pool.query(sql, params, function(err, result) {
// "this" context in sqlite callback contains changes, mock it if needed
if (cb) cb(err);
});
}
};
module.exports = { db: dbWrapper, init };

17
backend/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "obsidian-backend",
"version": "1.0.0",
"description": "Backend for Obsidian RP Platform",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"express-session": "^1.17.3",
"passport": "^0.6.0",
"passport-discord": "^0.1.4",
"cors": "^2.8.5",
"mysql2": "^3.6.0"
}
}

923
backend/server.js Normal file
View File

@@ -0,0 +1,923 @@
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const DiscordStrategy = require('passport-discord').Strategy;
const cors = require('cors');
const { db, init } = require('./database');
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';
init(); // Initialize DB
// Middleware
app.use(express.json());
app.use(cors({ origin: FRONTEND_URL, credentials: true }));
app.use(session({
secret: process.env.SESSION_SECRET || 'dev_secret',
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set true if using https
}));
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', passport.authenticate('discord'));
app.get('/auth/discord/callback', passport.authenticate('discord', {
failureRedirect: FRONTEND_URL + '?error=login_failed'
}), (req, res) => {
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),
inventory: JSON.parse(r.inventory),
isOnline: !!r.isOnline
}));
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'];
const updateFields = [];
const values = [];
// Handle organizationId specially - it goes into stats JSON
if (updates.organizationId !== undefined) {
// First get current player data
db.get("SELECT stats FROM players WHERE uuid = ?", [req.params.uuid], (err, row) => {
if (err) return res.status(500).json({error: err.message});
if (!row) return res.status(404).json({error: 'Spieler nicht gefunden'});
try {
const currentStats = JSON.parse(row.stats || '{}');
const updatedStats = { ...currentStats, organizationId: updates.organizationId };
updateFields.push('stats = ?');
values.push(JSON.stringify(updatedStats));
} catch (e) {
return res.status(500).json({error: 'Fehler beim Verarbeiten der Stats'});
}
// Continue with other 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'});
});
});
return; // Exit early since we're handling this asynchronously
}
// Handle other fields normally
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),
cityStats: r.cityStats ? JSON.parse(r.cityStats) : undefined
}));
res.json(parsed);
});
});
// 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: JSON.parse(r.gallery),
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, bannerUrl, shopCatalog, gallery)
VALUES (?, ?, ?, ?, 'active', 0, ?, '[]', 0, ?, ?, '', '[]', '[]')`,
[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
}),
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, inventory, storyMarkdown, discordId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[npcData.uuid, npcData.username, npcData.isOnline, npcData.isNpc, npcData.tags,
npcData.stats, 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, bannerUrl, shopCatalog, gallery)
VALUES (?, ?, ?, ?, 'active', ?, ?, '[]', 0, ?, ?, '', ?, '[]')`,
[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'
});
}
);
});
// 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/:imageIndex', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).send();
const { projectId, imageIndex } = req.params;
const index = parseInt(imageIndex);
// 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 || '[]');
if (index < 0 || index >= gallery.length) {
return res.status(404).json({error: 'Bild nicht gefunden'});
}
gallery.splice(index, 1);
db.run("UPDATE projects SET gallery = ? WHERE id = ?", [JSON.stringify(gallery), projectId], function(err) {
if (err) {
console.error('Error deleting gallery image:', err);
return res.status(500).json({error: 'Fehler beim Löschen'});
}
res.json({success: true, gallery});
});
} catch (e) {
res.status(500).json({error: 'Fehler beim Verarbeiten der Daten'});
}
});
});
app.listen(PORT, () => {
console.log(`Backend running on http://localhost:${PORT}`);
});