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 */
export async function deleteProductKey(adminToken, key) {
return request(adminToken, 'v1/admin/product-keys/delete', {
+11
View File
@@ -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'));
+3
View File
@@ -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));
},
+67 -8
View File
@@ -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 = `
<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;
const backdrop = document.createElement('div');
@@ -84,9 +130,10 @@ export function renderLicenseListPage(root) {
const devices = lic.activatedDevices?.length
? `<div class="license-detail">deviceId: ${lic.activatedDevices.join(', ')}</div>`
: '';
const deleteBtn = !lic.revoked
? `<div class="license-actions"><button type="button" class="secondary danger" data-delete="${lic.key}">Удалить</button></div>`
const revokeBtn = !lic.revoked
? `<button type="button" class="secondary" data-revoke="${lic.key}">Отозвать</button>`
: '';
const deleteBtn = `<button type="button" class="secondary danger" data-delete="${lic.key}">Удалить</button>`;
return `
<div class="license-card${lic.revoked ? ' revoked' : ''}">
<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">Статус: ${lic.revoked ? 'Отозвана' : 'Активна'}</div>
${devices}
${deleteBtn}
<div class="license-actions">${revokeBtn}${deleteBtn}</div>
</div>
`;
})
@@ -104,13 +151,24 @@ export function renderLicenseListPage(root) {
$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) => {
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');
};
}
+14 -3
View File
@@ -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,