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:
1 parent
05a2a58ace
commit
5202b2c586
19 files changed
+1572
-31
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
// ============================================================
|
||||
|
||||
Reference in new issue
Block a user