Initial commit: Club DynaSQY

This commit is contained in:
2026-08-23 09:04:47 +00:00
commit 0875890dc7
145 changed files with 16947 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
const { Router } = require('express');
const { listByMember, create, remove, stats } = require('../controllers/absenceController');
const { authenticate, adminOnly } = require('../middleware/auth');
const router = Router();
router.get('/stats', authenticate, adminOnly, stats);
router.get('/member/:id', authenticate, adminOnly, listByMember);
router.post('/', authenticate, adminOnly, create);
router.delete('/:id', authenticate, adminOnly, remove);
module.exports = router;
+8
View File
@@ -0,0 +1,8 @@
const { Router } = require('express');
const { getLogs } = require('../controllers/activityLogController');
const { authenticate, adminOnly } = require('../middleware/auth');
const router = Router();
router.get('/', authenticate, adminOnly, getLogs);
module.exports = router;
+9
View File
@@ -0,0 +1,9 @@
const { Router } = require('express');
const { listUsers, updateUserRole } = require('../controllers/adminController');
const { authenticate, adminOnly } = require('../middleware/auth');
const router = Router();
router.get('/users', authenticate, adminOnly, listUsers);
router.put('/users/:id/role', authenticate, adminOnly, updateUserRole);
module.exports = router;
+11
View File
@@ -0,0 +1,11 @@
const { Router } = require('express');
const { login, me, changePassword } = require('../controllers/authController');
const { authenticate } = require('../middleware/auth');
const router = Router();
router.post('/login', login);
router.get('/me', authenticate, me);
router.put('/password', authenticate, changePassword);
module.exports = router;
+13
View File
@@ -0,0 +1,13 @@
const { Router } = require('express');
const { list, create, update, remove, scrape, deletePast } = require('../controllers/eventController');
const { authenticate, adminOnly } = require('../middleware/auth');
const router = Router();
router.get('/', authenticate, list);
router.post('/', authenticate, adminOnly, create);
router.post('/scrape', authenticate, adminOnly, scrape);
router.delete('/past', authenticate, adminOnly, deletePast);
router.put('/:id', authenticate, adminOnly, update);
router.delete('/:id', authenticate, adminOnly, remove);
module.exports = router;
+8
View File
@@ -0,0 +1,8 @@
const { Router } = require('express');
const { exportCsv } = require('../controllers/exportController');
const { authenticate, adminOnly } = require('../middleware/auth');
const router = Router();
router.get('/csv', authenticate, adminOnly, exportCsv);
module.exports = router;
+52
View File
@@ -0,0 +1,52 @@
const { Router } = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const sharp = require('sharp');
const { list, upload, deletePhoto } = require('../controllers/galleryController');
const { authenticate } = require('../middleware/auth');
const galleryDir = path.join(__dirname, '..', 'gallery');
if (!fs.existsSync(galleryDir)) fs.mkdirSync(galleryDir, { recursive: true });
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, galleryDir),
filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname),
});
const uploadMulter = multer({ storage, limits: { fileSize: 20 * 1024 * 1024 } });
const router = Router();
router.get('/', authenticate, list);
router.post('/upload', authenticate, uploadMulter.array('photos', 20), upload);
router.get('/thumbnail/:date/:filename', async (req, res) => {
try {
const { date, filename } = req.params;
const filePath = path.join(galleryDir, date, filename);
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Fichier introuvable' });
const ext = path.extname(filename).toLowerCase();
if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) {
return res.sendFile(filePath);
}
const thumbDir = path.join(galleryDir, date, '.thumbnails');
if (!fs.existsSync(thumbDir)) fs.mkdirSync(thumbDir, { recursive: true });
const thumbPath = path.join(thumbDir, filename);
if (!fs.existsSync(thumbPath)) {
await sharp(filePath)
.resize(300, 200, { fit: 'cover', position: 'centre' })
.jpeg({ quality: 70 })
.toFile(thumbPath);
}
res.sendFile(thumbPath);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur' });
}
});
router.delete('/:date/:filename', authenticate, deletePhoto);
module.exports = router;
+16
View File
@@ -0,0 +1,16 @@
const { Router } = require('express');
const { list, getById, create, update, remove, search, sendInvitation, candidature, nextEventGuestCount } = require('../controllers/guestController');
const { authenticate } = require('../middleware/auth');
const router = Router();
router.get('/', authenticate, list);
router.get('/search', authenticate, search);
router.get('/:id', authenticate, getById);
router.post('/', authenticate, create);
router.put('/:id', authenticate, update);
router.delete('/:id', authenticate, remove);
router.post('/:id/send-invitation', authenticate, sendInvitation);
router.post('/:id/candidature', authenticate, candidature);
router.get('/next-count', authenticate, nextEventGuestCount);
module.exports = router;
+11
View File
@@ -0,0 +1,11 @@
const { Router } = require('express');
const { list, create, update, remove } = require('../controllers/honorController');
const { authenticate, adminOnly } = require('../middleware/auth');
const router = Router();
router.get('/', authenticate, list);
router.post('/', authenticate, adminOnly, create);
router.put('/:id', authenticate, adminOnly, update);
router.delete('/:id', authenticate, adminOnly, remove);
module.exports = router;
+21
View File
@@ -0,0 +1,21 @@
const { Router } = require('express');
const multer = require('multer');
const path = require('path');
const { showPage, respond, uploadPhoto } = require('../controllers/invitationController');
const router = Router();
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, path.join(__dirname, '..', 'uploads')),
filename: (req, file, cb) => {
const { token } = req.params;
cb(null, `invite_${token}_${Date.now()}${path.extname(file.originalname)}`);
},
});
const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 } });
router.get('/:token', showPage);
router.post('/:token/respond', respond);
router.post('/:token/photo', upload.single('photo'), uploadPhoto);
module.exports = router;
+100
View File
@@ -0,0 +1,100 @@
const { Router } = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const pool = require('../config/db');
const { authenticate } = require('../middleware/auth');
const { logActivity } = require('../services/activityLog');
const slidesDir = path.join(__dirname, '..', 'member_slides');
if (!fs.existsSync(slidesDir)) fs.mkdirSync(slidesDir, { recursive: true });
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, slidesDir),
filename: (req, file, cb) => {
const memberId = req.body.member_id && req.user.role === 'admin' ? req.body.member_id : req.user.id;
const dateKey = req.body.date_key || '00000000';
const ext = path.extname(file.originalname);
cb(null, `${memberId}_${dateKey}_${Date.now()}${ext}`);
},
});
const uploadMulter = multer({ storage, limits: { fileSize: 300 * 1024 * 1024 } });
const router = Router();
router.post('/upload', authenticate, uploadMulter.single('file'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'Aucun fichier' });
const { date_key } = req.body;
if (!date_key) return res.status(400).json({ error: 'date_key requis' });
const memberId = req.body.member_id && req.user.role === 'admin' ? req.body.member_id : req.user.id;
let filename = req.file.filename;
const ext = path.extname(filename).toLowerCase();
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
const videoExts = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv'];
const allowedExts = [...imageExts, ...videoExts];
if (!allowedExts.includes(ext)) {
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: 'Seules les images et vidéos sont acceptées' });
}
const [result] = await pool.query(
'INSERT INTO member_slides (member_id, filename, original_name, date_key) VALUES (?, ?, ?, ?)',
[memberId, filename, req.file.originalname, date_key]
);
res.json({ success: true, id: result.insertId, filename });
logActivity(req, 'slide_upload', { memberId, date_key, original_name: req.file.originalname });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur upload' });
}
});
router.get('/:memberId', authenticate, async (req, res) => {
try {
const { memberId } = req.params;
if (Number(memberId) !== req.user.id && req.user.role !== 'admin')
return res.status(403).json({ error: 'Interdit' });
const [rows] = await pool.query(
'SELECT id, filename, original_name, date_key, created_at FROM member_slides WHERE member_id = ? ORDER BY created_at DESC',
[memberId]
);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur' });
}
});
router.delete('/:id', authenticate, async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM member_slides WHERE id = ?', [req.params.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Introuvable' });
const slide = rows[0];
if (slide.member_id !== req.user.id && req.user.role !== 'admin')
return res.status(403).json({ error: 'Interdit' });
const filePath = path.join(slidesDir, slide.filename);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
await pool.query('DELETE FROM member_slides WHERE id = ?', [req.params.id]);
res.json({ success: true });
logActivity(req, 'slide_suppression', { slideId: req.params.id, filename: slide.filename });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur' });
}
});
router.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({ error: 'Le fichier dépasse la limite autorisée (300 Mo maximum)' });
}
return res.status(400).json({ error: err.message });
}
if (err) return res.status(500).json({ error: 'Erreur serveur' });
next();
});
module.exports = router;
+22
View File
@@ -0,0 +1,22 @@
const { Router } = require('express');
const multer = require('multer');
const path = require('path');
const { list, getById, update, uploadPhoto, create, remove } = require('../controllers/memberController');
const { authenticate, adminOnly } = require('../middleware/auth');
const router = Router();
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, 'uploads/'),
filename: (req, file, cb) => cb(null, `user_${req.user.id}_${Date.now()}${path.extname(file.originalname)}`)
});
const upload = multer({ storage });
router.get('/', authenticate, list);
router.get('/:id', authenticate, getById);
router.put('/:id', authenticate, update);
router.post('/upload-photo', authenticate, upload.single('photo'), uploadPhoto);
router.post('/', authenticate, adminOnly, create);
router.delete('/:id', authenticate, adminOnly, remove);
module.exports = router;
+97
View File
@@ -0,0 +1,97 @@
const { Router } = require('express');
const pool = require('../config/db');
const { authenticate } = require('../middleware/auth');
const router = Router();
router.get('/', authenticate, async (req, res) => {
try {
const [rows] = await pool.query(
`SELECT n.id, n.title, n.body, n.created_at,
u.prenom AS sender_prenom, u.nom AS sender_nom,
nr.read_at, 'admin' AS source,
NULL AS invite_id,
NULL AS guest_name
FROM notifications n
JOIN notification_recipients nr ON nr.notification_id = n.id
JOIN users u ON u.id = n.sender_id
WHERE nr.user_id = ?
UNION ALL
SELECT -(i.id) AS id,
CONCAT('Invitation en attente - ', i.prenom, ' ', i.nom) AS title,
CONCAT('Vous n''avez pas encore envoy?? l''email d''invitation ?? ', i.prenom, ' ', i.nom) AS body,
i.created_at,
NULL AS sender_prenom, NULL AS sender_nom,
NULL AS read_at,
'pending_invite' AS source,
i.id AS invite_id,
CONCAT(i.prenom, ' ', i.nom) AS guest_name
FROM invites i
WHERE i.invite_par_id = ? AND (i.mail_envoye IS NULL OR i.mail_envoye = 0)
ORDER BY created_at DESC`,
[req.user.id, req.user.id]
);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.get('/unread-count', authenticate, async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT COUNT(*) AS count FROM notification_recipients WHERE user_id = ? AND read_at IS NULL',
[req.user.id]
);
const [inviteRows] = await pool.query(
'SELECT COUNT(*) AS count FROM invites WHERE invite_par_id = ? AND (mail_envoye IS NULL OR mail_envoye = 0)',
[req.user.id]
);
res.json({ count: Number(rows[0].count) + Number(inviteRows[0].count) });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.post('/:id/read', authenticate, async (req, res) => {
try {
await pool.query(
'UPDATE notification_recipients SET read_at = NOW() WHERE notification_id = ? AND user_id = ?',
[req.params.id, req.user.id]
);
res.json({ message: 'Marqu?? comme lu' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.post('/read-all', authenticate, async (req, res) => {
try {
await pool.query(
'UPDATE notification_recipients SET read_at = NOW() WHERE user_id = ? AND read_at IS NULL',
[req.user.id]
);
res.json({ message: 'Tout marqu?? comme lu' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.delete('/:id/recipient', authenticate, async (req, res) => {
try {
await pool.query(
'DELETE FROM notification_recipients WHERE notification_id = ? AND user_id = ?',
[req.params.id, req.user.id]
);
res.json({ message: 'Supprim??' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
module.exports = router;
+136
View File
@@ -0,0 +1,136 @@
const { Router } = require('express');
const pool = require('../config/db');
const { authenticate } = require('../middleware/auth');
const router = Router();
router.get('/:date', authenticate, async (req, res) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
try {
const { date } = req.params;
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
return res.status(400).json({ error: 'Format de date invalide (AAAA-MM-JJ)' });
}
const [absences] = await pool.query(`
SELECT a.membre_id, a.prevenu, a.motif,
a.remplace_par_type, a.remplace_par_id, a.remplace_par_guest_id,
r.prenom AS remplace_prenom, r.nom AS remplace_nom,
rg.prenom AS remplace_guest_prenom, rg.nom AS remplace_guest_nom
FROM absences a
LEFT JOIN users r ON r.id = a.remplace_par_id AND (a.remplace_par_type = 'member' OR a.remplace_par_type IS NULL)
LEFT JOIN invites rg ON rg.id = a.remplace_par_guest_id AND a.remplace_par_type = 'guest'
WHERE a.date_absence = ?
`, [date]);
const [allMembers] = await pool.query(
`SELECT id, prenom, nom, societe, activite, photo_url, merged_display_name, fonction, fonction_emoji,
telephone, site_web, email
FROM users WHERE actif = 1 ORDER BY fonction IS NULL, fonction, prenom`
);
const absentWithoutReplacement = new Set(
absences.filter(a => !a.remplace_par_id && !a.remplace_par_guest_id).map(a => a.membre_id)
);
const members = allMembers.filter(m => !absentWithoutReplacement.has(m.id));
const [guests] = await pool.query(
`SELECT i.id, i.prenom, i.nom, i.societe, i.activite, i.photo_url,
i.telephone, i.site_web, i.email, i.statut,
u.prenom AS invite_par_prenom, u.nom AS invite_par_nom
FROM invites i
LEFT JOIN users u ON u.id = i.invite_par_id
WHERE i.date_club = ? AND (i.statut != 'refusee' OR i.statut IS NULL)
ORDER BY i.prenom`,
[date]
);
const [currentHonorRow] = await pool.query(
`SELECT CONCAT(u.prenom, ' ', u.nom) AS currentHonor
FROM mises_en_honneur mh
JOIN users u ON u.id = mh.membre_id
WHERE mh.date = ?
LIMIT 1`,
[date]
);
const currentHonor = currentHonorRow[0]?.currentHonor || null;
const [nextHonorRow] = await pool.query(
`SELECT CONCAT(u.prenom, ' ', u.nom) AS nextHonor
FROM mises_en_honneur mh
JOIN users u ON u.id = mh.membre_id
WHERE mh.date > ?
ORDER BY mh.date ASC LIMIT 1`,
[date]
);
const nextHonor = nextHonorRow[0]?.nextHonor || null;
const [events] = await pool.query(
`SELECT nom_evenement AS titre, date AS date_event, horaire AS heure_debut, lieu
FROM evenements WHERE date >= ? ORDER BY date ASC LIMIT 5`,
[date]
);
const [y, m] = date.split('-')
const monthStart = `${y}-${m}-01`
const lastDay = new Date(y, m, 0).getDate()
const monthEnd = `${y}-${m}-${lastDay}`
const [recommendations] = await pool.query(
`SELECT r.id, r.type, r.nom_prospect, r.ca_realise, r.created_at,
u.prenom, u.nom, u.societe
FROM recommandations r
JOIN users u ON u.id = r.membre_id
WHERE r.created_at >= ? AND r.created_at <= ?
ORDER BY r.created_at DESC LIMIT 10`,
[monthStart, monthEnd]
);
const [yr, mo, dy] = date.split('-')
const dateKey = `${dy}${mo}${yr}`
const [memberSlides] = await pool.query(
'SELECT id, member_id, filename FROM member_slides WHERE date_key = ?',
[dateKey]
);
const [topInviters] = await pool.query(
`SELECT u.id, u.prenom, u.nom, u.photo_url, COUNT(i.id) AS total
FROM users u
LEFT JOIN invites i ON i.invite_par_id = u.id AND i.created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
WHERE u.actif = 1
GROUP BY u.id, u.prenom, u.nom, u.photo_url
ORDER BY total DESC LIMIT 3`
);
const [topDonnees] = await pool.query(
`SELECT u.id, u.prenom, u.nom, u.photo_url, COUNT(r.id) AS total
FROM users u
LEFT JOIN recommandations r ON r.membre_id = u.id
WHERE u.actif = 1
GROUP BY u.id, u.prenom, u.nom, u.photo_url
ORDER BY total DESC LIMIT 3`
);
const [topConversion] = await pool.query(
`SELECT u.id, u.prenom, u.nom, u.photo_url, COUNT(m2.id) AS total
FROM users u
LEFT JOIN invites i ON i.invite_par_id = u.id AND i.statut = 'acceptee'
LEFT JOIN users m2 ON m2.email = i.email
WHERE u.actif = 1
GROUP BY u.id, u.prenom, u.nom, u.photo_url
HAVING total > 0
ORDER BY total DESC LIMIT 3`
);
const [overrides] = await pool.query(
'SELECT slide_key, content, hidden, sort_order FROM presentation_overrides WHERE date_key = ?',
[dateKey]
);
res.json({ members, guests, absences, currentHonor: currentHonor || null, nextHonor: nextHonor || null, events, recommendations, memberSlides, topInviters, topDonnees, topConversion, overrides });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
module.exports = router;
+55
View File
@@ -0,0 +1,55 @@
const { Router } = require('express');
const pool = require('../config/db');
const { authenticate } = require('../middleware/auth');
const router = Router();
router.get('/:date', authenticate, async (req, res) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
try {
const { date } = req.params;
const [yr, mo, dy] = date.split('-');
const dateKey = `${dy}${mo}${yr}`;
const [rows] = await pool.query(
'SELECT id, slide_key, content, hidden, sort_order FROM presentation_overrides WHERE date_key = ? ORDER BY sort_order ASC',
[dateKey]
);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.put('/:date', authenticate, async (req, res) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
try {
const { date } = req.params;
const { overrides } = req.body;
const [yr, mo, dy] = date.split('-');
const dateKey = `${dy}${mo}${yr}`;
if (!Array.isArray(overrides)) return res.status(400).json({ error: 'overrides doit être un tableau' });
for (const ov of overrides) {
const { slide_key, content, hidden, sort_order } = ov;
if (!slide_key) continue;
await pool.query(
`INSERT INTO presentation_overrides (date_key, slide_key, content, hidden, sort_order)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE content = VALUES(content), hidden = VALUES(hidden), sort_order = VALUES(sort_order)`,
[dateKey, slide_key, content ? JSON.stringify(content) : null, hidden ? 1 : 0, sort_order ?? 0]
);
}
const [rows] = await pool.query(
'SELECT slide_key, content, hidden, sort_order FROM presentation_overrides WHERE date_key = ? ORDER BY sort_order ASC',
[dateKey]
);
res.json({ overrides: rows });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
module.exports = router;
+175
View File
@@ -0,0 +1,175 @@
const { Router } = require('express');
const pool = require('../config/db');
const { authenticate } = require('../middleware/auth');
const webPush = require('web-push');
const router = Router();
const publicVapidKey = process.env.VAPID_PUBLIC_KEY;
const privateVapidKey = process.env.VAPID_PRIVATE_KEY;
if (publicVapidKey && privateVapidKey) {
webPush.setVapidDetails('mailto:pierre@secourit.fr', publicVapidKey, privateVapidKey);
}
router.post('/subscribe', authenticate, async (req, res) => {
try {
const { endpoint, keys, reminderDays } = req.body;
if (!endpoint || !keys?.p256dh || !keys?.auth) {
return res.status(400).json({ error: 'endpoint, p256dh, and auth required' });
}
await pool.query(
`INSERT INTO push_subscriptions (user_id, endpoint, p256dh, auth, reminder_days)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE p256dh = VALUES(p256dh), auth = VALUES(auth), reminder_days = VALUES(reminder_days)`,
[req.user.id, endpoint, keys.p256dh, keys.auth, reminderDays || 3]
);
res.json({ message: 'Abonnement aux notifications enregistré' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.delete('/unsubscribe', authenticate, async (req, res) => {
try {
const { endpoint } = req.body;
if (!endpoint) return res.status(400).json({ error: 'endpoint required' });
await pool.query('DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [req.user.id, endpoint]);
res.json({ message: 'Désabonné' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.get('/subscriptions', authenticate, async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT id, endpoint, reminder_days, last_sent_at, created_at FROM push_subscriptions WHERE user_id = ?',
[req.user.id]
);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.get('/vapid-public-key', (req, res) => {
res.json({ publicKey: publicVapidKey });
});
router.get('/members-with-push', authenticate, async (req, res) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
try {
const [rows] = await pool.query(
`SELECT u.id, u.prenom, u.nom, u.societe,
(SELECT COUNT(*) FROM push_subscriptions ps WHERE ps.user_id = u.id) AS has_push
FROM users u WHERE u.actif = 1 ORDER BY u.prenom`
);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.post('/broadcast', authenticate, async (req, res) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
try {
const { title, body, url, userIds } = req.body;
if (!title || !body) return res.status(400).json({ error: 'Titre et message requis' });
let subs;
if (userIds && Array.isArray(userIds) && userIds.length > 0) {
const placeholders = userIds.map(() => '?').join(',');
[subs] = await pool.query(
`SELECT endpoint, p256dh, auth, user_id FROM push_subscriptions WHERE user_id IN (${placeholders})`,
userIds
);
} else {
[subs] = await pool.query('SELECT endpoint, p256dh, auth, user_id FROM push_subscriptions');
}
if (subs.length === 0) return res.status(400).json({ error: 'Aucun abonné' });
const [notifResult] = await pool.query(
'INSERT INTO notifications (sender_id, title, body) VALUES (?, ?, ?)',
[req.user.id, title.slice(0, 100), body.slice(0, 255)]
);
const notifId = notifResult.insertId;
const recipientIds = [...new Set(subs.map((s) => s.user_id))];
for (const uid of recipientIds) {
await pool.query(
'INSERT IGNORE INTO notification_recipients (notification_id, user_id) VALUES (?, ?)',
[notifId, uid]
);
}
const payload = JSON.stringify({
title: title.slice(0, 100),
body: body.slice(0, 255),
icon: '/logo.svg',
badge: '/favicon.svg',
data: { url: '/notifications' },
});
let sent = 0;
for (const sub of subs) {
try {
await webPush.sendNotification(
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
payload
);
sent++;
} catch (err) {
if (err.statusCode === 410 || err.statusCode === 404) {
await pool.query('DELETE FROM push_subscriptions WHERE endpoint = ?', [sub.endpoint]);
}
}
}
res.json({ message: `Notification envoyée à ${sent} abonné(s) sur ${recipientIds.length} destinataire(s)` });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
router.post('/test', authenticate, async (req, res) => {
try {
const [subs] = await pool.query(
'SELECT endpoint, p256dh, auth FROM push_subscriptions WHERE user_id = ?',
[req.user.id]
);
if (subs.length === 0) return res.status(400).json({ error: 'Aucun abonnement trouvé' });
const payload = JSON.stringify({
title: 'Dyna Biz Coignières',
body: 'Notification de test ✓',
icon: '/logo.svg',
badge: '/favicon.svg',
data: { url: '/' },
});
for (const sub of subs) {
try {
await webPush.sendNotification(
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
payload
);
} catch (err) {
if (err.statusCode === 410 || err.statusCode === 404) {
await pool.query('DELETE FROM push_subscriptions WHERE endpoint = ?', [sub.endpoint]);
}
}
}
res.json({ message: 'Notification de test envoyée' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
});
module.exports = router;
+13
View File
@@ -0,0 +1,13 @@
const { Router } = require('express');
const { list, getById, create, update, remove, setStatus } = require('../controllers/recommendationController');
const { authenticate } = require('../middleware/auth');
const router = Router();
router.get('/', authenticate, list);
router.get('/:id', authenticate, getById);
router.post('/', authenticate, create);
router.put('/:id', authenticate, update);
router.patch('/:id/status', authenticate, setStatus);
router.delete('/:id', authenticate, remove);
module.exports = router;
+9
View File
@@ -0,0 +1,9 @@
const { Router } = require('express');
const { dashboard, memberStats } = require('../controllers/statsController');
const { authenticate } = require('../middleware/auth');
const router = Router();
router.get('/dashboard', authenticate, dashboard);
router.get('/member/:id?', authenticate, memberStats);
module.exports = router;