diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d7d45c7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +# .dockerignore +node_modules +database + +*/node_modules* \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..ea7fcd9 --- /dev/null +++ b/.env @@ -0,0 +1,14 @@ +# Discord OAuth Configuration +DISCORD_CLIENT_ID=1454649755513655491 +DISCORD_CLIENT_SECRET=TqGBxbyE3NBoJCp1riuu2eJe6Y_0zwtu + +# Session Security +SESSION_SECRET=dhu2rb9gt82vrn9th2847t2nv45t8v29n4g5tu4gtib + +# Note: Replace the placeholder values above with your actual Discord application credentials +# Get these from: https://discord.com/developers/applications + +DB_HOST=db +DB_USER=obsidian_user +DB_PASS=obsidian_pass +DB_NAME=obsidian_db \ No newline at end of file diff --git a/App.tsx b/App.tsx index c394cd4..750045b 100644 --- a/App.tsx +++ b/App.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import Layout from './components/Layout'; import Dashboard from './pages/Dashboard'; import PlayerProfile from './pages/PlayerProfile'; @@ -9,7 +9,12 @@ import Cities from './pages/Cities'; import CityProfile from './pages/CityProfile'; import ProjectProfile from './pages/ProjectProfile'; import DatapackGenerator from './pages/DatapackGenerator'; -import { MOCK_PLAYERS, MOCK_ORGS, MOCK_PROJECTS } from './constants'; +import DatabaseManager from './pages/DatabaseManager'; // Ensure this file exists or remove import if not +import LinkPlayer from './pages/LinkPlayer'; +import AdminPage from './pages/Admin'; +import { dbService } from './services/DatabaseService'; +import { authService } from './services/AuthService'; +import { DiscordUser } from './types'; import { Icons } from './components/IconSet'; function App() { @@ -19,6 +24,44 @@ function App() { const [selectedProjectId, setSelectedProjectId] = useState(null); const [selectedOrgId, setSelectedOrgId] = useState(null); + // Auth state + const [user, setUser] = useState(null); + const [authChecked, setAuthChecked] = useState(false); + + // State for data from DB (Async) + const [players, setPlayers] = useState(dbService.getPlayers()); + const [orgs, setOrgs] = useState(dbService.getOrgs()); + const [projects, setProjects] = useState(dbService.getProjects()); + + // Subscribe to DB updates (when fetch completes or edits happen) + useEffect(() => { + const unsub = dbService.subscribe(() => { + setPlayers([...dbService.getPlayers()]); + setOrgs([...dbService.getOrgs()]); + setProjects([...dbService.getProjects()]); + }); + return unsub; + }, []); + + // Subscribe to auth updates + useEffect(() => { + const unsub = authService.subscribe((currentUser) => { + setUser(currentUser); + setAuthChecked(true); + + // If user is logged in but not linked, redirect to link page + if (currentUser && !currentUser.linkedPlayerUuid) { + setActiveTab('link-player'); + } else if (currentUser && currentUser.linkedPlayerUuid) { + // User is fully authenticated, redirect to dashboard if on link page + if (activeTab === 'link-player') { + setActiveTab('dashboard'); + } + } + }); + return unsub; + }, [activeTab]); + const handleNavigate = (tab: string) => { setActiveTab(tab); if (tab !== 'players') setSelectedPlayerId(null); @@ -27,21 +70,18 @@ function App() { if (tab !== 'organizations') setSelectedOrgId(null); }; - // Helper to jump to a player from another view const navigateToPlayer = (id: string) => { setSelectedPlayerId(id); setActiveTab('players'); }; - // Helper to jump to a project from another view const navigateToProject = (id: string) => { setSelectedProjectId(id); setActiveTab('projects'); }; - // Helper to jump to an org/city const navigateToOrg = (id: string) => { - const org = MOCK_ORGS.find(o => o.id === id); + const org = orgs.find(o => o.id === id); if (org?.type === 'City') { setSelectedCityId(id); setActiveTab('cities'); @@ -52,11 +92,16 @@ function App() { }; const renderContent = () => { + // Show link player page if user is logged in but not linked + if (activeTab === 'link-player') { + return ; + } + if (activeTab === 'dashboard') return ; - + if (activeTab === 'projects') { if (selectedProjectId) { - const project = MOCK_PROJECTS.find(p => p.id === selectedProjectId); + const project = projects.find(p => p.id === selectedProjectId); if (project) return ( ); } - return ; + return ; } if (activeTab === 'organizations') { if (selectedOrgId) { - const org = MOCK_ORGS.find(o => o.id === selectedOrgId); + const org = orgs.find(o => o.id === selectedOrgId); if (org) return ( ; if (activeTab === 'datapack') return ; - + if (activeTab === 'cities') { if (selectedCityId) { - const city = MOCK_ORGS.find(o => o.id === selectedCityId); + const city = orgs.find(o => o.id === selectedCityId); if (city) return ( p.uuid === selectedPlayerId); + const player = players.find(p => p.uuid === selectedPlayerId); if (player) return setSelectedPlayerId(null)} />; } @@ -126,7 +171,7 @@ function App() {
- {MOCK_PLAYERS.map(player => ( + {players.map(player => (
setSelectedPlayerId(player.uuid)} @@ -149,6 +194,16 @@ function App() {
); } + + // Admin panel (only for admins) + if (activeTab === 'admin') { + return setActiveTab('dashboard')} />; + } + + // Placeholder for database manager if user navigates there (needs explicit page or component) + if (activeTab === 'database') { + return
Datenbank wird jetzt über das Backend (SQLite) verwaltet.
+ } return (
@@ -165,4 +220,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/DOCKER-SETUP.md b/DOCKER-SETUP.md new file mode 100644 index 0000000..7efe68c --- /dev/null +++ b/DOCKER-SETUP.md @@ -0,0 +1,210 @@ +# Docker Setup for Vollidioten Project + +This document describes the Docker setup for the Vollidioten project, including frontend, backend, and database services with Traefik integration. + +## 🚀 Quick Start + +1. **Create Traefik network** (if not already exists): + ```bash + docker network create traefik_network + ``` + +2. **Set up environment variables**: + Create a `.env` file with the following variables: + ```env + DISCORD_CLIENT_ID=your_discord_client_id + DISCORD_CLIENT_SECRET=your_discord_client_secret + SESSION_SECRET=your_session_secret + ``` + +3. **Build and start the containers**: + ```bash + docker-compose up -d --build + ``` + +4. **Access the application**: + - Frontend: `https://vollidioten.ceraticsoft.de` + - phpMyAdmin: `http://localhost:8081` + +## 📁 Project Structure + +``` +. +├── Dockerfile.frontend # Frontend Dockerfile +├── Dockerfile.backend # Backend Dockerfile +├── docker-compose.yml # Docker Compose configuration +├── nginx.conf # Nginx configuration with fallback +├── public/ +│ ├── mock/ # Mock data files +│ │ ├── players.json # Mock player data +│ │ ├── orgs.json # Mock organization data +│ │ └── projects.json # Mock project data +│ └── auth-unavailable.html # Auth unavailable page +└── test-docker-setup.sh # Test script +``` + +## 🔧 Services + +### Frontend +- **Image**: Custom built from `Dockerfile.frontend` +- **Port**: 80 (internal, exposed via Traefik) +- **Traefik Configuration**: + - Host: `vollidioten.ceraticsoft.de` + - EntryPoint: `websecure` (HTTPS) + - TLS: Let's Encrypt +- **Features**: + - Automatic fallback to mock data when backend is unavailable + - Nginx proxy with error handling + - Static file serving with caching + +### Backend +- **Image**: Custom built from `Dockerfile.backend` +- **Port**: 3000 (internal only) +- **Environment Variables**: + - `DISCORD_CLIENT_ID`: Discord OAuth client ID + - `DISCORD_CLIENT_SECRET`: Discord OAuth client secret + - `SESSION_SECRET`: Session encryption secret + - `CALLBACK_URL`: OAuth callback URL + - `FRONTEND_URL`: Frontend base URL + - `DB_*`: Database connection details + +### Database (MySQL) +- **Image**: `mysql:8.0` +- **Port**: 3306 (internal only) +- **Credentials**: + - User: `obsidian_user` + - Password: `obsidian_pass` + - Database: `obsidian_db` +- **Persistent Storage**: Docker volume (`db_data`) + +### phpMyAdmin +- **Image**: `phpmyadmin/phpmyadmin` +- **Port**: 8081 (localhost only) +- **Access**: `http://localhost:8081` + +## 🛡️ Fallback Mechanism + +The setup includes a robust fallback mechanism: + +1. **Nginx Proxy Fallback**: + - When the backend is unavailable (502/503/504 errors), nginx automatically serves mock data + - Mock data is served from `/public/mock/` directory + - Response headers indicate mock data usage: + - `X-Mock-Data: true` + - `X-Backend-Status: unavailable` + +2. **Frontend Detection**: + - The frontend checks for the `X-Mock-Data` header + - Console warnings indicate when mock data is being used + - Users see appropriate messages in the UI + +3. **Auth Handling**: + - Auth endpoints fail gracefully when backend is down + - Users see a friendly error page (`auth-unavailable.html`) + - Application remains usable in read-only mode + +## 🔄 Traefik Integration + +The frontend service is configured with Traefik labels: + +```yaml +labels: + - "traefik.enable=true" + - "traefik.http.routers.vollidioten.rule=Host(`vollidioten.ceraticsoft.de`)" + - "traefik.http.routers.vollidioten.entrypoints=websecure" + - "traefik.http.routers.vollidioten.tls.certresolver=lets-encrypt" + - "traefik.http.services.vollidioten.loadbalancer.server.port=80" +``` + +## 🧪 Testing + +Run the test script to verify the setup: + +```bash +./test-docker-setup.sh +``` + +The script checks: +- Docker is running +- Traefik network exists +- All required files are present +- Docker containers can be built + +## 📝 Environment Variables + +Create a `.env` file in the project root: + +```env +# Discord OAuth +DISCORD_CLIENT_ID=your_client_id +DISCORD_CLIENT_SECRET=your_client_secret + +# Session +SESSION_SECRET=your_strong_secret_here +``` + +## 🔧 Customization + +### Adding More Mock Data +Add JSON files to the `public/mock/` directory following the same structure as existing files. + +### Updating Nginx Configuration +Edit `nginx.conf` to modify: +- Proxy settings +- Cache headers +- Error handling +- Fallback behavior + +### Scaling +The setup supports horizontal scaling: +- Frontend: Can be scaled to multiple instances +- Backend: Can be scaled with proper session handling +- Database: Use external managed database for production + +## 🚨 Troubleshooting + +**Docker Network Issues**: +```bash +docker network create traefik_network +``` + +**Permission Issues**: +```bash +chmod +x test-docker-setup.sh +``` + +**Port Conflicts**: +- Ensure port 8081 is available for phpMyAdmin +- Check Traefik is running and listening on port 443 + +**SSL Issues**: +- Ensure Traefik is configured with Let's Encrypt +- Check DNS records for `vollidioten.ceraticsoft.de` + +## 🎯 Production Notes + +1. **Security**: + - Use strong secrets for all environment variables + - Configure proper CORS settings + - Set up database backups + +2. **Performance**: + - Configure proper caching headers + - Optimize database queries + - Use CDN for static assets + +3. **Monitoring**: + - Set up health checks + - Configure logging + - Monitor backend availability + +4. **Updates**: + - Regularly update Docker images + - Test updates in staging first + - Monitor after deployments + +## 📚 References + +- [Traefik Documentation](https://doc.traefik.io/traefik/) +- [Docker Compose Reference](https://docs.docker.com/compose/) +- [Nginx Configuration Guide](https://nginx.org/en/docs/) diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..86fdbb7 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,20 @@ +# Backend Dockerfile +FROM node:20-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY backend/package.json . + +# Install dependencies +RUN npm install + +# Copy source files +COPY backend/ /app/ + +# Expose port +EXPOSE 3000 + +# Start the application +CMD ["node", "server.js"] diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..0b46a37 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,33 @@ +# Frontend Dockerfile +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json . + +# Install dependencies +RUN npm install + +# Copy source files +COPY . . +COPY public /app/public + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built files from builder +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/backend/database.js b/backend/database.js new file mode 100644 index 0000000..b879af9 --- /dev/null +++ b/backend/database.js @@ -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 }; diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..4b7a11c --- /dev/null +++ b/backend/package.json @@ -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" + } +} \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..ca1c108 --- /dev/null +++ b/backend/server.js @@ -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}`); +}); diff --git a/components/BannerManagementModal.tsx b/components/BannerManagementModal.tsx new file mode 100644 index 0000000..75edfad --- /dev/null +++ b/components/BannerManagementModal.tsx @@ -0,0 +1,230 @@ +import React, { useState, useEffect } from 'react'; +import { Icons } from './IconSet'; + +interface BannerManagementModalProps { + isOpen: boolean; + onClose: () => void; + projectId: string; + currentBannerUrl: string; + onUpdate: () => void; +} + +const BannerManagementModal: React.FC = ({ + isOpen, + onClose, + projectId, + currentBannerUrl, + onUpdate +}) => { + const [bannerUrl, setBannerUrl] = useState(currentBannerUrl); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [previewLoading, setPreviewLoading] = useState(false); + + useEffect(() => { + if (isOpen) { + setBannerUrl(currentBannerUrl); + setError(null); + } + }, [isOpen, currentBannerUrl]); + + const updateBanner = async () => { + if (!bannerUrl.trim()) { + setError('Banner-URL ist erforderlich'); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ bannerUrl: bannerUrl.trim() }) + }); + + if (response.ok) { + onUpdate(); + onClose(); + } else { + const errorData = await response.json(); + setError(errorData.error || 'Fehler beim Aktualisieren des Banners'); + } + } catch (err) { + console.error('Error updating banner:', err); + setError('Netzwerkfehler'); + } finally { + setLoading(false); + } + }; + + const handleImageLoad = () => { + setPreviewLoading(false); + }; + + const handleImageError = () => { + setPreviewLoading(false); + setError('Bild konnte nicht geladen werden'); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ + Banner bearbeiten +

+ +
+ +
+ {error && ( +
+

{error}

+
+ )} + + {/* Current Banner Preview */} +
+

Aktuelles Banner

+
+ {currentBannerUrl ? ( + <> + {previewLoading && ( +
+
+
+ )} + Current banner + + ) : ( +
+ +
+ )} +
+
+ + {/* Banner URL Input */} +
+

Neue Banner-URL

+ setBannerUrl(e.target.value)} + placeholder="https://example.com/banner-image.jpg" + className="w-full bg-[#0b0b0d] border border-border rounded p-3 text-sm" + /> +

+ Geben Sie eine direkte URL zu einem Bild ein. Empfohlene Größe: 1200x400 Pixel oder größer. +

+
+ + {/* Preview */} + {bannerUrl && bannerUrl !== currentBannerUrl && ( +
+

Vorschau

+
+ Banner preview setError('Vorschau-Bild konnte nicht geladen werden')} + /> +
+
+ )} + + {/* Common Banner Suggestions */} +
+

Beispiele

+
+ + + + +
+
+ + {/* Info */} +
+
+ +
+

Banner-Empfehlungen

+
    +
  • • Verwenden Sie hochwertige Bilder mit 16:9 Seitenverhältnis
  • +
  • • Stellen Sie sicher, dass die Bilder öffentlich zugänglich sind
  • +
  • • Dunklere Bilder funktionieren oft besser mit dem Text-Overlay
  • +
+
+
+
+
+ +
+ + +
+
+
+ ); +}; + +export default BannerManagementModal; diff --git a/components/CreateProjectModal.tsx b/components/CreateProjectModal.tsx new file mode 100644 index 0000000..5e64674 --- /dev/null +++ b/components/CreateProjectModal.tsx @@ -0,0 +1,218 @@ +import React, { useState } from 'react'; +import { Project } from '../types'; +import { Icons } from './IconSet'; + +interface CreateProjectModalProps { + isOpen: boolean; + onClose: () => void; + onCreate: (projectData: { + title: string; + description: string; + category: Project['category']; + }) => Promise; + linkedPlayerName?: string | null; +} + +const CreateProjectModal: React.FC = ({ isOpen, onClose, onCreate, linkedPlayerName }) => { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [category, setCategory] = useState('Enterprise'); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const categories = [ + { value: 'Enterprise', label: 'Unternehmen', description: 'Firmen, Läden, Dienstleistungen' }, + { value: 'Story Arc', label: 'Story Arc', description: 'Rollenspiel-Handlung, Quest' }, + { value: 'Faction', label: 'Fraktion', description: 'Gruppe, Gilde, Organisation' }, + { value: 'Black Market', label: 'Schwarzmarkt', description: 'Illegale Geschäfte (Vorsicht!)' }, + ]; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!title.trim() || !description.trim()) { + setError('Titel und Beschreibung sind erforderlich'); + return; + } + + try { + setLoading(true); + setError(null); + await onCreate({ title: title.trim(), description: description.trim(), category }); + onClose(); + // Reset form + setTitle(''); + setDescription(''); + setCategory('Enterprise'); + } catch (err) { + console.error('Error creating project:', err); + setError('Fehler beim Erstellen des Projekts'); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ + Neues Unternehmen erstellen +

+ +
+ +
+
+ {/* Title */} +
+ + setTitle(e.target.value)} + className="w-full bg-[#0b0b0d] border border-border rounded-lg p-3 text-sm text-gray-300 focus:border-accentInfo focus:outline-none" + placeholder="z.B. DrKButz Architektur GmbH" + required + maxLength={100} + /> +
+ + {/* Category */} +
+ +
+ {categories.map((cat) => ( + + ))} +
+
+ + {/* Description */} +
+ +