/** * Serveur web pour héberger les transcripts avec authentification Discord * Interface professionnelle avec dashboard et statistiques */ require('dotenv').config(); const express = require('express'); const session = require('express-session'); const passport = require('passport'); const path = require('path'); const fs = require('fs'); const db = require('../functions/database/db.js'); // --- DEBUG: Fonction pour afficher l'arborescence --- function printTree(dir, prefix = '') { if (!fs.existsSync(dir)) return console.log(`${prefix}❌ ${dir} (Introuvable)`); console.log(`${prefix}📁 ${path.basename(dir)}/`); try { const files = fs.readdirSync(dir); files.forEach(file => { const fullPath = path.join(dir, file); const isDir = fs.statSync(fullPath).isDirectory(); if (isDir) { printTree(fullPath, prefix + ' '); } else { console.log(`${prefix} 📄 ${file}`); } }); } catch (e) { console.log(`${prefix} ❌ Erreur lecture: ${e.message}`); } } let DiscordStrategy; try { DiscordStrategy = require('passport-discord').Strategy; } catch (err) { console.warn('⚠️ passport-discord non installé.'); DiscordStrategy = null; } const app = express(); const PORT = process.env.TRANSCRIPT_PORT || 3000; // --- DEBUG: Middleware de log HTTP --- app.use((req, res, next) => { if (!req.url.includes('.css') && !req.url.includes('.js') && !req.url.includes('.png')) { console.log(`[HTTP] ${req.method} ${req.url}`); } next(); }); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(session({ secret: process.env.SESSION_SECRET || 'change-me-in-production', resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 } })); app.use(passport.initialize()); app.use(passport.session()); let discordStrategyConfigured = false; const discordClientId = process.env.DISCORD_CLIENT_ID || process.env.CLIENT_ID; const discordClientSecret = process.env.DISCORD_CLIENT_SECRET || process.env.CLIENT_SECRET; if (DiscordStrategy && discordClientId && discordClientSecret) { try { passport.use('discord', new DiscordStrategy({ clientID: discordClientId, clientSecret: discordClientSecret, callbackURL: process.env.DISCORD_CALLBACK_URL || `http://localhost:${PORT}/auth/discord/callback`, scope: ['identify', 'guilds'] }, (accessToken, refreshToken, profile, done) => { return done(null, profile); })); discordStrategyConfigured = true; console.log('✅ Stratégie Discord OAuth configurée avec succès'); } catch (err) { console.error('❌ Erreur config Discord:', err); discordStrategyConfigured = false; } } passport.serializeUser((user, done) => done(null, user)); passport.deserializeUser((obj, done) => done(null, obj)); function isAuthenticated(req, res, next) { if (req.isAuthenticated()) return next(); res.redirect('/login'); } function isModerator(user) { const moderatorIds = process.env.MODERATOR_IDS ? process.env.MODERATOR_IDS.split(',') : []; return moderatorIds.includes(user.id); } async function hasPermission(req, res, next) { if (!req.isAuthenticated()) return res.redirect('/login'); const filePath = req.params[0]; if (filePath.endsWith('.css')) return next(); try { const [tickets] = await db.query( 'SELECT * FROM tickets WHERE transcriptPath = ? OR transcriptPath = ? OR transcriptPath LIKE ?', [filePath, `transcripts/${filePath}`, `%${filePath}`] ); if (tickets.length === 0) return res.status(404).send('Transcript introuvable.'); const ticket = tickets[0]; const userId = req.user.id; if (ticket.userId === userId) return next(); if (isModerator(req.user)) return next(); return res.status(403).send('Accès refusé.'); } catch (err) { console.error('Erreur permissions:', err); return res.status(500).send('Erreur serveur.'); } } // --- HEAD COMMUN (Lien vers style.css) --- const COMMON_HEAD = ` `; function generateDashboardHTML(user, stats, userTickets, allTickets, isMod) { let userAvatar; if (user.avatar) userAvatar = `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=128`; else if (user.discriminator && user.discriminator !== '0') userAvatar = `https://cdn.discordapp.com/embed/avatars/${user.discriminator % 5}.png`; else userAvatar = `https://cdn.discordapp.com/embed/avatars/${parseInt(user.id) % 5}.png`; const statsHTML = isMod ? `

Total Tickets

${stats.totalTickets}

Ouverts

${stats.openTickets}

Fermés

${stats.closedTickets}

Mes Tickets (Gérés)

${stats.myTickets}

` : ''; const allTicketsHTML = isMod ? `

🌐 Tous les Transcripts

${allTickets.length > 0 ? `
${allTickets.slice(0, 6).map(ticket => generateTicketCard(ticket)).join('')}
${allTickets.length > 6 ? `

Voir tous (${allTickets.length})

` : ''}` : '
Aucun transcript disponible.
'}
` : ''; const myTicketsTitle = isMod ? "📄 Mes Tickets (Gérés)" : "📄 Mes Tickets (Créés)"; return ` Dashboard • Femboy Croissant ${COMMON_HEAD}

Vue d'ensemble

Bienvenue, ${escapeHtml(user.username)}
${statsHTML}

${myTicketsTitle}

${userTickets.length > 0 ? `
${userTickets.map(ticket => generateTicketCard(ticket)).join('')}
` : '
Aucun ticket trouvé.
'}
${allTicketsHTML}
`; } function generateTranscriptsPageHTML(user, tickets) { const userAvatar = user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png` : `https://cdn.discordapp.com/embed/avatars/${parseInt(user.id) % 5}.png`; return ` Transcripts • Femboy Croissant ${COMMON_HEAD}

Archives des Tickets

${tickets.length > 0 ? `
${tickets.map(t => generateTableRow(t, true)).join('')}
ID Type Créateur Géré par Date Statut Action
` : `

Aucun transcript disponible.

`}
`; } function generateTableRow(ticket, showModerator = false) { const date = new Date(ticket.createdAt).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }); let url = ticket.transcriptPath || '#'; if (url !== '#' && !url.startsWith('transcripts/') && !url.startsWith('/transcripts/')) { url = `/transcripts/${url}`; } else if (url !== '#' && !url.startsWith('/')) { url = `/${url}`; } const statusClass = `status-${ticket.status.toLowerCase().replace(' ', '-')}`; let modCell = ''; if (showModerator) { const mod = ticket.claimedByTag ? escapeHtml(ticket.claimedByTag) : (ticket.claimedBy ? 'Modérateur' : '-'); modCell = `${mod}`; } return ` ${ticket.ticketId} ${ticket.type} ${escapeHtml(ticket.userTag)} ${modCell} ${date} ${ticket.status} ${ticket.transcriptPath ? ` Voir` : `` } `; } function generateTicketCard(ticket) { const date = formatDate(ticket.createdAt); let url = ticket.transcriptPath || '#'; if (url !== '#' && !url.startsWith('transcripts/') && !url.startsWith('/transcripts/')) { url = `/transcripts/${url}`; } else if (url !== '#' && !url.startsWith('/')) { url = `/${url}`; } const typeEmojis = { 'Support': '💬', 'Plainte': '📢', 'Plainte Staff': '⚠️', 'Candidature': '📝', 'Problème Technique': '🔧' }; return `
${typeEmojis[ticket.type] || '🎫'} ${ticket.ticketId}
${ticket.type}
Créé par: ${escapeHtml(ticket.userTag)}
${ticket.claimedBy ? `
Géré par: ${ticket.claimedByTag || 'Modérateur'}
` : ''}
Date: ${date}
${ticket.status}
${ticket.transcriptPath ? `Voir le transcript` : 'Transcript non disponible'}
`; } function formatDate(timestamp) { try { return new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(timestamp)); } catch { return new Date(timestamp).toLocaleString(); } } function escapeHtml(text) { if (!text) return ''; return text.replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m]); } // Routes app.get('/login', (req, res) => { res.send(` Connexion ${COMMON_HEAD}

🔐 Accès Restreint

Veuillez vous connecter avec votre compte Discord pour accéder aux archives.

Se connecter avec Discord
`); }); app.get('/auth/discord', (req, res, next) => { if (!discordStrategyConfigured) return res.send('Erreur configuration OAuth'); passport.authenticate('discord')(req, res, next); }); app.get('/auth/discord/callback', (req, res, next) => { if (!discordStrategyConfigured) return res.redirect('/login?error=not_configured'); passport.authenticate('discord', { failureRedirect: '/login?error=auth_failed' })(req, res, next); }, (req, res) => res.redirect('/') ); app.get('/auth/logout', (req, res) => { req.logout(() => res.redirect('/login')); }); app.use('/static', express.static(path.join(__dirname, 'public'))); // --- FIX CSS: Route explicite avec Header --- app.get('*/transcript.css', (req, res) => { res.setHeader('Content-Type', 'text/css'); res.sendFile(path.join(process.cwd(), 'transcripts', 'transcript.css')); }); app.get(['/tickets/*', '/transcripts/*'], isAuthenticated, hasPermission, (req, res) => { const filePath = req.params[0]; const fullPath = path.join(process.cwd(), 'transcripts', filePath); if (!fs.existsSync(fullPath)) return res.status(404).send('Transcript introuvable.'); res.sendFile(fullPath); }); app.get('/', isAuthenticated, async (req, res) => { try { const userId = req.user.id; const isMod = isModerator(req.user); let stats = {}; let userTickets = []; let allTickets = []; if (isMod) { const [totalStats] = await db.query('SELECT COUNT(*) as total FROM tickets'); const [myTicketsStats] = await db.query('SELECT COUNT(*) as total FROM tickets WHERE claimedBy = ?', [userId]); const [openStats] = await db.query('SELECT COUNT(*) as total FROM tickets WHERE status = ?', ['Ouvert']); const [closedStats] = await db.query('SELECT COUNT(*) as total FROM tickets WHERE status = ?', ['Fermé']); stats = { totalTickets: totalStats[0].total, myTickets: myTicketsStats[0].total, openTickets: openStats[0].total, closedTickets: closedStats[0].total }; [userTickets] = await db.query('SELECT * FROM tickets WHERE claimedBy = ? AND transcriptPath IS NOT NULL ORDER BY createdAt DESC LIMIT 10', [userId]); [allTickets] = await db.query('SELECT * FROM tickets WHERE transcriptPath IS NOT NULL ORDER BY createdAt DESC LIMIT 10', []); } else { [userTickets] = await db.query('SELECT * FROM tickets WHERE userId = ? AND transcriptPath IS NOT NULL ORDER BY createdAt DESC LIMIT 20', [userId]); } res.send(generateDashboardHTML(req.user, stats, userTickets, allTickets, isMod)); } catch (err) { console.error('Erreur dashboard:', err); res.status(500).send('Erreur serveur.'); } }); app.get('/transcripts', isAuthenticated, async (req, res) => { if (!isModerator(req.user)) return res.status(403).send('Accès refusé.'); try { const [allTickets] = await db.query('SELECT * FROM tickets WHERE transcriptPath IS NOT NULL ORDER BY createdAt DESC', []); res.send(generateTranscriptsPageHTML(req.user, allTickets)); } catch (err) { console.error('Erreur transcripts:', err); res.status(500).send('Erreur serveur.'); } }); app.listen(PORT, () => { console.log(`🌐 Serveur de transcripts démarré sur le port ${PORT}`); console.log('--- STRUCTURE DES FICHIERS ---'); printTree(path.join(__dirname, 'public'), 'server/public'); printTree(path.join(process.cwd(), 'transcripts'), 'transcripts'); console.log('------------------------------'); });