From 91e7502d3b990ebe8f51f21f855fa22e4fa2f1df Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Sun, 13 Sep 2026 16:29:40 +0200 Subject: [PATCH] Restore admin login unlock and password reset, plus desktop episode Sub actions. --- apps/admin-ui/src/app/downloads/page.tsx | 40 +++++-- apps/admin-ui/src/app/viewers/page.tsx | 132 +++++++++++++++++++---- apps/master-api/src/viewer/service.ts | 6 +- 3 files changed, 149 insertions(+), 29 deletions(-) diff --git a/apps/admin-ui/src/app/downloads/page.tsx b/apps/admin-ui/src/app/downloads/page.tsx index 05cd9a1..6e09d64 100644 --- a/apps/admin-ui/src/app/downloads/page.tsx +++ b/apps/admin-ui/src/app/downloads/page.tsx @@ -1140,13 +1140,39 @@ export default function DownloadsPage() { {j.movieTitle} {j.movieYear ? ` (${j.movieYear})` : ""} - {(() => { - const sub = subtitleStatus(j); - if (!sub || (j.episodes && j.episodes.length > 0)) return null; - if (sub.kind === "ok") return
{sub.text}
; - if (sub.kind === "pending") return
{sub.text}
; - return
{sub.text}
; - })()} + {j.episodes && j.episodes.length > 0 ? ( + + ) : ( + (() => { + const sub = subtitleStatus(j); + if (!sub) return null; + if (sub.kind === "ok") return
{sub.text}
; + if (sub.kind === "pending") return
{sub.text}
; + return
{sub.text}
; + })() + )} {j.sourceTitle} {j.nodeName} diff --git a/apps/admin-ui/src/app/viewers/page.tsx b/apps/admin-ui/src/app/viewers/page.tsx index ea7db5c..0a7d89a 100644 --- a/apps/admin-ui/src/app/viewers/page.tsx +++ b/apps/admin-ui/src/app/viewers/page.tsx @@ -166,6 +166,7 @@ export default function ViewersPage() { const [iptvLocalErr, setIptvLocalErr] = useState>({}); const [addonInstallUrls, setAddonInstallUrls] = useState>({}); const [claimCodes, setClaimCodes] = useState>({}); + const [newPasswords, setNewPasswords] = useState>({}); const load = useCallback(() => { fetch("/api/v1/admin/viewers", { credentials: "include" }) @@ -584,6 +585,7 @@ export default function ViewersPage() { async function unlockLogin(v: Viewer) { setActionId(`unlock-${v.id}`); + setMsg(null); setErr(null); try { const res = await fetch(`/api/v1/admin/viewers/${v.id}/unlock-login`, { @@ -602,6 +604,35 @@ export default function ViewersPage() { } } + async function changeViewerPassword(v: Viewer) { + const next = (newPasswords[v.id] ?? "").trim(); + if (next.length < 8) { + setErr("Nieuw wachtwoord minstens 8 tekens"); + return; + } + setActionId(`pw-${v.id}`); + setMsg(null); + setErr(null); + try { + const res = await fetch(`/api/v1/admin/viewers/${v.id}`, { + method: "PATCH", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: next }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setErr(data.error?.message ?? "Wachtwoord wijzigen mislukt"); + return; + } + setNewPasswords((prev) => ({ ...prev, [v.id]: "" })); + setMsg(`Wachtwoord bijgewerkt voor ${v.email}`); + load(); + } finally { + setActionId(null); + } + } + async function deleteViewer(v: Viewer) { const label = v.name || v.email; if ( @@ -917,28 +948,72 @@ export default function ViewersPage() {

Bepaalt of deze gebruiker kan inloggen en de app-inhoud mag gebruiken.

- {(selected.loginLocked || (selected.loginFailedCount ?? 0) > 0) && ( -
-

- {selected.loginLocked - ? `Login vergrendeld tot ${ - selected.loginLockedUntil - ? new Date(selected.loginLockedUntil).toLocaleString("nl-NL") - : "…" - } (3 foute pogingen).` - : `${selected.loginFailedCount} foute wachtwoordpoging${ +

0 + ? "login-lock-box" + : "login-lock-box idle" + } + > +

+ {selected.loginLocked + ? `Login vergrendeld tot ${ + selected.loginLockedUntil + ? new Date(selected.loginLockedUntil).toLocaleString("nl-NL") + : "…" + } (3 foute pogingen).` + : (selected.loginFailedCount ?? 0) > 0 + ? `${selected.loginFailedCount} foute wachtwoordpoging${ (selected.loginFailedCount ?? 0) === 1 ? "" : "en" - } (lock na 3).`} -

- -
- )} + } (lock na 3).` + : "Geen login-blokkade actief. Ontgrendelen wist foute-pogingen-teller."} +

+ +
+
+ + +
@@ -1593,6 +1668,11 @@ export default function ViewersPage() { border-radius: 10px; border: 1px solid rgba(248, 113, 113, 0.35); background: rgba(248, 113, 113, 0.08); + margin-bottom: 0.85rem; + } + .login-lock-box.idle { + border-color: var(--border-dim); + background: var(--bg-elevated); } .login-lock-box p { margin: 0; @@ -1600,6 +1680,16 @@ export default function ViewersPage() { min-width: 180px; font-size: 0.88rem; } + .password-change { + margin-bottom: 0.85rem; + padding: 0.75rem 0.9rem; + border-radius: 10px; + border: 1px solid var(--border-dim); + background: var(--bg-elevated); + } + .password-change .btn-primary { + justify-self: start; + } .toggle-rows { display: grid; gap: 0.55rem; diff --git a/apps/master-api/src/viewer/service.ts b/apps/master-api/src/viewer/service.ts index bd04cd2..35667b5 100644 --- a/apps/master-api/src/viewer/service.ts +++ b/apps/master-api/src/viewer/service.ts @@ -252,7 +252,7 @@ export class ViewerService { } patch.passwordHash = await hashPassword(data.password); } - return prisma.viewerUser.update({ + const updated = await prisma.viewerUser.update({ where: { id }, data: patch, select: { @@ -266,6 +266,10 @@ export class ViewerService { createdAt: true, }, }); + if (patch.passwordHash) { + await this.clearLoginLock(updated.email.toLowerCase()); + } + return updated; } async revokeDevice(deviceId: string) {