diff --git a/apps/admin-ui/src/app/accounts/page.tsx b/apps/admin-ui/src/app/accounts/page.tsx new file mode 100644 index 0000000..ce3a866 --- /dev/null +++ b/apps/admin-ui/src/app/accounts/page.tsx @@ -0,0 +1,575 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { Nav, useAuth } from "@/components/Nav"; + +interface AddonTokenRow { + id: string; + label: string | null; + revoked: boolean; + createdAt: string; + hasIptv?: boolean; + iptvOk?: boolean; +} + +interface IptvForm { + label: string; + baseUrl: string; + username: string; + password: string; + enableLive: boolean; + enableVod: boolean; + enableSeries: boolean; + lastOkAt?: string | null; + lastError?: string | null; + hasPassword?: boolean; +} + +const emptyIptv = (): IptvForm => ({ + label: "", + baseUrl: "", + username: "", + password: "", + enableLive: true, + enableVod: true, + enableSeries: true, +}); + +const MASTER_URL = "https://master.vonas.nl"; + +export default function AccountsPage() { + useAuth(); + const [tokens, setTokens] = useState([]); + const [addonToken, setAddonToken] = useState(""); + const [installUrl, setInstallUrl] = useState(""); + const [label, setLabel] = useState("Stremio"); + const [iptvTokenId, setIptvTokenId] = useState(null); + const [iptv, setIptv] = useState(emptyIptv()); + const [iptvBusy, setIptvBusy] = useState(false); + const [iptvMsg, setIptvMsg] = useState(null); + const [iptvErr, setIptvErr] = useState(null); + const [adminEmail, setAdminEmail] = useState(""); + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [pwBusy, setPwBusy] = useState(false); + const [pwMsg, setPwMsg] = useState(null); + const [pwErr, setPwErr] = useState(null); + + const loadTokens = useCallback(() => { + fetch("/api/v1/admin/addon-tokens", { credentials: "include" }) + .then((r) => r.json()) + .then((d) => setTokens(d.tokens ?? [])); + }, []); + + useEffect(() => { + loadTokens(); + fetch("/api/v1/auth/me", { credentials: "include" }) + .then((r) => r.json()) + .then((d) => setAdminEmail(d.user?.email ?? "")) + .catch(() => undefined); + }, [loadTokens]); + + async function changePassword() { + setPwMsg(null); + setPwErr(null); + if (!currentPassword || !newPassword) { + setPwErr("Vul huidig en nieuw wachtwoord in"); + return; + } + if (newPassword.length < 8) { + setPwErr("Nieuw wachtwoord moet minstens 8 tekens zijn"); + return; + } + if (newPassword !== confirmPassword) { + setPwErr("Nieuwe wachtwoorden komen niet overeen"); + return; + } + setPwBusy(true); + try { + const res = await fetch("/api/v1/auth/change-password", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currentPassword, newPassword, confirmPassword }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setPwErr(data.error?.message ?? "Wachtwoord wijzigen mislukt"); + return; + } + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + setPwMsg("Wachtwoord gewijzigd. Andere sessies zijn uitgelogd."); + } finally { + setPwBusy(false); + } + } + + async function createAddonToken() { + const res = await fetch("/api/v1/admin/addon-tokens", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ label }), + }); + const data = await res.json(); + if (!res.ok) { + alert(data.error?.message ?? "Aanmaken mislukt"); + return; + } + setAddonToken(data.token); + 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", + }); + if (iptvTokenId === id) { + setIptvTokenId(null); + setIptv(emptyIptv()); + } + loadTokens(); + } + + async function openIptv(tokenId: string) { + setIptvTokenId(tokenId); + setIptvMsg(null); + setIptvErr(null); + setIptvBusy(true); + try { + const res = await fetch(`/api/v1/admin/addon-tokens/${tokenId}/iptv`, { + credentials: "include", + }); + const data = await res.json(); + if (!res.ok) { + setIptvErr(data.error?.message ?? "Laden mislukt"); + setIptv(emptyIptv()); + return; + } + if (data.iptv) { + setIptv({ + label: data.iptv.label ?? "", + baseUrl: data.iptv.baseUrl ?? "", + username: data.iptv.username ?? "", + password: "", + enableLive: data.iptv.enableLive ?? true, + enableVod: data.iptv.enableVod ?? true, + enableSeries: data.iptv.enableSeries ?? true, + lastOkAt: data.iptv.lastOkAt, + lastError: data.iptv.lastError, + hasPassword: data.iptv.hasPassword, + }); + } else { + setIptv(emptyIptv()); + } + } finally { + setIptvBusy(false); + } + } + + async function saveIptv() { + if (!iptvTokenId) return; + setIptvBusy(true); + setIptvMsg(null); + setIptvErr(null); + try { + const res = await fetch(`/api/v1/admin/addon-tokens/${iptvTokenId}/iptv`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + label: iptv.label || undefined, + baseUrl: iptv.baseUrl, + username: iptv.username, + password: iptv.password || undefined, + enableLive: iptv.enableLive, + enableVod: iptv.enableVod, + enableSeries: iptv.enableSeries, + }), + }); + const data = await res.json(); + if (!res.ok) { + setIptvErr(data.error?.message ?? "Opslaan mislukt"); + return; + } + setIptvMsg( + `Verbonden · ${data.iptv?.liveCategories ?? 0} live-bouquets. Herlaad de Stremio-addon om Live TV te zien.` + ); + setIptv((s) => ({ ...s, password: "", hasPassword: true, lastError: null })); + loadTokens(); + await openIptv(iptvTokenId); + } finally { + setIptvBusy(false); + } + } + + async function testIptv() { + if (!iptvTokenId) return; + setIptvBusy(true); + setIptvMsg(null); + setIptvErr(null); + try { + const res = await fetch(`/api/v1/admin/addon-tokens/${iptvTokenId}/iptv/test`, { + method: "POST", + credentials: "include", + }); + const data = await res.json(); + if (!res.ok) { + setIptvErr(data.error?.message ?? "Test mislukt"); + return; + } + setIptvMsg( + `OK · status ${data.status}` + + (data.expDate ? ` · exp ${data.expDate}` : "") + + ` · live ${data.liveCategories} · films ${data.vodCategories} · series ${data.seriesCategories}` + ); + loadTokens(); + } finally { + setIptvBusy(false); + } + } + + async function deleteIptv() { + if (!iptvTokenId) return; + if (!confirm("IPTV-userline ontkoppelen van deze addon?")) return; + setIptvBusy(true); + setIptvErr(null); + try { + await fetch(`/api/v1/admin/addon-tokens/${iptvTokenId}/iptv`, { + method: "DELETE", + credentials: "include", + }); + setIptv(emptyIptv()); + setIptvMsg("IPTV ontkoppeld"); + loadTokens(); + } finally { + setIptvBusy(false); + } + } + + const activeToken = tokens.find((t) => t.id === iptvTokenId); + + return ( + <> +