Add admin control to list/disable/delete addons and permanently remove nodes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cc7b7c8505
commit
b5236188c6
4 changed files with 186 additions and 13 deletions
|
|
@ -100,6 +100,26 @@ export default function NodesPage() {
|
|||
loadNodes();
|
||||
}
|
||||
|
||||
async function deleteNode(id: string, name: string) {
|
||||
if (
|
||||
!confirm(
|
||||
`Node "${name}" definitief verwijderen?\n\nAlle bestanden van deze node verdwijnen uit de catalogus. Dit kan niet ongedaan worden gemaakt.`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const res = await fetch(`/api/v1/admin/nodes/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
alert(data.error?.message ?? "Verwijderen mislukt");
|
||||
return;
|
||||
}
|
||||
loadNodes();
|
||||
}
|
||||
|
||||
async function disableNode(id: string) {
|
||||
if (!confirm("Node tijdelijk uitschakelen?")) return;
|
||||
await fetch(`/api/v1/admin/nodes/${id}/disable`, {
|
||||
|
|
@ -266,6 +286,9 @@ export default function NodesPage() {
|
|||
</button>
|
||||
</>
|
||||
)}
|
||||
<button className="danger" onClick={() => deleteNode(node.id, node.name)}>
|
||||
Verwijderen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,34 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Nav, useAuth } from "@/components/Nav";
|
||||
|
||||
interface AddonTokenRow {
|
||||
id: string;
|
||||
label: string | null;
|
||||
revoked: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const MASTER_URL = "https://master.vonas.nl";
|
||||
|
||||
export default function SettingsPage() {
|
||||
useAuth();
|
||||
const [tokens, setTokens] = useState<AddonTokenRow[]>([]);
|
||||
const [addonToken, setAddonToken] = useState("");
|
||||
const [installUrl, setInstallUrl] = useState("");
|
||||
const [label, setLabel] = useState("Stremio");
|
||||
|
||||
const loadTokens = useCallback(() => {
|
||||
fetch("/api/v1/admin/addon-tokens", { credentials: "include" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setTokens(d.tokens ?? []));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadTokens();
|
||||
}, [loadTokens]);
|
||||
|
||||
async function createAddonToken() {
|
||||
const res = await fetch("/api/v1/admin/addon-tokens", {
|
||||
method: "POST",
|
||||
|
|
@ -17,9 +37,39 @@ export default function SettingsPage() {
|
|||
body: JSON.stringify({ label }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert(data.error?.message ?? "Aanmaken mislukt");
|
||||
return;
|
||||
}
|
||||
setAddonToken(data.token);
|
||||
// Addon draait op Master, niet op Admin
|
||||
setInstallUrl(`https://master.vonas.nl/stremio/${data.token}/manifest.json`);
|
||||
setInstallUrl(`${MASTER_URL}/stremio/${data.token}/manifest.json`);
|
||||
loadTokens();
|
||||
}
|
||||
|
||||
async function revokeToken(id: string) {
|
||||
if (!confirm("Addon deactiveren? Bestaande Stremio-installaties stoppen met werken.")) return;
|
||||
await fetch(`/api/v1/admin/addon-tokens/${id}/revoke`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
loadTokens();
|
||||
}
|
||||
|
||||
async function activateToken(id: string) {
|
||||
await fetch(`/api/v1/admin/addon-tokens/${id}/activate`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
loadTokens();
|
||||
}
|
||||
|
||||
async function deleteToken(id: string) {
|
||||
if (!confirm("Addon definitief verwijderen? Dit kan niet ongedaan worden gemaakt.")) return;
|
||||
await fetch(`/api/v1/admin/addon-tokens/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
loadTokens();
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -28,29 +78,85 @@ export default function SettingsPage() {
|
|||
<div className="container">
|
||||
<h1 style={{ marginBottom: "2rem", fontSize: "1.75rem" }}>Instellingen</h1>
|
||||
|
||||
<div className="card">
|
||||
<div className="card" style={{ marginBottom: "2rem" }}>
|
||||
<h2 style={{ fontSize: "1rem", color: "#fafafa", textTransform: "none", marginBottom: "1rem" }}>
|
||||
Stremio Addon
|
||||
Nieuwe Stremio-addon
|
||||
</h2>
|
||||
<p style={{ color: "#a1a1aa", marginBottom: "1rem", fontSize: "0.875rem" }}>
|
||||
Genereer een addon token om de media library in Stremio te installeren.
|
||||
Genereer een addon-token om de bibliotheek in Stremio te installeren. Bewaar de URL —
|
||||
die wordt maar één keer getoond.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>Label</label>
|
||||
<input value={label} onChange={(e) => setLabel(e.target.value)} />
|
||||
<input value={label} onChange={(e) => setLabel(e.target.value)} placeholder="bijv. Thuis / TV" />
|
||||
</div>
|
||||
<button onClick={createAddonToken}>Addon token genereren</button>
|
||||
<button onClick={createAddonToken}>Addon aanmaken</button>
|
||||
{addonToken && (
|
||||
<>
|
||||
<div className="token-display">{addonToken}</div>
|
||||
<p style={{ fontSize: "0.875rem", color: "#a1a1aa" }}>Install URL:</p>
|
||||
<p style={{ fontSize: "0.875rem", color: "#a1a1aa", marginTop: "1rem" }}>
|
||||
Installatie-URL (nu kopiëren):
|
||||
</p>
|
||||
<div className="token-display">{installUrl}</div>
|
||||
<p style={{ fontSize: "0.875rem", color: "#71717a", marginTop: "0.5rem" }}>
|
||||
Plak deze URL in Stremio → Addons → Community Addons → URL invoeren
|
||||
Stremio → Addons → Community Addons → URL invoeren
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2 style={{ fontSize: "1rem", color: "#fafafa", textTransform: "none", marginBottom: "1rem" }}>
|
||||
Bestaande addons
|
||||
</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Status</th>
|
||||
<th>Aangemaakt</th>
|
||||
<th>Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.map((t) => (
|
||||
<tr key={t.id}>
|
||||
<td>{t.label || "—"}</td>
|
||||
<td>
|
||||
<span className={`status ${t.revoked ? "revoked" : "online"}`}>
|
||||
{t.revoked ? "Inactief" : "Actief"}
|
||||
</span>
|
||||
</td>
|
||||
<td>{new Date(t.createdAt).toLocaleString()}</td>
|
||||
<td style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
{t.revoked ? (
|
||||
<button onClick={() => activateToken(t.id)} style={{ background: "#166534" }}>
|
||||
Activeren
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={() => revokeToken(t.id)} style={{ background: "#b45309" }}>
|
||||
Deactiveren
|
||||
</button>
|
||||
)}
|
||||
<button className="danger" onClick={() => deleteToken(t.id)}>
|
||||
Verwijderen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{tokens.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} style={{ textAlign: "center", color: "#71717a" }}>
|
||||
Nog geen addons aangemaakt
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<p style={{ fontSize: "0.8rem", color: "#71717a", marginTop: "1rem" }}>
|
||||
Deactiveren of verwijderen zorgt dat de bijbehorende Stremio-addon niet meer werkt.
|
||||
De installatie-URL zelf wordt om veiligheidsredenen niet opnieuw getoond.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
const body = request.body as { label?: string };
|
||||
const token = generateSecureToken(24);
|
||||
|
||||
await prisma.addonToken.create({
|
||||
const created = await prisma.addonToken.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
tokenHash: hashToken(token),
|
||||
|
|
@ -265,6 +265,8 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
|
||||
return {
|
||||
token,
|
||||
id: created.id,
|
||||
label: created.label,
|
||||
installUrl: `/stremio/${token}/manifest.json`,
|
||||
};
|
||||
});
|
||||
|
|
@ -287,7 +289,33 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
|||
|
||||
app.post("/api/v1/admin/addon-tokens/:id/revoke", { preHandler: requireAdmin }, async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
await prisma.addonToken.update({ where: { id }, data: { revoked: true } });
|
||||
const user = (request as import("../auth/routes").AuthenticatedRequest).user;
|
||||
const updated = await prisma.addonToken.updateMany({
|
||||
where: { id, userId: user.id },
|
||||
data: { revoked: true },
|
||||
});
|
||||
if (updated.count === 0) throw new AppError("NOT_FOUND", "Addon token not found", 404);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
app.post("/api/v1/admin/addon-tokens/:id/activate", { preHandler: requireAdmin }, async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const user = (request as import("../auth/routes").AuthenticatedRequest).user;
|
||||
const updated = await prisma.addonToken.updateMany({
|
||||
where: { id, userId: user.id },
|
||||
data: { revoked: false },
|
||||
});
|
||||
if (updated.count === 0) throw new AppError("NOT_FOUND", "Addon token not found", 404);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
app.delete("/api/v1/admin/addon-tokens/:id", { preHandler: requireAdmin }, async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const user = (request as import("../auth/routes").AuthenticatedRequest).user;
|
||||
const deleted = await prisma.addonToken.deleteMany({
|
||||
where: { id, userId: user.id },
|
||||
});
|
||||
if (deleted.count === 0) throw new AppError("NOT_FOUND", "Addon token not found", 404);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -236,6 +236,22 @@ export async function registerNodeRoutes(app: FastifyInstance) {
|
|||
|
||||
return { node: { id: node.id, status: node.status } };
|
||||
});
|
||||
|
||||
app.delete("/api/v1/admin/nodes/:id", { preHandler: requireAdmin }, async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const existing = await prisma.node.findUnique({ where: { id } });
|
||||
if (!existing) throw new AppError("NOT_FOUND", "Node not found", 404);
|
||||
|
||||
const { nodeConnectionManager } = await import("../websocket/manager");
|
||||
nodeConnectionManager.disconnect(id);
|
||||
|
||||
await prisma.playbackSession.deleteMany({ where: { nodeId: id } });
|
||||
await prisma.mediaFile.deleteMany({ where: { nodeId: id } });
|
||||
await prisma.nodeCredential.deleteMany({ where: { nodeId: id } });
|
||||
await prisma.node.delete({ where: { id } });
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
}
|
||||
|
||||
function serializeNode(n: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue