Setup commercial launch: licenze + backend Cloudflare Worker

Client (Python/JS):
- core/license.py: attivazione, validazione, deactivate (HTTP client + JWT decode)
- core/config.py: campi license_* + URL endpoint + costanti grace/revalidate
- api/bridge.py: get_init_data ritorna license status; activate_license/
  deactivate_license/revalidate_license/open_purchase_page
- check_update riscritto: punta a musictools.djluza.com/api/latest con
  Bearer token, URL di download firmato dal server solo se licensed
- webui: schermata di attivazione bloccante all'avvio; sezione Licenza
  in Impostazioni con verifica/disattiva

Backend (server/):
- Cloudflare Worker + D1 + R2 (vedi server/README.md)
- Endpoints: /api/license/{activate,validate,deactivate}, /api/latest,
  /api/webhook/lemonsqueezy, /api/health
- JWT HS256 con rotazione; max 3 attivazioni/licenza
- Generazione licenze via webhook Lemon Squeezy + email Resend
- Schema D1 in migrations/0001_init.sql

Memory: nuova musictools-commercial-launch.md, rimosso pending obsoleto

NON taggare finche backend non e' deployato (l'app e' bloccante)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
luzadevandClaude Opus 4.7 committed 2026-06-08 23:36:41 +02:00
1 parent 05a2a58ace
commit 5202b2c586
19 files changed
+1572 -31

No files matched your search

+154
View File
@@ -1256,3 +1256,157 @@ input[type="number"]::-webkit-inner-spin-button {
max-height: none;
}
}
/* ============================================================
SCHERMATA ATTIVAZIONE LICENZA
============================================================ */
.activate-screen {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
background:
radial-gradient(ellipse at top, rgba(29, 185, 84, 0.18), transparent 60%),
linear-gradient(180deg, #0a0a0a 0%, #060606 100%);
overflow: auto;
}
.activate-screen[hidden] { display: none; }
.activate-card {
width: 100%;
max-width: 460px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 18px;
padding: 40px 36px;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
text-align: center;
}
.activate-logo {
width: 72px;
height: 72px;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 22px;
background: linear-gradient(135deg, #1db954 0%, #137a37 100%);
color: #fff;
font-size: 36px;
box-shadow: 0 12px 30px rgba(29, 185, 84, 0.4);
}
.activate-title {
margin: 0 0 8px;
font-size: 22px;
font-weight: 800;
color: var(--text);
}
.activate-sub {
margin: 0 0 28px;
font-size: 13.5px;
color: var(--text-muted);
line-height: 1.55;
}
.activate-form {
text-align: left;
}
.activate-form .field {
margin-bottom: 14px;
}
.activate-error {
background: rgba(220, 53, 69, 0.12);
border: 1px solid rgba(220, 53, 69, 0.4);
color: #ff6b7a;
padding: 10px 14px;
border-radius: 12px;
font-size: 13px;
margin: 6px 0 14px;
}
.activate-actions {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 6px;
}
.activate-actions .btn {
width: 100%;
justify-content: center;
}
.activate-foot {
margin-top: 24px;
text-align: center;
font-size: 12px;
color: var(--text-muted);
}
.activate-foot a {
color: var(--accent);
text-decoration: none;
}
.activate-foot a:hover { text-decoration: underline; }
/* ============================================================
IMPOSTAZIONI > LICENZA
============================================================ */
.license-box {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 14px;
}
.lic-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 8px 0;
border-bottom: 1px dashed var(--divider);
font-size: 13.5px;
}
.lic-row:last-child { border-bottom: none; }
.lic-row span {
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.6px;
font-size: 11px;
font-weight: 600;
}
.lic-row strong {
color: var(--text);
font-weight: 700;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
word-break: break-all;
}
.lic-warn {
padding: 14px;
border-radius: 12px;
background: rgba(220, 53, 69, 0.12);
border: 1px solid rgba(220, 53, 69, 0.35);
color: #ff6b7a;
font-weight: 700;
text-align: center;
}
.license-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
+54
View File
@@ -520,6 +520,22 @@
</div>
</div>
<h2 class="section-label">Licenza</h2>
<div class="card">
<div class="license-box" id="licenseStatusBox"></div>
<div class="license-actions">
<button class="btn btn-ghost pill" id="revalidateLicenseBtn">Verifica ora</button>
<button class="btn btn-danger pill" id="deactivateLicenseBtn">Disattiva su questo device</button>
</div>
<div class="hint">
<span class="hint-ico">ⓘ</span>
<div>
Disattivando liberi uno slot di attivazione, utile se sposti l'app su un altro Mac/PC.
Puoi riattivare in qualsiasi momento con la stessa email e chiave.
</div>
</div>
</div>
<h2 class="section-label">Aggiornamenti</h2>
<div class="card">
<div class="row spread">
@@ -813,6 +829,44 @@
</main>
</div>
<!-- ============== SCHERMATA ATTIVAZIONE LICENZA ============== -->
<div class="activate-screen" id="activateScreen" hidden>
<div class="activate-card">
<div class="activate-logo">♫</div>
<h1 class="activate-title">Benvenuto in MusicTools</h1>
<p class="activate-sub">
Inserisci la tua licenza per iniziare. Se non ne hai ancora una,
acquistala dal nostro sito ed entra in pochi secondi.
</p>
<div class="activate-form">
<div class="field">
<label class="field-label">Email dell'acquisto</label>
<input type="email" id="actEmail" class="input pill" placeholder="tu@esempio.com" autocomplete="email" />
</div>
<div class="field">
<label class="field-label">Chiave di licenza</label>
<input type="text" id="actKey" class="input pill" placeholder="XXXX-XXXX-XXXX-XXXX" autocomplete="off" />
</div>
<div class="activate-error" id="actError" hidden></div>
<div class="activate-actions">
<button class="btn btn-primary pill btn-lg" id="actActivateBtn">
<span class="ico">🔓</span> Attiva
</button>
<button class="btn btn-ghost pill btn-lg" id="actBuyBtn">
Compra una licenza
</button>
</div>
</div>
<div class="activate-foot">
Hai problemi? Scrivici a <a href="mailto:info@djluza.com">info@djluza.com</a>
</div>
</div>
</div>
<!-- ============== MODAL: Spotify Guide ============== -->
<div class="modal-backdrop" id="guideModal" hidden>
<div class="modal">
+127
View File
@@ -7,6 +7,8 @@ const $$ = (sel) => Array.from(document.querySelectorAll(sel));
const state = {
config: {},
license: { licensed: false },
purchaseUrl: "https://musictools.djluza.com",
loaded: null, // { kind: "urls"|"tracks", urls?, tracks?, count }
dlOutputDir: "",
upDir: "",
@@ -45,6 +47,90 @@ function parseQueryToTrack(q) {
return { artist: "", name: s };
}
// ============================================================
// Licenza: gating della UI principale
// ============================================================
function applyLicenseGate() {
const lic = state.license || { licensed: false };
const screen = $("#activateScreen");
const appEl = document.querySelector(".app");
if (!lic.licensed) {
if (screen) screen.hidden = false;
if (appEl) appEl.style.display = "none";
} else {
if (screen) screen.hidden = true;
if (appEl) appEl.style.display = "";
}
}
function fmtDate(epoch) {
if (!epoch) return "—";
try {
return new Date(epoch * 1000).toLocaleDateString("it-IT", {
day: "2-digit", month: "short", year: "numeric"
});
} catch (_e) { return "—"; }
}
function renderLicenseStatus() {
const lic = state.license || {};
const box = $("#licenseStatusBox");
if (!box) return; // panel non ancora montato
if (!lic.licensed) {
box.innerHTML = `<div class="lic-warn">Licenza non attiva</div>`;
return;
}
box.innerHTML = `
<div class="lic-row"><span>Email</span><strong>${lic.email || "—"}</strong></div>
<div class="lic-row"><span>Chiave</span><strong>${lic.key || "—"}</strong></div>
<div class="lic-row"><span>Attivata il</span><strong>${fmtDate(lic.activated_at)}</strong></div>
<div class="lic-row"><span>Ultima verifica</span><strong>${fmtDate(lic.last_validated_at)}</strong></div>
`;
}
async function activateLicense() {
const email = $("#actEmail").value.trim();
const key = $("#actKey").value.trim();
const err = $("#actError");
err.hidden = true;
err.textContent = "";
if (!email || !key) {
err.textContent = "Inserisci email e chiave di licenza.";
err.hidden = false;
return;
}
const btn = $("#actActivateBtn");
btn.disabled = true;
const oldText = btn.innerHTML;
btn.innerHTML = "Attivazione in corso…";
try {
const res = await window.pywebview.api.activate_license({ email, key });
if (res.ok) {
state.license = res.license;
applyLicenseGate();
renderLicenseStatus();
toast("Licenza attivata. Benvenuto!", "success");
} else {
err.textContent = res.error || "Errore di attivazione.";
err.hidden = false;
}
} finally {
btn.disabled = false;
btn.innerHTML = oldText;
}
}
async function deactivateLicense() {
if (!confirm("Disattivare la licenza su questo dispositivo?")) return;
const res = await window.pywebview.api.deactivate_license();
if (res.ok) {
state.license = res.license;
applyLicenseGate();
renderLicenseStatus();
toast("Licenza disattivata", "info");
}
}
// ============================================================
// Bridge — eventi spinti dal backend
// ============================================================
@@ -234,10 +320,15 @@ async function init() {
await waitApi();
const data = await window.pywebview.api.get_init_data();
state.config = data.config || {};
state.license = data.license || { licensed: false };
state.purchaseUrl = data.purchase_url || "https://musictools.djluza.com";
$("#footerVersion").textContent = data.version;
$("#currentVersionLabel").textContent = data.version;
$("#guideBody").textContent = data.spotify_guide;
applyLicenseGate();
renderLicenseStatus();
// Populate Settings fields
$("#clientIdInput").value = state.config.client_id || "";
$("#clientSecretInput").value = state.config.client_secret || "";
@@ -855,6 +946,42 @@ $("#checkUpdateBtn").addEventListener("click", async () => {
}
});
// ============================================================
// Licenza: listener dello schermo di attivazione + impostazioni
// ============================================================
$("#actActivateBtn")?.addEventListener("click", activateLicense);
$("#actBuyBtn")?.addEventListener("click", async () => {
await window.pywebview.api.open_purchase_page();
});
$("#actKey")?.addEventListener("keydown", (e) => {
if (e.key === "Enter") activateLicense();
});
$("#deactivateLicenseBtn")?.addEventListener("click", deactivateLicense);
$("#revalidateLicenseBtn")?.addEventListener("click", async () => {
const btn = $("#revalidateLicenseBtn");
btn.disabled = true;
const old = btn.textContent;
btn.textContent = "Verifica in corso…";
try {
const res = await window.pywebview.api.revalidate_license();
if (res.ok) {
state.license = res.license;
renderLicenseStatus();
if (res.license.licensed) {
toast("Licenza verificata", "success");
} else {
applyLicenseGate();
toast("Licenza non piu valida", "error");
}
}
} finally {
btn.disabled = false;
btn.textContent = old;
}
});
// ============================================================
// Boot
// ============================================================