feat(aep): carte AEP — push Gitea 2026-04-28
This commit is contained in:
94
server/api/report-general.post.ts
Normal file
94
server/api/report-general.post.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* POST /api/report-general
|
||||
*
|
||||
* Signalement général (bug, contenu inapproprié, suggestion)
|
||||
*
|
||||
* Body : { category: string, description: string, email?: string }
|
||||
* Rate limit : 5/IP/jour
|
||||
* Envoi vers jules@trans-former.fr via Resend API
|
||||
*/
|
||||
|
||||
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||
|
||||
const EMAIL_JULES = process.env.EMAIL_JULES || 'jules@trans-former.fr'
|
||||
|
||||
const VALID_CATEGORIES = ['Une fiche', 'Le chatbot', 'La carte', 'Autre'] as const
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 1. IP
|
||||
const ip =
|
||||
getHeader(event, 'x-forwarded-for')?.split(',')[0].trim() ||
|
||||
event.node.req.socket?.remoteAddress ||
|
||||
'0.0.0.0'
|
||||
|
||||
// 2. Rate limit 5/IP/jour
|
||||
const allowed = checkRateLimitJson(ip, 'report-general', 5)
|
||||
if (!allowed) {
|
||||
throw createError({
|
||||
statusCode: 429,
|
||||
statusMessage: 'Limite de 5 signalements par jour atteinte.',
|
||||
})
|
||||
}
|
||||
|
||||
// 3. Lire le body
|
||||
const body = await readBody(event)
|
||||
const category: string = (body?.category ?? '').trim()
|
||||
const description: string = (body?.description ?? '').trim()
|
||||
const email: string = (body?.email ?? '').trim()
|
||||
|
||||
// 4. Validation
|
||||
if (!VALID_CATEGORIES.includes(category as any)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Catégorie invalide.' })
|
||||
}
|
||||
if (!description || description.length < 5 || description.length > 500) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Description requise (5-500 caractères).' })
|
||||
}
|
||||
if (email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(email)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Email invalide.' })
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Envoi via Resend
|
||||
const resendApiKey = process.env.RESEND_API_KEY
|
||||
if (!resendApiKey) {
|
||||
console.error('[report-general] RESEND_API_KEY manquante')
|
||||
throw createError({ statusCode: 500, statusMessage: 'Configuration email manquante.' })
|
||||
}
|
||||
|
||||
const submittedAt = new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })
|
||||
|
||||
try {
|
||||
await $fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${resendApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: 'AEP Signalement <noreply@trans-former.fr>',
|
||||
to: EMAIL_JULES,
|
||||
subject: `[AEP] Signalement — ${category}`,
|
||||
html: `
|
||||
<h2>Signalement AEP — ${category}</h2>
|
||||
<p><strong>Date :</strong> ${submittedAt}</p>
|
||||
<p><strong>Catégorie :</strong> ${category}</p>
|
||||
${email ? `<p><strong>Email expéditeur :</strong> ${email}</p>` : '<p><em>Pas d\'email fourni</em></p>'}
|
||||
<p><strong>Description :</strong></p>
|
||||
<blockquote style="border-left:3px solid #ccc;padding-left:12px;color:#555;">
|
||||
${description.replace(/\n/g, '<br/>')}
|
||||
</blockquote>
|
||||
`,
|
||||
}),
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.error('[report-general] Erreur Resend:', e?.message ?? e)
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
statusMessage: 'Erreur envoi email — réessaie dans quelques instants.',
|
||||
})
|
||||
}
|
||||
|
||||
return { ok: true, message: 'Signalement envoyé, merci !' }
|
||||
})
|
||||
Reference in New Issue
Block a user