Asset brand + pagina download + 1-click download endpoint

- assets/: script generate_logo.py + generate_ls_images.py per produrre
  loghi (mark/horizontal light+dark/stacked light+dark) e immagini
  prodotto Lemon Squeezy (thumbnail 1024x1024, cover 1920x1080,
  OG 1200x630). Riproducibili: rilanciare gli script per rigenerare.
- landing/download.html: pagina con form email+chiave o autofill da
  query string ?key=&email= (link 1-click dall'email). Bottoni
  Mac/Windows + istruzioni installazione per OS.
- landing/index.html: link Download in nav, hero ("Hai gia
  acquistato?"), pricing card ("Hai gia una licenza?"), footer.
- landing/js/main.js: rimosso warning placeholder LS (URL ora reale).
- server/src/downloads.js: endpoint GET /api/download/:platform
  che verifica (key, email) contro DB e redirect a URL firmato del
  binario. Errori chiari (400/401/403/503) leggibili dall'utente.
- server/src/email.js: template Resend con 2 bottoni 1-click
  (link con key+email gia compilati).
- server/src/server.js: route aggiunta.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
luzadevandClaude Opus 4.7 committed 2026-06-09 01:29:48 +02:00
1 parent 8272985459
commit bcd4f22b1e
17 files changed
+812 -28

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
/**
* Endpoint "1-click download" per i clienti.
*
* URL: GET /api/download/:platform?key=XXXX&email=YYY
*
* Verifica la coppia (key, email) contro la tabella licenses, recupera
* l'ultima release attiva per la piattaforma, genera un signed URL e
* fa redirect 302. Il link nell'email viene cliccato dall'utente: con
* un click parte il download.
*
* Sicurezza:
* - Niente token sulla URL: la chiave e' gia "il segreto"
* - URL signed con scadenza breve (default 5 min)
* - Nessuna info di licenza leakata in risposta (ritorna solo redirect o 401/404)
*/
import crypto from "node:crypto";
import { one } from "./db.js";
const DOWNLOAD_TTL = Number(process.env.DOWNLOAD_URL_TTL_SECONDS || 300);
const PUBLIC_BASE = process.env.PUBLIC_BASE_URL || "https://musictools.djluza.com";
function signPayload(payload, secret) {
return crypto.createHmac("sha256", secret).update(payload).digest("base64url");
}
const normEmail = (s) => String(s || "").trim().toLowerCase();
const normKey = (s) => String(s || "").trim().toUpperCase();
export async function downloadByKey(req, res) {
const platform = String(req.params.platform || "").toLowerCase();
if (platform !== "macos" && platform !== "windows") {
return res.status(400).type("text/plain")
.send("Piattaforma non valida. Usa /api/download/macos o /api/download/windows.");
}
const key = normKey(req.query.key);
const email = normEmail(req.query.email);
if (!key || !email) {
return res.status(400).type("text/plain")
.send("Mancano email o chiave. Controlla il link nella tua email di acquisto.");
}
const license = await one(
"SELECT id, status FROM licenses WHERE license_key=? AND email=? LIMIT 1",
[key, email],
);
if (!license) {
return res.status(401).type("text/plain")
.send("Email o chiave non valide. Verifica il link nell'email o scrivici a info@djluza.com.");
}
if (license.status !== "active") {
return res.status(403).type("text/plain")
.send("Licenza non piu' attiva (rimborsata o revocata).");
}
const release = await one(
`SELECT file_path FROM releases
WHERE platform=? ORDER BY published_at DESC LIMIT 1`,
[platform],
);
if (!release) {
return res.status(503).type("text/plain")
.send(`Build ${platform} non ancora disponibile. Riprova fra qualche ora o scrivici a info@djluza.com.`);
}
const exp = Math.floor(Date.now() / 1000) + DOWNLOAD_TTL;
const sig = signPayload(`${release.file_path}.${exp}`, process.env.JWT_SECRET);
const url = `${PUBLIC_BASE}/api/download?file=${encodeURIComponent(release.file_path)}&exp=${exp}&sig=${sig}`;
res.redirect(302, url);
}