436 lines
14 KiB
JavaScript
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");
|
|
}
|