Add repository management functionality with CRUD operations

- Implemented repoService for database interactions including count, list, get, create, update, and delete operations.
- Created RepoManager component for managing repositories with a user interface.
- Added forms for creating and editing repositories, including validation and error handling.
- Integrated API calls for fetching, creating, updating, and deleting repositories.
- Enhanced user experience with loading states and action feedback messages.
This commit is contained in:
2026-02-01 04:30:17 +00:00
parent f1ed790cab
commit 29cd473190
13 changed files with 855 additions and 116 deletions

View File

@@ -18,6 +18,15 @@ import {
listLicensesByUser,
touchLicenseHostname
} from "./lib/licenseService.js";
import {
countRepos,
createRepo as createRepoRecord,
deleteRepo as deleteRepoRecord,
findRepoByOwnerRepo,
getRepoById,
listRepos,
updateRepo as updateRepoRecord
} from "./lib/repoService.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";
@@ -29,6 +38,7 @@ app.use(express.json());
try {
await ensureSchema();
await seedLegacyRepos();
} catch (error) {
console.error("Kon database schema niet initialiseren:", error);
process.exit(1);
@@ -50,12 +60,18 @@ async function resolveRepoMetaFromRequest(body = {}) {
return null;
}
const provider = (body.provider || body.repoProvider || "github").toLowerCase();
const baseUrl = body.baseUrl || body.repoBaseUrl || (provider === "github" ? "https://github.com" : null);
const requestedProvider = (body.provider || body.repoProvider || "").toLowerCase() || null;
const requestedBaseUrl = body.baseUrl || body.repoBaseUrl || null;
const ownerRepo = `${owner}/${repository}`;
const existing = await findRepoByOwnerRepo(ownerRepo, requestedProvider || undefined);
const provider = existing?.provider || requestedProvider || "github";
const baseUrl =
existing?.baseUrl || requestedBaseUrl || (provider === "github" ? "https://github.com" : null);
const repoEntry = normalizeRepoInput(
{ repo: `${owner}/${repository}`, provider, baseUrl },
{ repo: `${owner}/${repository}`, provider, baseUrl }
{ repo: ownerRepo, provider, baseUrl },
{ repo: ownerRepo, provider, baseUrl }
);
if (!repoEntry) {
@@ -68,9 +84,10 @@ async function resolveRepoMetaFromRequest(body = {}) {
const releases = await fetchReleases(repoEntry).catch(() => []);
const latestRelease = releases[0];
const version = manifest?.version || latestRelease?.tag || latestRelease?.name || info.defaultBranch || null;
const pluginName = manifest?.plugin_name || info.name || repository;
const pluginName = manifest?.plugin_name || existing?.label || info.name || repository;
return {
repoId: existing?.id || null,
repoEntry,
pluginName,
version
@@ -80,6 +97,30 @@ async function resolveRepoMetaFromRequest(body = {}) {
}
}
async function seedLegacyRepos() {
try {
const repoCount = await countRepos();
if (repoCount > 0) {
return;
}
const legacy = await readRepos(PATHS.reposFile);
if (!Array.isArray(legacy) || legacy.length === 0) {
return;
}
for (const entry of legacy) {
try {
await createRepoRecord(entry);
} catch (error) {
if (error?.meta !== "DUPLICATE") {
console.warn("Kon legacy repo niet importeren:", error.message);
}
}
}
} catch (error) {
console.warn("Legacy repos importeren mislukt:", error.message);
}
}
app.post("/api/auth/register", async (req, res) => {
try {
const { username, name, email, password } = req.body || {};
@@ -158,34 +199,136 @@ app.post("/api/admin/users", requireAuth, requireAdmin, async (req, res) => {
}
});
function resolveOwnerRepoFromBody(body = {}) {
if (body.ownerRepo) {
return String(body.ownerRepo).trim();
}
const owner = typeof body.owner === "string" ? body.owner.trim() : "";
const repo = typeof body.repo === "string" ? body.repo.trim() : "";
if (owner && repo) {
return `${owner}/${repo}`;
}
return "";
}
app.get("/api/repos", requireAuth, requireAdmin, async (_req, res) => {
try {
const repos = await listRepos();
res.json({ count: repos.length, items: repos });
} catch (error) {
res.status(500).json({ error: "Kon repos niet laden." });
}
});
app.post("/api/repos", requireAuth, requireAdmin, async (req, res) => {
try {
const ownerRepo = resolveOwnerRepoFromBody(req.body);
if (!ownerRepo) {
return res.status(400).json({ error: "Geef een owner en repo op." });
}
const provider = (req.body?.provider || "github").toLowerCase();
const baseUrl = req.body?.baseUrl || null;
const label = req.body?.label || null;
const repo = await createRepoRecord({
provider,
ownerRepo,
baseUrl,
label
});
res.status(201).json({ repo });
} catch (error) {
if (error?.meta === "DUPLICATE") {
return res.status(409).json({ error: error.message });
}
res.status(500).json({ error: "Kon repo niet opslaan." });
}
});
app.patch("/api/repos/:id", requireAuth, requireAdmin, async (req, res) => {
try {
const repoId = Number(req.params.id);
if (Number.isNaN(repoId)) {
return res.status(400).json({ error: "Ongeldige repo id." });
}
const ownerRepo = resolveOwnerRepoFromBody(req.body);
const payload = {
provider: req.body?.provider,
ownerRepo: ownerRepo || undefined,
baseUrl: req.body?.baseUrl,
label: req.body?.label
};
const repo = await updateRepoRecord(repoId, payload);
res.json({ repo });
} catch (error) {
if (error?.meta === "NOT_FOUND") {
return res.status(404).json({ error: error.message });
}
if (error?.meta === "DUPLICATE") {
return res.status(409).json({ error: error.message });
}
res.status(500).json({ error: "Kon repo niet bijwerken." });
}
});
app.delete("/api/repos/:id", requireAuth, requireAdmin, async (req, res) => {
try {
const repoId = Number(req.params.id);
if (Number.isNaN(repoId)) {
return res.status(400).json({ error: "Ongeldige repo id." });
}
await deleteRepoRecord(repoId);
res.status(204).end();
} catch (error) {
if (error?.meta === "NOT_FOUND") {
return res.status(404).json({ error: error.message });
}
if (error?.meta === "IN_USE") {
return res.status(409).json({ error: error.message });
}
res.status(500).json({ error: "Kon repo niet verwijderen." });
}
});
app.get("/api/plugins", async (_req, res) => {
try {
const repos = await readRepos(PATHS.reposFile);
const repos = await listRepos();
const results = await Promise.all(
repos.map(async (repo) => {
const entry = {
provider: repo.provider,
repo: repo.ownerRepo,
baseUrl: repo.baseUrl
};
try {
const info = await fetchRepo(repo);
const manifest = await fetchManifest(repo, info.defaultBranch || info.default_branch);
return { ...info, manifest };
const info = await fetchRepo(entry);
const manifest = await fetchManifest(entry, info.defaultBranch || info.default_branch);
return {
...info,
manifest,
provider: repo.provider,
baseUrl: repo.baseUrl,
repoId: repo.id,
label: repo.label,
ownerRepo: repo.ownerRepo
};
} catch {
const parsed = parseRepoEntry(repo);
const parsed = parseRepoEntry(entry);
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}`,
repoUrl: buildRepoUrl({ provider: repo.provider, baseUrl: repo.baseUrl, repo: repo.ownerRepo }),
stars: 0,
forks: 0,
issues: 0,
updatedAt: null,
topics: [],
manifest: null,
provider: parsed.provider,
ownerRepo: parsed.ownerRepo,
baseUrl: parsed.baseUrl
provider: repo.provider,
ownerRepo: repo.ownerRepo,
baseUrl: repo.baseUrl,
repoId: repo.id,
label: repo.label
};
}
})
@@ -223,9 +366,24 @@ function buildPluginDownloadEndpoint(ownerRepo, { provider, baseUrl, version } =
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 baseUrlQuery = req.query.baseUrl || (provider === "github" ? "https://github.com" : "");
const entry = provider === "github" ? ownerRepo : { provider, repo: ownerRepo, baseUrl: baseUrlQuery };
const repoIdQuery = Number(req.query.repoId);
let repoRecord = null;
if (!Number.isNaN(repoIdQuery)) {
repoRecord = await getRepoById(repoIdQuery);
}
if (!repoRecord) {
repoRecord = await findRepoByOwnerRepo(ownerRepo);
}
const provider = (req.query.provider || repoRecord?.provider || "github").toLowerCase();
const baseUrlQuery =
req.query.baseUrl || repoRecord?.baseUrl || (provider === "github" ? "https://github.com" : "");
const entry =
repoRecord && repoRecord.ownerRepo
? { provider: repoRecord.provider, repo: repoRecord.ownerRepo, baseUrl: repoRecord.baseUrl }
: provider === "github"
? ownerRepo
: { provider, repo: ownerRepo, baseUrl: baseUrlQuery };
const normalizedEntry = normalizeRepoInput(entry, { repo: ownerRepo, provider, baseUrl: baseUrlQuery });
const info = await fetchRepo(entry);
@@ -289,6 +447,8 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
res.json({
...info,
repoId: repoRecord?.id || null,
label: repoRecord?.label || null,
version: resolvedVersion,
versionSource,
manifest,
@@ -379,6 +539,10 @@ app.get("/api/licenses", requireAuth, async (req, res) => {
app.post("/api/licenses", requireAuth, async (req, res) => {
try {
const body = req.body || {};
const repoId = body.repoId ? Number(body.repoId) : null;
if (body.repoId && Number.isNaN(repoId)) {
return res.status(400).json({ error: "Ongeldig repo id." });
}
const repoInput =
typeof body.repo === "object"
? body.repo
@@ -392,7 +556,7 @@ app.post("/api/licenses", requireAuth, async (req, res) => {
baseUrl: body.baseUrl
}) || null;
if (!repoEntry) {
if (!repoId && !repoEntry) {
return res.status(400).json({ error: "Kies een plugin om de licentie aan te koppelen." });
}
@@ -418,7 +582,8 @@ app.post("/api/licenses", requireAuth, async (req, res) => {
const payload = await createLicense(ownerUserId, {
label: body.label?.trim(),
note: body.note?.trim(),
repo: repoEntry
repo: repoEntry,
repoId: repoId || undefined
});
res.status(201).json(payload);
} catch (error) {
@@ -443,6 +608,7 @@ app.post("/api/licenses/verify", async (req, res) => {
pluginVersion: repoMeta.version,
repoFullName: repoMeta.repoEntry.repo,
repoUrl: buildRepoUrl(repoMeta.repoEntry),
repoId: repoMeta.repoId || null,
repo: repoMeta.repoEntry
}
}