import express from "express"; import path from "path"; import { Readable } from "node:stream"; import { fetchCommits, fetchManifest, fetchRepo, fetchReleases, normalizeRepoInput, parseRepoEntry, readRepos } from "./lib/pluginService.js"; import { buildLicensePayload, createLicense, findLicenseByKey, getLicenseById, listLicensesByUser, touchLicenseHostname } from "./lib/licenseService.js"; 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()); try { await ensureSchema(); } catch (error) { console.error("Kon database schema niet initialiseren:", error); process.exit(1); } app.post("/api/auth/register", async (req, res) => { try { const { username, name, email, password } = req.body || {}; if (!username || !name || !email || !password) { return res.status(400).json({ error: "Vul gebruikersnaam, naam, e-mail en wachtwoord in." }); } if (password.length < 8) { return res.status(400).json({ error: "Wachtwoord moet minimaal 8 karakters zijn." }); } const { user, token } = await registerUser({ username: String(username).trim(), name: String(name).trim(), email: String(email).trim().toLowerCase(), password: String(password) }); res.status(201).json({ token, user }); } catch (error) { if (error?.code === "ER_DUP_ENTRY") { const field = error.meta === "EMAIL" ? "e-mailadres" : "gebruikersnaam"; return res.status(409).json({ error: `Dit ${field} is al in gebruik.` }); } res.status(500).json({ error: "Registratie mislukt." }); } }); app.post("/api/auth/login", async (req, res) => { try { const { identifier, password } = req.body || {}; if (!identifier || !password) { return res.status(400).json({ error: "Vul gebruikersnaam/e-mail en wachtwoord in." }); } const { user, token } = await authenticateUser(String(identifier).trim(), String(password)); res.json({ token, user }); } catch (error) { const message = error?.meta === "INVALID_CREDENTIALS" ? "Onjuiste inloggegevens." : "Login mislukt."; res.status(401).json({ error: message }); } }); app.get("/api/auth/me", requireAuth, (req, res) => { res.json({ user: req.user }); }); app.get("/api/admin/users", requireAuth, requireAdmin, async (_req, res) => { try { const users = await listUsers(); res.json({ count: users.length, items: users }); } catch (error) { res.status(500).json({ error: "Kon gebruikers niet laden." }); } }); app.post("/api/admin/users", requireAuth, requireAdmin, async (req, res) => { try { const { username, name, email, password, isAdmin } = req.body || {}; if (!username || !name || !email || !password) { return res.status(400).json({ error: "Alle velden zijn verplicht." }); } if (String(password).length < 8) { return res.status(400).json({ error: "Wachtwoord moet minimaal 8 karakters zijn." }); } const user = await adminCreateUser({ username: String(username).trim(), name: String(name).trim(), email: String(email).trim().toLowerCase(), password: String(password), isAdmin: Boolean(isAdmin) }); res.status(201).json({ user }); } catch (error) { if (error?.code === "ER_DUP_ENTRY") { const field = error.meta === "EMAIL" ? "e-mailadres" : "gebruikersnaam"; return res.status(409).json({ error: `Dit ${field} is al in gebruik.` }); } res.status(500).json({ error: "Gebruiker aanmaken mislukt." }); } }); app.get("/api/plugins", async (_req, res) => { try { const repos = await readRepos(PATHS.reposFile); const results = await Promise.all( repos.map(async (repo) => { try { const info = await fetchRepo(repo); const manifest = await fetchManifest(repo, info.defaultBranch || info.default_branch); return { ...info, manifest }; } catch { const parsed = parseRepoEntry(repo); return { fullName: parsed.ownerRepo, name: parsed.ownerRepo.split("/")[1] || parsed.ownerRepo, description: "Kon gegevens niet ophalen.", repoUrl: parsed.provider === "gitea" ? `${parsed.baseUrl.replace(/\/$/, "")}/${parsed.ownerRepo}` : `https://github.com/${parsed.ownerRepo}`, stars: 0, forks: 0, issues: 0, updatedAt: null, topics: [], manifest: null, provider: parsed.provider, ownerRepo: parsed.ownerRepo, baseUrl: parsed.baseUrl }; } }) ); res.json({ count: results.length, updatedAt: new Date().toISOString(), items: results }); } catch (error) { res.status(500).json({ error: "Kon plugins niet laden." }); } }); app.get("/api/plugins/:owner/:repo", async (req, res) => { const ownerRepo = `${req.params.owner}/${req.params.repo}`; try { 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([ fetchManifest(entry, info.defaultBranch).catch(() => null), fetchReleases(entry).catch(() => []), 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: releasesWithDownload, download: downloadMeta, commits }); } catch (error) { res.status(500).json({ error: "Kon plugin details niet laden." }); } }); app.get("/api/licenses", requireAuth, async (req, res) => { try { let targetUserId = req.user.id; if (req.user.isAdmin && req.query.userId) { const parsed = Number(req.query.userId); if (Number.isNaN(parsed)) { return res.status(400).json({ error: "Ongeldige userId." }); } const targetUser = await getUserById(parsed); if (!targetUser) { return res.status(404).json({ error: "Gebruiker niet gevonden." }); } targetUserId = parsed; } const payload = await listLicensesByUser(targetUserId); res.json({ count: payload.length, updatedAt: new Date().toISOString(), items: payload }); } catch (error) { res.status(500).json({ error: "Kon licenties niet laden." }); } }); app.post("/api/licenses", requireAuth, async (req, res) => { try { const body = req.body || {}; const repoInput = typeof body.repo === "object" ? body.repo : typeof body.plugin === "object" ? body.plugin : body.repo || body.ownerRepo || body.fullName || body.plugin; const repoEntry = normalizeRepoInput(repoInput, { repo: body.ownerRepo || body.fullName || body.repo, provider: body.provider, baseUrl: body.baseUrl }) || null; if (!repoEntry) { return res.status(400).json({ error: "Kies een plugin om de licentie aan te koppelen." }); } try { await fetchRepo(repoEntry); } catch (error) { return res.status(400).json({ error: "Kon plugin gegevens niet ophalen." }); } let ownerUserId = req.user.id; if (req.user.isAdmin && body.userId) { const parsed = Number(body.userId); if (Number.isNaN(parsed)) { return res.status(400).json({ error: "Ongeldige gebruiker." }); } const target = await getUserById(parsed); if (!target) { return res.status(404).json({ error: "Gebruiker niet gevonden." }); } ownerUserId = parsed; } const payload = await createLicense(ownerUserId, { label: body.label?.trim(), note: body.note?.trim(), repo: repoEntry }); res.status(201).json(payload); } catch (error) { res.status(500).json({ error: "Kon licentie niet aanmaken." }); } }); app.post("/api/licenses/verify", async (req, res) => { try { const { key, hostname } = req.body || {}; if (!key || !hostname) { return res.status(400).json({ valid: false, error: "Licentiecode en hostname zijn verplicht." }); } const license = await findLicenseByKey(String(key).trim()); if (!license) { return res.status(404).json({ valid: false, error: "Licentie niet gevonden." }); } const result = await touchLicenseHostname(license, hostname); if (!result.ok) { const status = result.conflict ? 403 : 400; return res.status(status).json({ valid: false, error: result.error, boundHostname: license.primary_hostname }); } const freshLicense = await getLicenseById(license.id); const payload = await buildLicensePayload(freshLicense); res.json({ valid: true, hostname: payload.primaryHostname, boundNow: !!result.boundNow, license: payload }); } catch (error) { res.status(500).json({ valid: false, error: "Validatie mislukt." }); } }); 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")); }); app.listen(PORT, HOST, () => { console.log(`Server draait op http://${HOST}:${PORT}`); });