- Implemented repoService for database interactions including count, list, get, create, update, and delete operations. - Created RepoManager component for managing repositories with a user interface. - Added forms for creating and editing repositories, including validation and error handling. - Integrated API calls for fetching, creating, updating, and deleting repositories. - Enhanced user experience with loading states and action feedback messages.
254 lines
8.8 KiB
JavaScript
254 lines
8.8 KiB
JavaScript
import crypto from "crypto";
|
|
import db from "./db.js";
|
|
import { fetchManifest, fetchRepo, normalizeRepoInput } from "./pluginService.js";
|
|
import { getRepoById } from "./repoService.js";
|
|
|
|
function toIso(value) {
|
|
return value ? new Date(value).toISOString() : null;
|
|
}
|
|
|
|
export function generateLicenseKey() {
|
|
const raw = crypto.randomBytes(8).toString("hex").toUpperCase();
|
|
const segments = raw.match(/.{1,4}/g) || [];
|
|
return `SITI-${segments.slice(0, 4).join("-")}`;
|
|
}
|
|
|
|
export function normalizeHostname(value) {
|
|
return value ? value.trim().toLowerCase() : null;
|
|
}
|
|
|
|
export async function listLicensesByUser(userId) {
|
|
const [rows] = await db.query(`SELECT * FROM licenses WHERE user_id = ? ORDER BY created_at DESC`, [userId]);
|
|
return Promise.all(rows.map((row) => buildLicensePayload(row)));
|
|
}
|
|
|
|
export async function getLicenseById(id) {
|
|
const [rows] = await db.query(`SELECT * FROM licenses WHERE id = ? LIMIT 1`, [id]);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
export async function findLicenseByKey(key) {
|
|
const [rows] = await db.query(`SELECT * FROM licenses WHERE license_key = ? LIMIT 1`, [key]);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
export async function createLicense(userId, { label, note, repo, repoId }) {
|
|
let repoEntry = null;
|
|
let repoRow = null;
|
|
|
|
if (repoId) {
|
|
repoRow = await getRepoById(repoId);
|
|
if (!repoRow) {
|
|
const error = new Error("Onbekende repository.");
|
|
error.meta = "INVALID_REPO";
|
|
throw error;
|
|
}
|
|
repoEntry = {
|
|
repo: repoRow.ownerRepo,
|
|
provider: repoRow.provider,
|
|
baseUrl: repoRow.baseUrl
|
|
};
|
|
} else {
|
|
repoEntry = normalizeRepoInput(repo);
|
|
if (!repoEntry) {
|
|
const error = new Error("Ongeldige plugin referentie.");
|
|
error.meta = "INVALID_REPO";
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
let licenseId = null;
|
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
const id = crypto.randomUUID();
|
|
const licenseKey = generateLicenseKey();
|
|
try {
|
|
await db.query(
|
|
`INSERT INTO licenses (
|
|
id, user_id, license_key, label, note,
|
|
repo_provider, repo_name, repo_base_url, repo_id,
|
|
created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
|
[
|
|
id,
|
|
userId,
|
|
licenseKey,
|
|
label || repoEntry.repo,
|
|
note || null,
|
|
repoEntry.provider || "github",
|
|
repoEntry.repo,
|
|
repoEntry.baseUrl || (repoEntry.provider === "github" ? "https://github.com" : null),
|
|
repoRow?.id || null
|
|
]
|
|
);
|
|
licenseId = id;
|
|
break;
|
|
} catch (error) {
|
|
if (error?.code === "ER_DUP_ENTRY") {
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (!licenseId) {
|
|
throw new Error("Kon licentie niet opslaan.");
|
|
}
|
|
|
|
return buildLicensePayload(await getLicenseById(licenseId));
|
|
}
|
|
|
|
export async function buildLicensePayload(row) {
|
|
if (!row) return null;
|
|
|
|
let repoRow = null;
|
|
if (row.repo_id) {
|
|
repoRow = await getRepoById(row.repo_id);
|
|
}
|
|
|
|
const repoEntry =
|
|
repoRow && repoRow.ownerRepo
|
|
? {
|
|
repo: repoRow.ownerRepo,
|
|
provider: repoRow.provider,
|
|
baseUrl: repoRow.baseUrl
|
|
}
|
|
: normalizeRepoInput({
|
|
repo: row.repo_name,
|
|
provider: row.repo_provider,
|
|
baseUrl: row.repo_base_url
|
|
});
|
|
const [hostnameRows] = await db.query(
|
|
`SELECT hostname, normalized, first_seen_at, last_seen_at, hits
|
|
FROM license_hostnames WHERE license_id = ? ORDER BY first_seen_at ASC`,
|
|
[row.id]
|
|
);
|
|
const hostnames = hostnameRows.map((entry) => ({
|
|
hostname: entry.hostname,
|
|
normalized: entry.normalized,
|
|
firstSeenAt: toIso(entry.first_seen_at),
|
|
lastSeenAt: toIso(entry.last_seen_at),
|
|
hits: entry.hits
|
|
}));
|
|
|
|
if (!repoEntry) {
|
|
return {
|
|
id: row.id,
|
|
key: row.license_key,
|
|
label: row.label,
|
|
note: row.note,
|
|
hostnames,
|
|
createdAt: toIso(row.created_at),
|
|
updatedAt: toIso(row.updated_at),
|
|
lastVersionCheckAt: toIso(row.last_version_check_at),
|
|
primaryHostname: row.primary_hostname,
|
|
primaryHostnameNormalized: row.primary_hostname_normalized,
|
|
repoFullName: row.repo_name,
|
|
repoUrl: null,
|
|
repoId: repoRow?.id || null,
|
|
repoLabel: repoRow?.label || null,
|
|
pluginName: row.label,
|
|
pluginVersion: row.last_used_version || null,
|
|
lastUsedVersion: row.last_used_version,
|
|
repo: null
|
|
};
|
|
}
|
|
|
|
try {
|
|
const info = await fetchRepo(repoEntry);
|
|
const manifest = await fetchManifest(repoEntry, info.defaultBranch).catch(() => null);
|
|
return {
|
|
id: row.id,
|
|
key: row.license_key,
|
|
label: row.label,
|
|
note: row.note,
|
|
createdAt: toIso(row.created_at),
|
|
updatedAt: toIso(row.updated_at),
|
|
lastVersionCheckAt: toIso(row.last_version_check_at),
|
|
primaryHostname: row.primary_hostname,
|
|
primaryHostnameNormalized: row.primary_hostname_normalized,
|
|
repoFullName: info.fullName,
|
|
repoUrl: info.repoUrl,
|
|
repoId: repoRow?.id || null,
|
|
repoLabel: repoRow?.label || null,
|
|
pluginName: manifest?.plugin_name || repoRow?.label || info.name || row.label,
|
|
pluginVersion: manifest?.version || row.last_used_version || null,
|
|
lastUsedVersion: row.last_used_version,
|
|
repo: repoEntry,
|
|
hostnames
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
id: row.id,
|
|
key: row.license_key,
|
|
label: row.label,
|
|
note: row.note,
|
|
createdAt: toIso(row.created_at),
|
|
updatedAt: toIso(row.updated_at),
|
|
lastVersionCheckAt: toIso(row.last_version_check_at),
|
|
primaryHostname: row.primary_hostname,
|
|
primaryHostnameNormalized: row.primary_hostname_normalized,
|
|
repoFullName: repoRow?.ownerRepo || row.repo_name,
|
|
repoUrl:
|
|
(repoRow?.baseUrl || repoEntry?.baseUrl)
|
|
? `${(repoRow?.baseUrl || repoEntry.baseUrl).replace(/\/$/, "")}/${repoRow?.ownerRepo || row.repo_name}`
|
|
: null,
|
|
repoId: repoRow?.id || null,
|
|
repoLabel: repoRow?.label || null,
|
|
pluginName: repoRow?.label || row.label,
|
|
pluginVersion: row.last_used_version || null,
|
|
lastUsedVersion: row.last_used_version,
|
|
repo: repoEntry,
|
|
hostnames
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function touchLicenseHostname(license, hostname, options = {}) {
|
|
const normalizedHost = normalizeHostname(hostname);
|
|
if (!normalizedHost) {
|
|
return { ok: false, error: "Ongeldige hostname." };
|
|
}
|
|
const trimmed = hostname.trim();
|
|
const rawVersion =
|
|
typeof options?.pluginVersion === "string" && options.pluginVersion.trim().length > 0
|
|
? options.pluginVersion.trim()
|
|
: null;
|
|
const pluginVersion = rawVersion ? rawVersion.slice(0, 64) : null;
|
|
|
|
const updateFields = [];
|
|
const params = [];
|
|
|
|
if (!license.primary_hostname_normalized) {
|
|
updateFields.push("primary_hostname = ?", "primary_hostname_normalized = ?");
|
|
params.push(trimmed, normalizedHost);
|
|
} else if (license.primary_hostname_normalized !== normalizedHost) {
|
|
return {
|
|
ok: false,
|
|
conflict: true,
|
|
error: `Licentie hoort bij ${license.primary_hostname || "een andere site"}.`
|
|
};
|
|
} else {
|
|
// No hostname change, but still keep the hostname casing in sync.
|
|
updateFields.push("primary_hostname = ?");
|
|
params.push(trimmed);
|
|
}
|
|
|
|
updateFields.push("last_version_check_at = NOW()", "updated_at = NOW()");
|
|
|
|
if (pluginVersion) {
|
|
updateFields.push("last_used_version = ?");
|
|
params.push(pluginVersion);
|
|
}
|
|
|
|
await db.query(`UPDATE licenses SET ${updateFields.join(", ")} WHERE id = ?`, [...params, license.id]);
|
|
|
|
await db.query(
|
|
`INSERT INTO license_hostnames (license_id, hostname, normalized, first_seen_at, last_seen_at, hits)
|
|
VALUES (?, ?, ?, NOW(), NOW(), 1)
|
|
ON DUPLICATE KEY UPDATE hostname = VALUES(hostname), last_seen_at = NOW(), hits = hits + 1`,
|
|
[license.id, trimmed, normalizedHost]
|
|
);
|
|
|
|
return { ok: true, boundNow: !license.primary_hostname_normalized, normalized: normalizedHost };
|
|
}
|