Restore revoke action and fix main content scroll layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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'));
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user