Allow admins to link a viewer device by entering its pairing code.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7869cd6661
commit
95a5c17a6c
3 changed files with 116 additions and 2 deletions
|
|
@ -156,6 +156,7 @@ export default function ViewersPage() {
|
||||||
const [iptvLocalMsg, setIptvLocalMsg] = useState<Record<string, string | null>>({});
|
const [iptvLocalMsg, setIptvLocalMsg] = useState<Record<string, string | null>>({});
|
||||||
const [iptvLocalErr, setIptvLocalErr] = useState<Record<string, string | null>>({});
|
const [iptvLocalErr, setIptvLocalErr] = useState<Record<string, string | null>>({});
|
||||||
const [addonInstallUrls, setAddonInstallUrls] = useState<Record<string, string | null>>({});
|
const [addonInstallUrls, setAddonInstallUrls] = useState<Record<string, string | null>>({});
|
||||||
|
const [claimCodes, setClaimCodes] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
fetch("/api/v1/admin/viewers", { credentials: "include" })
|
fetch("/api/v1/admin/viewers", { credentials: "include" })
|
||||||
|
|
@ -519,6 +520,35 @@ export default function ViewersPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function claimDeviceCode(viewerId: string) {
|
||||||
|
const raw = (claimCodes[viewerId] ?? "").trim();
|
||||||
|
if (!raw) {
|
||||||
|
setErr("Voer de koppelcode van het apparaat in");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActionId(`claim-${viewerId}`);
|
||||||
|
setErr(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/v1/admin/viewers/${viewerId}/devices/claim`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ code: raw }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
setErr(data.error?.message ?? "Code koppelen mislukt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = data.deviceName ? ` (${data.deviceName})` : "";
|
||||||
|
setMsg(`Apparaat gekoppeld${name} — de app/TV rondt zelf af`);
|
||||||
|
setClaimCodes((prev) => ({ ...prev, [viewerId]: "" }));
|
||||||
|
load();
|
||||||
|
} finally {
|
||||||
|
setActionId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function unlockLogin(v: Viewer) {
|
async function unlockLogin(v: Viewer) {
|
||||||
setActionId(`unlock-${v.id}`);
|
setActionId(`unlock-${v.id}`);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
|
|
@ -577,8 +607,8 @@ export default function ViewersPage() {
|
||||||
<p className="page-kicker">Mensen</p>
|
<p className="page-kicker">Mensen</p>
|
||||||
<h1>Gebruikers</h1>
|
<h1>Gebruikers</h1>
|
||||||
<p className="page-lead muted">
|
<p className="page-lead muted">
|
||||||
Unified accounts voor app, IPTV en Stremio-addons. Koppelen via{" "}
|
Unified accounts voor app, IPTV en Stremio-addons. Apparaten koppelen via code hieronder
|
||||||
<Link href="/link">/link</Link> met een code.
|
of via <Link href="/link">/link</Link>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="btn-primary" onClick={() => setShowCreate((v) => !v)}>
|
<button type="button" className="btn-primary" onClick={() => setShowCreate((v) => !v)}>
|
||||||
|
|
@ -783,6 +813,41 @@ export default function ViewersPage() {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
<div className="claim-code-row">
|
||||||
|
<p className="muted detail-copy">
|
||||||
|
Openstaande code van TV/app (zonder gebruikerswachtwoord).
|
||||||
|
</p>
|
||||||
|
<div className="claim-code-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="text"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="ABCD-EFGH"
|
||||||
|
value={claimCodes[v.id] ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setClaimCodes((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[v.id]: e.target.value.toUpperCase(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
void claimDeviceCode(v.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-primary"
|
||||||
|
disabled={actionId === `claim-${v.id}` || !(claimCodes[v.id] ?? "").trim()}
|
||||||
|
onClick={() => void claimDeviceCode(v.id)}
|
||||||
|
>
|
||||||
|
{actionId === `claim-${v.id}` ? "Koppelen…" : "Code koppelen"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div className="detail-grid">
|
<div className="detail-grid">
|
||||||
|
|
@ -1529,6 +1594,24 @@ export default function ViewersPage() {
|
||||||
.devices-fold .detail-copy {
|
.devices-fold .detail-copy {
|
||||||
margin: 0.25rem 0 0.5rem;
|
margin: 0.25rem 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
.claim-code-row {
|
||||||
|
margin-top: 0.65rem;
|
||||||
|
padding-top: 0.55rem;
|
||||||
|
border-top: 1px solid var(--border-dim);
|
||||||
|
}
|
||||||
|
.claim-code-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.claim-code-form input {
|
||||||
|
flex: 1 1 10rem;
|
||||||
|
min-width: 8rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
.device-rows {
|
.device-rows {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0.35rem 0 0;
|
margin: 0.35rem 0 0;
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,23 @@ export function registerViewerRoutes(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Admin koppelt openstaande TV/app-code aan deze kijker (zonder gebruikerswachtwoord). */
|
||||||
|
app.post(
|
||||||
|
"/api/v1/admin/viewers/:id/devices/claim",
|
||||||
|
{
|
||||||
|
preHandler: requireAdmin,
|
||||||
|
config: {
|
||||||
|
rateLimit: { max: 30, timeWindow: "1 minute" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const body = (request.body || {}) as { code?: string };
|
||||||
|
if (!body.code?.trim()) throw new AppError("INVALID_REQUEST", "code verplicht", 400);
|
||||||
|
return viewers.adminClaimDeviceCode(id, body.code);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// ——— Device link (TV) ———
|
// ——— Device link (TV) ———
|
||||||
app.post(
|
app.post(
|
||||||
"/api/v1/client/device/code",
|
"/api/v1/client/device/code",
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,20 @@ export class ViewerService {
|
||||||
* In-app goedkeuren: viewer is al ingelogd (Bearer) en bevestigt een TV/device-code.
|
* In-app goedkeuren: viewer is al ingelogd (Bearer) en bevestigt een TV/device-code.
|
||||||
*/
|
*/
|
||||||
async approveDeviceCode(viewerId: string, codeRaw: string) {
|
async approveDeviceCode(viewerId: string, codeRaw: string) {
|
||||||
|
return this.claimLinkCodeForViewer(viewerId, codeRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin koppelt een openstaande app/TV-code aan een kijker (zonder diens wachtwoord).
|
||||||
|
* Zelfde effect als /link claim of in-app approve — het apparaat pollt daarna succesvol.
|
||||||
|
*/
|
||||||
|
async adminClaimDeviceCode(viewerId: string, codeRaw: string) {
|
||||||
|
const viewer = await prisma.viewerUser.findUnique({ where: { id: viewerId } });
|
||||||
|
if (!viewer) throw new AppError("NOT_FOUND", "Gebruiker niet gevonden", 404);
|
||||||
|
return this.claimLinkCodeForViewer(viewerId, codeRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async claimLinkCodeForViewer(viewerId: string, codeRaw: string) {
|
||||||
const viewer = await prisma.viewerUser.findUnique({ where: { id: viewerId } });
|
const viewer = await prisma.viewerUser.findUnique({ where: { id: viewerId } });
|
||||||
if (!viewer || !viewer.enabled || !viewer.appAccess) {
|
if (!viewer || !viewer.enabled || !viewer.appAccess) {
|
||||||
throw new AppError("FORBIDDEN", "Geen app-toegang voor dit account", 403);
|
throw new AppError("FORBIDDEN", "Geen app-toegang voor dit account", 403);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue