Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -8,3 +8,10 @@ DB_NAME=siti_plugin_repo
|
|||||||
# Authentication
|
# Authentication
|
||||||
JWT_SECRET=please-change-me
|
JWT_SECRET=please-change-me
|
||||||
JWT_EXPIRES_IN=7d
|
JWT_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# Gitea tokens
|
||||||
|
# GITEA_TOKEN wordt gebruikt als default token voor alle Gitea servers.
|
||||||
|
GITEA_TOKEN=
|
||||||
|
# Optioneel: override per host via GITEA_TOKENS (JSON of comma-separated baseUrl=token)
|
||||||
|
# Voorbeeld: GITEA_TOKENS=git.robert.ooo=my-token,https://git.example.com=another-token
|
||||||
|
GITEA_TOKENS=
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
|
.env
|
||||||
1
dist/assets/index-BOPMLc5y.css
vendored
Normal file
1
dist/assets/index-BOPMLc5y.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-CFgb3VsA.css
vendored
1
dist/assets/index-CFgb3VsA.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Siti Plugin Repo</title>
|
<title>Siti Plugin Repo</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DDjGuoSe.js"></script>
|
<script type="module" crossorigin src="/assets/index-CfHTe5gv.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CFgb3VsA.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BOPMLc5y.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ services:
|
|||||||
DB_NAME: "${DB_NAME:-siti_plugin_repo}"
|
DB_NAME: "${DB_NAME:-siti_plugin_repo}"
|
||||||
JWT_SECRET: "${JWT_SECRET:-change-me}"
|
JWT_SECRET: "${JWT_SECRET:-change-me}"
|
||||||
JWT_EXPIRES_IN: "${JWT_EXPIRES_IN:-7d}"
|
JWT_EXPIRES_IN: "${JWT_EXPIRES_IN:-7d}"
|
||||||
|
GITEA_TOKEN: "${GITEA_TOKEN:-change-me}"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mysql2": "^3.16.2",
|
"mysql2": "^3.16.2",
|
||||||
@@ -1354,6 +1355,17 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
|
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mysql2": "^3.16.2",
|
"mysql2": "^3.16.2",
|
||||||
|
|||||||
118
server/index.js
118
server/index.js
@@ -1,3 +1,4 @@
|
|||||||
|
import "dotenv/config";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
@@ -328,7 +329,8 @@ app.get("/api/plugins", async (_req, res) => {
|
|||||||
ownerRepo: repo.ownerRepo,
|
ownerRepo: repo.ownerRepo,
|
||||||
baseUrl: repo.baseUrl,
|
baseUrl: repo.baseUrl,
|
||||||
repoId: repo.id,
|
repoId: repo.id,
|
||||||
label: repo.label
|
label: repo.label,
|
||||||
|
isPrivate: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -375,23 +377,77 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
|||||||
repoRecord = await findRepoByOwnerRepo(ownerRepo);
|
repoRecord = await findRepoByOwnerRepo(ownerRepo);
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = (req.query.provider || repoRecord?.provider || "github").toLowerCase();
|
let provider = (req.query.provider || repoRecord?.provider || "github").toLowerCase();
|
||||||
const baseUrlQuery =
|
if (repoRecord?.provider) {
|
||||||
req.query.baseUrl || repoRecord?.baseUrl || (provider === "github" ? "https://github.com" : "");
|
provider = repoRecord.provider;
|
||||||
|
}
|
||||||
|
const baseUrlFallback = provider === "github" ? "https://github.com" : "";
|
||||||
|
let baseUrlQuery = req.query.baseUrl || repoRecord?.baseUrl || baseUrlFallback;
|
||||||
|
if (repoRecord?.baseUrl) {
|
||||||
|
baseUrlQuery = repoRecord.baseUrl;
|
||||||
|
}
|
||||||
const entry =
|
const entry =
|
||||||
repoRecord && repoRecord.ownerRepo
|
repoRecord && repoRecord.ownerRepo
|
||||||
? { provider: repoRecord.provider, repo: repoRecord.ownerRepo, baseUrl: repoRecord.baseUrl }
|
? {
|
||||||
|
provider: repoRecord.provider,
|
||||||
|
repo: repoRecord.ownerRepo,
|
||||||
|
baseUrl: repoRecord.baseUrl || baseUrlQuery || undefined
|
||||||
|
}
|
||||||
: provider === "github"
|
: provider === "github"
|
||||||
? ownerRepo
|
? ownerRepo
|
||||||
: { provider, repo: ownerRepo, baseUrl: baseUrlQuery };
|
: { provider, repo: ownerRepo, baseUrl: baseUrlQuery };
|
||||||
const normalizedEntry = normalizeRepoInput(entry, { repo: ownerRepo, provider, baseUrl: baseUrlQuery });
|
const normalizedEntry =
|
||||||
|
normalizeRepoInput(entry, { repo: ownerRepo, provider, baseUrl: baseUrlQuery }) ||
|
||||||
|
null;
|
||||||
|
|
||||||
const info = await fetchRepo(entry);
|
if (!normalizedEntry) {
|
||||||
const [manifest, releases, commits] = await Promise.all([
|
return res.status(400).json({ error: "Ongeldige repository." });
|
||||||
fetchManifest(entry, info.defaultBranch).catch(() => null),
|
}
|
||||||
fetchReleases(entry).catch(() => []),
|
|
||||||
fetchCommits(entry).catch(() => [])
|
let info;
|
||||||
]);
|
let manifest = null;
|
||||||
|
let releases = [];
|
||||||
|
let commits = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
info = await fetchRepo(normalizedEntry);
|
||||||
|
[manifest, releases, commits] = await Promise.all([
|
||||||
|
fetchManifest(normalizedEntry, info.defaultBranch).catch(() => null),
|
||||||
|
fetchReleases(normalizedEntry).catch(() => []),
|
||||||
|
fetchCommits(normalizedEntry).catch(() => [])
|
||||||
|
]);
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.warn("Kon plugin info niet ophalen:", fetchError.message);
|
||||||
|
const parsed = parseRepoEntry(normalizedEntry);
|
||||||
|
const fallbackRepo = {
|
||||||
|
provider: repoRecord?.provider || parsed.provider,
|
||||||
|
baseUrl: repoRecord?.baseUrl || parsed.baseUrl,
|
||||||
|
repo: repoRecord?.ownerRepo || parsed.ownerRepo
|
||||||
|
};
|
||||||
|
const repoUrl = buildRepoUrl(fallbackRepo);
|
||||||
|
const name = repoRecord?.label || parsed.ownerRepo.split("/")[1] || parsed.ownerRepo;
|
||||||
|
return res.json({
|
||||||
|
fullName: fallbackRepo.repo,
|
||||||
|
name,
|
||||||
|
description: repoRecord?.label ? `Plugin: ${repoRecord.label}` : "Kon gegevens niet ophalen.",
|
||||||
|
repoUrl,
|
||||||
|
provider: fallbackRepo.provider,
|
||||||
|
ownerRepo: fallbackRepo.repo,
|
||||||
|
baseUrl: fallbackRepo.baseUrl,
|
||||||
|
repoId: repoRecord?.id || null,
|
||||||
|
label: repoRecord?.label || null,
|
||||||
|
manifest: null,
|
||||||
|
releases: [],
|
||||||
|
commits: [],
|
||||||
|
download: null,
|
||||||
|
version: null,
|
||||||
|
versionSource: null,
|
||||||
|
isPrivate: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPrivate = Boolean(info?.isPrivate);
|
||||||
|
const allowDownloads = normalizedEntry && !isPrivate;
|
||||||
|
|
||||||
const effectiveBaseUrl =
|
const effectiveBaseUrl =
|
||||||
normalizedEntry?.baseUrl ||
|
normalizedEntry?.baseUrl ||
|
||||||
@@ -400,7 +456,7 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
|||||||
(provider === "github" ? "https://github.com" : "");
|
(provider === "github" ? "https://github.com" : "");
|
||||||
|
|
||||||
const releasesWithDownload =
|
const releasesWithDownload =
|
||||||
normalizedEntry && releases.length > 0
|
allowDownloads && releases.length > 0
|
||||||
? releases.map((release) => {
|
? releases.map((release) => {
|
||||||
const tagOrName = release.tag || release.name;
|
const tagOrName = release.tag || release.name;
|
||||||
if (!tagOrName) {
|
if (!tagOrName) {
|
||||||
@@ -420,7 +476,7 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
|||||||
: releases;
|
: releases;
|
||||||
|
|
||||||
let downloadMeta = null;
|
let downloadMeta = null;
|
||||||
if (normalizedEntry) {
|
if (allowDownloads) {
|
||||||
const latestRelease = releasesWithDownload[0];
|
const latestRelease = releasesWithDownload[0];
|
||||||
const sourceType = latestRelease ? "release" : "branch";
|
const sourceType = latestRelease ? "release" : "branch";
|
||||||
const version = latestRelease?.tag || latestRelease?.name || info.defaultBranch || "main";
|
const version = latestRelease?.tag || latestRelease?.name || info.defaultBranch || "main";
|
||||||
@@ -454,6 +510,7 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
|
|||||||
manifest,
|
manifest,
|
||||||
releases: releasesWithDownload,
|
releases: releasesWithDownload,
|
||||||
download: downloadMeta,
|
download: downloadMeta,
|
||||||
|
isPrivate,
|
||||||
commits
|
commits
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -502,6 +559,9 @@ app.get("/api/plugins/:owner/:repo/download", async (req, res) => {
|
|||||||
});
|
});
|
||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error?.meta === "PRIVATE_REPO") {
|
||||||
|
return res.status(403).json({ error: "Downloads voor private plugins zijn uitgeschakeld." });
|
||||||
|
}
|
||||||
console.error("Plugin download endpoint error:", error);
|
console.error("Plugin download endpoint error:", error);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({ error: "Download mislukt." });
|
res.status(500).json({ error: "Download mislukt." });
|
||||||
@@ -543,20 +603,34 @@ app.post("/api/licenses", requireAuth, async (req, res) => {
|
|||||||
if (body.repoId && Number.isNaN(repoId)) {
|
if (body.repoId && Number.isNaN(repoId)) {
|
||||||
return res.status(400).json({ error: "Ongeldig repo id." });
|
return res.status(400).json({ error: "Ongeldig repo id." });
|
||||||
}
|
}
|
||||||
|
let repoRecord = null;
|
||||||
|
if (repoId) {
|
||||||
|
repoRecord = await getRepoById(repoId);
|
||||||
|
if (!repoRecord) {
|
||||||
|
return res.status(404).json({ error: "Plugin niet gevonden." });
|
||||||
|
}
|
||||||
|
}
|
||||||
const repoInput =
|
const repoInput =
|
||||||
typeof body.repo === "object"
|
typeof body.repo === "object"
|
||||||
? body.repo
|
? body.repo
|
||||||
: typeof body.plugin === "object"
|
: typeof body.plugin === "object"
|
||||||
? body.plugin
|
? body.plugin
|
||||||
: body.repo || body.ownerRepo || body.fullName || body.plugin;
|
: body.repo || body.ownerRepo || body.fullName || body.plugin;
|
||||||
|
const repoFromRecord = repoRecord
|
||||||
|
? {
|
||||||
|
repo: repoRecord.ownerRepo,
|
||||||
|
provider: repoRecord.provider,
|
||||||
|
baseUrl: repoRecord.baseUrl
|
||||||
|
}
|
||||||
|
: null;
|
||||||
const repoEntry =
|
const repoEntry =
|
||||||
normalizeRepoInput(repoInput, {
|
normalizeRepoInput(repoInput, {
|
||||||
repo: body.ownerRepo || body.fullName || body.repo,
|
repo: body.ownerRepo || body.fullName || body.repo || repoFromRecord?.repo,
|
||||||
provider: body.provider,
|
provider: body.provider || repoFromRecord?.provider,
|
||||||
baseUrl: body.baseUrl
|
baseUrl: body.baseUrl || repoFromRecord?.baseUrl
|
||||||
}) || null;
|
}) || repoFromRecord;
|
||||||
|
|
||||||
if (!repoId && !repoEntry) {
|
if (!repoEntry) {
|
||||||
return res.status(400).json({ error: "Kies een plugin om de licentie aan te koppelen." });
|
return res.status(400).json({ error: "Kies een plugin om de licentie aan te koppelen." });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,6 +751,12 @@ async function handleLicenseDownload(res, { key, hostname, version = "latest" })
|
|||||||
});
|
});
|
||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error?.meta === "PRIVATE_REPO") {
|
||||||
|
if (!res.headersSent) {
|
||||||
|
return res.status(403).json({ error: "Downloads voor private plugins zijn uitgeschakeld." });
|
||||||
|
}
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
console.error("Download endpoint error:", error);
|
console.error("Download endpoint error:", error);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({ error: "Download mislukt." });
|
res.status(500).json({ error: "Download mislukt." });
|
||||||
|
|||||||
@@ -27,3 +27,63 @@ export const DB_CONFIG = {
|
|||||||
|
|
||||||
export const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
export const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
||||||
export const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
|
export const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
|
||||||
|
|
||||||
|
function normalizeHostKey(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
return url.host.toLowerCase();
|
||||||
|
} catch {
|
||||||
|
return value.replace(/^[a-z]+:\/\//i, "").replace(/\/.*$/, "").trim().toLowerCase() || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGiteaTokenMap(rawValue) {
|
||||||
|
if (!rawValue) return {};
|
||||||
|
const map = {};
|
||||||
|
try {
|
||||||
|
const parsed = typeof rawValue === "string" ? JSON.parse(rawValue) : rawValue;
|
||||||
|
if (parsed && typeof parsed === "object") {
|
||||||
|
for (const [key, token] of Object.entries(parsed)) {
|
||||||
|
const host = normalizeHostKey(key);
|
||||||
|
if (host && typeof token === "string" && token.trim()) {
|
||||||
|
map[host] = token.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to comma-separated parsing.
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = String(rawValue)
|
||||||
|
.split(",")
|
||||||
|
.map((segment) => segment.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const segment of segments) {
|
||||||
|
const [key, token] = segment.split("=").map((part) => part.trim());
|
||||||
|
const host = normalizeHostKey(key);
|
||||||
|
if (host && token) {
|
||||||
|
map[host] = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_GITEA_TOKEN = process.env.GITEA_TOKEN?.trim() || null;
|
||||||
|
const GITEA_TOKENS = parseGiteaTokenMap(process.env.GITEA_TOKENS);
|
||||||
|
|
||||||
|
function getEnvTokenForHost(host) {
|
||||||
|
if (!host) return null;
|
||||||
|
const envKey = `GITEA_TOKEN_${host.replace(/[^A-Z0-9]/gi, "_").toUpperCase()}`;
|
||||||
|
return process.env[envKey]?.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGiteaToken(baseUrl) {
|
||||||
|
const host = normalizeHostKey(baseUrl);
|
||||||
|
return (
|
||||||
|
(host && (GITEA_TOKENS[host] || getEnvTokenForHost(host))) ||
|
||||||
|
DEFAULT_GITEA_TOKEN ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export async function resolveRepoDownload(repoEntry, requestedVersion = "latest"
|
|||||||
}
|
}
|
||||||
|
|
||||||
const repoInfo = await fetchRepo(normalizedEntry);
|
const repoInfo = await fetchRepo(normalizedEntry);
|
||||||
|
if (repoInfo.isPrivate) {
|
||||||
|
const error = new Error("Downloads niet beschikbaar voor private repositories.");
|
||||||
|
error.meta = "PRIVATE_REPO";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
const releases = await fetchReleases(normalizedEntry).catch(() => []);
|
const releases = await fetchReleases(normalizedEntry).catch(() => []);
|
||||||
|
|
||||||
let targetVersion = requestedVersion || "latest";
|
let targetVersion = requestedVersion || "latest";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { readJsonFile } from "./storage.js";
|
import { readJsonFile } from "./storage.js";
|
||||||
import { getCached, setCached } from "./cache.js";
|
import { getCached, setCached } from "./cache.js";
|
||||||
|
import { getGiteaToken } from "./config.js";
|
||||||
|
|
||||||
export function parseRepoEntry(entry) {
|
export function parseRepoEntry(entry) {
|
||||||
if (typeof entry === "string") {
|
if (typeof entry === "string") {
|
||||||
@@ -51,6 +52,12 @@ async function fetchJson(url, cacheKey, opts = {}) {
|
|||||||
if (!opts.provider || opts.provider === "github") {
|
if (!opts.provider || opts.provider === "github") {
|
||||||
headers.Accept = "application/vnd.github+json";
|
headers.Accept = "application/vnd.github+json";
|
||||||
}
|
}
|
||||||
|
if (opts.provider === "gitea") {
|
||||||
|
const token = opts.token || getGiteaToken(opts.baseUrl || url);
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = `token ${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
const response = await fetch(url, { headers });
|
const response = await fetch(url, { headers });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`${opts.provider || "git"} request failed for ${url}`);
|
throw new Error(`${opts.provider || "git"} request failed for ${url}`);
|
||||||
@@ -76,7 +83,7 @@ export async function fetchRepo(entry) {
|
|||||||
let data;
|
let data;
|
||||||
if (provider === "gitea") {
|
if (provider === "gitea") {
|
||||||
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/repos/${ownerRepo}`;
|
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/repos/${ownerRepo}`;
|
||||||
data = await fetchJson(url, `repo-raw:${provider}:${ownerRepo}`, { provider });
|
data = await fetchJson(url, `repo-raw:${provider}:${ownerRepo}`, { provider, baseUrl });
|
||||||
const mapped = {
|
const mapped = {
|
||||||
fullName: data.full_name || `${ownerRepo}`,
|
fullName: data.full_name || `${ownerRepo}`,
|
||||||
name: data.name || ownerRepo.split("/")[1] || ownerRepo,
|
name: data.name || ownerRepo.split("/")[1] || ownerRepo,
|
||||||
@@ -90,13 +97,16 @@ export async function fetchRepo(entry) {
|
|||||||
topics: data.topics || [],
|
topics: data.topics || [],
|
||||||
provider,
|
provider,
|
||||||
ownerRepo,
|
ownerRepo,
|
||||||
baseUrl
|
baseUrl,
|
||||||
|
isPrivate: Boolean(data.private)
|
||||||
};
|
};
|
||||||
setCached(cacheKey, mapped);
|
setCached(cacheKey, mapped);
|
||||||
return mapped;
|
return mapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
data = await fetchJson(`https://api.github.com/repos/${ownerRepo}`, `repo-raw:github:${ownerRepo}`, { provider: "github" });
|
data = await fetchJson(`https://api.github.com/repos/${ownerRepo}`, `repo-raw:github:${ownerRepo}`, {
|
||||||
|
provider: "github"
|
||||||
|
});
|
||||||
const mapped = {
|
const mapped = {
|
||||||
fullName: data.full_name,
|
fullName: data.full_name,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -110,7 +120,8 @@ export async function fetchRepo(entry) {
|
|||||||
topics: data.topics || [],
|
topics: data.topics || [],
|
||||||
provider,
|
provider,
|
||||||
ownerRepo,
|
ownerRepo,
|
||||||
baseUrl: "https://github.com"
|
baseUrl: "https://github.com",
|
||||||
|
isPrivate: Boolean(data.private)
|
||||||
};
|
};
|
||||||
setCached(cacheKey, mapped);
|
setCached(cacheKey, mapped);
|
||||||
return mapped;
|
return mapped;
|
||||||
@@ -131,7 +142,14 @@ export async function fetchManifest(entry, defaultBranch) {
|
|||||||
} else {
|
} else {
|
||||||
url = `https://raw.githubusercontent.com/${ownerRepo}/${branch}/manifest.json`;
|
url = `https://raw.githubusercontent.com/${ownerRepo}/${branch}/manifest.json`;
|
||||||
}
|
}
|
||||||
const response = await fetch(url, { headers: { "User-Agent": "siti-plugin-repo" } });
|
const headers = { "User-Agent": "siti-plugin-repo" };
|
||||||
|
if (provider === "gitea") {
|
||||||
|
const token = getGiteaToken(baseUrl);
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = `token ${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await fetch(url, { headers });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const manifest = await response.json().catch(() => null);
|
const manifest = await response.json().catch(() => null);
|
||||||
setCached(cacheKey, manifest);
|
setCached(cacheKey, manifest);
|
||||||
@@ -145,7 +163,7 @@ export async function fetchReleases(entry) {
|
|||||||
const { provider, ownerRepo, baseUrl } = parseRepoEntry(entry);
|
const { provider, ownerRepo, baseUrl } = parseRepoEntry(entry);
|
||||||
if (provider === "gitea") {
|
if (provider === "gitea") {
|
||||||
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/repos/${ownerRepo}/releases?limit=5`;
|
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/repos/${ownerRepo}/releases?limit=5`;
|
||||||
const data = await fetchJson(url, `releases:${provider}:${ownerRepo}`, { provider });
|
const data = await fetchJson(url, `releases:${provider}:${ownerRepo}`, { provider, baseUrl });
|
||||||
return Array.isArray(data)
|
return Array.isArray(data)
|
||||||
? data.map((release) => ({
|
? data.map((release) => ({
|
||||||
tag: release.tag_name || release.name,
|
tag: release.tag_name || release.name,
|
||||||
@@ -175,7 +193,7 @@ export async function fetchCommits(entry) {
|
|||||||
const { provider, ownerRepo, baseUrl } = parseRepoEntry(entry);
|
const { provider, ownerRepo, baseUrl } = parseRepoEntry(entry);
|
||||||
if (provider === "gitea") {
|
if (provider === "gitea") {
|
||||||
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/repos/${ownerRepo}/commits?limit=5`;
|
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/repos/${ownerRepo}/commits?limit=5`;
|
||||||
const data = await fetchJson(url, `commits:${provider}:${ownerRepo}`, { provider });
|
const data = await fetchJson(url, `commits:${provider}:${ownerRepo}`, { provider, baseUrl });
|
||||||
return Array.isArray(data)
|
return Array.isArray(data)
|
||||||
? data.map((commit) => ({
|
? data.map((commit) => ({
|
||||||
sha: commit.sha,
|
sha: commit.sha,
|
||||||
|
|||||||
35
src/App.css
35
src/App.css
@@ -90,6 +90,10 @@
|
|||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-size: clamp(2.4rem, 4vw, 3.4rem);
|
font-size: clamp(2.4rem, 4vw, 3.4rem);
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
@@ -180,6 +184,10 @@
|
|||||||
.card h2 {
|
.card h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card p {
|
.card p {
|
||||||
@@ -241,6 +249,11 @@
|
|||||||
color: #166534;
|
color: #166534;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.state.warning {
|
||||||
|
background: #fef9c3;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
.state.inline {
|
.state.inline {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -621,3 +634,25 @@
|
|||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.privacy-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default function Home() {
|
|||||||
{plugins.map((plugin) => {
|
{plugins.map((plugin) => {
|
||||||
const displayName = plugin.label || plugin.manifest?.plugin_name || plugin.name;
|
const displayName = plugin.label || plugin.manifest?.plugin_name || plugin.name;
|
||||||
const description = plugin.manifest?.description || plugin.description;
|
const description = plugin.manifest?.description || plugin.description;
|
||||||
|
const isPrivate = Boolean(plugin.isPrivate);
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
if (plugin.provider) {
|
if (plugin.provider) {
|
||||||
searchParams.set("provider", plugin.provider);
|
searchParams.set("provider", plugin.provider);
|
||||||
@@ -65,7 +66,15 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<article className="card" key={plugin.fullName}>
|
<article className="card" key={plugin.fullName}>
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h2>{displayName}</h2>
|
<h2>
|
||||||
|
{displayName}
|
||||||
|
{isPrivate && (
|
||||||
|
<span className="privacy-indicator" title="Privé plugin">
|
||||||
|
<span aria-hidden="true">🔒</span>
|
||||||
|
<span className="sr-only">Privé plugin</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
<span className="pill">{plugin.fullName}</span>
|
<span className="pill">{plugin.fullName}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export default function PluginDetail() {
|
|||||||
const author = manifest?.author || "-";
|
const author = manifest?.author || "-";
|
||||||
const version = manifest?.version || "-";
|
const version = manifest?.version || "-";
|
||||||
const repositoryLabel = data?.provider === "gitea" ? "Gitea" : "GitHub";
|
const repositoryLabel = data?.provider === "gitea" ? "Gitea" : "GitHub";
|
||||||
const latestDownload = data?.download;
|
const isPrivate = Boolean(data?.isPrivate);
|
||||||
|
const latestDownload = isPrivate ? null : data?.download;
|
||||||
|
|
||||||
const releases = useMemo(() => data?.releases || [], [data]);
|
const releases = useMemo(() => data?.releases || [], [data]);
|
||||||
const commits = useMemo(() => data?.commits || [], [data]);
|
const commits = useMemo(() => data?.commits || [], [data]);
|
||||||
@@ -59,7 +60,15 @@ export default function PluginDetail() {
|
|||||||
<header className="detail-hero">
|
<header className="detail-hero">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Plugin details</p>
|
<p className="eyebrow">Plugin details</p>
|
||||||
<h1>{displayName}</h1>
|
<h1>
|
||||||
|
{displayName}
|
||||||
|
{isPrivate && (
|
||||||
|
<span className="privacy-indicator" title="Privé plugin">
|
||||||
|
<span aria-hidden="true">🔒</span>
|
||||||
|
<span className="sr-only">Privé plugin</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
<p className="subtitle">{description}</p>
|
<p className="subtitle">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-actions">
|
<div className="detail-actions">
|
||||||
@@ -77,6 +86,12 @@ export default function PluginDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{isPrivate && (
|
||||||
|
<div className="state warning">
|
||||||
|
<strong>Privé plugin.</strong> Downloads via deze repo zijn niet beschikbaar.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && <div className="state">Bezig met laden…</div>}
|
{loading && <div className="state">Bezig met laden…</div>}
|
||||||
{error && <div className="state error">{error}</div>}
|
{error && <div className="state error">{error}</div>}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user