View | Details | Raw Unified | Return to bug 39224
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/shibboleth/shibboleth.tt (+27 lines)
Line 0 Link Here
1
[% USE raw %]
2
[% USE To %]
3
[% SET footerjs = 1 %]
4
[% INCLUDE 'doc-head-open.inc' %]
5
<title> Shibboleth &rsaquo; Koha </title>
6
[% INCLUDE 'doc-head-close.inc' %]
7
</head>
8
9
<body id="admin_shibboleth" class="admin">
10
[% WRAPPER 'header.inc' %]
11
    [% INCLUDE 'prefs-admin-search.inc' %]
12
[% END %]
13
14
<div id="shibboleth">
15
    <!-- this is closed in intranet-bottom.inc -->
16
17
    [% MACRO jsinclude BLOCK %]
18
        [% INCLUDE 'calendar.inc' %]
19
        [% INCLUDE 'datatables.inc' %]
20
        <script>
21
            const logged_in_user = [% To.json(logged_in_user.unblessed) | $raw %];
22
            window.borrower_columns = [% To.json(borrower_columns) | $raw %];
23
        </script>
24
        [% Asset.js("js/vue/dist/shibboleth.js") | $raw %]
25
    [% END %]
26
    [% INCLUDE 'intranet-bottom.inc' %]
27
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/shibboleth-api-client.js (+60 lines)
Line 0 Link Here
1
export class ShibbolethAPIClient {
2
    constructor(HttpClient) {
3
        this.httpClient = new HttpClient({
4
            baseURL: "/api/v1/shibboleth/",
5
        });
6
    }
7
8
    get config() {
9
        return {
10
            get: () =>
11
                this.httpClient.get({
12
                    endpoint: "config",
13
                }),
14
            update: config =>
15
                this.httpClient.put({
16
                    endpoint: "config",
17
                    body: config,
18
                }),
19
        };
20
    }
21
22
    get mappings() {
23
        return {
24
            get: id =>
25
                this.httpClient.get({
26
                    endpoint: "mappings/" + id,
27
                }),
28
            getAll: params =>
29
                this.httpClient.getAll({
30
                    endpoint: "mappings",
31
                }),
32
            delete: id =>
33
                this.httpClient.delete({
34
                    endpoint: "mappings/" + id,
35
                }),
36
            create: mapping =>
37
                this.httpClient.post({
38
                    endpoint: "mappings",
39
                    body: mapping,
40
                }),
41
            update: (mapping, id) =>
42
                this.httpClient.put({
43
                    endpoint: "mappings/" + id,
44
                    body: mapping,
45
                }),
46
            count: (query = {}) =>
47
                this.httpClient.count({
48
                    endpoint:
49
                        "mappings?" +
50
                        new URLSearchParams({
51
                            _page: 1,
52
                            _per_page: 1,
53
                            ...(query && { q: JSON.stringify(query) }),
54
                        }),
55
                }),
56
        };
57
    }
58
}
59
60
export default ShibbolethAPIClient;
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Home.vue (+18 lines)
Line 0 Link Here
1
<template>
2
    <div>
3
        <h2>{{ $__("Shibboleth Configuration") }}</h2>
4
        <p>
5
            {{
6
                $__(
7
                    "Configure Shibboleth authentication for your Koha installation"
8
                )
9
            }}
10
        </p>
11
    </div>
12
</template>
13
14
<script>
15
export default {
16
    name: "ShibbolethHome",
17
};
18
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/Main.vue (+95 lines)
Line 0 Link Here
1
<template>
2
    <div v-if="initialized">
3
        <div id="sub-header">
4
            <Breadcrumbs />
5
            <Help />
6
        </div>
7
        <div class="main container-fluid">
8
            <div class="row">
9
                <div class="col-md-10 order-md-2 order-sm-1">
10
                    <main>
11
                        <Dialog />
12
                        <router-view />
13
                    </main>
14
                </div>
15
                <div class="col-md-2 order-sm-2 order-md-1">
16
                    <LeftMenu :title="$__('Shibboleth')"></LeftMenu>
17
                </div>
18
            </div>
19
        </div>
20
    </div>
21
</template>
22
23
<script>
24
import { inject, onBeforeMount, ref } from "vue";
25
import Breadcrumbs from "../Breadcrumbs.vue";
26
import { storeToRefs } from "pinia";
27
import Help from "../Help.vue";
28
import LeftMenu from "../LeftMenu.vue";
29
import Dialog from "../Dialog.vue";
30
import "vue-select/dist/vue-select.css";
31
32
export default {
33
    setup() {
34
        const mainStore = inject("mainStore");
35
        const { loading, loaded } = mainStore;
36
37
        const initialized = ref(false);
38
39
        onBeforeMount(() => {
40
            loading();
41
            setTimeout(() => {
42
                loaded();
43
                initialized.value = true;
44
            }, 0);
45
        });
46
47
        return {
48
            initialized,
49
        };
50
    },
51
    components: {
52
        Breadcrumbs,
53
        Dialog,
54
        Help,
55
        LeftMenu,
56
    },
57
};
58
</script>
59
60
<style>
61
#menu ul ul,
62
#navmenulist ul ul {
63
    padding-left: 2em;
64
    font-size: 100%;
65
}
66
67
form .v-select {
68
    display: inline-block;
69
    background-color: white;
70
    width: 30%;
71
}
72
73
.v-select,
74
input:not([type="submit"]):not([type="search"]):not([type="button"]):not(
75
        [type="checkbox"]
76
    ):not([type="radio"]),
77
textarea {
78
    border-color: rgba(60, 60, 60, 0.26);
79
    border-width: 1px;
80
    border-radius: 4px;
81
    min-width: 30%;
82
}
83
84
#navmenulist ul li a.current.disabled {
85
    background-color: inherit;
86
    border-left: 5px solid #e6e6e6;
87
    color: #000;
88
}
89
90
#navmenulist ul li a.disabled {
91
    color: #666;
92
    pointer-events: none;
93
    font-weight: 700;
94
}
95
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethConfigResource.vue (+155 lines)
Line 0 Link Here
1
<template>
2
    <div v-if="!initialized">{{ $__("Loading") }}</div>
3
    <BaseResource v-else :routeAction="routeAction" :instancedResource="this" />
4
</template>
5
6
<script>
7
import { inject, ref, onMounted, reactive } from "vue";
8
import BaseResource from "./../BaseResource.vue";
9
import { useBaseResource } from "../../composables/base-resource.js";
10
import { storeToRefs } from "pinia";
11
import { APIClient } from "../../fetch/api-client.js";
12
import { $__ } from "@koha-vue/i18n";
13
14
export default {
15
    name: "ShibbolethConfigResource",
16
    components: {
17
        BaseResource,
18
    },
19
    props: {
20
        routeAction: String,
21
    },
22
    emits: ["select-resource"],
23
    setup(props) {
24
        const initialized = ref(false);
25
        const configData = ref(null);
26
        const resourceAttrs = [
27
            {
28
                name: "force_opac_sso",
29
                type: "boolean",
30
                label: __("Force OPAC SSO"),
31
                group: "SSO Settings",
32
                toolTip: __(
33
                    "Automatically redirect OPAC users to Shibboleth login"
34
                ),
35
            },
36
            {
37
                name: "force_staff_sso",
38
                type: "boolean",
39
                label: __("Force staff SSO"),
40
                group: "SSO Settings",
41
                toolTip: __(
42
                    "Automatically redirect staff users to Shibboleth login"
43
                ),
44
            },
45
            {
46
                name: "autocreate",
47
                type: "boolean",
48
                label: __("Auto create users"),
49
                group: "User Management",
50
                toolTip: __(
51
                    "Automatically create patron records for new Shibboleth users"
52
                ),
53
            },
54
            {
55
                name: "sync",
56
                type: "boolean",
57
                label: __("Sync user attributes"),
58
                group: "User Management",
59
                toolTip: __(
60
                    "Update patron attributes from Shibboleth on each login"
61
                ),
62
            },
63
            {
64
                name: "welcome",
65
                type: "boolean",
66
                label: __("Send welcome email"),
67
                group: "User Management",
68
                toolTip: __(
69
                    "Send welcome email to new users created via Shibboleth"
70
                ),
71
            },
72
        ];
73
74
        const additionalToolbarButtons = (resource, componentData) => {
75
            const buttons = {
76
                form: [
77
                    {
78
                        title: $__("Submit"),
79
                        form: componentData.resourceForm,
80
                    },
81
                    {
82
                        to: {
83
                            name: "ShibbolethHome",
84
                        },
85
                        title: $__("Cancel"),
86
                        cssClass: "btn btn-link",
87
                    },
88
                ],
89
            };
90
            return buttons;
91
        };
92
93
        const baseResource = useBaseResource({
94
            resourceName: "config",
95
            nameAttr: "shibboleth_config_id",
96
            idAttr: "shibboleth_config_id",
97
            components: {
98
                show: "ShibbolethConfigShow",
99
                list: "ShibbolethHome",
100
                add: "ShibbolethConfigFormAdd",
101
                edit: "ShibbolethConfigFormEdit",
102
            },
103
            apiClient: APIClient.shibboleth.config,
104
            i18n: {
105
                displayName: $__("Shibboleth Configuration"),
106
                editLabel: $__("Edit Shibboleth Configuration"),
107
            },
108
            additionalToolbarButtons,
109
            stickyToolbar: ["Form"],
110
            embedded: props.embedded,
111
            formGroupsDisplayMode: "accordion",
112
            resourceAttrs,
113
            props,
114
            moduleStore: "ShibbolethStore",
115
        });
116
117
        const onFormSave = async (e, configToSave) => {
118
            e.preventDefault();
119
120
            const config = JSON.parse(JSON.stringify(configToSave));
121
            delete config.shibboleth_config_id;
122
123
            const client = APIClient.shibboleth.config;
124
125
            try {
126
                await client.update(config);
127
                baseResource.setMessage(__("Configuration updated"));
128
                baseResource.router.push({ name: "ShibbolethHome" });
129
            } catch (error) {
130
                // Errors handled by base resource
131
            }
132
        };
133
134
        // Fetch the singleton config on mount and replace newResource getter
135
        onMounted(async () => {
136
            try {
137
                configData.value = await APIClient.shibboleth.config.get();
138
                initialized.value = true;
139
            } catch (error) {
140
                console.error("Failed to load config:", error);
141
                initialized.value = true;
142
            }
143
        });
144
145
        return {
146
            ...baseResource,
147
            initialized,
148
            onFormSave,
149
            get newResource() {
150
                return configData.value || baseResource.newResource;
151
            },
152
        };
153
    },
154
};
155
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Shibboleth/ShibbolethMappingResource.vue (+166 lines)
Line 0 Link Here
1
<template>
2
    <BaseResource :routeAction="routeAction" :instancedResource="this" />
3
</template>
4
5
<script>
6
import { inject } from "vue";
7
import BaseResource from "./../BaseResource.vue";
8
import { useBaseResource } from "../../composables/base-resource.js";
9
import { storeToRefs } from "pinia";
10
import { APIClient } from "../../fetch/api-client.js";
11
import { $__ } from "@koha-vue/i18n";
12
13
export default {
14
    name: "ShibbolethMappingResource",
15
    components: {
16
        BaseResource,
17
    },
18
    props: {
19
        routeAction: String,
20
    },
21
    emits: ["select-resource"],
22
    setup(props) {
23
        const getBorrowerColumns = () => {
24
            return window.borrower_columns || [];
25
        };
26
27
        const borrowerColumnsArray = getBorrowerColumns();
28
29
        const resourceAttrs = [
30
            {
31
                name: "koha_field",
32
                required: true,
33
                type: "select",
34
                options: borrowerColumnsArray,
35
                requiredKey: "value",
36
                selectLabel: "label",
37
                label: __("Koha field"),
38
                group: "Details",
39
                toolTip: __("The field name in the Koha borrowers table"),
40
            },
41
            {
42
                name: "idp_field",
43
                type: "text",
44
                label: __("Identity Provider attribute"),
45
                group: "Details",
46
                toolTip: __(
47
                    "The attribute name provided by the Shibboleth Identity Provider"
48
                ),
49
            },
50
            {
51
                name: "default_content",
52
                type: "text",
53
                label: __("Default value"),
54
                group: "Details",
55
                toolTip: __(
56
                    "Default value to use if the IdP doesn't provide this attribute"
57
                ),
58
            },
59
            {
60
                name: "is_matchpoint",
61
                type: "boolean",
62
                label: __("Use as matchpoint"),
63
                group: "Details",
64
                toolTip: __(
65
                    "Use this field to match existing users (only one matchpoint allowed)"
66
                ),
67
            },
68
        ];
69
70
        const additionalToolbarButtons = (resource, componentData) => {
71
            const buttons = {
72
                form: [
73
                    {
74
                        title: $__("Submit"),
75
                        form: componentData.resourceForm,
76
                    },
77
                    {
78
                        to: {
79
                            name: "ShibbolethMappingsList",
80
                        },
81
                        title: $__("Cancel"),
82
                        cssClass: "btn btn-link",
83
                    },
84
                ],
85
            };
86
            return buttons;
87
        };
88
89
        const baseResource = useBaseResource({
90
            resourceName: "mapping",
91
            nameAttr: "koha_field",
92
            idAttr: "mapping_id",
93
            components: {
94
                show: "ShibbolethMappingsShow",
95
                list: "ShibbolethMappingsList",
96
                add: "ShibbolethMappingsFormAdd",
97
                edit: "ShibbolethMappingsFormEdit",
98
            },
99
            apiClient: APIClient.shibboleth.mappings,
100
            i18n: {
101
                deleteConfirmationMessage: $__(
102
                    "Are you sure you want to remove this field mapping?"
103
                ),
104
                deleteSuccessMessage: $__("Mapping deleted"),
105
                displayName: $__("Field Mapping"),
106
                editLabel: $__("Edit field mapping"),
107
                emptyListMessage: $__("There are no field mappings defined"),
108
                newLabel: $__("New field mapping"),
109
            },
110
            table: {
111
                resourceTableUrl:
112
                    APIClient.shibboleth.httpClient._baseURL + "mappings",
113
                options: {},
114
            },
115
            additionalToolbarButtons,
116
            stickyToolbar: ["Form"],
117
            embedded: props.embedded,
118
            formGroupsDisplayMode: "accordion",
119
            resourceAttrs,
120
            props,
121
            moduleStore: "ShibbolethStore",
122
        });
123
124
        const tableOptions = {
125
            url: () => baseResource.getResourceTableUrl(),
126
            options: {},
127
            table_settings: null,
128
            actions: {
129
                0: ["show"],
130
                "-1": ["edit", "delete"],
131
            },
132
        };
133
134
        const onFormSave = async (e, mappingToSave) => {
135
            e.preventDefault();
136
137
            const mapping = JSON.parse(JSON.stringify(mappingToSave));
138
            const mapping_id = mapping.mapping_id;
139
140
            delete mapping.mapping_id;
141
142
            const client = APIClient.shibboleth.mappings;
143
144
            try {
145
                if (mapping_id) {
146
                    await client.update(mapping, mapping_id);
147
                    baseResource.setMessage(__("Mapping updated"));
148
                } else {
149
                    await client.create(mapping);
150
                    baseResource.setMessage(__("Mapping created"));
151
                }
152
153
                baseResource.router.push({ name: "ShibbolethMappingsList" });
154
            } catch (error) {
155
                // Errors handled by base resource
156
            }
157
        };
158
159
        return {
160
            ...baseResource,
161
            tableOptions,
162
            onFormSave,
163
        };
164
    },
165
};
166
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/shibboleth.ts (+72 lines)
Line 0 Link Here
1
import { createApp } from "vue";
2
import { createWebHistory, createRouter } from "vue-router";
3
import { createPinia } from "pinia";
4
5
import { library } from "@fortawesome/fontawesome-svg-core";
6
import {
7
    faPlus,
8
    faMinus,
9
    faPencil,
10
    faTrash,
11
    faSpinner,
12
    faSave,
13
    faCog,
14
    faExchangeAlt,
15
} from "@fortawesome/free-solid-svg-icons";
16
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
17
import vSelect from "vue-select";
18
19
library.add(
20
    faPlus,
21
    faMinus,
22
    faPencil,
23
    faTrash,
24
    faSpinner,
25
    faSave,
26
    faCog,
27
    faExchangeAlt
28
);
29
30
import App from "../components/Shibboleth/Main.vue";
31
32
import { routes as routesDef } from "../routes/shibboleth";
33
34
import { useMainStore } from "../stores/main";
35
import { useShibbolethStore } from "../stores/shibboleth";
36
import { useNavigationStore } from "../stores/navigation";
37
import i18n from "../i18n";
38
39
const pinia = createPinia();
40
41
const mainStore = useMainStore(pinia);
42
const navigationStore = useNavigationStore(pinia);
43
const routes = navigationStore.setRoutes(routesDef);
44
45
const router = createRouter({
46
    history: createWebHistory(),
47
    linkActiveClass: "current",
48
    routes,
49
});
50
51
const app = createApp(App);
52
53
const rootComponent = app
54
    .use(i18n)
55
    .use(pinia)
56
    .use(router)
57
    .component("font-awesome-icon", FontAwesomeIcon)
58
    .component("v-select", vSelect);
59
60
app.config.unwrapInjectedRef = true;
61
app.provide("mainStore", mainStore);
62
app.provide("navigationStore", navigationStore);
63
const ShibbolethStore = useShibbolethStore(pinia);
64
app.provide("ShibbolethStore", ShibbolethStore);
65
66
app.mount("#shibboleth");
67
68
const { removeMessages } = mainStore;
69
router.beforeEach((to, from) => {
70
    navigationStore.$patch({ current: to.matched, params: to.params || {} });
71
    removeMessages();
72
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/shibboleth.js (+69 lines)
Line 0 Link Here
1
import { markRaw } from "vue";
2
3
import Home from "../components/Shibboleth/Home.vue";
4
import ResourceWrapper from "../components/ResourceWrapper.vue";
5
import { $__ } from "../i18n";
6
7
export const routes = [
8
    {
9
        path: "/cgi-bin/koha/shibboleth/shibboleth.pl",
10
        is_default: true,
11
        is_base: true,
12
        title: $__("Shibboleth"),
13
        children: [
14
            {
15
                path: "",
16
                name: "ShibbolethHome",
17
                component: markRaw(Home),
18
                is_navigation_item: false,
19
            },
20
            {
21
                path: "/cgi-bin/koha/shibboleth/config",
22
                title: $__("Configuration"),
23
                icon: "fa fa-cog",
24
                is_end_node: true,
25
                resource: "Shibboleth/ShibbolethConfigResource.vue",
26
                children: [
27
                    {
28
                        path: "",
29
                        name: "ShibbolethConfigFormEdit",
30
                        component: markRaw(ResourceWrapper),
31
                        title: $__("Edit configuration"),
32
                    },
33
                ],
34
            },
35
            {
36
                path: "/cgi-bin/koha/shibboleth/mappings",
37
                title: $__("Field Mappings"),
38
                icon: "fa fa-exchange-alt",
39
                is_end_node: true,
40
                resource: "Shibboleth/ShibbolethMappingResource.vue",
41
                children: [
42
                    {
43
                        path: "",
44
                        name: "ShibbolethMappingsList",
45
                        component: markRaw(ResourceWrapper),
46
                    },
47
                    {
48
                        path: ":mapping_id",
49
                        name: "ShibbolethMappingsShow",
50
                        component: markRaw(ResourceWrapper),
51
                        title: $__("Show field mapping"),
52
                    },
53
                    {
54
                        path: "add",
55
                        name: "ShibbolethMappingsFormAdd",
56
                        component: markRaw(ResourceWrapper),
57
                        title: $__("Add field mapping"),
58
                    },
59
                    {
60
                        path: "edit/:mapping_id",
61
                        name: "ShibbolethMappingsFormEdit",
62
                        component: markRaw(ResourceWrapper),
63
                        title: $__("Edit field mapping"),
64
                    },
65
                ],
66
            },
67
        ],
68
    },
69
];
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/shibboleth.js (+16 lines)
Line 0 Link Here
1
import { defineStore } from "pinia";
2
3
export const useShibbolethStore = defineStore("shibboleth", {
4
    state: () => ({
5
        config: null,
6
        mappings: [],
7
    }),
8
    actions: {
9
        setConfig(config) {
10
            this.config = config;
11
        },
12
        setMappings(mappings) {
13
            this.mappings = mappings;
14
        },
15
    },
16
});
(-)a/shibboleth/shibboleth.pl (-1 / +41 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use CGI        qw ( -utf8 );
6
use C4::Auth   qw( get_template_and_user );
7
use C4::Output qw( output_html_with_http_headers );
8
use Koha::Patrons;
9
10
my $input = CGI->new;
11
12
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
13
    {
14
        template_name => "shibboleth/shibboleth.tt",
15
        query         => $input,
16
        type          => "intranet",
17
        flagsrequired => { parameters => 'manage_identity_providers' },
18
    }
19
);
20
21
my $borrowers_source = Koha::Patrons->_resultset->result_source;
22
23
my @borrower_columns;
24
my %skip_columns = map { $_ => 1 } qw( password updated_on timestamp );
25
26
foreach my $column ( sort $borrowers_source->columns ) {
27
    next if $skip_columns{$column};
28
29
    my $column_info = $borrowers_source->column_info($column);
30
    my $label       = $column_info->{comments} || $column;
31
32
    push @borrower_columns,
33
        {
34
        value => $column,
35
        label => $label,
36
        };
37
}
38
39
$template->param( borrower_columns => \@borrower_columns );
40
41
output_html_with_http_headers $input, $cookie, $template->output;

Return to bug 39224