From 1bf3fa466d8e4f94eb8ac65aecc053c8395cee76 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Thu, 2 Jul 2026 00:44:13 +0800 Subject: [PATCH] Restore revoke action and fix main content scroll layout. Co-authored-by: Cursor --- src/license/api.mjs | 8 ++++ src/main.js | 11 +++++ src/preload.cjs | 3 ++ src/renderer/pages/licenseList.mjs | 75 ++++++++++++++++++++++++++---- src/styles/app.css | 17 +++++-- 5 files changed, 103 insertions(+), 11 deletions(-) diff --git a/src/license/api.mjs b/src/license/api.mjs index 6e71b07..f271a80 100644 --- a/src/license/api.mjs +++ b/src/license/api.mjs @@ -61,6 +61,14 @@ export async function createProductKeys(adminToken, body) { }); } +/** @param {string} adminToken @param {string} key */ +export async function revokeProductKey(adminToken, key) { + return request(adminToken, 'v1/admin/revoke', { + method: 'POST', + body: JSON.stringify({ key }), + }); +} + /** @param {string} adminToken @param {string} key */ export async function deleteProductKey(adminToken, key) { return request(adminToken, 'v1/admin/product-keys/delete', { diff --git a/src/main.js b/src/main.js index a0aab7d..32dda1b 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ import { deleteProductKey, getDownloadStats, listLicenses, + revokeProductKey, LicenseApiError, } from './license/api.mjs'; import { loadLicenseSettings, saveLicenseSettings } from './license/settings.mjs'; @@ -109,6 +110,16 @@ app.whenReady().then(() => { } }); + ipcMain.handle('license.revokeProductKey', async (_e, key) => { + try { + const { adminToken } = loadLicenseSettings(app.getPath('userData')); + await revokeProductKey(adminToken, String(key ?? '')); + return { ok: true }; + } catch (err) { + return licenseError(err); + } + }); + ipcMain.handle('license.deleteProductKey', async (_e, key) => { try { const { adminToken } = loadLicenseSettings(app.getPath('userData')); diff --git a/src/preload.cjs b/src/preload.cjs index d5ffb7c..87db3d6 100644 --- a/src/preload.cjs +++ b/src/preload.cjs @@ -27,6 +27,9 @@ contextBridge.exposeInMainWorld('license', { const res = unwrapLicense(await ipcRenderer.invoke('license.createProductKeys', body)); return res.result; }, + revokeProductKey: async (key) => { + unwrapLicense(await ipcRenderer.invoke('license.revokeProductKey', key)); + }, deleteProductKey: async (key) => { unwrapLicense(await ipcRenderer.invoke('license.deleteProductKey', key)); }, diff --git a/src/renderer/pages/licenseList.mjs b/src/renderer/pages/licenseList.mjs index a992a03..56317b0 100644 --- a/src/renderer/pages/licenseList.mjs +++ b/src/renderer/pages/licenseList.mjs @@ -19,6 +19,8 @@ export function renderLicenseListPage(root) { /** @type {import('../../license/models.mjs').LicenseEntry | null} */ let confirmDelete = null; + /** @type {import('../../license/models.mjs').LicenseEntry | null} */ + let confirmRevoke = null; function showError(msg) { if (!msg) { @@ -30,9 +32,53 @@ export function renderLicenseListPage(root) { $listError.textContent = msg; } - function renderModal() { - const existing = document.getElementById('delete-modal'); - if (existing) existing.remove(); + function closeModal(id) { + document.getElementById(id)?.remove(); + } + + function renderRevokeModal() { + closeModal('revoke-modal'); + if (!confirmRevoke) return; + + const backdrop = document.createElement('div'); + backdrop.id = 'revoke-modal'; + backdrop.className = 'modal-backdrop'; + backdrop.innerHTML = ` + + `; + document.body.appendChild(backdrop); + + backdrop.querySelector('#cancelRevoke')?.addEventListener('click', () => { + confirmRevoke = null; + backdrop.remove(); + }); + + backdrop.querySelector('#confirmRevoke')?.addEventListener('click', async () => { + const key = confirmRevoke?.key; + if (!key || !window.license) return; + backdrop.remove(); + confirmRevoke = null; + $refreshBtn.disabled = true; + try { + await window.license.revokeProductKey(key); + await loadLicenses(); + } catch (err) { + showError(err instanceof Error ? err.message : String(err)); + } finally { + $refreshBtn.disabled = false; + } + }); + } + + function renderDeleteModal() { + closeModal('delete-modal'); if (!confirmDelete) return; const backdrop = document.createElement('div'); @@ -84,9 +130,10 @@ export function renderLicenseListPage(root) { const devices = lic.activatedDevices?.length ? `
deviceId: ${lic.activatedDevices.join(', ')}
` : ''; - const deleteBtn = !lic.revoked - ? `
` + const revokeBtn = !lic.revoked + ? `` : ''; + const deleteBtn = ``; return `
${lic.key}
@@ -96,7 +143,7 @@ export function renderLicenseListPage(root) {
Срок: ${formatLicenseExpiry(lic)}
Статус: ${lic.revoked ? 'Отозвана' : 'Активна'}
${devices} - ${deleteBtn} +
${revokeBtn}${deleteBtn}
`; }) @@ -104,13 +151,24 @@ export function renderLicenseListPage(root) { $listContent.innerHTML = `
${html}
`; + $listContent.querySelectorAll('[data-revoke]').forEach((btn) => { + btn.addEventListener('click', () => { + const key = btn.getAttribute('data-revoke'); + const lic = licenses.find((l) => l.key === key); + if (lic) { + confirmRevoke = lic; + renderRevokeModal(); + } + }); + }); + $listContent.querySelectorAll('[data-delete]').forEach((btn) => { btn.addEventListener('click', () => { const key = btn.getAttribute('data-delete'); const lic = licenses.find((l) => l.key === key); if (lic) { confirmDelete = lic; - renderModal(); + renderDeleteModal(); } }); }); @@ -139,6 +197,7 @@ export function renderLicenseListPage(root) { void loadLicenses(); return () => { - document.getElementById('delete-modal')?.remove(); + closeModal('delete-modal'); + closeModal('revoke-modal'); }; } diff --git a/src/styles/app.css b/src/styles/app.css index 5685967..b747edb 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -17,22 +17,30 @@ box-sizing: border-box; } +html, +body { + height: 100%; + overflow: hidden; +} + body { margin: 0; font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); - min-height: 100vh; } .app-shell { display: flex; - min-height: 100vh; + height: 100vh; + overflow: hidden; } .sidebar { width: 200px; flex-shrink: 0; + height: 100vh; + overflow: hidden; border-right: 1px solid var(--border); background: rgba(0, 0, 0, 0.2); padding: 20px 0; @@ -90,7 +98,9 @@ body { .main { flex: 1; - overflow: auto; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; padding: 24px 28px 32px; max-width: 760px; } @@ -408,6 +418,7 @@ button.danger:hover:not(:disabled) { margin-top: 12px; display: flex; justify-content: flex-end; + gap: 8px; } .empty,