Restore revoke action and fix main content scroll layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-02 00:44:13 +08:00
parent f20c7e2f63
commit 1bf3fa466d
5 changed files with 103 additions and 11 deletions
+8
View File
@@ -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 */ /** @param {string} adminToken @param {string} key */
export async function deleteProductKey(adminToken, key) { export async function deleteProductKey(adminToken, key) {
return request(adminToken, 'v1/admin/product-keys/delete', { return request(adminToken, 'v1/admin/product-keys/delete', {
+11
View File
@@ -8,6 +8,7 @@ import {
deleteProductKey, deleteProductKey,
getDownloadStats, getDownloadStats,
listLicenses, listLicenses,
revokeProductKey,
LicenseApiError, LicenseApiError,
} from './license/api.mjs'; } from './license/api.mjs';
import { loadLicenseSettings, saveLicenseSettings } from './license/settings.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) => { ipcMain.handle('license.deleteProductKey', async (_e, key) => {
try { try {
const { adminToken } = loadLicenseSettings(app.getPath('userData')); const { adminToken } = loadLicenseSettings(app.getPath('userData'));
+3
View File
@@ -27,6 +27,9 @@ contextBridge.exposeInMainWorld('license', {
const res = unwrapLicense(await ipcRenderer.invoke('license.createProductKeys', body)); const res = unwrapLicense(await ipcRenderer.invoke('license.createProductKeys', body));
return res.result; return res.result;
}, },
revokeProductKey: async (key) => {
unwrapLicense(await ipcRenderer.invoke('license.revokeProductKey', key));
},
deleteProductKey: async (key) => { deleteProductKey: async (key) => {
unwrapLicense(await ipcRenderer.invoke('license.deleteProductKey', key)); unwrapLicense(await ipcRenderer.invoke('license.deleteProductKey', key));
}, },
+67 -8
View File
@@ -19,6 +19,8 @@ export function renderLicenseListPage(root) {
/** @type {import('../../license/models.mjs').LicenseEntry | null} */ /** @type {import('../../license/models.mjs').LicenseEntry | null} */
let confirmDelete = null; let confirmDelete = null;
/** @type {import('../../license/models.mjs').LicenseEntry | null} */
let confirmRevoke = null;
function showError(msg) { function showError(msg) {
if (!msg) { if (!msg) {
@@ -30,9 +32,53 @@ export function renderLicenseListPage(root) {
$listError.textContent = msg; $listError.textContent = msg;
} }
function renderModal() { function closeModal(id) {
const existing = document.getElementById('delete-modal'); document.getElementById(id)?.remove();
if (existing) existing.remove(); }
function renderRevokeModal() {
closeModal('revoke-modal');
if (!confirmRevoke) return;
const backdrop = document.createElement('div');
backdrop.id = 'revoke-modal';
backdrop.className = 'modal-backdrop';
backdrop.innerHTML = `
<div class="modal">
<h2>Отозвать лицензию?</h2>
<p>Ключ <strong>${confirmRevoke.key}</strong> перестанет работать у всех пользователей. Запись останется в списке.</p>
<div class="modal-actions">
<button type="button" class="secondary" id="cancelRevoke">Отмена</button>
<button type="button" class="secondary danger" id="confirmRevoke">Отозвать</button>
</div>
</div>
`;
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; if (!confirmDelete) return;
const backdrop = document.createElement('div'); const backdrop = document.createElement('div');
@@ -84,9 +130,10 @@ export function renderLicenseListPage(root) {
const devices = lic.activatedDevices?.length const devices = lic.activatedDevices?.length
? `<div class="license-detail">deviceId: ${lic.activatedDevices.join(', ')}</div>` ? `<div class="license-detail">deviceId: ${lic.activatedDevices.join(', ')}</div>`
: ''; : '';
const deleteBtn = !lic.revoked const revokeBtn = !lic.revoked
? `<div class="license-actions"><button type="button" class="secondary danger" data-delete="${lic.key}">Удалить</button></div>` ? `<button type="button" class="secondary" data-revoke="${lic.key}">Отозвать</button>`
: ''; : '';
const deleteBtn = `<button type="button" class="secondary danger" data-delete="${lic.key}">Удалить</button>`;
return ` return `
<div class="license-card${lic.revoked ? ' revoked' : ''}"> <div class="license-card${lic.revoked ? ' revoked' : ''}">
<div class="license-key">${lic.key}</div> <div class="license-key">${lic.key}</div>
@@ -96,7 +143,7 @@ export function renderLicenseListPage(root) {
<div class="license-detail">Срок: ${formatLicenseExpiry(lic)}</div> <div class="license-detail">Срок: ${formatLicenseExpiry(lic)}</div>
<div class="license-detail">Статус: ${lic.revoked ? 'Отозвана' : 'Активна'}</div> <div class="license-detail">Статус: ${lic.revoked ? 'Отозвана' : 'Активна'}</div>
${devices} ${devices}
${deleteBtn} <div class="license-actions">${revokeBtn}${deleteBtn}</div>
</div> </div>
`; `;
}) })
@@ -104,13 +151,24 @@ export function renderLicenseListPage(root) {
$listContent.innerHTML = `<div class="license-list">${html}</div>`; $listContent.innerHTML = `<div class="license-list">${html}</div>`;
$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) => { $listContent.querySelectorAll('[data-delete]').forEach((btn) => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const key = btn.getAttribute('data-delete'); const key = btn.getAttribute('data-delete');
const lic = licenses.find((l) => l.key === key); const lic = licenses.find((l) => l.key === key);
if (lic) { if (lic) {
confirmDelete = lic; confirmDelete = lic;
renderModal(); renderDeleteModal();
} }
}); });
}); });
@@ -139,6 +197,7 @@ export function renderLicenseListPage(root) {
void loadLicenses(); void loadLicenses();
return () => { return () => {
document.getElementById('delete-modal')?.remove(); closeModal('delete-modal');
closeModal('revoke-modal');
}; };
} }
+14 -3
View File
@@ -17,22 +17,30 @@
box-sizing: border-box; box-sizing: border-box;
} }
html,
body {
height: 100%;
overflow: hidden;
}
body { body {
margin: 0; margin: 0;
font-family: 'Segoe UI', system-ui, sans-serif; font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
min-height: 100vh;
} }
.app-shell { .app-shell {
display: flex; display: flex;
min-height: 100vh; height: 100vh;
overflow: hidden;
} }
.sidebar { .sidebar {
width: 200px; width: 200px;
flex-shrink: 0; flex-shrink: 0;
height: 100vh;
overflow: hidden;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
padding: 20px 0; padding: 20px 0;
@@ -90,7 +98,9 @@ body {
.main { .main {
flex: 1; flex: 1;
overflow: auto; min-height: 0;
overflow-x: hidden;
overflow-y: auto;
padding: 24px 28px 32px; padding: 24px 28px 32px;
max-width: 760px; max-width: 760px;
} }
@@ -408,6 +418,7 @@ button.danger:hover:not(:disabled) {
margin-top: 12px; margin-top: 12px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 8px;
} }
.empty, .empty,