Add account-based event registration and robust email HTML
This commit is contained in:
1 parent
c647c90078
commit
e170d57577
7 files changed
+340
-95
No files matched your search
@@ -41,7 +41,8 @@ const MEMBER_SQL = `
|
||||
LEFT JOIN users u ON u.member_id = m.id`;
|
||||
|
||||
const RENC_SQL = `
|
||||
SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places, r.image_path,
|
||||
SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places,
|
||||
r.participants_par_compte, r.image_path,
|
||||
COUNT(i.id)::int AS inscrits
|
||||
FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id`;
|
||||
|
||||
@@ -174,9 +175,9 @@ adminRouter.post('/rencontres', validate(rencontreSchema), async (req, res, next
|
||||
try {
|
||||
const d = req.data;
|
||||
const result = await query(
|
||||
`INSERT INTO rencontres (titre, date_renc, heure, lieu, description, places)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
||||
[d.titre, d.date_renc, d.heure, d.lieu, d.description, d.places]
|
||||
`INSERT INTO rencontres (titre, date_renc, heure, lieu, description, places, participants_par_compte)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
||||
[d.titre, d.date_renc, d.heure, d.lieu, d.description, d.places, d.participants_par_compte]
|
||||
);
|
||||
res.status(201).json({ id: result.rows[0].id });
|
||||
} catch (err) {
|
||||
@@ -188,9 +189,11 @@ adminRouter.put('/rencontres/:id', validate(idParam, 'params'), validate(rencont
|
||||
try {
|
||||
const d = req.data;
|
||||
const result = await query(
|
||||
`UPDATE rencontres SET titre=$1, date_renc=$2, heure=$3, lieu=$4, description=$5, places=$6
|
||||
WHERE id=$7 RETURNING id`,
|
||||
[d.titre, d.date_renc, d.heure, d.lieu, d.description, d.places, req.params.id]
|
||||
`UPDATE rencontres SET titre=$1, date_renc=$2, heure=$3, lieu=$4, description=$5,
|
||||
places=$6, participants_par_compte=$7
|
||||
WHERE id=$8 RETURNING id`,
|
||||
[d.titre, d.date_renc, d.heure, d.lieu, d.description, d.places,
|
||||
d.participants_par_compte, req.params.id]
|
||||
);
|
||||
if (result.rowCount === 0) return res.status(404).json({ error: 'Rencontre introuvable' });
|
||||
res.json({ ok: true });
|
||||
|
||||
+133
-32
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { query } from '../db.js';
|
||||
import { pool, query } from '../db.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { validate } from '../middleware/validate.js';
|
||||
import { publicFormLimiter } from '../middleware/security.js';
|
||||
import { demandeSchema, inscriptionPublicSchema, idParam } from '../schemas.js';
|
||||
@@ -19,7 +20,8 @@ publicRouter.get('/bootstrap', async (_req, res, next) => {
|
||||
WHERE m.valide = true ORDER BY m.nom`
|
||||
),
|
||||
query(
|
||||
`SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places, r.image_path,
|
||||
`SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places,
|
||||
r.participants_par_compte, r.image_path,
|
||||
COUNT(i.id)::int AS inscrits
|
||||
FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id
|
||||
WHERE r.date_renc >= CURRENT_DATE
|
||||
@@ -73,42 +75,141 @@ publicRouter.post(
|
||||
}
|
||||
);
|
||||
|
||||
// Event registration (public modal)
|
||||
publicRouter.post(
|
||||
'/rencontres/:id/inscriptions',
|
||||
publicFormLimiter,
|
||||
// Registration context for the connected member account. Existing names are
|
||||
// returned so the member can review or amend the complete participant list.
|
||||
publicRouter.get(
|
||||
'/rencontres/:id/inscription',
|
||||
requireAuth('member'),
|
||||
validate(idParam, 'params'),
|
||||
validate(inscriptionPublicSchema),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { nom, entreprise, email, tel } = req.data;
|
||||
const renc = await query(
|
||||
`SELECT r.places, COUNT(i.id)::int AS inscrits
|
||||
FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id
|
||||
WHERE r.id = $1 AND r.date_renc >= CURRENT_DATE
|
||||
GROUP BY r.id`,
|
||||
[id]
|
||||
);
|
||||
if (renc.rowCount === 0) return res.status(404).json({ error: 'Rencontre introuvable' });
|
||||
if (renc.rows[0].inscrits >= renc.rows[0].places) {
|
||||
return res.status(409).json({ error: 'Cette rencontre est complète.' });
|
||||
const [member, rencontre, participants] = await Promise.all([
|
||||
query('SELECT id, nom, dirigeant, email, tel, valide FROM members WHERE id = $1', [req.user.memberId]),
|
||||
query(
|
||||
`SELECT r.id, r.places, r.participants_par_compte, COUNT(i.id)::int AS inscrits
|
||||
FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id
|
||||
WHERE r.id = $1 AND r.date_renc >= CURRENT_DATE
|
||||
GROUP BY r.id`,
|
||||
[req.params.id]
|
||||
),
|
||||
query(
|
||||
`SELECT nom, statut FROM inscriptions
|
||||
WHERE rencontre_id = $1 AND member_id = $2 ORDER BY created_at, id`,
|
||||
[req.params.id, req.user.memberId]
|
||||
),
|
||||
]);
|
||||
if (member.rowCount === 0) return res.status(403).json({ error: 'Compte membre introuvable.' });
|
||||
if (!member.rows[0].valide) {
|
||||
return res.status(403).json({ error: "Votre adhésion doit être validée pour vous inscrire." });
|
||||
}
|
||||
const dup = await query(
|
||||
'SELECT 1 FROM inscriptions WHERE rencontre_id = $1 AND email = $2',
|
||||
[id, email]
|
||||
);
|
||||
if (dup.rowCount > 0) {
|
||||
return res.status(409).json({ error: 'Une inscription existe déjà avec cet email.' });
|
||||
}
|
||||
await query(
|
||||
`INSERT INTO inscriptions (rencontre_id, nom, entreprise, email, tel, statut)
|
||||
VALUES ($1, $2, $3, $4, $5, 'en_attente')`,
|
||||
[id, nom, entreprise, email, tel]
|
||||
);
|
||||
res.status(201).json({ ok: true });
|
||||
if (rencontre.rowCount === 0) return res.status(404).json({ error: 'Rencontre introuvable' });
|
||||
res.json({ member: member.rows[0], rencontre: rencontre.rows[0], participants: participants.rows });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// One authenticated member account can register several named people, within
|
||||
// both the per-account limit and the event's total capacity.
|
||||
publicRouter.post(
|
||||
'/rencontres/:id/inscriptions',
|
||||
publicFormLimiter,
|
||||
requireAuth('member'),
|
||||
validate(idParam, 'params'),
|
||||
validate(inscriptionPublicSchema),
|
||||
async (req, res, next) => {
|
||||
let client;
|
||||
try {
|
||||
client = await pool.connect();
|
||||
const { id } = req.params;
|
||||
const participants = req.data.participants;
|
||||
const names = participants.map((name) => name.toLocaleLowerCase('fr-FR'));
|
||||
if (new Set(names).size !== names.length) {
|
||||
return res.status(400).json({ error: 'Chaque participant doit avoir un nom différent.' });
|
||||
}
|
||||
|
||||
await client.query('BEGIN');
|
||||
// Lock the event before counting places so concurrent registrations
|
||||
// cannot overbook it.
|
||||
const renc = await client.query(
|
||||
`SELECT id, places, participants_par_compte FROM rencontres
|
||||
WHERE id = $1 AND date_renc >= CURRENT_DATE FOR UPDATE`,
|
||||
[id]
|
||||
);
|
||||
if (renc.rowCount === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(404).json({ error: 'Rencontre introuvable' });
|
||||
}
|
||||
const member = await client.query(
|
||||
'SELECT id, nom, email, tel, valide FROM members WHERE id = $1',
|
||||
[req.user.memberId]
|
||||
);
|
||||
if (member.rowCount === 0 || !member.rows[0].valide) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(403).json({ error: "Votre adhésion doit être validée pour vous inscrire." });
|
||||
}
|
||||
if (participants.length > renc.rows[0].participants_par_compte) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(409).json({
|
||||
error: `Cette rencontre autorise ${renc.rows[0].participants_par_compte} participant(s) maximum par compte.`,
|
||||
});
|
||||
}
|
||||
|
||||
const otherCount = await client.query(
|
||||
`SELECT COUNT(*)::int AS n FROM inscriptions
|
||||
WHERE rencontre_id = $1 AND (member_id IS NULL OR member_id <> $2)`,
|
||||
[id, req.user.memberId]
|
||||
);
|
||||
if (otherCount.rows[0].n + participants.length > renc.rows[0].places) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(409).json({ error: 'Il ne reste pas assez de places pour ces participants.' });
|
||||
}
|
||||
|
||||
const existing = await client.query(
|
||||
`SELECT id, nom FROM inscriptions
|
||||
WHERE rencontre_id = $1 AND member_id = $2 ORDER BY created_at, id`,
|
||||
[id, req.user.memberId]
|
||||
);
|
||||
const available = new Map();
|
||||
for (const row of existing.rows) {
|
||||
const key = row.nom.toLocaleLowerCase('fr-FR');
|
||||
if (!available.has(key)) available.set(key, []);
|
||||
available.get(key).push(row);
|
||||
}
|
||||
|
||||
const keptIds = [];
|
||||
const account = member.rows[0];
|
||||
for (const nom of participants) {
|
||||
const key = nom.toLocaleLowerCase('fr-FR');
|
||||
const row = available.get(key)?.shift();
|
||||
if (row) {
|
||||
keptIds.push(row.id);
|
||||
await client.query(
|
||||
'UPDATE inscriptions SET nom=$1, entreprise=$2, email=$3, tel=$4 WHERE id=$5',
|
||||
[nom, account.nom, account.email, account.tel, row.id]
|
||||
);
|
||||
} else {
|
||||
const inserted = await client.query(
|
||||
`INSERT INTO inscriptions (rencontre_id, member_id, nom, entreprise, email, tel, statut)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'en_attente') RETURNING id`,
|
||||
[id, account.id, nom, account.nom, account.email, account.tel]
|
||||
);
|
||||
keptIds.push(inserted.rows[0].id);
|
||||
}
|
||||
}
|
||||
await client.query(
|
||||
`DELETE FROM inscriptions
|
||||
WHERE rencontre_id = $1 AND member_id = $2 AND NOT (id = ANY($3::int[]))`,
|
||||
[id, account.id, keptIds]
|
||||
);
|
||||
await client.query('COMMIT');
|
||||
res.status(201).json({ ok: true, participants: participants.length });
|
||||
} catch (err) {
|
||||
if (client) await client.query('ROLLBACK').catch(() => {});
|
||||
next(err);
|
||||
} finally {
|
||||
client?.release();
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in new issue
Block a user