feat: add download functionality for licenses and enhance plugin detail view

This commit is contained in:
2026-02-01 02:44:02 +00:00
parent 73025c84c5
commit 65b0de63a9
5 changed files with 489 additions and 21 deletions

View File

@@ -1,5 +1,6 @@
import express from "express";
import path from "path";
import { Readable } from "node:stream";
import {
fetchCommits,
fetchManifest,
@@ -21,6 +22,7 @@ import { HOST, PATHS, PORT } from "./lib/config.js";
import { ensureSchema } from "./lib/schema.js";
import { authenticateUser, registerUser, adminCreateUser, listUsers, getUserById } from "./lib/userService.js";
import { requireAuth, requireAdmin } from "./middleware/auth.js";
import { getDownloadSourceForLicense, resolveRepoDownload, buildDownloadUrl } from "./lib/downloadService.js";
const app = express();
app.use(express.json());
@@ -159,6 +161,7 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
const provider = (req.query.provider || "github").toLowerCase();
const baseUrl = req.query.baseUrl || (provider === "github" ? "https://github.com" : "");
const entry = provider === "github" ? ownerRepo : { provider, repo: ownerRepo, baseUrl };
const normalizedEntry = normalizeRepoInput(entry, { repo: ownerRepo, provider, baseUrl });
const info = await fetchRepo(entry);
const [manifest, releases, commits] = await Promise.all([
@@ -167,10 +170,36 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
fetchCommits(entry).catch(() => [])
]);
const releasesWithDownload =
normalizedEntry && releases.length > 0
? releases.map((release) => {
const tagOrName = release.tag || release.name;
return {
...release,
downloadUrl: tagOrName ? buildDownloadUrl(normalizedEntry, tagOrName, "release") : null
};
})
: releases;
let downloadMeta = null;
if (normalizedEntry) {
const latestRelease = releasesWithDownload[0];
const sourceType = latestRelease ? "release" : "branch";
const version = latestRelease?.tag || latestRelease?.name || info.defaultBranch || "main";
if (version) {
downloadMeta = {
url: buildDownloadUrl(normalizedEntry, version, sourceType),
version,
sourceType
};
}
}
res.json({
...info,
manifest,
releases,
releases: releasesWithDownload,
download: downloadMeta,
commits
});
} catch (error) {
@@ -283,6 +312,51 @@ app.post("/api/licenses/verify", async (req, res) => {
}
});
app.post("/api/licenses/download", async (req, res) => {
try {
const { key, hostname, version = "latest" } = req.body || {};
if (!key || !hostname) {
return res.status(400).json({ error: "Licentiecode en hostname zijn verplicht." });
}
const license = await findLicenseByKey(String(key).trim());
if (!license) {
return res.status(404).json({ error: "Licentie niet gevonden." });
}
const result = await touchLicenseHostname(license, hostname);
if (!result.ok) {
const status = result.conflict ? 403 : 400;
return res.status(status).json({ error: result.error, boundHostname: license.primary_hostname });
}
const source = await getDownloadSourceForLicense(license, version);
const remoteResponse = await fetch(source.url);
if (!remoteResponse.ok || !remoteResponse.body) {
return res.status(502).json({ error: "Kon plugin versie niet ophalen." });
}
res.setHeader("Content-Type", remoteResponse.headers.get("content-type") || "application/zip");
const length = remoteResponse.headers.get("content-length");
if (length) {
res.setHeader("Content-Length", length);
}
res.setHeader("Content-Disposition", `attachment; filename=\"${source.filename}\"`);
res.setHeader("X-Plugin-Version", source.version);
const stream = Readable.fromWeb(remoteResponse.body);
stream.on("error", (err) => {
console.error("Download stream error", err);
res.destroy(err);
});
stream.pipe(res);
} catch (error) {
console.error("Download endpoint error:", error);
if (!res.headersSent) {
res.status(500).json({ error: "Download mislukt." });
} else {
res.end();
}
}
});
app.use(express.static(PATHS.distDir));
app.get("*", (_req, res) => {
res.sendFile(path.join(PATHS.distDir, "index.html"));