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 */
|
/** @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
@@ -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'));
|
||||||
|
|||||||
@@ -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));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user