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.
This commit is contained in:
192
server/lib/licenseService.js
Normal file
192
server/lib/licenseService.js
Normal file
@@ -0,0 +1,192 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user