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
|
||||
// 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. */
|
||||
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)}`);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// ── 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
|
||||
// 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);
|
||||
@@ -124,7 +167,10 @@ export async function provision(params: {
|
||||
// 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
|
||||
// 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