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:
1 parent
8272985459
commit
bcd4f22b1e
17 files changed
+812
-28
No files matched your search
@@ -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);
|
||||
}
|
||||
+29
-3
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
const FROM = process.env.EMAIL_FROM || "MusicTools <noreply@djluza.com>";
|
||||
const BASE = process.env.PUBLIC_BASE_URL || "https://musictools.djluza.com";
|
||||
|
||||
export async function sendLicenseEmail(to, licenseKey) {
|
||||
if (!process.env.RESEND_API_KEY) {
|
||||
@@ -15,6 +16,10 @@ export async function sendLicenseEmail(to, licenseKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const q = `key=${encodeURIComponent(licenseKey)}&email=${encodeURIComponent(to)}`;
|
||||
const macUrl = `${BASE}/api/download/macos?${q}`;
|
||||
const winUrl = `${BASE}/api/download/windows?${q}`;
|
||||
|
||||
const html = `
|
||||
<div style="font-family:-apple-system,Segoe UI,sans-serif;max-width:560px;margin:0 auto;padding:24px;color:#111">
|
||||
<h1 style="color:#1db954;margin:0 0 14px;font-size:22px">Grazie per aver scelto MusicTools!</h1>
|
||||
@@ -24,13 +29,34 @@ export async function sendLicenseEmail(to, licenseKey) {
|
||||
<p style="font-size:22px;letter-spacing:2px;font-family:monospace;background:#f4f4f4;padding:16px;border-radius:10px;text-align:center;margin:0 0 24px;color:#000">
|
||||
${licenseKey}
|
||||
</p>
|
||||
|
||||
<p style="font-size:15px;margin:0 0 12px"><strong>Scarica l'app:</strong></p>
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px">
|
||||
<tr>
|
||||
<td style="padding-right:10px">
|
||||
<a href="${macUrl}" style="display:inline-block;background:#1db954;color:#fff;text-decoration:none;padding:12px 22px;border-radius:999px;font-weight:700;font-size:14px">
|
||||
🍎 Scarica per Mac
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="${winUrl}" style="display:inline-block;background:#1db954;color:#fff;text-decoration:none;padding:12px 22px;border-radius:999px;font-weight:700;font-size:14px">
|
||||
🪟 Scarica per Windows
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="font-size:15px;margin:0 0 8px"><strong>Come attivare:</strong></p>
|
||||
<ol style="font-size:14.5px;line-height:1.6;padding-left:22px">
|
||||
<li>Scarica MusicTools per <a href="https://musictools.djluza.com#pricing">Mac o Windows</a></li>
|
||||
<li>Apri l'app: alla prima schermata ti chiedera' email e chiave</li>
|
||||
<li>Inserisci questa email (<code>${to}</code>) e la chiave qui sopra</li>
|
||||
<li>Clicca uno dei bottoni qui sopra per scaricare l'installer</li>
|
||||
<li>Installa l'app (su Mac: tasto destro → Apri → Apri; su Windows: Esegui comunque)</li>
|
||||
<li>Apri MusicTools: la prima schermata ti chiede email e chiave</li>
|
||||
<li>Inserisci <strong>${to}</strong> e la chiave qui sopra</li>
|
||||
</ol>
|
||||
<p style="font-size:14px;color:#555;margin:18px 0 0">Puoi attivare la licenza fino a 3 dispositivi (Mac e Windows mixati).</p>
|
||||
<p style="font-size:13px;color:#888;margin:8px 0 0">
|
||||
Hai bisogno di riscaricare in futuro? Vai su <a href="${BASE}/download" style="color:#1db954">${BASE.replace(/^https?:\/\//, "")}/download</a> e inserisci email + chiave.
|
||||
</p>
|
||||
<hr style="border:none;border-top:1px solid #eee;margin:28px 0"/>
|
||||
<p style="color:#666;font-size:12px;margin:0">Hai problemi? Scrivici a <a href="mailto:info@djluza.com">info@djluza.com</a></p>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import express from "express";
|
||||
import * as license from "./license.js";
|
||||
import * as updates from "./updates.js";
|
||||
import * as ls from "./lemonsqueezy.js";
|
||||
import * as dl from "./downloads.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -45,6 +46,9 @@ app.post("/api/license/deactivate", license.deactivate);
|
||||
app.get("/api/latest", updates.latest);
|
||||
app.get("/api/download", updates.download);
|
||||
|
||||
// 1-click download dall'email (key+email come query -> redirect signed)
|
||||
app.get("/api/download/:platform", dl.downloadByKey);
|
||||
|
||||
// 404 JSON solo per /api/*
|
||||
app.use("/api", (_req, res) => res.status(404).json({ error: "Not found" }));
|
||||
|
||||
|
||||
Reference in new issue
Block a user