Add download stats, key deletion, batch creation, and newest-first listing.

Track landing downloads per platform/month, support admin delete and bulk key issue, and sort licenses by createdAtSec.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-02 00:21:21 +08:00
parent 4b568ceb56
commit 6d4b74d410
8 changed files with 349 additions and 32 deletions
+52
View File
@@ -84,6 +84,7 @@ export function enrichLicenseEntry(pk, devices, revoked) {
sub: pk.sub,
pid: pk.pid,
maxDevices: pk.maxDevices,
createdAtSec: pk.createdAtSec ?? null,
expiryMode: getExpiryMode(pk),
expiresAtSec: pk.expiresAtSec ?? null,
validDays: pk.validDays ?? null,
@@ -95,3 +96,54 @@ export function enrichLicenseEntry(pk, devices, revoked) {
activatedCount: devices.length,
};
}
export function sortLicensesNewestFirst(licenses) {
return [...licenses].sort((a, b) => {
const aSec = a.createdAtSec ?? a.activatedAtSec ?? 0;
const bSec = b.createdAtSec ?? b.activatedAtSec ?? 0;
return bSec - aSec;
});
}
export function buildProductKeyEntry(body, { generateKey, generateSub, nowSec }) {
const expiry = parseProductKeyExpiry(body, nowSec);
if (!expiry.ok) return expiry;
const entry = {
key: body.key ?? generateKey(),
sub: body.sub ?? generateSub(),
pid: body.pid ?? 'dnd_player',
maxDevices: body.maxDevices ?? 3,
createdAtSec: nowSec,
...(expiry.expiresAtSec != null
? { expiresAtSec: expiry.expiresAtSec }
: { validDays: expiry.validDays }),
};
if (!entry.key || !entry.sub) return { ok: false, error: 'invalid_entry' };
return { ok: true, entry };
}
export function deleteProductKey(data, key) {
const idx = data.productKeys?.findIndex((x) => x.key === key) ?? -1;
if (idx < 0) return { ok: false, error: 'unknown_product_key' };
const pk = data.productKeys[idx];
data.productKeys.splice(idx, 1);
if (Array.isArray(data.revokedKeys)) {
data.revokedKeys = data.revokedKeys.filter((k) => k !== key);
}
const subStillUsed = data.productKeys.some((x) => x.sub === pk.sub);
if (!subStillUsed) {
if (data.activations && pk.sub in data.activations) {
delete data.activations[pk.sub];
}
if (Array.isArray(data.revokedSubs)) {
data.revokedSubs = data.revokedSubs.filter((s) => s !== pk.sub);
}
}
return { ok: true, deleted: pk.key, sub: pk.sub };
}