Deploy stack su Proxmox / deploy (push) Failing after 3s
Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA), apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/; infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e workflow Gitea Actions per il deploy via SSH a ogni push su master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
import type { FastifyPluginAsyncZod } from "fastify-type-provider-zod"
|
|
import { z } from "zod"
|
|
import { contactRequests } from "../../db/schema/index.js"
|
|
import { findUserById, normalizeEmail } from "../../services/auth.js"
|
|
import { recordConsents } from "../../services/consents.js"
|
|
import { createFormToken, FORM_MIN_SECONDS, formGuardFields, passesFormGuard } from "../../services/form-guard.js"
|
|
import { notifyStaff } from "../../services/notifications.js"
|
|
|
|
/** Campi comuni dei form pubblici con dati personali (contatti, showroom). */
|
|
export const personalDataFields = {
|
|
name: z.string().trim().min(1, "Nome obbligatorio").max(120),
|
|
email: z.string().trim().toLowerCase().pipe(z.email("Email non valida")),
|
|
phone: z.string().trim().min(5).max(30).optional(),
|
|
acceptPrivacy: z.literal(true, { error: "Il consenso al trattamento dei dati è obbligatorio" }),
|
|
acceptMarketing: z.boolean().optional(),
|
|
...formGuardFields,
|
|
}
|
|
|
|
export const messageResponse = z.object({ message: z.string() })
|
|
|
|
const CONTACT_RECEIVED = "Messaggio inviato: ti risponderemo al più presto"
|
|
|
|
/** Form contatti e token anti-spam dei form pubblici (vedi services/form-guard.ts). */
|
|
const contactRoutes: FastifyPluginAsyncZod = async (app) => {
|
|
const { config, db } = app
|
|
|
|
// GET /forms/token — da chiedere all'apertura del form; il form va inviato dopo FORM_MIN_SECONDS
|
|
app.get(
|
|
"/forms/token",
|
|
{
|
|
config: { rateLimit: { max: 60, timeWindow: "1 minute" } },
|
|
schema: { response: { 200: z.object({ token: z.string(), minSeconds: z.number().int(), expiresAt: z.string() }) } },
|
|
},
|
|
async (_request, reply) => {
|
|
reply.header("cache-control", "no-store")
|
|
const { token, expiresAt } = createFormToken(config.COOKIE_SECRET)
|
|
return { token, minSeconds: FORM_MIN_SECONDS, expiresAt: expiresAt.toISOString() }
|
|
},
|
|
)
|
|
|
|
// POST /contact — salva la richiesta e avvisa lo staff. Spam (honeypot, invio troppo rapido) → stessa risposta, nulla salvato
|
|
app.post(
|
|
"/contact",
|
|
{
|
|
onRequest: [app.optionalAuthenticate],
|
|
config: { rateLimit: { max: 5, timeWindow: "15 minutes" } },
|
|
schema: {
|
|
body: z.object({
|
|
...personalDataFields,
|
|
subject: z.string().trim().min(1).max(200).optional(),
|
|
message: z.string().trim().min(10, "Messaggio troppo breve").max(5000),
|
|
}),
|
|
response: { 200: messageResponse },
|
|
},
|
|
},
|
|
async (request) => {
|
|
const body = request.body
|
|
if (!passesFormGuard(request, config.COOKIE_SECRET, body, "contact")) return { message: CONTACT_RECEIVED }
|
|
|
|
const user = request.user ? await findUserById(db, request.user.sub) : undefined
|
|
const userId = user && !user.disabledAt ? user.id : null
|
|
const email = normalizeEmail(body.email)
|
|
|
|
const created = await db.transaction(async (tx) => {
|
|
const [row] = await tx
|
|
.insert(contactRequests)
|
|
.values({ userId, name: body.name, email, phone: body.phone ?? null, subject: body.subject ?? null, message: body.message })
|
|
.returning({ id: contactRequests.id })
|
|
await recordConsents(tx, {
|
|
userId,
|
|
email,
|
|
version: config.PRIVACY_POLICY_VERSION,
|
|
source: "contact",
|
|
ip: request.ip,
|
|
marketing: body.acceptMarketing === true,
|
|
})
|
|
return row!
|
|
})
|
|
|
|
await notifyStaff(app, {
|
|
title: `Nuovo messaggio dal sito - ${body.name}`,
|
|
intro: "È arrivato un nuovo messaggio dal form contatti.",
|
|
rows: [
|
|
{ label: "Nome", value: body.name },
|
|
{ label: "Email", value: email },
|
|
{ label: "Telefono", value: body.phone ?? "-" },
|
|
{ label: "Oggetto", value: body.subject ?? "-" },
|
|
{ label: "Messaggio", value: body.message },
|
|
],
|
|
url: `${config.PRIVATE_APP_URL}/admin/contatti/${created.id}`,
|
|
})
|
|
request.log.info({ contactRequestId: created.id, userId }, "nuova richiesta di contatto")
|
|
return { message: CONTACT_RECEIVED }
|
|
},
|
|
)
|
|
}
|
|
|
|
export default contactRoutes
|