grant every library on creation, by sending back the whole policy
A new Jellyfin user can see nothing: EnableAllFolders is false by default, so a provisioned account
signs in to an empty home screen and looks like a bug in Officer rather than an unset flag.
POST /Users/{id}/Policy replaces rather than merges, so the policy Jellyfin returned from /Users/New
goes straight back with one field flipped. A hand-built body would quietly reset the other 43
properties. EnabledFolders is left as-is — ignored while the blanket flag is on, and still there if
the owner ever turns it off.
Not fatal if it fails: the account exists and works, and failing the create here would leave an
orphan upstream with no mapping row, which is worse than a user who cannot see films yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+48
-2
@@ -24,6 +24,37 @@ import { planReconcile, describePlan, normalize, type JellyfinUser, type Reconci
|
|||||||
// and here the sign-in is not merely a check, it is how the token is obtained. The verification and the
|
// and here the sign-in is not merely a check, it is how the token is obtained. The verification and the
|
||||||
// useful work are the same call, which is the only reason it cannot be skipped when someone is in a hurry.
|
// useful work are the same call, which is the only reason it cannot be skipped when someone is in a hurry.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn on access to every library, preserving everything else about the policy.
|
||||||
|
*
|
||||||
|
* Not fatal if it fails: the account exists and can sign in, and library access is fixable from Jellyfin's
|
||||||
|
* own UI. Failing the whole creation here would leave an orphan account upstream with no mapping row,
|
||||||
|
* which is a worse state than a user who cannot see films yet.
|
||||||
|
*/
|
||||||
|
async function grantAllLibraries(
|
||||||
|
cfg: UpstreamTarget,
|
||||||
|
jellyfinUserId: string,
|
||||||
|
policy: Record<string, unknown> | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!policy) {
|
||||||
|
console.warn(`[jellyfin:provision] ${jellyfinUserId}: no Policy on the created user — library access not set`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await callUpstream(cfg, {
|
||||||
|
method: 'POST',
|
||||||
|
path: `/Users/${jellyfinUserId}/Policy`,
|
||||||
|
contentType: 'application/json',
|
||||||
|
// `EnabledFolders` is left as Jellyfin returned it. With `EnableAllFolders` true it is ignored, and
|
||||||
|
// clearing it would lose the per-folder list if the owner ever turns the blanket flag back off.
|
||||||
|
body: JSON.stringify({ ...policy, EnableAllFolders: true }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error(
|
||||||
|
`[jellyfin:provision] ${jellyfinUserId}: POST Policy → ${res.status}: ${(await res.text()).slice(0, 200)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Long enough that it does not matter that it is never rotated, since nobody ever knows it. */
|
/** Long enough that it does not matter that it is never rotated, since nobody ever knows it. */
|
||||||
const generatePassword = (): string => randomBytes(32).toString('base64url');
|
const generatePassword = (): string => randomBytes(32).toString('base64url');
|
||||||
|
|
||||||
@@ -72,9 +103,21 @@ async function createOne(
|
|||||||
throw new Error(`POST /Users/New → ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
throw new Error(`POST /Users/New → ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = (await res.json()) as { Id?: string };
|
const created = (await res.json()) as { Id?: string; Policy?: Record<string, unknown> };
|
||||||
if (!created.Id) throw new Error('POST /Users/New returned no Id');
|
if (!created.Id) throw new Error('POST /Users/New returned no Id');
|
||||||
|
|
||||||
|
// ── Give them every library ──
|
||||||
|
//
|
||||||
|
// A new Jellyfin user can see NOTHING by default: `EnableAllFolders` is false and `EnabledFolders` is
|
||||||
|
// empty, so the account exists, signs in, and shows an empty home screen. Provisioning that and calling
|
||||||
|
// it done would look like a bug in Officer rather than an unset flag.
|
||||||
|
//
|
||||||
|
// `POST /Users/{id}/Policy` REPLACES the policy rather than merging it — so the whole object goes back,
|
||||||
|
// taken from what Jellyfin just returned and modified by one field. Sending a hand-built policy would
|
||||||
|
// silently reset the 43 properties not mentioned, which is how a user ends up with no transcoding or no
|
||||||
|
// remote access for reasons nobody can trace.
|
||||||
|
await grantAllLibraries(cfg, created.Id, created.Policy);
|
||||||
|
|
||||||
// Rule 4, and the token in one call. A per-user device id keeps this session distinct from the owner's
|
// Rule 4, and the token in one call. A per-user device id keeps this session distinct from the owner's
|
||||||
// in Jellyfin's Devices screen, so revoking one does not sign out the other.
|
// in Jellyfin's Devices screen, so revoking one does not sign out the other.
|
||||||
const auth = await authenticate(cfg.base, `officer-${user.id}`, user.username!, password);
|
const auth = await authenticate(cfg.base, `officer-${user.id}`, user.username!, password);
|
||||||
@@ -124,7 +167,10 @@ export async function provision(params: {
|
|||||||
// One person failing must not stop the rest. A partially provisioned server is recoverable by
|
// One person failing must not stop the rest. A partially provisioned server is recoverable by
|
||||||
// running again; a run that aborts on the first error leaves everyone after them unprovisioned and
|
// running again; a run that aborts on the first error leaves everyone after them unprovisioned and
|
||||||
// gives no clue which ones those were.
|
// gives no clue which ones those were.
|
||||||
outcome.failed.push({ username: user.username ?? String(user.id), error: error instanceof Error ? error.message : String(error) });
|
outcome.failed.push({
|
||||||
|
username: user.username ?? String(user.id),
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user