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:
@@ -0,0 +1,42 @@
|
||||
export const DOWNLOAD_PLATFORMS = ['windows', 'macos', 'linux'];
|
||||
|
||||
export function monthKeyFromDate(date = new Date()) {
|
||||
const y = date.getUTCFullYear();
|
||||
const m = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
return `${y}-${m}`;
|
||||
}
|
||||
|
||||
export function normalizeDownloadPlatform(platform) {
|
||||
const p = String(platform ?? '').trim().toLowerCase();
|
||||
if (DOWNLOAD_PLATFORMS.includes(p)) return p;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ensureDownloadStats(data) {
|
||||
data.downloadStats ??= { byMonth: {} };
|
||||
data.downloadStats.byMonth ??= {};
|
||||
return data.downloadStats;
|
||||
}
|
||||
|
||||
export function recordDownload(data, platform, date = new Date()) {
|
||||
const normalized = normalizeDownloadPlatform(platform);
|
||||
if (!normalized) return { ok: false, error: 'invalid_platform' };
|
||||
|
||||
const stats = ensureDownloadStats(data);
|
||||
const month = monthKeyFromDate(date);
|
||||
stats.byMonth[month] ??= { windows: 0, macos: 0, linux: 0 };
|
||||
stats.byMonth[month][normalized] += 1;
|
||||
return { ok: true, month, platform: normalized, count: stats.byMonth[month][normalized] };
|
||||
}
|
||||
|
||||
export function getDownloadStatsForMonth(data, month) {
|
||||
const stats = ensureDownloadStats(data);
|
||||
const row = stats.byMonth[month] ?? { windows: 0, macos: 0, linux: 0 };
|
||||
const total = row.windows + row.macos + row.linux;
|
||||
return { month, windows: row.windows, macos: row.macos, linux: row.linux, total };
|
||||
}
|
||||
|
||||
export function listDownloadMonths(data) {
|
||||
const stats = ensureDownloadStats(data);
|
||||
return Object.keys(stats.byMonth).sort((a, b) => b.localeCompare(a));
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user