diff --git a/apps/admin-ui/src/app/link/page.tsx b/apps/admin-ui/src/app/link/page.tsx new file mode 100644 index 0000000..65a59e2 --- /dev/null +++ b/apps/admin-ui/src/app/link/page.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { FormEvent, useEffect, useState } from "react"; + +export default function LinkDevicePage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [code, setCode] = useState(""); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(null); + const [ok, setOk] = useState(null); + + useEffect(() => { + const q = new URLSearchParams(window.location.search); + const c = q.get("code"); + if (c) setCode(c.toUpperCase()); + }, []); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + setBusy(true); + setErr(null); + setOk(null); + try { + const res = await fetch("/api/v1/link/claim", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, code }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setErr(data.error?.message ?? "Koppelen mislukt"); + return; + } + setOk(`Apparaat “${data.deviceName ?? "TV"}” is gekoppeld. Je kunt dit venster sluiten.`); + setPassword(""); + } finally { + setBusy(false); + } + } + + return ( +
+
+

Media Cluster

+

Apparaat koppelen

+

+ Log in met je kijker-account en voer de code van je TV of telefoon in. +

+
+ + + + {err &&

{err}

} + {ok &&

{ok}

} + +
+
+ +
+ ); +} diff --git a/apps/admin-ui/src/app/viewers/page.tsx b/apps/admin-ui/src/app/viewers/page.tsx new file mode 100644 index 0000000..dd45a75 --- /dev/null +++ b/apps/admin-ui/src/app/viewers/page.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { FormEvent, useCallback, useEffect, useState } from "react"; +import { Nav, useAuth } from "@/components/Nav"; + +type Device = { + id: string; + name: string; + platform: string; + lastSeenAt: string | null; + createdAt: string; +}; + +type Viewer = { + id: string; + email: string; + name: string | null; + enabled: boolean; + appAccess: boolean; + createdAt: string; + deviceCount: number; + devices: Device[]; +}; + +export default function ViewersPage() { + useAuth(); + const [viewers, setViewers] = useState([]); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [name, setName] = useState(""); + const [busy, setBusy] = useState(false); + const [msg, setMsg] = useState(null); + const [err, setErr] = useState(null); + + const load = useCallback(() => { + fetch("/api/v1/admin/viewers", { credentials: "include" }) + .then((r) => r.json()) + .then((d) => setViewers(d.viewers ?? [])) + .catch(() => setErr("Kon kijkers niet laden")); + }, []); + + useEffect(() => { + load(); + }, [load]); + + async function createViewer(e: FormEvent) { + e.preventDefault(); + setBusy(true); + setMsg(null); + setErr(null); + try { + const res = await fetch("/api/v1/admin/viewers", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, name: name || undefined, appAccess: true }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setErr(data.error?.message ?? "Aanmaken mislukt"); + return; + } + setMsg(`Kijker ${data.viewer.email} aangemaakt`); + setEmail(""); + setPassword(""); + setName(""); + load(); + } finally { + setBusy(false); + } + } + + async function toggle(v: Viewer, field: "enabled" | "appAccess") { + await fetch(`/api/v1/admin/viewers/${v.id}`, { + method: "PATCH", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ [field]: !v[field] }), + }); + load(); + } + + async function revokeDevice(deviceId: string) { + if (!confirm("Apparaat intrekken?")) return; + await fetch(`/api/v1/admin/viewers/devices/${deviceId}/revoke`, { + method: "POST", + credentials: "include", + }); + load(); + } + + return ( + <> +