To reduce bot traffic you must login to view /lordwelch/jellyfin-plugin-oidc/commit/ee2da78e46e71e0c6d30d7817963819a64a66977?files=build.yaml.
The GitHub login only links via username.
Files
2026-08-19 01:04:57 -07:00

436 lines
14 KiB
JavaScript

// TODO: Fix messages for proper rename support
// TODO: Mark tab as current for current provider
// TODO: Show URLs for easy copy into IDP
const oidcConfigurationPage = {
pluginUniqueId: "c2e83d4a-1a92-11f1-a789-f84d896c963f",
defaultConfig: {},
users: [],
loadConfiguration: (page) => {
ApiClient.getPluginConfiguration(oidcConfigurationPage.pluginUniqueId).then(
(config) => {
oidcConfigurationPage.populateProviders(page, config.Configs);
},
);
ApiClient.fetch({ url: ApiClient.getUrl("oidc/Get/default") }).then(
(resp) => {
resp.json().then((cfg) => {
oidcConfigurationPage.defaultConfig = cfg;
});
},
);
},
populateProviders: (page, providers) => {
// Clear providers in case there are out of date ones
var tabs = document.getElementById("oidc-tab-insert");
ApiClient.getUsers().then((users) => {
oidcConfigurationPage.users = users;
const select = page.querySelector('#userSelect');
select.querySelectorAll('option').forEach((option) => {
option.remove();
})
var choice = new Option('', '');
select.appendChild(choice);
users.forEach((user) => {
var choice = new Option(user.Name, user.Id);
select.appendChild(choice);
});
});
// Hide config if no providers are defined yet
const providerNames = Object.keys(providers);
tabs.replaceChildren(
...providerNames.map((provider_name) => {
var button = document.createElement("button");
button.className =
"emby-tab-button emby-button emby-tab-button-active lastFocused";
button.setAttribute("is", "emby-button");
button.setAttribute("type", "button");
button.setAttribute("data-index", "0");
var div = document.createElement("div");
div.className = "emby-button-foreground";
div.textContent = provider_name;
button.appendChild(div);
button.addEventListener("click", (e) => {
oidcConfigurationPage.loadProvider(page, provider_name);
e.preventDefault();
return false;
});
return button;
}),
);
var collapse = document.querySelector(
"#oidc-new-provider > .verticalSection > .collapseContent",
);
if (providerNames.length < 1) {
collapse.classList.remove("expanded");
collapse.classList.add("hide");
collapse.parentNode.setAttribute("data-expanded", "false");
} else {
// populateForm ensures that the form is visible
oidcConfigurationPage.populateForm(page, providerNames[0], {
Configs: providers,
});
}
},
listArgumentsByType: (page) => {
const json_class = ".oidc-json";
const toggle_class = ".oidc-toggle";
const text_class = ".oidc-text";
const text_list_class = ".oidc-line-list";
const oidc_form = page.querySelector("#oidc-new-provider");
const text_fields = [...oidc_form.querySelectorAll(text_class)].map(
(e) => e.id,
);
const json_fields = [...oidc_form.querySelectorAll(json_class)].map(
(e) => e.id,
);
const text_list_fields = [
...oidc_form.querySelectorAll(text_list_class),
].map((e) => e.id);
const check_fields = [...oidc_form.querySelectorAll(toggle_class)].map(
(e) => e.id,
);
const output = {
json_fields,
text_list_fields,
text_fields,
check_fields,
};
return output;
},
fillTextList: (text_list, element) => {
// text_list is an array of strings
// element is an input element
const val = text_list.join("\n");
element.value = val;
},
parseTextList: (element) => {
// Return the parsed text list
var out = element.value
.split("\n")
.map((e) => e.trim())
.filter((e) => e);
return out;
},
loadProvider: (page, provider_name) => {
ApiClient.getPluginConfiguration(oidcConfigurationPage.pluginUniqueId).then(
(config) => {
oidcConfigurationPage.populateForm(page, provider_name, config);
},
);
},
populateForm: (page, provider_name, config) => {
var provider = config.Configs[provider_name] || oidcConfigurationPage.defaultConfig;
var collapse = document.querySelector("#oidc-new-provider > .verticalSection > .collapseContent");
// Enforce expanded if we populate the form
collapse.parentNode.setAttribute("data-expanded", "true");
collapse.classList.remove("hide");
collapse.classList.add("expanded");
const form_elements = oidcConfigurationPage.listArgumentsByType(page);
const provider_name_element = page.querySelector("#ProviderName");
provider_name_element.value = provider_name;
provider_name_element.setAttribute("data-provider", provider_name);
page.querySelector("#CallbackURL").value = ApiClient.getUrl(`oidc/redirect/${provider_name}`);
page.querySelector("#StartURL").value = ApiClient.getUrl(`oidc/start/${provider_name}`);
form_elements.text_fields.forEach((htmlID) => {
const item = page.querySelector(`#${htmlID}`);
if (oidcConfigurationPage.defaultConfig[htmlID]) {
item.placeholder = `e.g. ${oidcConfigurationPage.defaultConfig[htmlID]}`;
}
if (provider[htmlID]) {
item.value = provider[htmlID];
} else {
item.value = null;
}
});
form_elements.json_fields.forEach((htmlID) => {
const item = page.querySelector(`#${htmlID}`);
if (provider[htmlID]) {
item.value = JSON.stringify(provider[htmlID]);
} else {
item.value = null;
}
});
form_elements.text_list_fields.forEach((htmlID) => {
const item = page.querySelector(`#${htmlID}`);
if (oidcConfigurationPage.defaultConfig[htmlID] && oidcConfigurationPage.defaultConfig[htmlID].length > 0) {
item.placeholder = `e.g. ${oidcConfigurationPage.defaultConfig[htmlID].join("\n")}`;
}
if (provider[htmlID]) {
oidcConfigurationPage.fillTextList(provider[htmlID], item);
} else {
item.value = null;
}
});
form_elements.check_fields.forEach((htmlID) => {
const item = page.querySelector(`#${htmlID}`);
item.checked = false;
if (provider[htmlID]) {
item.checked = provider[htmlID];
}
});
},
deleteProvider: (page, provider_name) => {
if (
!window.confirm(
`Are you sure you want to delete the provider ${provider_name}?`,
)
) {
return;
}
return new Promise((resolve) => {
ApiClient.getPluginConfiguration(
oidcConfigurationPage.pluginUniqueId,
).then((config) => {
if (!Object.hasOwn(config.Configs, provider_name)) {
resolve();
return;
}
delete config.Configs[provider_name];
ApiClient.updatePluginConfiguration(
oidcConfigurationPage.pluginUniqueId,
config,
).then((result) => {
Dashboard.processPluginConfigurationUpdateResult(result);
oidcConfigurationPage.loadConfiguration(page);
Dashboard.alert("Provider removed");
resolve();
});
});
});
},
saveProvider: (page, provider_name) => {
return new Promise((resolve) => {
const form_elements = oidcConfigurationPage.listArgumentsByType(page);
ApiClient.getPluginConfiguration(
oidcConfigurationPage.pluginUniqueId,
).then((config) => {
var current_config = {};
if (Object.hasOwn(config.Configs, provider_name)) {
current_config = config.Configs[provider_name];
}
form_elements.text_fields.forEach((id) => {
const value = page.querySelector(`#${id}`).value;
if (value) {
current_config[id] = page.querySelector(`#${id}`).value;
} else {
// We set this to an empty string so that xml serialization will overwrite the default value.
current_config[id] = "";
}
});
form_elements.json_fields.forEach((id) => {
const value = page.querySelector(`#${id}`).value;
if (value) {
current_config[id] = JSON.parse(value);
} else {
current_config[id] = "";
}
});
form_elements.check_fields.forEach((id) => {
current_config[id] = page.querySelector(`#${id}`).checked;
});
form_elements.text_list_fields.forEach((id) => {
current_config[id] = oidcConfigurationPage.parseTextList(
page.querySelector(`#${id}`),
);
});
config.Configs[provider_name] = current_config;
ApiClient.updatePluginConfiguration(
oidcConfigurationPage.pluginUniqueId,
config,
).then((result) => {
Dashboard.processPluginConfigurationUpdateResult(result);
oidcConfigurationPage.loadConfiguration(page);
oidcConfigurationPage.loadProvider(page, provider_name);
Dashboard.alert("Settings saved.");
resolve();
});
});
});
},
addTextAreaStyle: (view) => {
var style = document.createElement("link");
style.rel = "stylesheet";
style.href = `${ApiClient.getUrl("web/configurationpage")}?name=OIDC-Auth-config.css`;
view.appendChild(style);
},
};
export default function (view) {
oidcConfigurationPage.addTextAreaStyle(view);
oidcConfigurationPage.loadConfiguration(view);
oidcConfigurationPage.listArgumentsByType(view);
view.querySelector("#SaveProvider").addEventListener("click", (e) => {
const target_provider = view
.querySelector("#ProviderName")
.getAttribute("data-provider");
oidcConfigurationPage.saveProvider(view, target_provider);
e.preventDefault();
return false;
});
view.querySelector("#ProviderName").addEventListener("input", (e) => {
view.querySelector("#CallbackURL").value = ApiClient.getUrl(`oidc/redirect/${e.target.value}`);
view.querySelector("#StartURL").value = ApiClient.getUrl(`oidc/start/${e.target.value}`);
});
view.querySelector("#NewProvider").addEventListener("click", (e) => {
// Construct basic HTML structure for the dialog content
const html = `
<div class="focuscontainer dialog formDialog dialog-fullscreen-lowres centeredDialog opened"
data-history="true"
modal="modal"
data-autofocus="true"
data-removeonclose="true"
style="animation: 180ms ease-out both scaleup; min-width: 400px;">
<div class="formDialogHeader">
<button
is="paper-icon-button-light"
class="btnCancel autoSize paper-icon-button-light"
tabindex="-1" title="Back">
<span class="material-icons arrow_back" aria-hidden="true"></span>
</button>
<h3 class="formDialogHeaderTitle">New Provider</h3>
</div>
<div class="formDialogContent smoothScrollY">
<div class="dialogContentInner dialog-content-centered dialogContentInner-mini" style="padding-top:2em">
<form>
<div class="inputContainer">
<input is="emby-input" type="text" id="txtInput" label="Name of OpenID Provider" class="">
<div class="fieldDescription hide"></div>
</div>
<div class="formDialogFooter">
<button is="emby-button" type="submit"
class="raised btnSubmit block formDialogFooterItem button-submit emby-button">
<span class="submitText">OK</span>
</button>
</div>
</form>
</div>
</div>
</div>
`;
// Use the built-in browser standard or native Emby/Jellyfin dialog helper if exposed globally
const dialog = document.createElement("div");
dialog.className = "dialogContainer";
dialog.innerHTML = html;
// Handle button events inside the modal
dialog.querySelector(".btnCancel").addEventListener("click", (e) => {
dialog.remove();
e.preventDefault();
});
dialog.querySelector(".btnSubmit").addEventListener("click", (e) => {
// Place your custom plugin API call or logic here
oidcConfigurationPage.loadProvider(
view,
dialog.querySelector("#txtInput").value,
);
dialog.remove();
e.preventDefault();
});
document.body.appendChild(dialog);
dialog.querySelector("#txtInput").focus();
e.preventDefault();
return false;
});
view.querySelector("#DeleteProvider").addEventListener("click", (e) => {
const target_provider = view
.querySelector("#ProviderName")
.getAttribute("data-provider");
oidcConfigurationPage.deleteProvider(view, target_provider);
e.preventDefault();
return false;
});
view.querySelector("#userSelect").addEventListener("change", async (event) => {
if (event.target.value === '') {
return;
}
console.log("Selected Value:", event.target.value);
const user = await ApiClient.getUser(event.target.value);
delete user.Policy.IsAdministrator;
delete user.Policy.IsDisabled;
const dialog_options = {
html: `
<div id="oidc-config-dialog">
<div>
User policy:
<br/>
This is policy about what actions a user can do.
<br/>
This <b><i>is</i></b> usually what you want to configure in an IDP
</div>
<pre id="oidc-user-policy" style="position: relative; text-align: left; max-height: 10em; overflow: scroll;"><code>${JSON.stringify(user.Policy, null, 2)}</code></pre>
<div>
User Configuration:
<br/>
This is user preferences such as default language.
<br/>
This is usually <b><i>not</i></b> what you want to configure in an IDP.
</div>
<pre id="oidc-user-config" style="position: relative; text-align: left; max-height: 10em; overflow: scroll;"><code>${JSON.stringify(user.Configuration, null, 2)}</code></pre>
</div>
`,
title: `Config for user: ${user.Name}`,
};
event.target.selectedIndex = -1
Dashboard.confirm(dialog_options, null, () => {});
setTimeout(() => {
document.querySelectorAll('#oidc-config-dialog > pre > code').forEach(el => {
const container = document.createElement('span');
const btn = document.createElement('span');
btn.className = 'copy-button';
btn.onclick = () => {
navigator.clipboard.writeText(el.innerText);
Dashboard.alert('Text Copied!');
};
container.style = 'position: absolute; inset: 5px 5px auto auto;cursor: pointer;'
btn.style = 'width: 1em; height: 1em; border: 1px solid; box-shadow: rgb(153, 153, 153) -3px 3px; display: inline-block; margin: 0 2px;'
container.append(btn);
container.append('copy');
el.parentNode.append(container);
});
}, 400);
});
// This page, OIDC-Auth-linking.html, needs to be injected into a loaded jellyfin application.
// Either by using IAmParadox27's PluginPages or by extracting the index.html and injecting the page into it.
// Note that extracting the index.html only works for logged in users
view.querySelector("#oidc-self-service-link").href = ApiClient.getUrl("oidc/OIDC-Auth-linking.html");
}