store the token provisioning mints, so members can open video
Provisioning already created each member's Jellyfin account and signed in as them to get a token — its own header says the token exists so "the person's Officer session can already read their library". It was then dropped, because linkJellyfinAccount had nowhere to put it. So every non-owner resolved to no config and got a 503 setup form, asking them to connect a Jellyfin server with a password nobody knows. It reads as a permissions fault and is not one: the grant, the role and the account list were all correct. jellyfin_accounts now carries access_token and device_id, getConfig falls back to the link row when the caller has no registry of their own, and there is no path from there to the owner's token — a member resolving to the owner's credential would read the owner's watch state and every library. POST /_config/:id/provision repairs accounts linked before the column existed. It resets the password to mint, which is free on an account Officer created (random, discarded, known to nobody) and destructive on an adopted one, so adopted accounts are skipped unless includeAdopted says otherwise. Applied on alpha: conceiba and guipinto minted and verified against the live server; fedra and diogo skipped as adopted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+51
-2
@@ -9,7 +9,7 @@ import {
|
||||
updateJellyfinServer,
|
||||
} from '../db';
|
||||
import { UpstreamError, authenticate, invalidateConfig, normalizeBase, probe, publicInfo } from './upstream';
|
||||
import { runProvisioning } from './provision-run';
|
||||
import { backfillAccountTokens, runProvisioning } from './provision-run';
|
||||
import { buildAccountView } from './account-view';
|
||||
|
||||
// `/_config` — the Jellyfin server registry, driven from the app.
|
||||
@@ -136,7 +136,14 @@ async function addServer(req: Request, userId: number): Promise<Response> {
|
||||
jellyfinUserId: auth.userId,
|
||||
deviceId,
|
||||
});
|
||||
return Response.json({ server, provisioning });
|
||||
// And the tokens for anyone this server already knew about. Reconnecting an instance that has been
|
||||
// registered before is the ordinary case for a moved server or a revoked credential, and its accounts
|
||||
// are all "already linked" by then — so provisioning alone would report success and change nothing.
|
||||
const tokens = await backfillAccountTokens({
|
||||
cfg: { base: url, token: auth.token, jellyfinUserId: auth.userId, deviceId },
|
||||
serverId: server.id,
|
||||
});
|
||||
return Response.json({ server, provisioning, tokens });
|
||||
} catch (err) {
|
||||
if (duplicateLabel(err)) return bad(`you already have a server called "${label}"`);
|
||||
throw err;
|
||||
@@ -211,6 +218,43 @@ async function editServer(req: Request, userId: number, id: number): Promise<Res
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-run provisioning against a server already registered, and mint whatever tokens are missing.
|
||||
*
|
||||
* The repair action, and the answer to "members cannot open Video". `addServer` does this once, at
|
||||
* connect; this is the same work without needing the owner to re-type a password they traded away.
|
||||
*
|
||||
* Owner-only WITHOUT a permission rule, and worth saying why out loud: every `/_config` route resolves its
|
||||
* server through `getJellyfinCredentials(userId, id)`, which is scoped to the caller's own rows. A member
|
||||
* holding `jellyfin` at write reaches this handler and gets a 404, because they have no server registry of
|
||||
* their own — the credential they read the library with lives on their link row, not here. So the scoping
|
||||
* that was already there for the registry does this job too.
|
||||
*/
|
||||
async function reprovision(req: Request, userId: number, id: number): Promise<Response> {
|
||||
const creds = await getJellyfinCredentials(userId, id);
|
||||
if (!creds) return bad('no such server', 404);
|
||||
|
||||
const body = ((await req.json().catch(() => null)) as { includeAdopted?: unknown } | null) ?? {};
|
||||
// `=== true` rather than truthy: this decides whether somebody else's Jellyfin password gets reset, and
|
||||
// a stray "false" string must not be the thing that turns it on.
|
||||
const includeAdopted = body.includeAdopted === true;
|
||||
|
||||
const base = normalizeBase(creds.url);
|
||||
const cfg = { base, token: creds.accessToken, jellyfinUserId: creds.jellyfinUserId, deviceId: creds.deviceId };
|
||||
|
||||
const provisioning = await runProvisioning({
|
||||
userId,
|
||||
serverId: id,
|
||||
base,
|
||||
token: creds.accessToken,
|
||||
jellyfinUserId: creds.jellyfinUserId,
|
||||
deviceId: creds.deviceId,
|
||||
});
|
||||
const tokens = await backfillAccountTokens({ cfg, serverId: id, includeAdopted });
|
||||
|
||||
return Response.json({ provisioning, tokens });
|
||||
}
|
||||
|
||||
/** Check one stored server without switching to it — what the "test" button on each row calls. */
|
||||
async function testServer(userId: number, id: number): Promise<Response> {
|
||||
const creds = await getJellyfinCredentials(userId, id);
|
||||
@@ -246,6 +290,11 @@ export async function handleConfigRoute(req: Request, userId: number, subpath: s
|
||||
return serverList(userId);
|
||||
}
|
||||
|
||||
if (action === 'provision') {
|
||||
if (req.method !== 'POST') return bad('method not allowed', 405);
|
||||
return reprovision(req, userId, id);
|
||||
}
|
||||
|
||||
// Who exists on each side, and which pairs are the same person. Read-only and computed on demand —
|
||||
// nothing is cached, because the interesting case is somebody adding or removing an account in
|
||||
// Jellyfin's own UI and wondering why Officer disagrees.
|
||||
|
||||
Reference in New Issue
Block a user