Skip to content

Commit

Permalink
Improve UX on config page (#27)
Browse files Browse the repository at this point in the history
* scope listArgumentsByType to oidc form, select on class

scope listArgumentsByType to correct form

Select arguments by class instead of type

* String arrays now given as newline separated lists

* Implement UI for checklisting folders, and role mapping

start making enabled folders checklist-able

Implement save/load folder lists

change enabledfolders to checklist

refactor: rename saveFolderList -> serializeEnabledFolders, fillFolderList -> populateEnabledFolders

refactor listArgumentsByType

start implementing load provider role mapping

implement role mapping

Move remove button markup, change hierachy

remove logging statements

resolve folder population promise before trying to check folders

* Improve markup, add stylesheet, update help strings

Add stylesheet

update markup + styling of specialized forms

improve markup, assign checkboxes as emby-checkbox es

imrpove markup, remove old comments

run stylesheet through prettier

Update help strings

* Markup changes: Adjust role mapping whitespace

* scope listArgumentsByType to oidc form, select on class

scope listArgumentsByType to correct form

Select arguments by class instead of type

* String arrays now given as newline separated lists

* Implement UI for checklisting folders, and role mapping

start making enabled folders checklist-able

Implement save/load folder lists

change enabledfolders to checklist

refactor: rename saveFolderList -> serializeEnabledFolders, fillFolderList -> populateEnabledFolders

refactor listArgumentsByType

start implementing load provider role mapping

implement role mapping

Move remove button markup, change hierachy

remove logging statements

resolve folder population promise before trying to check folders

* Improve markup, add stylesheet, update help strings

Add stylesheet

update markup + styling of specialized forms

improve markup, assign checkboxes as emby-checkbox es

imrpove markup, remove old comments

run stylesheet through prettier

Update help strings

* Markup changes: Adjust role mapping whitespace

* Proper stylesheet import with nonstandard base URL

* Style

* Ensure rolemappings are using arrays instead of dicts

* Add confirmation for deletion

* Linting

* Update admin page status in README

Co-authored-by: 9p4 <[email protected]>
  • Loading branch information
strazto and 9p4 authored May 22, 2022
1 parent f7d066d commit f985096
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 54 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ https://user-images.githubusercontent.com/17993169/149681516-f93b43f5-fa5c-4c1f-

This is 100% alpha software! PRs are welcome to improve the code.

There is NO admin configuration! You must use the API to configure the program!
~~There is NO admin configuration! You must use the API to configure the program!~~ Added by [matthewstrasiotto](https://github.com/matthewstrasiotto) in PR [#18](https://github.com/9p4/jellyfin-plugin-sso/pull/18) and [#27](https://github.com/9p4/jellyfin-plugin-sso/pull/27).

**[This is for Jellyfin 10.8](https://github.com/9p4/jellyfin-plugin-sso/issues/3) and only on the Web UI!**

Expand Down Expand Up @@ -66,7 +66,7 @@ Build the zipped plugin with `jprm --verbosity=debug plugin build .`.

## Roadmap

- [ ] Admin page
- [x] Admin page
- [ ] Automated tests
- [x] Add role/claims support
- [ ] Use canonical usernames instead of preferred usernames
Expand Down Expand Up @@ -191,7 +191,7 @@ These all require authorization. Append an API key to the end of the request: `c

There is no GUI to sign in. You have to make it yourself! The buttons should redirect to something like this: [https://myjellyfin.example.com/sso/SAML/p/clientid](https://myjellyfin.example.com/sso/SAML/p/clientid) replacing `clientid` with the provider client ID and `SAML` with the auth scheme (either `SAML` or `OID`).

Furthermore, there is no functional admin page (yet). PRs for this are welcome. In the meantime, you have to interact with the API to add or remove configurations.
~~Furthermore, there is no functional admin page (yet). PRs for this are welcome. In the meantime, you have to interact with the API to add or remove configurations.~~ Added by [matthewstrasiotto](https://github.com/matthewstrasiotto) in PR [#18](https://github.com/9p4/jellyfin-plugin-sso/pull/18) and [#27](https://github.com/9p4/jellyfin-plugin-sso/pull/27).

There is also no logout callback. Logging out of Jellyfin will log you out of Jellyfin only, instead of the SSO provider as well.

Expand Down
257 changes: 242 additions & 15 deletions SSO-Auth/Config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const ssoConfigurationPage = {
ssoConfigurationPage.populateProviders(page, config.OidConfigs);
}
);

const folder_container = page.querySelector("#EnabledFolders");
ssoConfigurationPage.populateFolders(folder_container);
},
populateProviders: (page, providers) => {
// Clear providers in case there are out of date ones
Expand All @@ -24,27 +27,187 @@ const ssoConfigurationPage = {
page.querySelector("#selectProvider").appendChild(choice);
});
},
populateEnabledFolders: (folder_list, container) => {
container.querySelectorAll(".folder-checkbox").forEach((e) => {
e.checked = folder_list.includes(e.getAttribute("data-id"));
});
},
serializeEnabledFolders: (container) => {
return [...container.querySelectorAll(".folder-checkbox")]
.filter((e) => e.checked)
.map((e) => {
return e.getAttribute("data-id");
});
},
populateFolders: (container) => {
return ApiClient.getJSON(
ApiClient.getUrl("Library/MediaFolders", {
IsHidden: false,
})
).then((folders) => {
ssoConfigurationPage._populateFolders(container, folders);
});
},
/*
container: html element
folders.Items: array of objects, with .Id & .Name
*/
_populateFolders: (container, folders) => {
container
.querySelectorAll(".emby-checkbox-label")
.forEach((e) => e.remove());

const checkboxes = folders.Items.map((folder) => {
var out = document.createElement("label");

out.innerHTML = `
<input
is="emby-checkbox"
class="folder-checkbox chkFolder"
data-id="${folder.Id}"
type="checkbox"
/>
<span>${folder.Name}</span>
`;

return out;
});

checkboxes.forEach((e) => {
container.appendChild(e);
});
},

populateRoleMappings: (folder_role_mappings, container) => {
container
.querySelectorAll(".sso-role-mapping-container")
.forEach((e) => e.remove());

const mapping_elements = folder_role_mappings.map((mapping) => {
var elem = document.createElement("div");

elem.classList.add("sso-role-mapping-container");
elem.innerHTML = `
<label
class="inputLabel inputLabelUnfocused sso-role-mapping-input-label"
>Role:</label>
<div class="listItem">
<input
is="emby-input"
required=""
type="text"
class="listItemBody sso-role-mapping-name"
/>
<button
type="button"
is="paper-icon-button-light"
class="listItemButton sso-remove-role-mapping"
>
<span class="material-icons remove_circle" aria-hidden="true"></span>
</button>
</div>
<div
class="checkboxList paperList sso-folder-list"
></div>
`;

var checklist = elem.querySelector(".sso-folder-list");
const enabled_folders = mapping["Folders"];

ssoConfigurationPage
.populateFolders(checklist)
.then(() =>
ssoConfigurationPage.populateEnabledFolders(
enabled_folders,
checklist
)
);

elem.querySelector(".sso-role-mapping-name").value = mapping["Role"];
elem
.querySelector(".sso-remove-role-mapping")
.addEventListener(
"click",
ssoConfigurationPage.handleRoleMappingRemove
);

return elem;
});

mapping_elements.forEach((e) => container.appendChild(e));
},
serializeRoleMappings: (container) => {
var out = [];
const roles = [
...container.querySelectorAll(".sso-role-mapping-container"),
].forEach((elem) => {
const role = elem.querySelector(".sso-role-mapping-name").value;
const checklist = elem.querySelector(".sso-folder-list");

out.push({
Role: role,
Folders: ssoConfigurationPage.serializeEnabledFolders(checklist),
});
});

return out;
},
handleRoleMappingRemove: (evt) => {
const targeted_mapping = evt.target.closest(".sso-role-mapping-container");
targeted_mapping.remove();
},
listArgumentsByType: (page) => {
const json_fields = [
"EnabledFolders",
"FolderRoleMapping",
"Roles",
"AdminRoles",
"OidScopes",
];

const text_fields = [...page.querySelectorAll("input[type='text']")]
.map((e) => e.id)
.filter((id) => !json_fields.includes(id));

const check_fields = [
...page.querySelectorAll("input[type='checkbox']"),
const json_class = ".sso-json";
const toggle_class = ".sso-toggle";
const text_class = ".sso-text";
const text_list_class = ".sso-line-list";

const folder_list_fields = ["EnabledFolders"];
const role_map_fields = ["FolderRoleMapping"];

const oidc_form = page.querySelector("#sso-new-oidc-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 output = { json_fields, text_fields, check_fields };
const check_fields = [...oidc_form.querySelectorAll(toggle_class)].map(
(e) => e.id
);

const output = {
json_fields,
text_list_fields,
text_fields,
check_fields,
folder_list_fields,
role_map_fields,
};

return output;
},
fillTextList: (text_list, element) => {
// text_list is an array of strings
// element is an input element
const val = text_list.join("\r\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(ssoConfigurationPage.pluginUniqueId).then(
(config) => {
Expand All @@ -63,13 +226,43 @@ const ssoConfigurationPage = {
page.querySelector("#" + id).value = JSON.stringify(provider[id]);
});

form_elements.text_list_fields.forEach((id) => {
if (provider[id])
ssoConfigurationPage.fillTextList(
provider[id],
page.querySelector("#" + id)
);
});

form_elements.folder_list_fields.forEach((id) => {
if (provider[id]) {
ssoConfigurationPage.populateEnabledFolders(
provider[id],
page.querySelector(`#${id}`)
);
}
});

form_elements.check_fields.forEach((id) => {
if (provider[id]) page.querySelector("#" + id).checked = provider[id];
});

form_elements.role_map_fields.forEach((id) => {
const elem = page.querySelector(`#${id}`);
if (provider[id])
ssoConfigurationPage.populateRoleMappings(provider[id], elem);
});
}
);
},
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(
ssoConfigurationPage.pluginUniqueId
Expand Down Expand Up @@ -120,6 +313,23 @@ const ssoConfigurationPage = {
current_config[id] = page.querySelector("#" + id).checked;
});

form_elements.text_list_fields.forEach((id) => {
current_config[id] = ssoConfigurationPage.parseTextList(
page.querySelector("#" + id)
);
});

form_elements.folder_list_fields.forEach((id) => {
const elem = page.querySelector(`#${id}`);
current_config[id] =
ssoConfigurationPage.serializeEnabledFolders(elem);
});

form_elements.role_map_fields.forEach((id) => {
const elem = page.querySelector(`#${id}`);
current_config[id] = ssoConfigurationPage.serializeRoleMappings(elem);
});

config.OidConfigs[provider_name] = current_config;

ApiClient.updatePluginConfiguration(
Expand All @@ -137,9 +347,17 @@ const ssoConfigurationPage = {
});
});
},
addTextAreaStyle: (view) => {
var style = document.createElement("link");
style.rel = "stylesheet";
style.href =
ApiClient.getUrl("web/configurationpage") + "?name=SSO-Auth.css";
view.appendChild(style);
},
};

export default function (view) {
ssoConfigurationPage.addTextAreaStyle(view);
ssoConfigurationPage.loadConfiguration(view);

ssoConfigurationPage.listArgumentsByType(view);
Expand Down Expand Up @@ -170,4 +388,13 @@ export default function (view) {
e.preventDefault();
return false;
});

view.querySelector("#AddRoleMapping").addEventListener("click", (e) => {
const container = view.querySelector("#FolderRoleMapping");
const current_mappings =
ssoConfigurationPage.serializeRoleMappings(container);
current_mappings.push({ Role: "", Folders: [] });
console.log(current_mappings);
ssoConfigurationPage.populateRoleMappings(current_mappings, container);
});
}
Loading

0 comments on commit f985096

Please sign in to comment.