MovixOpenSource/API/Mainapi/routes/helpFeedback.js
Movix 1401dc20f3 tird release
mon compte github a été détecté comme du spam je crois
2026-04-28 17:55:00 +02:00

157 lines
4.5 KiB
JavaScript

const express = require('express');
const crypto = require('crypto');
const rateLimit = require('express-rate-limit');
const { ipKeyGenerator } = require('express-rate-limit');
const router = express.Router();
const { getPool } = require('../mysqlPool');
const { verifyTurnstileFromRequest } = require('../utils/turnstile');
const { isAdmin } = require('../middleware/auth');
const { createRedisRateLimitStore } = require('../utils/redisRateLimitStore');
// Whitelist of valid tuto slugs. Must stay in sync with
// src/pages/help/tutoRegistry.tsx TUTO_REGISTRY entries.
const VALID_SLUGS = new Set([
'ca-marche-plus',
'dns',
'changer-lecteur',
'qualite-video',
'sous-titres',
'chromecast',
'compte',
'recuperer-compte',
'lier-compte',
'profils',
'watchparty',
'listes-partagees',
'installer-pwa',
'extension',
'premid',
'app-mobile',
'open-source',
'vip',
'debrid',
'telechargement',
'miroirs',
'live-tv',
'extraction',
'priorite-sources',
'dernier-lecteur',
'apparence',
]);
const IP_SALT =
process.env.TURNSTILE_IP_SALT ||
process.env.JWT_SECRET ||
'movix-help-feedback-default-salt';
function hashIp(req) {
const ip =
req.headers['cf-connecting-ip'] ||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
req.ip ||
'unknown';
return crypto.createHash('sha256').update(`${IP_SALT}:${ip}`).digest('hex');
}
const feedbackRateLimit = rateLimit({
windowMs: 15 * 60 * 1000,
max: 30,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) =>
req.headers['cf-connecting-ip'] ||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
ipKeyGenerator(req.ip),
store: createRedisRateLimitStore({ prefix: 'rate-limit:help-feedback:' }),
passOnStoreError: true,
validate: { xForwardedForHeader: false, ip: false },
message: { error: 'Trop de votes. Réessaie dans quelques minutes.' },
});
/**
* POST /api/help/feedback
* body: { slug: string, helpful: boolean, turnstileToken: string }
*
* Upserts a vote keyed on (slug, ip_hash) — same user can change their mind
* but we don't count duplicates.
*/
router.post('/feedback', feedbackRateLimit, async (req, res) => {
try {
const body = req.body && typeof req.body === 'object' ? req.body : {};
const { slug, helpful, turnstileToken } = body;
if (typeof slug !== 'string' || !VALID_SLUGS.has(slug)) {
return res.status(400).json({ error: 'Invalid slug' });
}
if (typeof helpful !== 'boolean') {
return res.status(400).json({ error: 'Invalid helpful value' });
}
if (turnstileToken !== undefined && typeof turnstileToken !== 'string') {
return res.status(400).json({ error: 'Invalid turnstile token' });
}
const turnstileCheck = await verifyTurnstileFromRequest(req, turnstileToken);
if (!turnstileCheck.valid) {
return res.status(turnstileCheck.status || 403).json({
error: turnstileCheck.error || 'Turnstile verification failed',
});
}
const ipHash = hashIp(req);
const userAgent = String(req.headers['user-agent'] || '').slice(0, 255);
const helpfulInt = helpful ? 1 : 0;
const pool = getPool();
await pool.execute(
`INSERT INTO help_feedback (slug, helpful, ip_hash, user_agent)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE helpful = VALUES(helpful), user_agent = VALUES(user_agent)`,
[slug, helpfulInt, ipHash, userAgent],
);
return res.json({ ok: true });
} catch (err) {
console.error('[helpFeedback] submit error:', err);
return res.status(500).json({ error: 'Internal error' });
}
});
/**
* GET /api/help/feedback/stats
* Admin-only. Returns aggregated stats per slug: { slug, up, down, total, ratio }.
*/
router.get('/feedback/stats', isAdmin, async (_req, res) => {
try {
const pool = getPool();
const [rows] = await pool.execute(
`SELECT
slug,
SUM(CASE WHEN helpful = 1 THEN 1 ELSE 0 END) AS up,
SUM(CASE WHEN helpful = 0 THEN 1 ELSE 0 END) AS down,
COUNT(*) AS total
FROM help_feedback
GROUP BY slug
ORDER BY total DESC`,
);
const stats = rows.map((r) => {
const up = Number(r.up) || 0;
const down = Number(r.down) || 0;
const total = Number(r.total) || 0;
return {
slug: r.slug,
up,
down,
total,
ratio: total > 0 ? up / total : 0,
};
});
return res.json({ stats });
} catch (err) {
console.error('[helpFeedback] stats error:', err);
return res.status(500).json({ error: 'Internal error' });
}
});
module.exports = router;