mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
Refactor CityProfile and PlayerProfile components for improved data fetching and error handling; add NPC management modals for banner, gallery, and logo with enhanced user experience and error feedback.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
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');
|
||||
@@ -58,14 +59,41 @@ const upload = multer({
|
||||
|
||||
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
|
||||
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());
|
||||
@@ -129,11 +157,34 @@ app.get('/api/status', (req, res) => {
|
||||
// --- ROUTES ---
|
||||
|
||||
// Auth
|
||||
app.get('/auth/discord', passport.authenticate('discord'));
|
||||
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) => {
|
||||
res.redirect(FRONTEND_URL);
|
||||
// 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) => {
|
||||
@@ -494,6 +545,282 @@ app.post('/api/admin/npc-company', (req, res) => {
|
||||
);
|
||||
});
|
||||
|
||||
// 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();
|
||||
@@ -1019,7 +1346,7 @@ app.get('/api/images/:imageId/meta', (req, res) => {
|
||||
});
|
||||
|
||||
// Upload banner image
|
||||
app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), (req, res) => {
|
||||
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;
|
||||
@@ -1081,7 +1408,7 @@ app.post('/api/projects/:projectId/banner/upload', upload.single('banner'), (req
|
||||
});
|
||||
|
||||
// Upload gallery image
|
||||
app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), (req, res) => {
|
||||
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;
|
||||
@@ -1152,7 +1479,7 @@ app.post('/api/projects/:projectId/gallery/upload', upload.single('gallery'), (r
|
||||
});
|
||||
|
||||
// Upload logo image
|
||||
app.post('/api/projects/:projectId/logo/upload', upload.single('logo'), (req, res) => {
|
||||
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;
|
||||
@@ -1344,7 +1671,7 @@ app.delete('/api/admin/cities/:cityId', (req, res) => {
|
||||
});
|
||||
|
||||
// Upload city banner
|
||||
app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), (req, res) => {
|
||||
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
|
||||
@@ -1408,7 +1735,7 @@ app.post('/api/admin/cities/:cityId/banner/upload', upload.single('banner'), (re
|
||||
});
|
||||
|
||||
// Upload city logo
|
||||
app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), (req, res) => {
|
||||
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
|
||||
@@ -1472,7 +1799,7 @@ app.post('/api/admin/cities/:cityId/logo/upload', upload.single('logo'), (req, r
|
||||
});
|
||||
|
||||
// Upload city gallery image
|
||||
app.post('/api/admin/cities/:cityId/gallery/upload', upload.single('gallery'), (req, res) => {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user