Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s
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
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dev": {
|
||||
"baseUrl": "http://localhost:9000"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dev": {
|
||||
"internalSecret": "<INTERNAL_API_SECRET di medusa/apps/backend/.env>",
|
||||
"publishableKey": "<MEDUSA_PUBLISHABLE_KEY stampata da seed-base.ts>",
|
||||
"adminEmail": "admin@iti.local",
|
||||
"adminPassword": "<password utente admin Medusa>"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
### Test manuali delle route /internal/* (formato HTTP Client JetBrains/WebStorm).
|
||||
### Variabili: http-client.env.json (pubbliche) + http-client.private.env.json (segreti, non committato,
|
||||
### copiare da http-client.private.env.example.json). Ambiente: "dev".
|
||||
### Prerequisiti: infra su, Medusa avviato (npm run dev in medusa/), seed-base.ts e seed-installatori.ts eseguiti.
|
||||
### Eseguire le richieste in ordine: ognuna salva le variabili usate dalle successive.
|
||||
|
||||
### 0a. Login admin Medusa (serve solo per preparare l'ordine guest di test)
|
||||
POST {{baseUrl}}/auth/user/emailpass
|
||||
Content-Type: application/json
|
||||
|
||||
{ "email": "{{adminEmail}}", "password": "{{adminPassword}}" }
|
||||
|
||||
> {%
|
||||
client.global.set("adminToken", response.body.token)
|
||||
client.global.set("testEmail", "Test.Claim" + Date.now() + "@Example.com")
|
||||
%}
|
||||
|
||||
### 0b. Regione Italia
|
||||
GET {{baseUrl}}/store/regions
|
||||
x-publishable-api-key: {{publishableKey}}
|
||||
|
||||
> {% client.global.set("regionId", response.body.regions[0].id) %}
|
||||
|
||||
### 0c. Variante della pompa di calore (prezzo standard 5490, installatori 4650)
|
||||
GET {{baseUrl}}/store/products?handle=pompa-di-calore-aria-acqua-8kw
|
||||
x-publishable-api-key: {{publishableKey}}
|
||||
|
||||
> {% client.global.set("variantId", response.body.products[0].variants[0].id) %}
|
||||
|
||||
### 0d. Sales channel Web
|
||||
GET {{baseUrl}}/admin/sales-channels?name=Web
|
||||
Authorization: Bearer {{adminToken}}
|
||||
|
||||
> {% client.global.set("salesChannelId", response.body.sales_channels[0].id) %}
|
||||
|
||||
### 1a. Senza header -> 401
|
||||
POST {{baseUrl}}/internal/customers
|
||||
Content-Type: application/json
|
||||
|
||||
{ "email": "a@example.com", "first_name": "A", "last_name": "B" }
|
||||
|
||||
> {% client.test("401 senza segreto", () => client.assert(response.status === 401)) %}
|
||||
|
||||
### 1b. Segreto errato -> 401
|
||||
GET {{baseUrl}}/internal/customers/cus_x/orders
|
||||
x-internal-secret: sbagliato
|
||||
|
||||
> {% client.test("401 segreto errato", () => client.assert(response.status === 401)) %}
|
||||
|
||||
### 2a. Ordine guest di test: draft order con email (customer guest has_account=false)...
|
||||
POST {{baseUrl}}/admin/draft-orders
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "{{testEmail}}",
|
||||
"region_id": "{{regionId}}",
|
||||
"sales_channel_id": "{{salesChannelId}}",
|
||||
"items": [{ "variant_id": "{{variantId}}", "quantity": 1 }],
|
||||
"shipping_address": {
|
||||
"first_name": "Test", "last_name": "Claim", "address_1": "Via Roma 1",
|
||||
"city": "Milano", "postal_code": "20100", "country_code": "it"
|
||||
}
|
||||
}
|
||||
|
||||
> {% client.global.set("guestOrderId", response.body.draft_order.id) %}
|
||||
|
||||
### 2b. ...convertito in ordine
|
||||
POST {{baseUrl}}/admin/draft-orders/{{guestOrderId}}/convert-to-order
|
||||
Authorization: Bearer {{adminToken}}
|
||||
|
||||
### 3a. Creazione customer -> 201, created=true (email normalizzata in minuscolo)
|
||||
POST {{baseUrl}}/internal/customers
|
||||
x-internal-secret: {{internalSecret}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "email": "{{testEmail}}", "first_name": "Test", "last_name": "Claim", "company_name": "Test srl" }
|
||||
|
||||
> {%
|
||||
client.test("customer creato", () => {
|
||||
client.assert(response.status === 201)
|
||||
client.assert(response.body.created === true)
|
||||
client.assert(response.body.customer.has_account === true)
|
||||
})
|
||||
client.global.set("customerId", response.body.customer.id)
|
||||
%}
|
||||
|
||||
### 3b. Stessa email -> 200, created=false, stesso id (idempotente)
|
||||
POST {{baseUrl}}/internal/customers
|
||||
x-internal-secret: {{internalSecret}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "email": "{{testEmail}}", "first_name": "Test", "last_name": "Claim" }
|
||||
|
||||
> {%
|
||||
client.test("idempotente", () => {
|
||||
client.assert(response.status === 200)
|
||||
client.assert(response.body.created === false)
|
||||
client.assert(response.body.customer.id === client.global.get("customerId"))
|
||||
})
|
||||
%}
|
||||
|
||||
### 4a. Carrello guest con la pompa di calore
|
||||
POST {{baseUrl}}/store/carts
|
||||
x-publishable-api-key: {{publishableKey}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "region_id": "{{regionId}}" }
|
||||
|
||||
> {% client.global.set("cartId", response.body.cart.id) %}
|
||||
|
||||
### 4b. Riga -> prezzo standard 5490
|
||||
POST {{baseUrl}}/store/carts/{{cartId}}/line-items
|
||||
x-publishable-api-key: {{publishableKey}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "variant_id": "{{variantId}}", "quantity": 1 }
|
||||
|
||||
> {% client.test("prezzo standard", () => client.assert(response.body.cart.items[0].unit_price === 5490)) %}
|
||||
|
||||
### 5. Customer nel gruppo installatori
|
||||
POST {{baseUrl}}/internal/customers/{{customerId}}/groups
|
||||
x-internal-secret: {{internalSecret}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "action": "add" }
|
||||
|
||||
> {% client.test("membro", () => client.assert(response.body.member === true)) %}
|
||||
|
||||
### 6. Associa il carrello al customer -> riprezzato col listino installatori (4650)
|
||||
POST {{baseUrl}}/internal/carts/{{cartId}}/customer
|
||||
x-internal-secret: {{internalSecret}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "customer_id": "{{customerId}}" }
|
||||
|
||||
> {%
|
||||
client.test("carrello associato e listino applicato", () => {
|
||||
client.assert(response.body.cart.customer_id === client.global.get("customerId"))
|
||||
client.assert(response.body.cart.items[0].unit_price === 4650)
|
||||
})
|
||||
%}
|
||||
|
||||
### 7a. Claim ordini guest con la stessa email -> l'ordine 2b passa al customer
|
||||
POST {{baseUrl}}/internal/orders/claim
|
||||
x-internal-secret: {{internalSecret}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "customer_id": "{{customerId}}", "email": "{{testEmail}}" }
|
||||
|
||||
> {%
|
||||
client.test("ordine collegato", () => {
|
||||
client.assert(response.body.claimed.includes(client.global.get("guestOrderId")))
|
||||
})
|
||||
%}
|
||||
|
||||
### 7b. Claim ripetuto -> niente da collegare
|
||||
POST {{baseUrl}}/internal/orders/claim
|
||||
x-internal-secret: {{internalSecret}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "customer_id": "{{customerId}}", "email": "{{testEmail}}" }
|
||||
|
||||
> {% client.test("idempotente", () => client.assert(response.body.claimed.length === 0)) %}
|
||||
|
||||
### 8. Ordini del customer -> contiene l'ordine collegato
|
||||
GET {{baseUrl}}/internal/customers/{{customerId}}/orders?limit=20&offset=0
|
||||
x-internal-secret: {{internalSecret}}
|
||||
|
||||
> {%
|
||||
client.test("ordine in elenco", () => {
|
||||
client.assert(response.body.orders.some((o) => o.id === client.global.get("guestOrderId")))
|
||||
})
|
||||
%}
|
||||
|
||||
### 9. Rimozione dal gruppo installatori
|
||||
POST {{baseUrl}}/internal/customers/{{customerId}}/groups
|
||||
x-internal-secret: {{internalSecret}}
|
||||
Content-Type: application/json
|
||||
|
||||
{ "action": "remove" }
|
||||
|
||||
> {% client.test("non più membro", () => client.assert(response.body.member === false)) %}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Receiver di test per i webhook Medusa -> Fastify, finché apps/api non implementa /webhooks/medusa.
|
||||
// Uso (in medusa/apps/backend): node http/webhook-receiver.mjs [porta]
|
||||
// Legge WEBHOOK_SECRET da .env, verifica la firma x-signature e stampa ogni richiesta.
|
||||
// Con FAIL=3 risponde 503 alle prime 3 richieste (per vedere i retry).
|
||||
import { createHmac, timingSafeEqual } from "node:crypto"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { createServer } from "node:http"
|
||||
|
||||
const port = Number(process.argv[2] ?? 3999)
|
||||
const secret =
|
||||
process.env.WEBHOOK_SECRET ??
|
||||
readFileSync(".env", "utf8").match(/^WEBHOOK_SECRET=(.*)$/m)?.[1]?.trim()
|
||||
let failuresLeft = Number(process.env.FAIL ?? 0)
|
||||
|
||||
if (!secret) {
|
||||
console.error("WEBHOOK_SECRET non trovato (env o .env)")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
createServer((req, res) => {
|
||||
let body = ""
|
||||
req.on("data", (chunk) => (body += chunk))
|
||||
req.on("end", () => {
|
||||
const expected = createHmac("sha256", secret).update(body).digest("hex")
|
||||
const got = String(req.headers["x-signature"] ?? "")
|
||||
const valid =
|
||||
got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected))
|
||||
const status = failuresLeft > 0 ? 503 : valid ? 200 : 401
|
||||
if (failuresLeft > 0) failuresLeft--
|
||||
|
||||
let payload = body
|
||||
try {
|
||||
const p = JSON.parse(body)
|
||||
payload = `${p.event} id=${p.id} occurred_at=${p.occurred_at} data=${JSON.stringify(p.data)}`
|
||||
} catch {}
|
||||
console.log(
|
||||
`${new Date().toISOString()} ${req.method} ${req.url} firma=${valid ? "OK" : "NON VALIDA"} -> ${status}\n ${payload}`
|
||||
)
|
||||
res.writeHead(status, { "content-type": "application/json" }).end('{"ok":true}')
|
||||
})
|
||||
}).listen(port, () => console.log(`webhook receiver su http://localhost:${port}/webhooks/medusa`))
|
||||
Reference in New Issue
Block a user