From 83dbeca44d685cc3c635977aa4881550ec77cd03 Mon Sep 17 00:00:00 2001 From: Jacob O'Mara Date: Tue, 4 Nov 2025 15:22:19 +0000 Subject: [PATCH] Bug 39224: add Vue UI for shibboleth configuration Add staff interface pages for managing Shibboleth configuration including settings and field mappings. Includes API client, Vue components, store, and routes. --- .../prog/en/modules/shibboleth/shibboleth.tt | 27 +++ .../prog/js/fetch/shibboleth-api-client.js | 60 +++++++ .../js/vue/components/Shibboleth/Home.vue | 18 ++ .../js/vue/components/Shibboleth/Main.vue | 95 ++++++++++ .../Shibboleth/ShibbolethConfigResource.vue | 155 ++++++++++++++++ .../Shibboleth/ShibbolethMappingResource.vue | 166 ++++++++++++++++++ .../prog/js/vue/modules/shibboleth.ts | 72 ++++++++ .../prog/js/vue/routes/shibboleth.js | 69 ++++++++ .../prog/js/vue/stores/shibboleth.js | 16 ++ shibboleth/shibboleth.pl | 41 +++++ 10 files changed, 719 insertions(+) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/shibboleth/shibboleth.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/js/fetch/shibboleth-api-client.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Home.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Main.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethConfigResource.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethMappingResource.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/modules/shibboleth.ts create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/routes/shibboleth.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/stores/shibboleth.js create mode 100755 shibboleth/shibboleth.pl diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/shibboleth/shibboleth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/shibboleth/shibboleth.tt new file mode 100644 index 00000000000..2f9a1c66d25 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/shibboleth/shibboleth.tt @@ -0,0 +1,27 @@ +[% USE raw %] +[% USE To %] +[% SET footerjs = 1 %] +[% INCLUDE 'doc-head-open.inc' %] + Shibboleth › Koha +[% INCLUDE 'doc-head-close.inc' %] + + + +[% WRAPPER 'header.inc' %] + [% INCLUDE 'prefs-admin-search.inc' %] +[% END %] + +
+ + + [% MACRO jsinclude BLOCK %] + [% INCLUDE 'calendar.inc' %] + [% INCLUDE 'datatables.inc' %] + + [% Asset.js("js/vue/dist/shibboleth.js") | $raw %] + [% END %] + [% INCLUDE 'intranet-bottom.inc' %] +
diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/shibboleth-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/shibboleth-api-client.js new file mode 100644 index 00000000000..e8d9314c0ea --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/shibboleth-api-client.js @@ -0,0 +1,60 @@ +export class ShibbolethAPIClient { + constructor(HttpClient) { + this.httpClient = new HttpClient({ + baseURL: "/api/v1/shibboleth/", + }); + } + + get config() { + return { + get: () => + this.httpClient.get({ + endpoint: "config", + }), + update: config => + this.httpClient.put({ + endpoint: "config", + body: config, + }), + }; + } + + get mappings() { + return { + get: id => + this.httpClient.get({ + endpoint: "mappings/" + id, + }), + getAll: params => + this.httpClient.getAll({ + endpoint: "mappings", + }), + delete: id => + this.httpClient.delete({ + endpoint: "mappings/" + id, + }), + create: mapping => + this.httpClient.post({ + endpoint: "mappings", + body: mapping, + }), + update: (mapping, id) => + this.httpClient.put({ + endpoint: "mappings/" + id, + body: mapping, + }), + count: (query = {}) => + this.httpClient.count({ + endpoint: + "mappings?" + + new URLSearchParams({ + _page: 1, + _per_page: 1, + ...(query && { q: JSON.stringify(query) }), + }), + }), + }; + } +} + +export default ShibbolethAPIClient; diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Home.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Home.vue new file mode 100644 index 00000000000..7326d5b878c --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Home.vue @@ -0,0 +1,18 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Main.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Main.vue new file mode 100644 index 00000000000..13e69bd8c38 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Main.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethConfigResource.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethConfigResource.vue new file mode 100644 index 00000000000..a06130bc8f0 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethConfigResource.vue @@ -0,0 +1,155 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethMappingResource.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethMappingResource.vue new file mode 100644 index 00000000000..1bda24b8015 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethMappingResource.vue @@ -0,0 +1,166 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/shibboleth.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/shibboleth.ts new file mode 100644 index 00000000000..82fe78e6ca3 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/shibboleth.ts @@ -0,0 +1,72 @@ +import { createApp } from "vue"; +import { createWebHistory, createRouter } from "vue-router"; +import { createPinia } from "pinia"; + +import { library } from "@fortawesome/fontawesome-svg-core"; +import { + faPlus, + faMinus, + faPencil, + faTrash, + faSpinner, + faSave, + faCog, + faExchangeAlt, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; +import vSelect from "vue-select"; + +library.add( + faPlus, + faMinus, + faPencil, + faTrash, + faSpinner, + faSave, + faCog, + faExchangeAlt +); + +import App from "../components/Shibboleth/Main.vue"; + +import { routes as routesDef } from "../routes/shibboleth"; + +import { useMainStore } from "../stores/main"; +import { useShibbolethStore } from "../stores/shibboleth"; +import { useNavigationStore } from "../stores/navigation"; +import i18n from "../i18n"; + +const pinia = createPinia(); + +const mainStore = useMainStore(pinia); +const navigationStore = useNavigationStore(pinia); +const routes = navigationStore.setRoutes(routesDef); + +const router = createRouter({ + history: createWebHistory(), + linkActiveClass: "current", + routes, +}); + +const app = createApp(App); + +const rootComponent = app + .use(i18n) + .use(pinia) + .use(router) + .component("font-awesome-icon", FontAwesomeIcon) + .component("v-select", vSelect); + +app.config.unwrapInjectedRef = true; +app.provide("mainStore", mainStore); +app.provide("navigationStore", navigationStore); +const ShibbolethStore = useShibbolethStore(pinia); +app.provide("ShibbolethStore", ShibbolethStore); + +app.mount("#shibboleth"); + +const { removeMessages } = mainStore; +router.beforeEach((to, from) => { + navigationStore.$patch({ current: to.matched, params: to.params || {} }); + removeMessages(); +}); diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/shibboleth.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/shibboleth.js new file mode 100644 index 00000000000..c55707e2cb4 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/shibboleth.js @@ -0,0 +1,69 @@ +import { markRaw } from "vue"; + +import Home from "../components/Shibboleth/Home.vue"; +import ResourceWrapper from "../components/ResourceWrapper.vue"; +import { $__ } from "../i18n"; + +export const routes = [ + { + path: "/cgi-bin/koha/shibboleth/shibboleth.pl", + is_default: true, + is_base: true, + title: $__("Shibboleth"), + children: [ + { + path: "", + name: "ShibbolethHome", + component: markRaw(Home), + is_navigation_item: false, + }, + { + path: "/cgi-bin/koha/shibboleth/config", + title: $__("Configuration"), + icon: "fa fa-cog", + is_end_node: true, + resource: "Shibboleth/ShibbolethConfigResource.vue", + children: [ + { + path: "", + name: "ShibbolethConfigFormEdit", + component: markRaw(ResourceWrapper), + title: $__("Edit configuration"), + }, + ], + }, + { + path: "/cgi-bin/koha/shibboleth/mappings", + title: $__("Field Mappings"), + icon: "fa fa-exchange-alt", + is_end_node: true, + resource: "Shibboleth/ShibbolethMappingResource.vue", + children: [ + { + path: "", + name: "ShibbolethMappingsList", + component: markRaw(ResourceWrapper), + }, + { + path: ":mapping_id", + name: "ShibbolethMappingsShow", + component: markRaw(ResourceWrapper), + title: $__("Show field mapping"), + }, + { + path: "add", + name: "ShibbolethMappingsFormAdd", + component: markRaw(ResourceWrapper), + title: $__("Add field mapping"), + }, + { + path: "edit/:mapping_id", + name: "ShibbolethMappingsFormEdit", + component: markRaw(ResourceWrapper), + title: $__("Edit field mapping"), + }, + ], + }, + ], + }, +]; diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/shibboleth.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/shibboleth.js new file mode 100644 index 00000000000..ed7a4989516 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/shibboleth.js @@ -0,0 +1,16 @@ +import { defineStore } from "pinia"; + +export const useShibbolethStore = defineStore("shibboleth", { + state: () => ({ + config: null, + mappings: [], + }), + actions: { + setConfig(config) { + this.config = config; + }, + setMappings(mappings) { + this.mappings = mappings; + }, + }, +}); diff --git a/shibboleth/shibboleth.pl b/shibboleth/shibboleth.pl new file mode 100755 index 00000000000..205e76eb514 --- /dev/null +++ b/shibboleth/shibboleth.pl @@ -0,0 +1,41 @@ +#!/usr/bin/perl + +use Modern::Perl; + +use CGI qw ( -utf8 ); +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); +use Koha::Patrons; + +my $input = CGI->new; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "shibboleth/shibboleth.tt", + query => $input, + type => "intranet", + flagsrequired => { parameters => 'manage_identity_providers' }, + } +); + +my $borrowers_source = Koha::Patrons->_resultset->result_source; + +my @borrower_columns; +my %skip_columns = map { $_ => 1 } qw( password updated_on timestamp ); + +foreach my $column ( sort $borrowers_source->columns ) { + next if $skip_columns{$column}; + + my $column_info = $borrowers_source->column_info($column); + my $label = $column_info->{comments} || $column; + + push @borrower_columns, + { + value => $column, + label => $label, + }; +} + +$template->param( borrower_columns => \@borrower_columns ); + +output_html_with_http_headers $input, $cookie, $template->output; -- 2.39.5