Files
siti-plugin-repo/server/lib/licenseService.js
Roberto Guagliardo 7b0ca40c4f feat: implement user authentication and license management system
- Added schema for users, licenses, and license hostnames in the database.
- Created storage utility for reading and writing JSON files.
- Developed user service for user registration, authentication, and retrieval.
- Implemented authentication middleware to protect routes.
- Built LicenseCard component to display license details.
- Created SiteNav component for navigation with user authentication status.
- Established AuthContext for managing authentication state and actions.
- Developed Home page to display available plugins.
- Created LicenseManager page for managing licenses with forms for creation and verification.
- Implemented PluginDetail page to show detailed information about a specific plugin.
- Added utility functions for date formatting.
2026-02-01 02:20:28 +00:00

193 lines
6.7 KiB
JavaScript

import crypto from "crypto";
import db from "./db.js";
import { fetchManifest, fetchRepo, normalizeRepoInput } from "./pluginService.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 }) {
const 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,
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)
]
);
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;
const repoEntry = 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,
pluginName: row.label,
pluginVersion: null,
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,
pluginName: manifest?.plugin_name || info.name || row.label,
pluginVersion: manifest?.version || null,
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: row.repo_name,
repoUrl: repoEntry?.baseUrl ? `${repoEntry.baseUrl.replace(/\/$/, "")}/${row.repo_name}` : null,
pluginName: row.label,
pluginVersion: null,
repo: repoEntry,
hostnames
};
}
}
export async function touchLicenseHostname(license, hostname) {
const normalizedHost = normalizeHostname(hostname);
if (!normalizedHost) {
return { ok: false, error: "Ongeldige hostname." };
}
const trimmed = hostname.trim();
if (!license.primary_hostname_normalized) {
await db.query(
`UPDATE licenses SET primary_hostname = ?, primary_hostname_normalized = ?, last_version_check_at = NOW(), updated_at = NOW()
WHERE id = ?`,
[trimmed, normalizedHost, license.id]
);
} else if (license.primary_hostname_normalized !== normalizedHost) {
return {
ok: false,
conflict: true,
error: `Licentie hoort bij ${license.primary_hostname || "een andere site"}.`
};
} else {
await db.query(`UPDATE licenses SET last_version_check_at = NOW(), updated_at = NOW() WHERE id = ?`, [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 };
}