Configurer DISCORD_CLIENT_ID et DISCORD_CLIENT_SECRET dans .env
Configurer DISCORD_CALLBACK_URL dans .env
Voir server/README.md pour plus d'informations.
`);
}
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');
});
});
// Servir les fichiers statiques (CSS, JS, images, etc.)
// IMPORTANT: Cette route doit être AVANT les routes avec wildcards
app.use('/static', express.static(path.join(__dirname, 'public')));
// Servir les fichiers de transcripts
// Support à la fois /tickets/* et /transcripts/* pour compatibilité
app.get('/tickets/*', isAuthenticated, hasPermission, (req, res) => {
const filePath = req.params[0]; // Ex: plainte/2025-11-09/file.html
// Les fichiers sont toujours dans transcripts/, même si l'URL est /tickets/
const fullPath = path.join(process.cwd(), 'transcripts', filePath);
if (!fs.existsSync(fullPath)) {
return res.status(404).send('Transcript introuvable.');
}
res.sendFile(fullPath);
});
app.get('/transcripts/*', isAuthenticated, hasPermission, (req, res) => {
const filePath = req.params[0]; // Ex: plainte/2025-11-09/file.html
// Les fichiers sont dans transcripts/
const fullPath = path.join(process.cwd(), 'transcripts', filePath);
if (!fs.existsSync(fullPath)) {
return res.status(404).send('Transcript introuvable.');
}
res.sendFile(fullPath);
});
// Page d'accueil (Dashboard)
app.get('/', isAuthenticated, async (req, res) => {
try {
const userId = req.user.id;
// Récupérer les statistiques
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é']);
const stats = {
totalTickets: totalStats[0].total,
myTickets: myTicketsStats[0].total,
openTickets: openStats[0].total,
closedTickets: closedStats[0].total
};
// Récupérer les tickets claimés par l'utilisateur (pour "Mes Tickets")
const [userTickets] = await db.query(
'SELECT * FROM tickets WHERE claimedBy = ? AND transcriptPath IS NOT NULL ORDER BY createdAt DESC LIMIT 20',
[userId]
);
// Récupérer tous les transcripts accessibles (avec transcriptPath) pour la page dédiée
const [allTickets] = await db.query(
'SELECT * FROM tickets WHERE transcriptPath IS NOT NULL ORDER BY createdAt DESC',
[]
);
const html = generateDashboardHTML(req.user, stats, userTickets, allTickets);
res.send(html);
} catch (err) {
console.error('Erreur lors de la génération du dashboard:', err);
res.status(500).send('Erreur serveur.');
}
});
// Page dédiée pour tous les transcripts (tableau)
app.get('/transcripts', isAuthenticated, async (req, res) => {
try {
const userId = req.user.id;
// Récupérer tous les transcripts avec les informations des modérateurs
const [allTickets] = await db.query(
`SELECT * FROM tickets
WHERE transcriptPath IS NOT NULL
ORDER BY createdAt DESC`,
[]
);
// Récupérer les informations des modérateurs qui ont claim les tickets
// Note: On pourrait utiliser l'API Discord pour récupérer les vrais noms, mais pour l'instant on utilise les tags stockés
// Pour améliorer, on pourrait créer une table de cache des utilisateurs Discord
const html = generateTranscriptsPageHTML(req.user, allTickets);
res.send(html);
} catch (err) {
console.error('Erreur lors de la génération de la page transcripts:', err);
res.status(500).send('Erreur serveur.');
}
});
// Fonction pour générer la page HTML des transcripts avec tableau
function generateTranscriptsPageHTML(user, tickets) {
// Gérer les nouveaux et anciens formats de Discord (discriminator peut être null ou '0')
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 typeEmojis = {
'Support': '💬',
'Plainte': '📢',
'Plainte Staff': '⚠️',
'Candidature': '📝',
'Problème Technique': '🔧'
};
const statusColors = {
'Ouvert': '#10b981',
'Fermé': '#ef4444',
'En attente': '#f59e0b',
'Supprimé': '#6b7280'
};
return `
Tous les Transcripts - France Femboy
📋 Tous les Transcripts
${tickets.length > 0 ? `
Ticket
Type
Modérateur
Date
Status
Action
${tickets.map(ticket => {
const typeClass = `type-${ticket.type.toLowerCase().replace(/\s+/g, '-')}`;
const statusColor = statusColors[ticket.status] || '#6b7280';
const date = formatDate(ticket.createdAt);
const url = ticket.transcriptPath ? `/${ticket.transcriptPath}` : '#';
// Récupérer le nom du modérateur qui a claim le ticket
let moderatorDisplay = 'Non assigné';
if (ticket.claimedBy && ticket.claimedByTag) {
moderatorDisplay = `Géré par: ${escapeHtml(ticket.claimedByTag)}`;
} else if (ticket.claimedBy) {
moderatorDisplay = 'Géré par: Modérateur';
}
return `