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

(-)a/C4/Auth.pm (-3 / +3 lines)
Lines 173-179 sub get_template_and_user { Link Here
173
    my $cookie_mgr = Koha::CookieManager->new;
173
    my $cookie_mgr = Koha::CookieManager->new;
174
174
175
    # Get shibboleth login attribute
175
    # Get shibboleth login attribute
176
    my $shib       = C4::Context->preference('ShibbolethAuthentication') && shib_ok();
176
    my $shib       = shib_ok();
177
    my $shib_login = $shib ? get_login_shib() : undef;
177
    my $shib_login = $shib ? get_login_shib() : undef;
178
178
179
    C4::Context->interface( $in->{type} );
179
    C4::Context->interface( $in->{type} );
Lines 811-817 sub checkauth { Link Here
811
    my $query = shift;
811
    my $query = shift;
812
812
813
    # Get shibboleth login attribute
813
    # Get shibboleth login attribute
814
    my $shib       = C4::Context->preference('ShibbolethAuthentication') && shib_ok();
814
    my $shib       = shib_ok();
815
    my $shib_login = $shib ? get_login_shib() : undef;
815
    my $shib_login = $shib ? get_login_shib() : undef;
816
816
817
    # $authnotrequired will be set for scripts which will run without authentication
817
    # $authnotrequired will be set for scripts which will run without authentication
Lines 2009-2015 sub checkpw { Link Here
2009
    $type = 'opac' unless $type;
2009
    $type = 'opac' unless $type;
2010
2010
2011
    # Get shibboleth login attribute
2011
    # Get shibboleth login attribute
2012
    my $shib       = C4::Context->preference('ShibbolethAuthentication') && shib_ok();
2012
    my $shib       = shib_ok();
2013
    my $shib_login = $shib ? get_login_shib() : undef;
2013
    my $shib_login = $shib ? get_login_shib() : undef;
2014
2014
2015
    my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2015
    my $anonymous_patron = C4::Context->preference('AnonymousPatron');
(-)a/admin/identity_providers.pl (-268 / +52 lines)
Lines 19-313 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use CGI          qw ( -utf8 );
22
use CGI qw ( -utf8 );
23
use Scalar::Util qw( blessed );
24
use Try::Tiny    qw( catch try );
25
23
26
use C4::Auth   qw( get_template_and_user );
24
use C4::Auth    qw( get_template_and_user );
27
use C4::Output qw( output_html_with_http_headers );
25
use C4::Context qw();
26
use C4::Output  qw( output_html_with_http_headers );
28
27
29
use Koha::Database;
28
use Koha::Patron::Attribute::Types;
30
use Koha::Auth::Identity::Providers;
29
use Koha::Libraries;
30
use Koha::Patron::Categories;
31
use Koha::Patrons;
31
32
32
my $input                = CGI->new;
33
my $input = CGI->new;
33
my $op                   = $input->param('op') || 'list';
34
my $domain_ops           = $input->param('domain_ops');
35
my $identity_provider_id = $input->param('identity_provider_id');
36
my $identity_provider;
37
38
$identity_provider = Koha::Auth::Identity::Providers->find($identity_provider_id)
39
    unless !$identity_provider_id;
40
41
my $template_name = $domain_ops ? 'admin/identity_provider_domains.tt' : 'admin/identity_providers.tt';
42
34
43
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
35
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
44
    {
36
    {
45
        template_name => $template_name,
37
        template_name => 'admin/identity_providers.tt',
46
        query         => $input,
38
        query         => $input,
47
        type          => "intranet",
39
        type          => "intranet",
48
        flagsrequired => { parameters => 'manage_identity_providers' },
40
        flagsrequired => { parameters => 'manage_identity_providers' },
49
    }
41
    }
50
);
42
);
51
43
52
my @messages;
44
my $borrowers_source = Koha::Patrons->_resultset->result_source;
53
45
my @borrower_columns;
54
if ( !$domain_ops && $op eq 'cud-add' ) {
46
my %skip_columns = map { $_ => 1 } qw( password updated_on timestamp );
55
47
foreach my $column ( sort $borrowers_source->columns ) {
56
    # IdP configuration params
48
    next if $skip_columns{$column};
57
    my $code        = $input->param('code');
49
    my $column_info = $borrowers_source->column_info($column);
58
    my $config      = $input->param('config');
50
    my $label       = $column_info->{comments} || $column;
59
    my $description = $input->param('description');
51
    push @borrower_columns, { value => $column, label => $label };
60
    my $icon_url    = $input->param('icon_url');
52
}
61
    my $mapping     = $input->param('mapping');
62
    my $matchpoint  = $input->param('matchpoint');
63
    my $protocol    = $input->param('protocol');
64
65
    # Domain configuration params
66
    my $allow_opac          = $input->param('allow_opac')          // 0;
67
    my $allow_staff         = $input->param('allow_staff')         // 0;
68
    my $auto_register_opac  = $input->param('auto_register_opac')  // 0;
69
    my $auto_register_staff = $input->param('auto_register_staff') // 0;
70
    my $default_category_id = $input->param('default_category_id');
71
    my $default_library_id  = $input->param('default_library_id');
72
    my $domain              = $input->param('domain');
73
    my $update_on_auth      = $input->param('update_on_auth');
74
75
    try {
76
        Koha::Database->new->schema->txn_do(
77
            sub {
78
                my $provider = Koha::Auth::Identity::Provider->new(
79
                    {
80
                        code        => $code,
81
                        config      => $config,
82
                        description => $description,
83
                        icon_url    => $icon_url,
84
                        mapping     => $mapping,
85
                        matchpoint  => $matchpoint,
86
                        protocol    => $protocol,
87
                    }
88
                )->store;
89
90
                Koha::Auth::Identity::Provider::Domain->new(
91
                    {
92
                        identity_provider_id => $provider->identity_provider_id,
93
                        allow_opac           => $allow_opac,
94
                        allow_staff          => $allow_staff,
95
                        auto_register_opac   => $auto_register_opac,
96
                        auto_register_staff  => $auto_register_staff,
97
                        default_category_id  => $default_category_id,
98
                        default_library_id   => $default_library_id,
99
                        domain               => $domain,
100
                        update_on_auth       => $update_on_auth,
101
                    }
102
                )->store;
103
104
                push @messages, { type => 'message', code => 'success_on_insert' };
105
            }
106
        );
107
    } catch {
108
        if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
109
            push @messages,
110
                {
111
                type   => 'alert',
112
                code   => 'error_on_insert',
113
                reason => 'duplicate_id'
114
                };
115
        }
116
    };
117
118
    # list servers after adding
119
    $op = 'list';
120
} elsif ( $domain_ops && $op eq 'cud-add' ) {
121
122
    my $allow_opac           = $input->param('allow_opac');
123
    my $allow_staff          = $input->param('allow_staff');
124
    my $identity_provider_id = $input->param('identity_provider_id');
125
    my $auto_register_opac   = $input->param('auto_register_opac');
126
    my $auto_register_staff  = $input->param('auto_register_staff');
127
    my $default_category_id  = $input->param('default_category_id') || undef;
128
    my $default_library_id   = $input->param('default_library_id')  || undef;
129
    my $domain               = $input->param('domain');
130
    my $update_on_auth       = $input->param('update_on_auth');
131
132
    try {
133
134
        Koha::Auth::Identity::Provider::Domain->new(
135
            {
136
                allow_opac           => $allow_opac,
137
                allow_staff          => $allow_staff,
138
                identity_provider_id => $identity_provider_id,
139
                auto_register_opac   => $auto_register_opac,
140
                auto_register_staff  => $auto_register_staff,
141
                default_category_id  => $default_category_id,
142
                default_library_id   => $default_library_id,
143
                domain               => $domain,
144
                update_on_auth       => $update_on_auth,
145
            }
146
        )->store;
147
148
        push @messages, { type => 'message', code => 'success_on_insert' };
149
    } catch {
150
        if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
151
            push @messages,
152
                {
153
                type   => 'alert',
154
                code   => 'error_on_insert',
155
                reason => 'duplicate_id'
156
                };
157
        }
158
    };
159
160
    # list servers after adding
161
    $op = 'list';
162
} elsif ( !$domain_ops && $op eq 'edit_form' ) {
163
164
    if ($identity_provider) {
165
        $template->param( identity_provider => $identity_provider );
166
    } else {
167
        push @messages,
168
            {
169
            type   => 'alert',
170
            code   => 'error_on_edit',
171
            reason => 'invalid_id'
172
            };
173
    }
174
} elsif ( $domain_ops && $op eq 'edit_form' ) {
175
    my $identity_provider_domain_id = $input->param('identity_provider_domain_id');
176
    my $identity_provider_domain;
177
178
    $identity_provider_domain = Koha::Auth::Identity::Provider::Domains->find($identity_provider_domain_id)
179
        unless !$identity_provider_domain_id;
180
181
    if ($identity_provider_domain) {
182
        $template->param( identity_provider_domain => $identity_provider_domain );
183
    } else {
184
        push @messages,
185
            {
186
            type   => 'alert',
187
            code   => 'error_on_edit',
188
            reason => 'invalid_id'
189
            };
190
    }
191
} elsif ( !$domain_ops && $op eq 'cud-edit_save' ) {
192
193
    if ($identity_provider) {
194
195
        my $code        = $input->param('code');
196
        my $config      = $input->param('config');
197
        my $description = $input->param('description');
198
        my $icon_url    = $input->param('icon_url');
199
        my $mapping     = $input->param('mapping');
200
        my $matchpoint  = $input->param('matchpoint');
201
        my $protocol    = $input->param('protocol');
202
203
        try {
204
53
205
            $identity_provider->set(
54
my @libraries_map = map { { value => $_->branchcode, label => $_->branchname } }
206
                {
55
    Koha::Libraries->search( {}, { order_by => 'branchname' } )->as_list;
207
                    code        => $code,
208
                    config      => $config,
209
                    description => $description,
210
                    icon_url    => $icon_url,
211
                    mapping     => $mapping,
212
                    matchpoint  => $matchpoint,
213
                    protocol    => $protocol,
214
                }
215
            )->store;
216
56
217
            push @messages,
57
my @categories_map = map { { value => $_->categorycode, label => $_->description } }
218
                {
58
    Koha::Patron::Categories->search( {}, { order_by => 'description' } )->as_list;
219
                type => 'message',
220
                code => 'success_on_update'
221
                };
222
        } catch {
223
            push @messages,
224
                {
225
                type => 'alert',
226
                code => 'error_on_update'
227
                };
228
        };
229
59
230
        # list servers after adding
60
my @unique_patron_attributes = map {
231
        $op = 'list';
61
    {
232
    } else {
62
        value => 'patron_attribute:' . $_->code,
233
        push @messages,
63
        label => sprintf( '%s (%s)', $_->description, $_->code ),
234
            {
235
            type   => 'alert',
236
            code   => 'error_on_update',
237
            reason => 'invalid_id'
238
            };
239
    }
64
    }
240
} elsif ( $domain_ops && $op eq 'cud-edit_save' ) {
65
} Koha::Patron::Attribute::Types->search(
241
66
    { unique_id => 1 },
242
    my $identity_provider_domain_id = $input->param('identity_provider_domain_id');
67
    { order_by  => 'description' }
243
    my $identity_provider_domain;
68
)->as_list;
244
245
    $identity_provider_domain = Koha::Auth::Identity::Provider::Domains->find($identity_provider_domain_id)
246
        unless !$identity_provider_domain_id;
247
69
248
    if ($identity_provider_domain) {
70
my @all_patron_attributes = map {
249
71
    {
250
        my $identity_provider_id = $input->param('identity_provider_id');
72
        value => 'patron_attribute:' . $_->code,
251
        my $domain               = $input->param('domain');
73
        label => sprintf( '%s (%s)', $_->description, $_->code ),
252
        my $auto_register_opac   = $input->param('auto_register_opac')  // 0;
253
        my $auto_register_staff  = $input->param('auto_register_staff') // 0;
254
        my $update_on_auth       = $input->param('update_on_auth');
255
        my $default_library_id   = $input->param('default_library_id')  || undef;
256
        my $default_category_id  = $input->param('default_category_id') || undef;
257
        my $allow_opac           = $input->param('allow_opac');
258
        my $allow_staff          = $input->param('allow_staff');
259
260
        try {
261
262
            $identity_provider_domain->set(
263
                {
264
                    identity_provider_id => $identity_provider_id,
265
                    domain               => $domain,
266
                    auto_register_opac   => $auto_register_opac,
267
                    auto_register_staff  => $auto_register_staff,
268
                    update_on_auth       => $update_on_auth,
269
                    default_library_id   => $default_library_id,
270
                    default_category_id  => $default_category_id,
271
                    allow_opac           => $allow_opac,
272
                    allow_staff          => $allow_staff,
273
                }
274
            )->store;
275
276
            push @messages,
277
                {
278
                type => 'message',
279
                code => 'success_on_update'
280
                };
281
        } catch {
282
            push @messages,
283
                {
284
                type => 'alert',
285
                code => 'error_on_update'
286
                };
287
        };
288
289
        # list servers after adding
290
        $op = 'list';
291
    } else {
292
        push @messages,
293
            {
294
            type   => 'alert',
295
            code   => 'error_on_update',
296
            reason => 'invalid_id'
297
            };
298
    }
74
    }
299
}
75
} Koha::Patron::Attribute::Types->search(
300
76
    {},
301
if ($domain_ops) {
77
    { order_by => 'description' }
302
    $template->param(
78
)->as_list;
303
        identity_provider_code => $identity_provider->code,
79
304
        identity_provider_id   => $identity_provider_id,
80
my @idp_default_hostnames;
305
    );
81
for my $pref (qw( OPACBaseURL staffClientBaseURL )) {
82
    my $url = C4::Context->preference($pref);
83
    next unless $url;
84
    my ($hostname) = $url =~ m|^https?://([^/:?#]+)|;
85
    push @idp_default_hostnames, $hostname if $hostname;
306
}
86
}
307
87
308
$template->param(
88
$template->param(
309
    op       => $op,
89
    borrower_columns         => \@borrower_columns,
310
    messages => \@messages,
90
    libraries_map            => \@libraries_map,
91
    categories_map           => \@categories_map,
92
    idp_default_hostnames    => \@idp_default_hostnames,
93
    unique_patron_attributes => \@unique_patron_attributes,
94
    all_patron_attributes    => \@all_patron_attributes,
311
);
95
);
312
96
313
output_html_with_http_headers $input, $cookie, $template->output;
97
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/debian/templates/apache-shared-intranet.conf (+1 lines)
Lines 27-32 RewriteRule ^/cgi-bin/koha/sip2/.*$ /cgi-bin/koha/sip2/sip2.pl [PT] Link Here
27
RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$
27
RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$
28
RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT]
28
RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT]
29
RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT]
29
RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT]
30
RewriteRule ^/cgi-bin/koha/admin/identity_providers(/.*)?$ /cgi-bin/koha/admin/identity_providers.pl [PT]
30
RewriteCond %{QUERY_STRING} booksellerid=(.*)
31
RewriteCond %{QUERY_STRING} booksellerid=(.*)
31
RewriteRule ^/cgi-bin/koha/acqui/supplier.pl$ /cgi-bin/koha/acquisition/vendors/%1? [R]
32
RewriteRule ^/cgi-bin/koha/acqui/supplier.pl$ /cgi-bin/koha/acquisition/vendors/%1? [R]
32
RewriteRule ^/cgi-bin/koha/acquisition/vendors(.*)?$ /cgi-bin/koha/acqui/vendors.pl$1 [PT]
33
RewriteRule ^/cgi-bin/koha/acquisition/vendors(.*)?$ /cgi-bin/koha/acqui/vendors.pl$1 [PT]
(-)a/etc/koha-httpd.conf (+1 lines)
Lines 239-244 Link Here
239
     RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$
239
     RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$
240
     RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT]
240
     RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT]
241
     RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT]
241
     RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT]
242
     RewriteRule ^/cgi-bin/koha/admin/identity_providers(/.*)?$ /cgi-bin/koha/admin/identity_providers.pl [PT]
242
     RewriteCond %{QUERY_STRING} booksellerid=(.*)
243
     RewriteCond %{QUERY_STRING} booksellerid=(.*)
243
     RewriteRule ^/cgi-bin/koha/acqui/supplier.pl$ /cgi-bin/koha/acquisition/vendors/%1? [R]
244
     RewriteRule ^/cgi-bin/koha/acqui/supplier.pl$ /cgi-bin/koha/acquisition/vendors/%1? [R]
244
     RewriteRule ^/cgi-bin/koha/acquisition/vendors(.*)?$ /cgi-bin/koha/acqui/vendors.pl$1 [PT]
245
     RewriteRule ^/cgi-bin/koha/acquisition/vendors(.*)?$ /cgi-bin/koha/acqui/vendors.pl$1 [PT]
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc (-1 lines)
Lines 45-51 Link Here
45
    can_user_parameters_manage_additional_fields="[% CAN_user_parameters_manage_additional_fields | html %]"
45
    can_user_parameters_manage_additional_fields="[% CAN_user_parameters_manage_additional_fields | html %]"
46
    can_user_parameters_manage_keyboard_shortcuts="[% CAN_user_parameters_manage_keyboard_shortcuts | html %]"
46
    can_user_parameters_manage_keyboard_shortcuts="[% CAN_user_parameters_manage_keyboard_shortcuts | html %]"
47
    can_user_ill="[% CAN_user_ill | html %]"
47
    can_user_ill="[% CAN_user_ill | html %]"
48
    shibbolethauthentication="[% Koha.Preference('ShibbolethAuthentication') | html %]"
49
    usecirculationdesks="[% Koha.Preference('UseCirculationDesks') | html %]"
48
    usecirculationdesks="[% Koha.Preference('UseCirculationDesks') | html %]"
50
    usecashregisters="[% Koha.Preference('UseCashRegisters') | html %]"
49
    usecashregisters="[% Koha.Preference('UseCashRegisters') | html %]"
51
    savedsearchfilters="[% Koha.Preference('SavedSearchFilters') | html %]"
50
    savedsearchfilters="[% Koha.Preference('SavedSearchFilters') | html %]"
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/identity_providers.tt (-584 / +19 lines)
Lines 1-609 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% USE Koha %]
3
[% USE Asset %]
2
[% USE Asset %]
4
[% USE Branches %]
3
[% USE To %]
5
[% USE Categories %]
6
[% PROCESS 'i18n.inc' %]
4
[% PROCESS 'i18n.inc' %]
7
[% SET footerjs = 1 %]
5
[% SET footerjs = 1 %]
8
[% INCLUDE 'doc-head-open.inc' %]
6
[% INCLUDE 'doc-head-open.inc' %]
9
<title
7
<title
10
    >[% FILTER collapse %]
8
    >[% FILTER collapse %]
11
        [% IF op == 'add_form' %]
12
            [% t("New identity provider") | html %]
13
            &rsaquo;
14
        [% ELSIF op == 'edit_form' %]
15
            [% tx("Modify identity provider '{id_provider}'", {id_provider = identity_provider.code}) | html %]
16
            &rsaquo;
17
        [% END %]
18
        [% t("Identity providers") | html %]
9
        [% t("Identity providers") | html %]
19
        &rsaquo; [% t("Administration") | html %] &rsaquo; [% t("Koha") | html %]
10
        &rsaquo; [% t("Administration") | html %] &rsaquo; [% t("Koha") | html %]
20
    [% END %]</title
11
    [% END %]
21
>
12
</title>
22
[% INCLUDE 'doc-head-close.inc' %]
13
[% INCLUDE 'doc-head-close.inc' %]
23
</head>
14
</head>
24
15
25
<body id="admin_identity_providers" class="admin">
16
<body id="admin_identity_providers" class="admin">
26
[% INCLUDE 'header.inc' %]
17
[% WRAPPER 'header.inc' %]
27
[% INCLUDE 'prefs-admin-search.inc' %]
18
    [% INCLUDE 'prefs-admin-search.inc' %]
19
[% END %]
28
20
29
[% WRAPPER 'sub-header.inc' %]
21
[% WRAPPER 'sub-header.inc' %]
30
    [% WRAPPER breadcrumbs %]
22
    <div id="vue-breadcrumbs-container"></div>
31
        [% WRAPPER breadcrumb_item %]
32
            <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
33
        [% END %]
34
35
        [% IF op == 'add_form' %]
36
            [% WRAPPER breadcrumb_item %]
37
                <a href="/cgi-bin/koha/admin/identity_providers.pl">Identity providers</a>
38
            [% END %]
39
            [% WRAPPER breadcrumb_item bc_active= 1 %]
40
                <span>New identity provider</span>
41
            [% END %]
42
        [% ELSIF op == 'edit_form' %]
43
            [% WRAPPER breadcrumb_item %]
44
                <a href="/cgi-bin/koha/admin/identity_providers.pl">Identity providers</a>
45
            [% END %]
46
            [% WRAPPER breadcrumb_item bc_active= 1 %]
47
                [% tx("Modify identity provider '{id_provider}'", {id_provider = identity_provider.code}) | html %]
48
            [% END %]
49
        [% ELSE %]
50
            [% WRAPPER breadcrumb_item bc_active= 1 %]
51
                <span>Identity providers</span>
52
            [% END %]
53
        [% END %]
54
    [% END #/ WRAPPER breadcrumbs %]
55
[% END #/ WRAPPER sub-header.inc %]
23
[% END #/ WRAPPER sub-header.inc %]
56
24
57
[% WRAPPER 'main-container.inc' aside='admin-menu' %]
25
[% WRAPPER 'main-container.inc' aside='admin-menu' %]
58
26
    <div id="identity-providers"></div>
59
    [% FOREACH m IN messages %]
27
[% END #/ WRAPPER main-container.inc %]
60
        <div class="alert alert-[% m.type | html %]" id="identity_provider_action_result_dialog">
61
            [% SWITCH m.code %]
62
            [% CASE 'error_on_update' %]
63
                <span>An error occurred trying to open the identity provider for editing. The passed ID is invalid.</span>
64
            [% CASE 'error_on_insert' %]
65
                <span>An error occurred when adding a new identity provider. Check the logs for details.</span>
66
            [% CASE 'success_on_update' %]
67
                <span>Identity provider updated successfully.</span>
68
            [% CASE 'success_on_insert' %]
69
                <div>Identity provider added successfully.</div>
70
                <div>You will need to restart Koha for the provider to work.</div>
71
            [% CASE %]
72
                <span>[% m.code | html %]</span>
73
            [% END %]
74
        </div>
75
    [% END %]
76
77
    <div class="alert alert-info" id="identity_provider_delete_success" style="display: none;"></div>
78
    <div class="alert alert-warning" id="identity_provider_delete_error" style="display: none;"></div>
79
80
    [% IF op == 'add_form' %]
81
        <h1>New identity provider</h1>
82
83
        <form action="/cgi-bin/koha/admin/identity_providers.pl" id="add" name="add" class="validated" method="post">
84
            [% INCLUDE 'csrf-token.inc' %]
85
            <input type="hidden" name="op" value="cud-add" />
86
            <fieldset class="rows">
87
                <legend id="identity_provider_basic">Basic configuration</legend>
88
                <ol>
89
                    <li>
90
                        <label for="code" class="required">Code: </label>
91
                        <input type="text" name="code" id="code" size="60" class="required" required="required" />
92
                        <span class="required">Required</span>
93
                        <div class="hint">Code that identifies this provider. Only alphanumeric and "_" characters are allowed</div>
94
                    </li>
95
                    <li>
96
                        <label for="description" class="required">Description: </label>
97
                        <input type="text" name="description" id="description" size="60" class="required" required="required" />
98
                        <span class="required">Required</span>
99
                        <div class="hint">User friendly name of this provider</div>
100
                    </li>
101
                    <li>
102
                        <label for="protocol">Protocol: </label>
103
                        <select name="protocol" id="protocol">
104
                            <option value="OAuth">OAuth</option>
105
                            <option value="OIDC">OIDC</option>
106
                            <!-- Not implemented yet
107
                            <option value="LDAP">LDAP</option>
108
                            <option value="CAS">CAS</option>
109
                            -->
110
                        </select>
111
                        <div class="hint">Choose the protocol this external identity provider uses</div>
112
                    </li>
113
                </ol>
114
            </fieldset>
115
116
            <fieldset class="rows">
117
                <legend id="identity_provider_advanced">Advanced configuration</legend>
118
                <ol>
119
                    <li>
120
                        <label for="config" class="required json">Configuration: </label>
121
                        <textarea name="config" id="config" class="required" cols="75" rows="10"></textarea>
122
                        <span class="required">Required</span>
123
                        <div class="hint"
124
                            >Provider's main configuration. <button class="more btn btn-light" data-target="config"><i class="fa fa-caret-down"></i> [% tp("Show more information", "More") | html %]</button></div
125
                        >
126
                        <div class="hint more-config" style="display: none">
127
                            <div>This configuration differs for each protocol.</div>
128
                            <div>It is recommended to add the default configuration, and then replace with appropriate values</div>
129
                        </div>
130
                        <div class="hint">
131
                            <button class="btn btn-default defaults" data-default-target="config" id="default-config">Add default OAuth configuration</button>
132
                        </div>
133
                    </li>
134
                    <li>
135
                        <label for="mapping" class="required json">Mapping: </label>
136
                        <textarea name="mapping" id="mapping" class="required" cols="75" rows="10"></textarea>
137
                        <span class="required">Required</span>
138
                        <div class="hint"
139
                            >Map provider's result to Koha patron's fields. <button class="more btn btn-light" data-target="mapping"><i class="fa fa-caret-down"></i> [% tp("Show more information", "More") | html %]</button></div
140
                        >
141
                        <div class="hint more-mapping" style="display: none">
142
                            <div>It is recommended to add the default mapping, and then modify to suit this provider's response</div>
143
                            <div>Keys represent Koha's fields, and values represent the keys in provider's result</div>
144
                            <div>For nested values in provider's results, you can use dot separation.</div>
145
                            <div>For example, <i>firstname: "users.0.name"</i> will match the 'name' attribute of the first object in the array named 'users', and place it in 'firstname' field</div>
146
                            <div>If you plan to use auto register feature, either <i>userid</i> or <i>cardnumber</i> must be present in this mapping</div>
147
                        </div>
148
                        <div class="hint">
149
                            <button class="btn btn-default defaults" data-default-target="mapping" id="default-mapping">Add default OAuth mapping</button>
150
                        </div>
151
                    </li>
152
                    <li>
153
                        <label for="matchpoint">Matchpoint: </label>
154
                        <select name="matchpoint" id="matchpoint">
155
                            <option value="email">Email</option>
156
                            <option value="userid">User ID</option>
157
                            <option value="cardnumber">Card number</option>
158
                        </select>
159
                        <div class="hint">Koha patron's field that will be used to match provider's user with Koha's</div>
160
                        <div class="hint">It must be present in mapping</div>
161
                    </li>
162
                    <li>
163
                        <label for="icon_url">Icon URL: </label>
164
                        <input type="text" name="icon_url" id="icon_url" size="60" />
165
                    </li>
166
                </ol>
167
            </fieldset>
168
169
            <fieldset class="rows">
170
                <legend id="identity_provider_domain">Domain configuration</legend>
171
                <ol>
172
                    <li>
173
                        <label for="domain" class="required">Domain: </label>
174
                        <input type="text" name="domain" id="domain" class="required" size="60" />
175
                        <span class="required">Required</span>
176
                        <div class="hint">Use &ast; for any domain. You can add new domains later on the dedicated admin page.</div>
177
                    </li>
178
                    <li>
179
                        <label for="default_library_id">Default library: </label>
180
                        <select id="default_library_id" name="default_library_id" class="mandatory">
181
                            [% PROCESS options_for_libraries libraries => Branches.all( unfiltered => 1, do_not_select_my_library => 1 ) %]
182
                        </select>
183
                        <span class="required">Required</span>
184
                        <div class="hint">Use this library for the patron on auto register</div>
185
                    </li>
186
                    <li>
187
                        <label for="default_category_id">Default category: </label>
188
                        [% SET categories = Categories.all() %]
189
                        <select name="default_category_id" id="default_category_id" class="mandatory">
190
                            [% FOREACH category IN categories %]
191
                                <option value="[% category.categorycode | html %]">[% category.description | html %]</option>
192
                            [% END %]
193
                        </select>
194
                        <span class="required">Required</span>
195
                        <div class="hint">Use this category for the patron on auto register</div>
196
                    </li>
197
                    <li>
198
                        <label for="allow_opac">Allow OPAC: </label>
199
                        <select name="allow_opac" id="allow_opac">
200
                            <option value="1">Yes</option>
201
                            <option value="0" selected="selected">No</option>
202
                        </select>
203
                        <div class="hint">Allow users of this domain to log into the OPAC using this identity provider.</div>
204
                    </li>
205
                    <li>
206
                        <label for="allow_staff">Allow staff: </label>
207
                        <select name="allow_staff" id="allow_staff">
208
                            <option value="1">Yes</option>
209
                            <option value="0" selected="selected">No</option>
210
                        </select>
211
                        <div class="hint">Allow staff access to users from this domain to login with this identity provider.</div>
212
                    </li>
213
                    <li>
214
                        <label for="auto_register_opac">Auto register (OPAC): </label>
215
                        <select name="auto_register_opac" id="auto_register_opac">
216
                            <option value="1">Allow</option>
217
                            <option value="0" selected="selected">Don't allow</option>
218
                        </select>
219
                        <span>users to auto register on login (OPAC)</span>
220
                    </li>
221
                    <li>
222
                        <label for="auto_register_staff">Auto register (Staff interface): </label>
223
                        <select name="auto_register_staff" id="auto_register_staff">
224
                            <option value="1">Allow</option>
225
                            <option value="0" selected="selected">Don't allow</option>
226
                        </select>
227
                        <span>users to auto register on login (Staff interface)</span>
228
                    </li>
229
                    <li>
230
                        <label for="update_on_auth">Update on login: </label>
231
                        <select name="update_on_auth" id="update_on_auth">
232
                            <option value="1">Yes</option>
233
                            <option value="0" selected="selected">No</option>
234
                        </select>
235
                        <div class="hint">Update user data on login.</div>
236
                    </li></ol
237
                >
238
            </fieldset>
239
240
            <fieldset class="action">
241
                <input type="submit" value="Submit" />
242
                <a class="cancel" href="/cgi-bin/koha/admin/identity_providers.pl">Cancel</a>
243
            </fieldset>
244
        </form>
245
    [% END %]
246
247
    [% IF op == 'edit_form' %]
248
        <h1>[% tx("Modify identity provider '{id_provider}'", {id_provider = identity_provider.code}) | html %]</h1>
249
250
        <form action="/cgi-bin/koha/admin/identity_providers.pl" id="edit_save" name="edit_save" class="validated" method="post">
251
            [% INCLUDE 'csrf-token.inc' %]
252
            <input type="hidden" name="op" value="cud-edit_save" />
253
            <input type="hidden" name="identity_provider_id" value="[%- identity_provider.identity_provider_id | html -%]" />
254
            <fieldset class="rows">
255
                <legend id="identity_provider_basic">Basic configuration</legend>
256
                <ol>
257
                    <li>
258
                        <label for="code" class="required">Code: </label>
259
                        <input type="text" name="code" id="code" size="60" class="required" required="required" value="[%- identity_provider.code | html -%]" />
260
                        <span class="required">Required</span>
261
                        <div class="hint">Code that identifies this provider. Only alphanumeric and "_" characters are allowed</div>
262
                    </li>
263
                    <li>
264
                        <label for="description" class="required">Description: </label>
265
                        <input type="text" name="description" id="description" size="60" class="required" required="required" value="[%- identity_provider.description | html -%]" />
266
                        <span class="required">Required</span>
267
                        <div class="hint">User friendly name of this provider</div>
268
                    </li>
269
                    <li>
270
                        <label for="protocol">Protocol: </label>
271
                        <select name="protocol" id="protocol">
272
                            [% IF identity_provider.protocol == 'OAuth' %]
273
                                <option value="OAuth" selected="selected">OAuth</option>
274
                                <option value="OIDC">OIDC</option>
275
                                <!-- Not implemented yet
276
                            <option value="LDAP">LDAP</option>
277
                            <option value="CAS">CAS</option>
278
                            -->
279
                            [% ELSE %]
280
                                <option value="OAuth">OAuth</option>
281
                                <option value="OIDC" selected="selected">OIDC</option>
282
                                <!-- Not implemented yet
283
                            <option value="LDAP">LDAP</option>
284
                            <option value="CAS">CAS</option>
285
                            -->
286
                            [% END %]
287
                        </select>
288
                        <div class="hint">Choose the protocol this external identity provider uses</div>
289
                    </li>
290
                </ol>
291
            </fieldset>
292
293
            <fieldset class="rows">
294
                <legend id="identity_provider_advanced">Advanced configuration</legend>
295
                <ol>
296
                    <li>
297
                        <label for="config" class="required json">Configuration: </label>
298
                        <textarea name="config" id="config" class="required" cols="75" rows="10">[%- identity_provider.config | html -%]</textarea>
299
                        <span class="required">Required</span>
300
                        <div class="hint"
301
                            >Provider's main configuration. <button class="more btn btn-light" data-target="config"><i class="fa fa-caret-down"></i> [% tp("Show more information", "More") | html %]</button></div
302
                        >
303
                        <div class="hint more-config" style="display: none">
304
                            <div>This configuration differs for each protocol.</div>
305
                            <div>It is recommended to add the default configuration, and then replace with appropriate values</div>
306
                        </div>
307
                        <div class="hint">
308
                            <button class="btn btn-light defaults" data-default-target="config" id="default-config">Add default [%- identity_provider.protocol | html -%] configuration</button>
309
                        </div>
310
                    </li>
311
                    <li>
312
                        <label for="mapping" class="required json">Mapping: </label>
313
                        <textarea name="mapping" id="mapping" class="required" cols="75" rows="10">[%- identity_provider.mapping | html -%]</textarea>
314
                        <span class="required">Required</span>
315
                        <div class="hint"
316
                            >Map provider's result to Koha patron's fields. <button class="more btn btn-light" data-target="mapping"><i class="fa fa-caret-down"></i> [% tp("Show more information", "More") | html %]</button></div
317
                        >
318
                        <div class="hint more-mapping" style="display: none">
319
                            <div>It is recommended to add the default mapping, and then modify to suit this provider's response</div>
320
                            <div>Keys represent Koha's fields, and values represent the keys in provider's result</div>
321
                            <div>For nested values in provider's results, you can use dot separation.</div>
322
                            <div>For example, <i>firstname: "users.0.name"</i> will match the 'name' attribute of the first object in the array named 'users', and place it in 'firstname' field</div>
323
                            <div>If you plan to use auto register feature, either <i>userid</i> or <i>cardnumber</i> must be present in this mapping</div>
324
                        </div>
325
                        <div class="hint">
326
                            <button class="btn btn-light defaults" data-default-target="mapping" id="default-mapping">Add default [%- identity_provider.protocol | html -%] mapping</button>
327
                        </div>
328
                    </li>
329
                    <li>
330
                        <label for="matchpoint">Matchpoint: </label>
331
                        <select name="matchpoint" id="matchpoint">
332
                            [%- IF identity_provider.matchpoint == 'email' -%]
333
                                <option value="email" selected="selected">Email</option>
334
                            [%- ELSE -%]
335
                                <option value="email">Email</option>
336
                            [%- END -%]
337
                            [%- IF identity_provider.matchpoint == 'userid' -%]
338
                                <option value="userid" selected="selected">User id</option>
339
                            [%- ELSE -%]
340
                                <option value="userid">User id</option>
341
                            [%- END -%]
342
                            [%- IF identity_provider.matchpoint == 'cardnumber' -%]
343
                                <option value="cardnumber" selected="selected">Card number</option>
344
                            [%- ELSE -%]
345
                                <option value="cardnumber">Card number</option>
346
                            [%- END -%]
347
                        </select>
348
                        <div class="hint">Koha patron's field that will be used to match provider's user with Koha's</div>
349
                        <div class="hint">It must be present in mapping</div>
350
                    </li>
351
                    <li>
352
                        <label for="icon_url">Icon URL: </label>
353
                        <input type="text" name="icon_url" id="icon_url" size="60" value="[%- identity_provider.icon_url | html -%]" />
354
                    </li>
355
                </ol>
356
            </fieldset>
357
            <fieldset class="action">
358
                <input type="submit" value="Submit" />
359
                <a class="cancel" href="/cgi-bin/koha/admin/identity_providers.pl">Cancel</a>
360
            </fieldset>
361
        </form>
362
    [% END %]
363
364
    [% IF op == 'list' %]
365
        <div id="toolbar" class="btn-toolbar">
366
            <a class="btn btn-default" id="new_identity_provider" href="/cgi-bin/koha/admin/identity_providers.pl?op=add_form"><i class="fa fa-plus"></i> New identity provider</a>
367
        </div>
368
369
        <h1>Identity providers</h1>
370
        <div class="page-section">
371
            <table id="identity_providers">
372
                <thead>
373
                    <tr>
374
                        <th>Code</th>
375
                        <th>Description</th>
376
                        <th>Protocol</th>
377
                        <th data-class-name="actions no-export">Actions</th>
378
                    </tr>
379
                </thead>
380
            </table>
381
        </div>
382
    [% END %]
383
384
    <div id="delete_confirm_modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="delete_confirm_modal_label" aria-hidden="true">
385
        <div class="modal-dialog">
386
            <div class="modal-content">
387
                <div class="modal-header">
388
                    <h1 class="modal-title" id="delete_confirm_modal_label">Confirm deletion of identity provider</h1>
389
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
390
                </div>
391
                <div class="modal-body">
392
                    <div id="delete_confirm_dialog"></div>
393
                </div>
394
                <div class="modal-footer">
395
                    <a href="#" class="btn btn-danger" id="delete_confirm_modal_button" role="button" data-bs-toggle="modal">Delete</a>
396
                    <button type="button" class="btn btn-default" data-bs-dismiss="modal">Close</button>
397
                </div>
398
            </div>
399
            <!-- /.modal-content -->
400
        </div>
401
        <!-- /.modal-dialog -->
402
    </div>
403
    <!-- #delete_confirm_modal -->
404
[% END %]
405
28
406
[% MACRO jsinclude BLOCK %]
29
[% MACRO jsinclude BLOCK %]
407
    [% Asset.js("js/admin-menu.js") | $raw %]
30
    [% INCLUDE 'calendar.inc' %]
408
    [% INCLUDE 'datatables.inc' %]
31
    [% INCLUDE 'datatables.inc' %]
32
    [% Asset.js("js/admin-menu.js") | $raw %]
409
    <script>
33
    <script>
410
        $(document).ready(function () {
34
        const logged_in_user = [% To.json(logged_in_user.unblessed) | $raw %];
411
            var identity_providers_url = "/api/v1/auth/identity_providers";
35
        window.borrower_columns = [% To.json(borrower_columns) | $raw %];
412
            window.identity_providers = $("#identity_providers").kohaTable({
36
        window.libraries_map = [% To.json(libraries_map) | $raw %];
413
                ajax: {
37
        window.categories_map = [% To.json(categories_map) | $raw %];
414
                    url: identity_providers_url,
38
        window.idp_default_hostnames = [% To.json(idp_default_hostnames) | $raw %];
415
                },
39
        window.unique_patron_attributes = [% To.json(unique_patron_attributes) || '[]' | $raw %];
416
                language: {
40
        window.all_patron_attributes = [% To.json(all_patron_attributes) || '[]' | $raw %];
417
                    emptyTable: '<div class="alert alert-info">' + _("There are no identity providers defined.") + "</div>",
418
                },
419
                columnDefs: [
420
                    {
421
                        targets: [0, 1, 2],
422
                        render: function (data, type, row, meta) {
423
                            if (type == "display") {
424
                                if (data != null) {
425
                                    return data.escapeHtml();
426
                                } else {
427
                                    return "Default";
428
                                }
429
                            }
430
                            return data;
431
                        },
432
                    },
433
                ],
434
                columns: [
435
                    {
436
                        data: "code",
437
                        searchable: true,
438
                        orderable: true,
439
                    },
440
                    {
441
                        data: "description",
442
                        searchable: true,
443
                        orderable: true,
444
                    },
445
                    {
446
                        data: "protocol",
447
                        searchable: true,
448
                        orderable: true,
449
                    },
450
                    {
451
                        data: function (row, type, val, meta) {
452
                            var result =
453
                                '<a class="btn btn-default btn-xs" role="button" href="/cgi-bin/koha/admin/identity_providers.pl?op=edit_form&amp;identity_provider_id=' +
454
                                encodeURIComponent(row.identity_provider_id) +
455
                                '"><i class="fa-solid fa-pencil" aria-hidden="true"></i> ' +
456
                                _("Edit") +
457
                                "</a>" +
458
                                "\n";
459
                            result +=
460
                                '<a class="btn btn-default btn-xs delete_identity_provider" role="button" href="#" data-bs-toggle="modal" data-bs-target="#delete_confirm_modal" data-auth-provider-id="' +
461
                                encodeURIComponent(row.identity_provider_id) +
462
                                '" data-auth-provider-code="' +
463
                                encodeURIComponent(row.code.escapeHtml()) +
464
                                '"><i class="fa fa-trash-can" aria-hidden="true"></i> ' +
465
                                _("Delete") +
466
                                "</a>" +
467
                                "\n";
468
                            result +=
469
                                '<a class="btn btn-default btn-xs edit_domains" role="button" href="/cgi-bin/koha/admin/identity_providers.pl?domain_ops=1&amp;identity_provider_id=' +
470
                                encodeURIComponent(row.identity_provider_id) +
471
                                '"><i class="fa fa-cog" aria-hidden="true"></i> ' +
472
                                _("Manage domains") +
473
                                "</a>";
474
                            return result;
475
                        },
476
                        searchable: false,
477
                        orderable: false,
478
                    },
479
                ],
480
                createdRow: function (row, data, dataIndex) {
481
                    if (data.debug) {
482
                        $(row).addClass("debug");
483
                    }
484
                },
485
            });
486
487
            $("#identity_providers").on("click", ".delete_identity_provider", function () {
488
                var identity_provider_id = $(this).data("auth-provider-id");
489
                var identity_provider_code = decodeURIComponent($(this).data("auth-provider-code"));
490
491
                $("#delete_confirm_dialog").html(_("You are about to delete the '%s' identity provider.").format(identity_provider_code));
492
                $("#delete_confirm_modal_button").data("auth-provider-id", identity_provider_id);
493
                $("#delete_confirm_modal_button").data("auth-provider-code", identity_provider_code);
494
            });
495
496
            $("#delete_confirm_modal_button").on("click", function () {
497
                var identity_provider_id = $(this).data("auth-provider-id");
498
                var identity_provider_code = $(this).data("auth-provider-code");
499
500
                $.ajax({
501
                    method: "DELETE",
502
                    url: identity_providers_url + "/" + identity_provider_id,
503
                })
504
                    .success(function () {
505
                        window.identity_providers.api().ajax.reload(function (data) {
506
                            $("#identity_provider_action_result_dialog").hide();
507
                            $("#identity_provider_delete_success").html(_("Server '%s' deleted successfully.").format(identity_provider_code)).show();
508
                        });
509
                    })
510
                    .fail(function () {
511
                        $("#identity_provider_delete_error").html(_("Error deleting server '%s'. Check the logs for details.").format(identity_provider_code)).show();
512
                    })
513
                    .done(function () {
514
                        $("#delete_confirm_modal").modal("hide");
515
                    });
516
            });
517
518
            $.validator.addMethod(
519
                "json",
520
                function (value, element) {
521
                    if (this.optional(element) && value === "") return true;
522
                    try {
523
                        JSON.parse(value);
524
                    } catch (error) {
525
                        return false;
526
                    }
527
                    return true;
528
                },
529
                _("Not a valid JSON")
530
            );
531
532
            $.validator.addMethod(
533
                "alphanum",
534
                function (value, element) {
535
                    if (this.optional(element) && value === "") return true;
536
                    return /^[a-zA-Z0-9_]+$/.test(value);
537
                },
538
                _("Value must have alphanumeric characters or '_'")
539
            );
540
541
            $("#config, #mapping").each(function () {
542
                $(this).rules("add", {
543
                    required: true,
544
                    json: true,
545
                });
546
            });
547
548
            $("button.more").on("click", function (event) {
549
                event.preventDefault();
550
                var target = $(this).hide().data("target");
551
                $(".more-" + target).show();
552
            });
553
554
            $("#code").rules("add", {
555
                alphanum: true,
556
                required: true,
557
            });
558
559
            var defaults = {
560
                OIDC: {
561
                    config: {
562
                        key: "<enter client id>",
563
                        secret: "<enter client secret>",
564
                        well_known_url: "<enter openid configuration endpoint>",
565
                        scope: "openid email",
566
                    },
567
                    mapping: {
568
                        email: "email",
569
                        firstname: "given_name",
570
                        surname: "family_name",
571
                    },
572
                },
573
                OAuth: {
574
                    config: {
575
                        key: "<enter client id>",
576
                        secret: "<enter client secret>",
577
                        authorize_url: "<enter authorization endpoint>",
578
                        token_url: "<enter token endpoint>",
579
                        userinfo_url: "<enter user info endpoint (optional)>",
580
                        scope: "email",
581
                    },
582
                    mapping: {
583
                        email: "email",
584
                        firstname: "given_name",
585
                        surname: "family_name",
586
                    },
587
                },
588
            };
589
590
            $("#protocol").on("change", function () {
591
                var protocol = $(this).val();
592
                $("#default-config").html(_("Add default %s configuration").format(protocol));
593
                $("#default-mapping").html(_("Add default %s mapping").format(protocol));
594
            });
595
596
            $("button.defaults").on("click", function (event) {
597
                event.preventDefault();
598
                var target = $(this).data("defaultTarget");
599
                if ($("#" + target).val() !== "" && !confirm(_("Are you sure you want to replace current %s contents?").format(target))) {
600
                    return;
601
                }
602
                var protocol = $("#protocol").val();
603
                $("#" + target).val(JSON.stringify(defaults[protocol][target], null, 2));
604
            });
605
        });
606
    </script>
41
    </script>
42
    [% Asset.js("js/vue/dist/admin/identity_providers.js") | $raw %]
607
[% END %]
43
[% END %]
608
609
[% INCLUDE 'intranet-bottom.inc' %]
44
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref (-6 lines)
Lines 1015-1023 OPAC: Link Here
1015
            - "fields (separate values with |). Tabs appear in the order listed.<br/>"
1015
            - "fields (separate values with |). Tabs appear in the order listed.<br/>"
1016
            - "<em>Currently supported values</em>: Item types (<strong>itemtypes</strong>), Collection, (<strong>ccode</strong>) and Shelving location (<strong>loc</strong>)."
1016
            - "<em>Currently supported values</em>: Item types (<strong>itemtypes</strong>), Collection, (<strong>ccode</strong>) and Shelving location (<strong>loc</strong>)."
1017
    Authentication:
1017
    Authentication:
1018
        -
1019
            - pref: OPACShibOnly
1020
              choices:
1021
                  1: "Don't allow"
1022
                  0: Allow
1023
            - patrons to login by means other than Shibboleth.
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref (-6 lines)
Lines 204-215 Staff interface: Link Here
204
            - 'records when downloading from the reports module.<br>NOTE: Only a positive value will enforce a limit. A specific limit in the report overrides this setting.'
204
            - 'records when downloading from the reports module.<br>NOTE: Only a positive value will enforce a limit. A specific limit in the report overrides this setting.'
205
205
206
    Authentication:
206
    Authentication:
207
        -
208
            - pref: staffShibOnly
209
              choices:
210
                  1: "Don't allow"
211
                  0: Allow
212
            - staff to log in by means other than Shibboleth.
213
        -
207
        -
214
            - pref: TwoFactorAuthentication
208
            - pref: TwoFactorAuthentication
215
              choices:
209
              choices:
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js (+3 lines)
Lines 197-202 export const APIClient = { Link Here
197
    patron: createClientProxy(() => import("./patron-api-client.js")),
197
    patron: createClientProxy(() => import("./patron-api-client.js")),
198
    patron_list: createClientProxy(() => import("./patron-list-api-client.js")),
198
    patron_list: createClientProxy(() => import("./patron-list-api-client.js")),
199
    recall: createClientProxy(() => import("./recall-api-client.js")),
199
    recall: createClientProxy(() => import("./recall-api-client.js")),
200
    identity_providers: createClientProxy(
201
        () => import("./identity-providers-api-client.js")
202
    ),
200
    sysprefs: createClientProxy(
203
    sysprefs: createClientProxy(
201
        () => import("./system-preferences-api-client.js")
204
        () => import("./system-preferences-api-client.js")
202
    ),
205
    ),
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/identity-providers-api-client.js (+171 lines)
Line 0 Link Here
1
export class IdentityProvidersAPIClient {
2
    constructor(HttpClient) {
3
        this.httpClient = new HttpClient({
4
            baseURL: "/api/v1/auth/identity_providers",
5
        });
6
        this.allHostnamesHttpClient = new HttpClient({
7
            baseURL: "/api/v1/auth/hostnames",
8
        });
9
    }
10
11
    get providers() {
12
        return {
13
            getAll: params =>
14
                this.httpClient.getAll({
15
                    endpoint: "/",
16
                    params,
17
                }),
18
            get: id =>
19
                this.httpClient.get({
20
                    endpoint: "/" + id,
21
                    headers: {
22
                        "x-koha-embed": "domains,hostnames,mappings",
23
                    },
24
                }),
25
            create: provider =>
26
                this.httpClient.post({
27
                    endpoint: "/",
28
                    body: provider,
29
                }),
30
            update: (provider, id) =>
31
                this.httpClient.put({
32
                    endpoint: "/" + id,
33
                    body: provider,
34
                }),
35
            delete: id =>
36
                this.httpClient.delete({
37
                    endpoint: "/" + id,
38
                }),
39
            count: (query = {}) =>
40
                this.httpClient.count({
41
                    endpoint:
42
                        "?" +
43
                        new URLSearchParams({
44
                            _page: 1,
45
                            _per_page: 1,
46
                            ...(query && { q: JSON.stringify(query) }),
47
                        }),
48
                }),
49
        };
50
    }
51
52
    get mappings() {
53
        return {
54
            getAll: (providerId, params) =>
55
                this.httpClient.getAll({
56
                    endpoint: `/${providerId}/mappings`,
57
                    params,
58
                }),
59
            get: (providerId, mappingId) =>
60
                this.httpClient.get({
61
                    endpoint: `/${providerId}/mappings/${mappingId}`,
62
                }),
63
            create: (providerId, mapping) =>
64
                this.httpClient.post({
65
                    endpoint: `/${providerId}/mappings`,
66
                    body: mapping,
67
                }),
68
            update: (providerId, mapping, mappingId) =>
69
                this.httpClient.put({
70
                    endpoint: `/${providerId}/mappings/${mappingId}`,
71
                    body: mapping,
72
                }),
73
            delete: (providerId, mappingId) =>
74
                this.httpClient.delete({
75
                    endpoint: `/${providerId}/mappings/${mappingId}`,
76
                }),
77
            count: (providerId, query = {}) =>
78
                this.httpClient.count({
79
                    endpoint:
80
                        `/${providerId}/mappings?` +
81
                        new URLSearchParams({
82
                            _page: 1,
83
                            _per_page: 1,
84
                            ...(query && { q: JSON.stringify(query) }),
85
                        }),
86
                }),
87
        };
88
    }
89
90
    get hostnames() {
91
        return {
92
            getAll: (providerId, params) =>
93
                this.httpClient.getAll({
94
                    endpoint: `/${providerId}/hostnames`,
95
                    params,
96
                }),
97
            get: (providerId, hostnameId) =>
98
                this.httpClient.get({
99
                    endpoint: `/${providerId}/hostnames/${hostnameId}`,
100
                }),
101
            create: (providerId, hostname) =>
102
                this.httpClient.post({
103
                    endpoint: `/${providerId}/hostnames`,
104
                    body: hostname,
105
                }),
106
            update: (providerId, hostname, hostnameId) =>
107
                this.httpClient.put({
108
                    endpoint: `/${providerId}/hostnames/${hostnameId}`,
109
                    body: hostname,
110
                }),
111
            delete: (providerId, hostnameId) =>
112
                this.httpClient.delete({
113
                    endpoint: `/${providerId}/hostnames/${hostnameId}`,
114
                }),
115
        };
116
    }
117
118
    get allHostnames() {
119
        return {
120
            getAll: params =>
121
                this.allHostnamesHttpClient.getAll({
122
                    endpoint: "/",
123
                    params,
124
                }),
125
            get: id =>
126
                this.allHostnamesHttpClient.get({
127
                    endpoint: "/" + id,
128
                }),
129
        };
130
    }
131
132
    get domains() {
133
        return {
134
            getAll: (providerId, params) =>
135
                this.httpClient.getAll({
136
                    endpoint: `/${providerId}/domains`,
137
                    params,
138
                }),
139
            get: (providerId, domainId) =>
140
                this.httpClient.get({
141
                    endpoint: `/${providerId}/domains/${domainId}`,
142
                }),
143
            create: (providerId, domain) =>
144
                this.httpClient.post({
145
                    endpoint: `/${providerId}/domains`,
146
                    body: domain,
147
                }),
148
            update: (providerId, domain, domainId) =>
149
                this.httpClient.put({
150
                    endpoint: `/${providerId}/domains/${domainId}`,
151
                    body: domain,
152
                }),
153
            delete: (providerId, domainId) =>
154
                this.httpClient.delete({
155
                    endpoint: `/${providerId}/domains/${domainId}`,
156
                }),
157
            count: (providerId, query = {}) =>
158
                this.httpClient.count({
159
                    endpoint:
160
                        `/${providerId}/domains?` +
161
                        new URLSearchParams({
162
                            _page: 1,
163
                            _per_page: 1,
164
                            ...(query && { q: JSON.stringify(query) }),
165
                        }),
166
                }),
167
        };
168
    }
169
}
170
171
export default IdentityProvidersAPIClient;
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/DomainResource.vue (+261 lines)
Line 0 Link Here
1
<template>
2
    <div>
3
        <BaseResource :routeAction="routeAction" :instancedResource="this" />
4
    </div>
5
</template>
6
7
<script>
8
import { ref, computed } from "vue";
9
import { useRoute } from "vue-router";
10
import BaseResource from "./../BaseResource.vue";
11
import { useBaseResource } from "../../composables/base-resource.js";
12
import { APIClient } from "../../fetch/api-client.js";
13
import { $__ } from "@koha-vue/i18n";
14
15
export default {
16
    name: "DomainResource",
17
    components: { BaseResource },
18
    props: {
19
        routeAction: String,
20
    },
21
    emits: ["select-resource"],
22
    setup(props) {
23
        const route = useRoute();
24
        const providerId = computed(() => route.params.identity_provider_id);
25
26
        const getLibraries = () => window.libraries_map || [];
27
        const getCategories = () => window.categories_map || [];
28
29
        const resourceAttrs = [
30
            {
31
                name: "domain",
32
                type: "text",
33
                label: __("Domain"),
34
                group: "Domain",
35
                toolTip: __(
36
                    "Email domain to match. Use '*' or leave empty for any domain. Wildcards like '*library.com' are supported."
37
                ),
38
            },
39
            {
40
                name: "allow_opac",
41
                type: "boolean",
42
                label: __("Allow OPAC login"),
43
                group: "Access",
44
                toolTip: __("Allow users of this domain to log into the OPAC"),
45
            },
46
            {
47
                name: "allow_staff",
48
                type: "boolean",
49
                label: __("Allow staff login"),
50
                group: "Access",
51
                toolTip: __(
52
                    "Allow users of this domain to log into the staff interface"
53
                ),
54
            },
55
            {
56
                name: "auto_register_opac",
57
                type: "boolean",
58
                label: __("Auto-register (OPAC)"),
59
                group: "Auto-registration",
60
                toolTip: __(
61
                    "Automatically create patron records for new OPAC users from this domain"
62
                ),
63
            },
64
            {
65
                name: "auto_register_staff",
66
                type: "boolean",
67
                label: __("Auto-register (staff)"),
68
                group: "Auto-registration",
69
                toolTip: __(
70
                    "Automatically create patron records for new staff users from this domain"
71
                ),
72
            },
73
            {
74
                name: "update_on_auth",
75
                type: "boolean",
76
                label: __("Update patron data on login"),
77
                group: "Auto-registration",
78
                toolTip: __(
79
                    "Sync patron attributes from the provider on each login"
80
                ),
81
            },
82
            {
83
                name: "default_library_id",
84
                type: "select",
85
                label: __("Default library"),
86
                group: "Auto-registration defaults",
87
                options: getLibraries(),
88
                requiredKey: "value",
89
                selectLabel: "label",
90
                toolTip: __("Library assigned to auto-registered patrons"),
91
            },
92
            {
93
                name: "default_category_id",
94
                type: "select",
95
                label: __("Default category"),
96
                group: "Auto-registration defaults",
97
                options: getCategories(),
98
                requiredKey: "value",
99
                selectLabel: "label",
100
                toolTip: __(
101
                    "Patron category assigned to auto-registered patrons"
102
                ),
103
            },
104
        ];
105
106
        const baseResource = useBaseResource({
107
            resourceName: "domain",
108
            nameAttr: "domain",
109
            idAttr: "identity_provider_domain_id",
110
            components: {
111
                show: "DomainShow",
112
                list: "DomainsList",
113
                add: "DomainsFormAdd",
114
                edit: "DomainsFormEdit",
115
            },
116
            apiClient: {
117
                getAll: params =>
118
                    APIClient.identity_providers.domains.getAll(
119
                        providerId.value,
120
                        params
121
                    ),
122
                get: id =>
123
                    APIClient.identity_providers.domains.get(
124
                        providerId.value,
125
                        id
126
                    ),
127
                create: domain =>
128
                    APIClient.identity_providers.domains.create(
129
                        providerId.value,
130
                        domain
131
                    ),
132
                update: (d, id) =>
133
                    APIClient.identity_providers.domains.update(
134
                        providerId.value,
135
                        d,
136
                        id
137
                    ),
138
                delete: id =>
139
                    APIClient.identity_providers.domains.delete(
140
                        providerId.value,
141
                        id
142
                    ),
143
                count: (query = {}) =>
144
                    APIClient.identity_providers.domains.count(
145
                        providerId.value,
146
                        query
147
                    ),
148
            },
149
            i18n: {
150
                deleteConfirmationMessage: $__(
151
                    "Are you sure you want to remove this domain configuration?"
152
                ),
153
                deleteSuccessMessage: $__("Domain deleted"),
154
                displayName: $__("Domain"),
155
                editLabel: $__("Edit domain"),
156
                emptyListMessage: $__(
157
                    "There are no domains configured for this provider"
158
                ),
159
                newLabel: $__("New domain"),
160
            },
161
            table: {
162
                resourceTableUrl: `/api/v1/auth/identity_providers/${providerId.value}/domains`,
163
                options: {},
164
            },
165
            stickyToolbar: ["Form"],
166
            embedded: props.embedded,
167
            formGroupsDisplayMode: "accordion",
168
            resourceAttrs,
169
            props,
170
            moduleStore: "IdentityProvidersStore",
171
        });
172
173
        const getResourceShowURL = id =>
174
            baseResource.router.resolve({
175
                name: "DomainShow",
176
                params: {
177
                    identity_provider_id: providerId.value,
178
                    identity_provider_domain_id: id,
179
                },
180
            }).href;
181
182
        const goToResourceAdd = () =>
183
            baseResource.router.push({
184
                name: "DomainsFormAdd",
185
                params: { identity_provider_id: providerId.value },
186
            });
187
188
        const goToResourceEdit = resource =>
189
            baseResource.router.push({
190
                name: "DomainsFormEdit",
191
                params: {
192
                    identity_provider_id: providerId.value,
193
                    identity_provider_domain_id:
194
                        resource.identity_provider_domain_id,
195
                },
196
            });
197
198
        const goToResourceList = () =>
199
            baseResource.router.push({
200
                name: "ProviderShow",
201
                params: { identity_provider_id: providerId.value },
202
            });
203
204
        const onFormSave = (e, domainToSave) => {
205
            e.preventDefault();
206
            const domain = JSON.parse(JSON.stringify(domainToSave));
207
            const domainId = domain.identity_provider_domain_id;
208
209
            delete domain.identity_provider_domain_id;
210
            delete domain.identity_provider_id;
211
212
            if (domainId) {
213
                return baseResource.apiClient.update(domain, domainId).then(
214
                    updatedDomain => {
215
                        baseResource.setMessage($__("Domain updated"));
216
                        baseResource.router.push({
217
                            name: "ProviderShow",
218
                            params: {
219
                                identity_provider_id: providerId.value,
220
                            },
221
                        });
222
                        return updatedDomain;
223
                    },
224
                    error => {}
225
                );
226
            } else {
227
                return baseResource.apiClient.create(domain).then(
228
                    newDomain => {
229
                        baseResource.setMessage($__("Domain created"));
230
                        baseResource.router.push({
231
                            name: "ProviderShow",
232
                            params: {
233
                                identity_provider_id: providerId.value,
234
                            },
235
                        });
236
                        return newDomain;
237
                    },
238
                    error => {}
239
                );
240
            }
241
        };
242
243
        const tableOptions = {
244
            url: baseResource.getResourceTableUrl(),
245
            actions: {
246
                "-1": ["edit", "delete"],
247
            },
248
        };
249
250
        return {
251
            ...baseResource,
252
            getResourceShowURL,
253
            goToResourceAdd,
254
            goToResourceEdit,
255
            goToResourceList,
256
            tableOptions,
257
            onFormSave,
258
        };
259
    },
260
};
261
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/Main.vue (+76 lines)
Line 0 Link Here
1
<template>
2
    <div v-if="initialized">
3
        <Teleport to="#vue-breadcrumbs-container">
4
            <Breadcrumbs />
5
        </Teleport>
6
        <Dialog />
7
        <router-view />
8
    </div>
9
</template>
10
11
<script>
12
import { inject, onBeforeMount, ref } from "vue";
13
import Breadcrumbs from "../Breadcrumbs.vue";
14
import Dialog from "../Dialog.vue";
15
import "vue-select/dist/vue-select.css";
16
17
export default {
18
    setup() {
19
        const mainStore = inject("mainStore");
20
        const { loading, loaded } = mainStore;
21
22
        const initialized = ref(false);
23
24
        onBeforeMount(() => {
25
            loading();
26
            setTimeout(() => {
27
                loaded();
28
                initialized.value = true;
29
            }, 0);
30
        });
31
32
        return { initialized };
33
    },
34
    components: {
35
        Breadcrumbs,
36
        Dialog,
37
    },
38
};
39
</script>
40
41
<style>
42
#menu ul ul,
43
#navmenulist ul ul {
44
    padding-left: 2em;
45
    font-size: 100%;
46
}
47
48
form .v-select {
49
    display: inline-block;
50
    background-color: white;
51
    width: 30%;
52
}
53
54
.v-select,
55
input:not([type="submit"]):not([type="search"]):not([type="button"]):not(
56
        [type="checkbox"]
57
    ):not([type="radio"]),
58
textarea {
59
    border-color: rgba(60, 60, 60, 0.26);
60
    border-width: 1px;
61
    border-radius: 4px;
62
    min-width: 30%;
63
}
64
65
#navmenulist ul li a.current.disabled {
66
    background-color: inherit;
67
    border-left: 5px solid #e6e6e6;
68
    color: #000;
69
}
70
71
#navmenulist ul li a.disabled {
72
    color: #666;
73
    pointer-events: none;
74
    font-weight: 700;
75
}
76
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/MappingResource.vue (+215 lines)
Line 0 Link Here
1
<template>
2
    <div>
3
        <BaseResource :routeAction="routeAction" :instancedResource="this" />
4
    </div>
5
</template>
6
7
<script>
8
import { ref, onMounted, computed } from "vue";
9
import { useRoute } from "vue-router";
10
import BaseResource from "./../BaseResource.vue";
11
import { useBaseResource } from "../../composables/base-resource.js";
12
import { APIClient } from "../../fetch/api-client.js";
13
import { $__ } from "@koha-vue/i18n";
14
15
export default {
16
    name: "MappingResource",
17
    components: { BaseResource },
18
    props: {
19
        routeAction: String,
20
    },
21
    emits: ["select-resource"],
22
    setup(props) {
23
        const route = useRoute();
24
        const providerId = computed(() => route.params.identity_provider_id);
25
26
        const getBorrowerColumns = () => window.borrower_columns || [];
27
        const borrowerColumnsArray = [
28
            ...getBorrowerColumns(),
29
            ...(window.all_patron_attributes || []),
30
        ];
31
32
        const resourceAttrs = [
33
            {
34
                name: "koha_field",
35
                required: true,
36
                type: "select",
37
                options: borrowerColumnsArray,
38
                requiredKey: "value",
39
                selectLabel: "label",
40
                label: __("Koha field"),
41
                group: "Mapping",
42
                toolTip: __(
43
                    "The field in the Koha borrowers table to populate, or a patron attribute"
44
                ),
45
            },
46
            {
47
                name: "provider_field",
48
                type: "text",
49
                label: __("Provider attribute"),
50
                group: "Mapping",
51
                toolTip: __(
52
                    "The attribute name supplied by the identity provider. Leave empty to use only the default value."
53
                ),
54
            },
55
            {
56
                name: "default_content",
57
                type: "text",
58
                label: __("Default value"),
59
                group: "Mapping",
60
                toolTip: __(
61
                    "Value to use when the provider does not supply this attribute"
62
                ),
63
            },
64
        ];
65
66
        const baseResource = useBaseResource({
67
            resourceName: "mapping",
68
            nameAttr: "koha_field",
69
            idAttr: "mapping_id",
70
            components: {
71
                show: "MappingShow",
72
                list: "MappingsList",
73
                add: "MappingsFormAdd",
74
                edit: "MappingsFormEdit",
75
            },
76
            apiClient: {
77
                getAll: params =>
78
                    APIClient.identity_providers.mappings.getAll(
79
                        providerId.value,
80
                        params
81
                    ),
82
                get: id =>
83
                    APIClient.identity_providers.mappings.get(
84
                        providerId.value,
85
                        id
86
                    ),
87
                create: mapping =>
88
                    APIClient.identity_providers.mappings.create(
89
                        providerId.value,
90
                        mapping
91
                    ),
92
                update: (m, id) =>
93
                    APIClient.identity_providers.mappings.update(
94
                        providerId.value,
95
                        m,
96
                        id
97
                    ),
98
                delete: id =>
99
                    APIClient.identity_providers.mappings.delete(
100
                        providerId.value,
101
                        id
102
                    ),
103
                count: q =>
104
                    APIClient.identity_providers.mappings.count(
105
                        providerId.value,
106
                        q
107
                    ),
108
            },
109
            i18n: {
110
                deleteConfirmationMessage: $__(
111
                    "Are you sure you want to remove this field mapping?"
112
                ),
113
                deleteSuccessMessage: $__("Mapping deleted"),
114
                displayName: $__("Field mapping"),
115
                editLabel: $__("Edit field mapping"),
116
                emptyListMessage: $__(
117
                    "There are no field mappings defined for this provider"
118
                ),
119
                newLabel: $__("New field mapping"),
120
            },
121
            table: {
122
                resourceTableUrl: `/api/v1/auth/identity_providers/${providerId.value}/mappings`,
123
                options: {},
124
            },
125
            stickyToolbar: ["Form"],
126
            embedded: props.embedded,
127
            formGroupsDisplayMode: "accordion",
128
            resourceAttrs,
129
            props,
130
            moduleStore: "IdentityProvidersStore",
131
        });
132
133
        const getResourceShowURL = id =>
134
            baseResource.router.resolve({
135
                name: "MappingShow",
136
                params: {
137
                    identity_provider_id: providerId.value,
138
                    identity_provider_mapping_id: id,
139
                },
140
            }).href;
141
142
        const goToResourceAdd = () =>
143
            baseResource.router.push({
144
                name: "MappingsFormAdd",
145
                params: { identity_provider_id: providerId.value },
146
            });
147
148
        const goToResourceEdit = resource =>
149
            baseResource.router.push({
150
                name: "MappingsFormEdit",
151
                params: {
152
                    identity_provider_id: providerId.value,
153
                    identity_provider_mapping_id: resource.mapping_id,
154
                },
155
            });
156
157
        const goToResourceList = () =>
158
            baseResource.router.push({
159
                name: "ProviderShow",
160
                params: { identity_provider_id: providerId.value },
161
            });
162
163
        const onFormSave = async (e, mappingToSave) => {
164
            e.preventDefault();
165
166
            const mapping = JSON.parse(JSON.stringify(mappingToSave));
167
            const mapping_id = mapping.mapping_id;
168
169
            delete mapping.mapping_id;
170
            delete mapping.identity_provider_id;
171
172
            try {
173
                if (mapping_id) {
174
                    await APIClient.identity_providers.mappings.update(
175
                        providerId.value,
176
                        mapping,
177
                        mapping_id
178
                    );
179
                    baseResource.setMessage(__("Mapping updated"));
180
                } else {
181
                    await APIClient.identity_providers.mappings.create(
182
                        providerId.value,
183
                        mapping
184
                    );
185
                    baseResource.setMessage(__("Mapping created"));
186
                }
187
188
                baseResource.router.push({
189
                    name: "ProviderShow",
190
                    params: { identity_provider_id: providerId.value },
191
                });
192
            } catch (error) {
193
                // Errors handled by base resource
194
            }
195
        };
196
197
        const tableOptions = {
198
            url: baseResource.getResourceTableUrl(),
199
            actions: {
200
                "-1": ["edit", "delete"],
201
            },
202
        };
203
204
        return {
205
            ...baseResource,
206
            getResourceShowURL,
207
            goToResourceAdd,
208
            goToResourceEdit,
209
            goToResourceList,
210
            tableOptions,
211
            onFormSave,
212
        };
213
    },
214
};
215
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/IdentityProviders/ProviderResource.vue (+856 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 { ref, onMounted, reactive, watch } from "vue";
8
import BaseResource from "./../BaseResource.vue";
9
import { useBaseResource } from "../../composables/base-resource.js";
10
import { APIClient } from "../../fetch/api-client.js";
11
import { $__ } from "@koha-vue/i18n";
12
13
const PROTOCOL_CONFIG_FIELDS = {
14
    OAuth: [
15
        {
16
            name: "key",
17
            label: __("Client ID"),
18
            required: true,
19
            type: "text",
20
            group: "OAuth settings",
21
        },
22
        {
23
            name: "secret",
24
            label: __("Client secret"),
25
            required: true,
26
            type: "text",
27
            group: "OAuth settings",
28
        },
29
        {
30
            name: "authorize_url",
31
            label: __("Authorization URL"),
32
            required: true,
33
            type: "text",
34
            group: "OAuth settings",
35
        },
36
        {
37
            name: "token_url",
38
            label: __("Token URL"),
39
            required: true,
40
            type: "text",
41
            group: "OAuth settings",
42
        },
43
        {
44
            name: "userinfo_url",
45
            label: __("User info URL"),
46
            required: false,
47
            type: "text",
48
            group: "OAuth settings",
49
        },
50
        {
51
            name: "scope",
52
            label: __("Scope"),
53
            required: false,
54
            type: "text",
55
            group: "OAuth settings",
56
            toolTip: __("Space-separated list of scopes, e.g. 'email profile'"),
57
        },
58
    ],
59
    OIDC: [
60
        {
61
            name: "key",
62
            label: __("Client ID"),
63
            required: true,
64
            type: "text",
65
            group: "OIDC settings",
66
        },
67
        {
68
            name: "secret",
69
            label: __("Client secret"),
70
            required: true,
71
            type: "text",
72
            group: "OIDC settings",
73
        },
74
        {
75
            name: "well_known_url",
76
            label: __("Well-known URL"),
77
            required: true,
78
            type: "text",
79
            group: "OIDC settings",
80
            toolTip: __(
81
                "OpenID Connect discovery endpoint, e.g. https://login.example.com/.well-known/openid-configuration"
82
            ),
83
        },
84
        {
85
            name: "scope",
86
            label: __("Scope"),
87
            required: false,
88
            type: "text",
89
            group: "OIDC settings",
90
            toolTip: __(
91
                "Space-separated list of scopes, e.g. 'openid email profile'"
92
            ),
93
        },
94
    ],
95
    SAML2: [
96
        {
97
            name: "saml2_sp_note",
98
            type: "group_placeholder",
99
            group: "SAML2 settings",
100
            description: __(
101
                "SAML2/Shibboleth connection is handled by the native service provider software (e.g. mod_shib) configured on this server. No Koha-side connection settings are required."
102
            ),
103
        },
104
    ],
105
};
106
107
export default {
108
    name: "ProviderResource",
109
    components: {
110
        BaseResource,
111
    },
112
    props: {
113
        routeAction: String,
114
    },
115
    emits: ["select-resource"],
116
    setup(props) {
117
        const initialized = ref(false);
118
119
        // Tracks the currently selected/loaded protocol so config field groups
120
        // can be shown or hidden reactively via hideIn closures.
121
        const selectedProtocol = ref(null);
122
123
        // Tracks the IDs of sub-resources that existed when the provider was
124
        // loaded, so we can delete any that the user removed during editing.
125
        const originalMappingIds = ref([]);
126
        const originalHostnameIds = ref([]);
127
        const originalDomainIds = ref([]);
128
129
        const staticResourceAttrs = [
130
            {
131
                name: "code",
132
                required: true,
133
                type: "text",
134
                label: __("Code"),
135
                group: "Basic configuration",
136
                toolTip: __(
137
                    "Unique identifier for this provider. Alphanumeric and underscore only."
138
                ),
139
            },
140
            {
141
                name: "description",
142
                required: true,
143
                type: "text",
144
                label: __("Description"),
145
                group: "Basic configuration",
146
                toolTip: __("User-friendly name displayed on login pages"),
147
            },
148
            {
149
                name: "protocol",
150
                required: true,
151
                type: "select",
152
                label: __("Protocol"),
153
                group: "Basic configuration",
154
                options: [
155
                    { value: "OAuth", label: "OAuth" },
156
                    { value: "OIDC", label: "OIDC" },
157
                    { value: "SAML2", label: "SAML2 / Shibboleth" },
158
                ],
159
                requiredKey: "value",
160
                selectLabel: "label",
161
            },
162
            {
163
                name: "icon_url",
164
                type: "text",
165
                label: __("Icon URL"),
166
                group: "Basic configuration",
167
                toolTip: __(
168
                    "URL to an icon image shown on the OPAC login button"
169
                ),
170
            },
171
            {
172
                name: "enabled",
173
                type: "boolean",
174
                label: __("Enabled"),
175
                group: "Basic configuration",
176
                toolTip: __(
177
                    "When disabled, this provider will not be shown on login pages"
178
                ),
179
            },
180
        ];
181
182
        const protocolPlaceholderAttr = {
183
            name: "_protocol_placeholder",
184
            type: "group_placeholder",
185
            group: "Protocol settings",
186
            description: __(
187
                "Select a protocol above to expose protocol-specific settings"
188
            ),
189
            hideIn: () =>
190
                selectedProtocol.value
191
                    ? ["Form", "Show", "List"]
192
                    : ["Show", "List"],
193
        };
194
195
        // Build config attrs for ALL protocols. Each gets a hideIn closure that
196
        // hides it when a different protocol is selected, or when no protocol
197
        // has been selected yet (add mode).
198
        const configResourceAttrs = Object.entries(
199
            PROTOCOL_CONFIG_FIELDS
200
        ).flatMap(([protocol, fields]) =>
201
            fields.map(f => ({
202
                ...f,
203
                name: `_config_${f.name}`,
204
                hideIn: () =>
205
                    !selectedProtocol.value ||
206
                    selectedProtocol.value !== protocol
207
                        ? ["Form", "Show", "List"]
208
                        : ["List"],
209
            }))
210
        );
211
212
        const borrowerColumnsArray = (window.borrower_columns || []).map(
213
            col => ({ value: col.value, label: col.label })
214
        );
215
216
        const matchpointOptions = [
217
            { value: "cardnumber", label: __("Card number") },
218
            { value: "userid", label: __("Username") },
219
            { value: "email", label: __("Email address") },
220
            ...(window.unique_patron_attributes || []),
221
        ];
222
        const librariesArray = (window.libraries_map || []).map(lib => ({
223
            value: lib.value,
224
            label: lib.label,
225
        }));
226
        const categoriesArray = (window.categories_map || []).map(cat => ({
227
            value: cat.value,
228
            label: cat.label,
229
        }));
230
231
        const hostnameAttr = {
232
            name: "hostnames",
233
            type: "relationshipWidget",
234
            group: "Network & Entry Settings",
235
            hideIn: ["List"],
236
            showElement: {
237
                type: "table",
238
                columnData: "hostnames",
239
                hidden: provider => !!provider.hostnames?.length,
240
                columns: [
241
                    { name: __("Hostname"), value: "hostname" },
242
                    { name: __("Force SSO"), value: "force_sso" },
243
                    { name: __("Matchpoint"), value: "matchpoint" },
244
                ],
245
            },
246
            componentProps: {
247
                resourceRelationships: { resourceProperty: "hostnames" },
248
                relationshipI18n: {
249
                    nameUpperCase: __("Hostname"),
250
                    removeThisMessage: __("Remove this hostname"),
251
                    addNewMessage: __("Add hostname"),
252
                    noneCreatedYetMessage: __(
253
                        "No hostnames configured. Add a hostname to surface this provider on its login page."
254
                    ),
255
                },
256
                newRelationshipDefaultAttrs: {
257
                    type: "object",
258
                    value: {
259
                        hostname: "",
260
                        force_sso: false,
261
                        matchpoint: null,
262
                    },
263
                },
264
            },
265
            relationshipFields: [
266
                {
267
                    name: "hostname",
268
                    required: true,
269
                    indexRequired: true,
270
                    type: "select",
271
                    label: __("Hostname"),
272
                    placeholder: __("Select or type to add a new hostname..."),
273
                    options: [],
274
                    selectLabel: "hostname",
275
                    requiredKey: "hostname",
276
                    taggable: true,
277
                    createOption: h => ({ hostname: h }),
278
                    toolTip: __(
279
                        "Base URL used to access this Koha inferface, protocol not required"
280
                    ),
281
                },
282
                {
283
                    name: "force_sso",
284
                    type: "boolean",
285
                    indexRequired: true,
286
                    label: __("Force SSO"),
287
                    toolTip: __(
288
                        "Automatically redirect users on this hostname to this provider"
289
                    ),
290
                    badgeTrueLabel: __("Force SSO"),
291
                    badgeTrueClass: "bg-primary",
292
                },
293
                {
294
                    name: "matchpoint",
295
                    required: true,
296
                    indexRequired: true,
297
                    type: "select",
298
                    label: __("Matchpoint"),
299
                    options: matchpointOptions,
300
                    requiredKey: "value",
301
                    selectLabel: "label",
302
                    toolTip: __(
303
                        "Koha field used to identify existing patrons logging in from this hostname"
304
                    ),
305
                },
306
            ],
307
        };
308
309
        const mappingsAttr = {
310
            name: "mappings",
311
            type: "relationshipWidget",
312
            label: __("Attribute Mappings"),
313
            group: "Attribute Mappings",
314
            hideIn: ["List"],
315
            showElement: {
316
                type: "table",
317
                columnData: "mappings",
318
                hidden: provider => !!provider.mappings?.length,
319
                columns: [
320
                    { name: __("IdP field"), value: "provider_field" },
321
                    { name: __("Koha field"), value: "koha_field" },
322
                    { name: __("Default value"), value: "default_content" },
323
                ],
324
            },
325
            componentProps: {
326
                resourceRelationships: { resourceProperty: "mappings" },
327
                relationshipI18n: {
328
                    nameUpperCase: __("Mapping"),
329
                    noneCreatedYetMessage: __("No attribute mappings defined."),
330
                    addNewMessage: __("Add mapping"),
331
                    removeThisMessage: __("Remove"),
332
                },
333
                newRelationshipDefaultAttrs: {
334
                    type: "object",
335
                    value: {
336
                        provider_field: "",
337
                        koha_field: borrowerColumnsArray[0]?.value || "",
338
                        default_content: "",
339
                    },
340
                },
341
            },
342
            relationshipFields: [
343
                {
344
                    name: "provider_field",
345
                    required: true,
346
                    indexRequired: true,
347
                    type: "text",
348
                    label: __("IdP field"),
349
                    placeholder: __("e.g. given_name"),
350
                },
351
                {
352
                    name: "koha_field",
353
                    required: true,
354
                    indexRequired: true,
355
                    type: "select",
356
                    label: __("Koha field"),
357
                    options: borrowerColumnsArray,
358
                    requiredKey: "value",
359
                    selectLabel: "label",
360
                },
361
                {
362
                    name: "default_content",
363
                    indexRequired: true,
364
                    type: "text",
365
                    label: __("Default value"),
366
                },
367
            ],
368
        };
369
370
        const domainsAttr = {
371
            name: "domains",
372
            type: "relationshipWidget",
373
            label: __("Email Domain Rules"),
374
            group: "Email Domain Rules",
375
            hideIn: ["List"],
376
            showElement: {
377
                type: "table",
378
                columnData: "domains",
379
                hidden: provider => !!provider.domains?.length,
380
                columns: [
381
                    { name: __("Domain"), value: "domain" },
382
                    { name: __("Allow OPAC"), value: "allow_opac" },
383
                    { name: __("Allow staff"), value: "allow_staff" },
384
                    {
385
                        name: __("Auto-register (OPAC)"),
386
                        value: "auto_register_opac",
387
                    },
388
                    {
389
                        name: __("Auto-register (staff)"),
390
                        value: "auto_register_staff",
391
                    },
392
                    { name: __("Update on auth"), value: "update_on_auth" },
393
                    {
394
                        name: __("Send welcome email"),
395
                        value: "send_welcome_email",
396
                    },
397
                    {
398
                        name: __("Default library"),
399
                        value: "default_library_id",
400
                    },
401
                    {
402
                        name: __("Default category"),
403
                        value: "default_category_id",
404
                    },
405
                ],
406
            },
407
            componentProps: {
408
                resourceRelationships: { resourceProperty: "domains" },
409
                relationshipI18n: {
410
                    nameUpperCase: __("Domain"),
411
                    removeThisMessage: __("Remove this domain"),
412
                    addNewMessage: __("Add domain rule"),
413
                    noneCreatedYetMessage: __("No domain rules defined."),
414
                },
415
                newRelationshipDefaultAttrs: {
416
                    type: "object",
417
                    value: {
418
                        domain: "",
419
                        allow_opac: false,
420
                        allow_staff: false,
421
                        auto_register_opac: false,
422
                        auto_register_staff: false,
423
                        update_on_auth: false,
424
                        send_welcome_email: false,
425
                        default_library_id: librariesArray[0]?.value || "",
426
                        default_category_id: categoriesArray[0]?.value || "",
427
                    },
428
                },
429
            },
430
            relationshipFields: [
431
                {
432
                    name: "domain",
433
                    type: "text",
434
                    indexRequired: true,
435
                    label: __("Domain"),
436
                    placeholder: __("e.g. library.org or *"),
437
                    toolTip: __(
438
                        "Email domain to match. Use '*' or leave empty for any domain."
439
                    ),
440
                },
441
                {
442
                    name: "allow_opac",
443
                    type: "boolean",
444
                    indexRequired: true,
445
                    label: __("Allow OPAC login"),
446
                    badgeTrueLabel: __("OPAC"),
447
                    badgeTrueClass: "bg-success",
448
                },
449
                {
450
                    name: "allow_staff",
451
                    type: "boolean",
452
                    indexRequired: true,
453
                    label: __("Allow staff login"),
454
                    badgeTrueLabel: __("Staff"),
455
                    badgeTrueClass: "bg-primary",
456
                },
457
                {
458
                    name: "auto_register_opac",
459
                    type: "boolean",
460
                    indexRequired: true,
461
                    label: __("Auto-register (OPAC)"),
462
                },
463
                {
464
                    name: "auto_register_staff",
465
                    type: "boolean",
466
                    indexRequired: true,
467
                    label: __("Auto-register (staff)"),
468
                },
469
                {
470
                    name: "update_on_auth",
471
                    type: "boolean",
472
                    indexRequired: true,
473
                    label: __("Update patron data on login"),
474
                },
475
                {
476
                    name: "send_welcome_email",
477
                    type: "boolean",
478
                    indexRequired: true,
479
                    label: __("Send welcome email"),
480
                    toolTip: __(
481
                        "Send a welcome email to the patron on their first login via this provider"
482
                    ),
483
                },
484
                {
485
                    name: "default_library_id",
486
                    type: "select",
487
                    indexRequired: true,
488
                    label: __("Default library"),
489
                    options: librariesArray,
490
                    requiredKey: "value",
491
                    selectLabel: "label",
492
                },
493
                {
494
                    name: "default_category_id",
495
                    type: "select",
496
                    indexRequired: true,
497
                    label: __("Default category"),
498
                    options: categoriesArray,
499
                    requiredKey: "value",
500
                    selectLabel: "label",
501
                },
502
            ],
503
        };
504
505
        const resourceAttrs = [
506
            ...staticResourceAttrs,
507
            protocolPlaceholderAttr,
508
            ...configResourceAttrs,
509
            hostnameAttr,
510
            mappingsAttr,
511
            domainsAttr,
512
        ];
513
514
        // Unpack the JSON config blob into flat _config_* fields so the form
515
        // and show views can bind to them individually.
516
        const afterResourceFetch = (componentData, resource) => {
517
            selectedProtocol.value = resource.protocol || null;
518
            const config = resource.config || {};
519
            const fields = PROTOCOL_CONFIG_FIELDS[resource.protocol] || [];
520
            fields.forEach(field => {
521
                if (field.type === "group_placeholder") return;
522
                resource[`_config_${field.name}`] =
523
                    field.type === "boolean"
524
                        ? (config[field.name] ?? false)
525
                        : (config[field.name] ?? "");
526
            });
527
            if (!resource.domains) resource.domains = [];
528
            if (!resource.mappings) resource.mappings = [];
529
            resource.hostnames = resource.hostnames || [];
530
531
            // Store the IDs of existing sub-resources so we can delete any
532
            // that the user removes during an edit.
533
            originalMappingIds.value = resource.mappings
534
                .map(m => m.mapping_id)
535
                .filter(Boolean);
536
            originalHostnameIds.value = resource.hostnames
537
                .map(h => h.identity_provider_hostname_id)
538
                .filter(Boolean);
539
            originalDomainIds.value = resource.domains
540
                .map(d => d.identity_provider_domain_id)
541
                .filter(Boolean);
542
        };
543
544
        const baseResource = useBaseResource({
545
            resourceName: "provider",
546
            nameAttr: "code",
547
            idAttr: "identity_provider_id",
548
            components: {
549
                show: "ProviderShow",
550
                list: "ProvidersList",
551
                add: "ProviderFormAdd",
552
                edit: "ProviderFormEdit",
553
            },
554
            apiClient: APIClient.identity_providers.providers,
555
            i18n: {
556
                deleteConfirmationMessage: $__(
557
                    "Are you sure you want to delete this identity provider? All related domain and mapping configuration will also be deleted."
558
                ),
559
                deleteSuccessMessage: $__("Identity provider deleted"),
560
                displayName: $__("Identity provider"),
561
                editLabel: $__("Edit identity provider"),
562
                emptyListMessage: $__(
563
                    "There are no identity providers configured"
564
                ),
565
                newLabel: $__("New identity provider"),
566
            },
567
            table: {
568
                resourceTableUrl: "/api/v1/auth/identity_providers",
569
                options: {},
570
            },
571
            stickyToolbar: ["Form"],
572
            embedded: props.embedded,
573
            formGroupsDisplayMode: "accordion",
574
            resourceAttrs,
575
            afterResourceFetch,
576
            props,
577
            moduleStore: "IdentityProvidersStore",
578
        });
579
580
        const tableOptions = {
581
            url: baseResource.getResourceTableUrl(),
582
            actions: {
583
                0: ["show"],
584
                "-1": ["edit", "delete"],
585
            },
586
        };
587
588
        // In add mode the form uses reactive(newResource) as its data object.
589
        // reactive() uses a WeakMap so calling it with the same plain object
590
        // returns the same proxy. Watching here therefore sees the same
591
        // mutations that ResourceFormSave makes when the user changes the
592
        // protocol dropdown.
593
        const formStateObj = reactive(baseResource.newResource.value);
594
        watch(
595
            () => formStateObj.protocol,
596
            newProtocol => {
597
                selectedProtocol.value = newProtocol || null;
598
            }
599
        );
600
601
        onMounted(async () => {
602
            try {
603
                const data =
604
                    await APIClient.identity_providers.allHostnames.getAll();
605
                hostnameAttr.relationshipFields[0].options = data || [];
606
            } catch (_) {
607
                // proceed with empty options; user can still type a hostname
608
            }
609
            initialized.value = true;
610
        });
611
612
        const onFormSave = async (e, providerToSave) => {
613
            e.preventDefault();
614
            const provider = JSON.parse(JSON.stringify(providerToSave));
615
            const providerId = provider.identity_provider_id;
616
            const protocol = provider.protocol;
617
618
            // Collect the _config_* fields for the selected protocol into a
619
            // config object, then strip all _config_* keys from the payload.
620
            const configFieldDefs = PROTOCOL_CONFIG_FIELDS[protocol] || [];
621
            const config = {};
622
            configFieldDefs.forEach(field => {
623
                if (field.type === "group_placeholder") return;
624
                const flatKey = `_config_${field.name}`;
625
                if (flatKey in provider) {
626
                    config[field.name] = provider[flatKey];
627
                }
628
            });
629
            // Strip all _-prefixed keys (UI-only fields: _config_*, _protocol_*, etc.)
630
            Object.keys(provider)
631
                .filter(k => k.startsWith("_"))
632
                .forEach(k => delete provider[k]);
633
            // matchpoint moved to hostname level; remove from provider payload
634
            delete provider.matchpoint;
635
            provider.config = config;
636
637
            // Hostnames are managed separately via the hostnames API.
638
            const hostnamesFromForm = (provider.hostnames || []).filter(
639
                h => h.hostname
640
            );
641
            delete provider.hostnames;
642
643
            // Mappings are managed separately via the mappings API.
644
            // Only items with a koha_field are saved.
645
            const mappingsFromForm = (provider.mappings || []).filter(
646
                m => m.koha_field
647
            );
648
            delete provider.mappings;
649
650
            // Validate that each hostname's matchpoint (if set) has a corresponding mapping.
651
            for (const h of hostnamesFromForm) {
652
                const matchpoint = h.matchpoint || null;
653
                if (matchpoint) {
654
                    const hasMapping = mappingsFromForm.some(
655
                        m => m.koha_field === matchpoint
656
                    );
657
                    if (!hasMapping) {
658
                        baseResource.setError(
659
                            __(
660
                                "The selected matchpoint must have a corresponding attribute mapping"
661
                            )
662
                        );
663
                        return;
664
                    }
665
                }
666
            }
667
668
            // Domains are managed separately via the domains API.
669
            const domainsFromForm = provider.domains || [];
670
            delete provider.domains;
671
672
            delete provider.identity_provider_id;
673
674
            try {
675
                if (providerId) {
676
                    const updatedProvider = await baseResource.apiClient.update(
677
                        provider,
678
                        providerId
679
                    );
680
681
                    // Sync hostnames: delete removed, update existing, create new.
682
                    const keptHostnameIds = hostnamesFromForm
683
                        .map(h => h.identity_provider_hostname_id)
684
                        .filter(Boolean);
685
                    for (const id of originalHostnameIds.value.filter(
686
                        id => !keptHostnameIds.includes(id)
687
                    )) {
688
                        await APIClient.identity_providers.hostnames.delete(
689
                            providerId,
690
                            id
691
                        );
692
                    }
693
                    for (const h of hostnamesFromForm) {
694
                        const body = {
695
                            hostname: h.hostname,
696
                            is_enabled: h.is_enabled ?? true,
697
                            force_sso: h.force_sso ?? false,
698
                            matchpoint: h.matchpoint || null,
699
                        };
700
                        if (h.identity_provider_hostname_id) {
701
                            await APIClient.identity_providers.hostnames.update(
702
                                providerId,
703
                                body,
704
                                h.identity_provider_hostname_id
705
                            );
706
                        } else {
707
                            await APIClient.identity_providers.hostnames.create(
708
                                providerId,
709
                                body
710
                            );
711
                        }
712
                    }
713
714
                    // Sync mappings: delete removed, update existing, create new.
715
                    const keptMappingIds = mappingsFromForm
716
                        .map(m => m.mapping_id)
717
                        .filter(Boolean);
718
                    for (const id of originalMappingIds.value.filter(
719
                        id => !keptMappingIds.includes(id)
720
                    )) {
721
                        await APIClient.identity_providers.mappings.delete(
722
                            providerId,
723
                            id
724
                        );
725
                    }
726
                    for (const m of mappingsFromForm) {
727
                        const body = {
728
                            provider_field: m.provider_field || null,
729
                            koha_field: m.koha_field,
730
                            default_content: m.default_content || null,
731
                        };
732
                        if (m.mapping_id) {
733
                            await APIClient.identity_providers.mappings.update(
734
                                providerId,
735
                                body,
736
                                m.mapping_id
737
                            );
738
                        } else {
739
                            await APIClient.identity_providers.mappings.create(
740
                                providerId,
741
                                body
742
                            );
743
                        }
744
                    }
745
746
                    // Sync domains: delete removed, update existing, create new.
747
                    const keptDomainIds = domainsFromForm
748
                        .map(d => d.identity_provider_domain_id)
749
                        .filter(Boolean);
750
                    for (const id of originalDomainIds.value.filter(
751
                        id => !keptDomainIds.includes(id)
752
                    )) {
753
                        await APIClient.identity_providers.domains.delete(
754
                            providerId,
755
                            id
756
                        );
757
                    }
758
                    for (const d of domainsFromForm) {
759
                        const body = {
760
                            domain: d.domain || null,
761
                            allow_opac: d.allow_opac || false,
762
                            allow_staff: d.allow_staff || false,
763
                            auto_register_opac: d.auto_register_opac || false,
764
                            auto_register_staff: d.auto_register_staff || false,
765
                            update_on_auth: d.update_on_auth || false,
766
                            send_welcome_email: d.send_welcome_email || false,
767
                            default_library_id: d.default_library_id || null,
768
                            default_category_id: d.default_category_id || null,
769
                        };
770
                        if (d.identity_provider_domain_id) {
771
                            await APIClient.identity_providers.domains.update(
772
                                providerId,
773
                                body,
774
                                d.identity_provider_domain_id
775
                            );
776
                        } else {
777
                            await APIClient.identity_providers.domains.create(
778
                                providerId,
779
                                body
780
                            );
781
                        }
782
                    }
783
784
                    baseResource.setMessage($__("Identity provider updated"));
785
                    return updatedProvider;
786
                } else {
787
                    const newProvider =
788
                        await baseResource.apiClient.create(provider);
789
                    const newId = newProvider.identity_provider_id;
790
791
                    // Create a bridge record for each linked hostname
792
                    for (const h of hostnamesFromForm) {
793
                        await APIClient.identity_providers.hostnames.create(
794
                            newId,
795
                            {
796
                                hostname: h.hostname,
797
                                is_enabled: true,
798
                                force_sso: h.force_sso ?? false,
799
                                matchpoint: h.matchpoint || null,
800
                            }
801
                        );
802
                    }
803
804
                    // Create each attribute mapping
805
                    for (const m of mappingsFromForm) {
806
                        await APIClient.identity_providers.mappings.create(
807
                            newId,
808
                            {
809
                                provider_field: m.provider_field || null,
810
                                koha_field: m.koha_field,
811
                                default_content: m.default_content || null,
812
                            }
813
                        );
814
                    }
815
816
                    // Create each domain rule
817
                    for (const d of domainsFromForm) {
818
                        await APIClient.identity_providers.domains.create(
819
                            newId,
820
                            {
821
                                domain: d.domain || null,
822
                                allow_opac: d.allow_opac || false,
823
                                allow_staff: d.allow_staff || false,
824
                                auto_register_opac:
825
                                    d.auto_register_opac || false,
826
                                auto_register_staff:
827
                                    d.auto_register_staff || false,
828
                                update_on_auth: d.update_on_auth || false,
829
                                send_welcome_email:
830
                                    d.send_welcome_email || false,
831
                                default_library_id:
832
                                    d.default_library_id || null,
833
                                default_category_id:
834
                                    d.default_category_id || null,
835
                            }
836
                        );
837
                    }
838
839
                    baseResource.setMessage($__("Identity provider created"));
840
                    return newProvider;
841
                }
842
            } catch (error) {
843
                // errors surfaced by the httpClient
844
            }
845
        };
846
847
        return {
848
            ...baseResource,
849
            initialized,
850
            PROTOCOL_CONFIG_FIELDS,
851
            tableOptions,
852
            onFormSave,
853
        };
854
    },
855
};
856
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Islands/AdminMenu.vue (-13 lines)
Lines 120-137 Link Here
120
                        >{{ $__("Self-service circulation (SIP2)") }}</a
120
                        >{{ $__("Self-service circulation (SIP2)") }}</a
121
                    >
121
                    >
122
                </li>
122
                </li>
123
                <li
124
                    v-if="
125
                        can_user_parameters_manage_identity_providers &&
126
                        shibbolethauthentication
127
                    "
128
                >
129
                    <a
130
                        :ref="el => templateRefs.push(el)"
131
                        href="/cgi-bin/koha/shibboleth/shibboleth.pl"
132
                        >{{ $__("Shibboleth configuration") }}</a
133
                    >
134
                </li>
135
                <li v-if="can_user_parameters_manage_item_circ_alerts">
123
                <li v-if="can_user_parameters_manage_item_circ_alerts">
136
                    <a
124
                    <a
137
                        :ref="el => templateRefs.push(el)"
125
                        :ref="el => templateRefs.push(el)"
Lines 601-607 export default { Link Here
601
        can_user_parameters_manage_additional_fields: Number,
589
        can_user_parameters_manage_additional_fields: Number,
602
        can_user_parameters_manage_keyboard_shortcuts: Number,
590
        can_user_parameters_manage_keyboard_shortcuts: Number,
603
        can_user_ill: Number,
591
        can_user_ill: Number,
604
        shibbolethauthentication: Number,
605
        usecirculationdesks: Number,
592
        usecirculationdesks: Number,
606
        usecashregisters: Number,
593
        usecashregisters: Number,
607
        savedsearchfilters: Number,
594
        savedsearchfilters: Number,
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js (+2 lines)
Lines 10-15 import ItemAPIClient from "@fetch/item-api-client"; Link Here
10
import RecordSourcesAPIClient from "@fetch/record-sources-api-client";
10
import RecordSourcesAPIClient from "@fetch/record-sources-api-client";
11
import SysprefAPIClient from "@fetch/system-preferences-api-client";
11
import SysprefAPIClient from "@fetch/system-preferences-api-client";
12
import SIP2APIClient from "@fetch/sip2-api-client";
12
import SIP2APIClient from "@fetch/sip2-api-client";
13
import IdentityProvidersAPIClient from "@fetch/identity-providers-api-client";
13
import PreservationAPIClient from "@fetch/preservation-api-client";
14
import PreservationAPIClient from "@fetch/preservation-api-client";
14
15
15
export const APIClient = {
16
export const APIClient = {
Lines 22-27 export const APIClient = { Link Here
22
    item: new ItemAPIClient(HttpClient),
23
    item: new ItemAPIClient(HttpClient),
23
    sysprefs: new SysprefAPIClient(HttpClient),
24
    sysprefs: new SysprefAPIClient(HttpClient),
24
    sip2: new SIP2APIClient(HttpClient),
25
    sip2: new SIP2APIClient(HttpClient),
26
    identity_providers: new IdentityProvidersAPIClient(HttpClient),
25
    preservation: new PreservationAPIClient(HttpClient),
27
    preservation: new PreservationAPIClient(HttpClient),
26
    record_sources: new RecordSourcesAPIClient(HttpClient),
28
    record_sources: new RecordSourcesAPIClient(HttpClient),
27
};
29
};
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/identity-providers.ts (+76 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
    faIdCard,
16
    faGlobe,
17
} from "@fortawesome/free-solid-svg-icons";
18
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
19
import vSelect from "vue-select";
20
21
library.add(
22
    faPlus,
23
    faMinus,
24
    faPencil,
25
    faTrash,
26
    faSpinner,
27
    faSave,
28
    faCog,
29
    faExchangeAlt,
30
    faIdCard,
31
    faGlobe
32
);
33
34
import App from "../components/IdentityProviders/Main.vue";
35
36
import { routes as routesDef } from "../routes/identity-providers";
37
38
import { useMainStore } from "../stores/main";
39
import { useIdentityProvidersStore } from "../stores/identity-providers";
40
import { useNavigationStore } from "../stores/navigation";
41
import i18n from "../i18n";
42
43
const pinia = createPinia();
44
45
const mainStore = useMainStore(pinia);
46
const navigationStore = useNavigationStore(pinia);
47
const routes = navigationStore.setRoutes(routesDef);
48
49
const router = createRouter({
50
    history: createWebHistory(),
51
    linkActiveClass: "current",
52
    routes,
53
});
54
55
const app = createApp(App);
56
57
const rootComponent = app
58
    .use(i18n)
59
    .use(pinia)
60
    .use(router)
61
    .component("font-awesome-icon", FontAwesomeIcon)
62
    .component("v-select", vSelect);
63
64
app.config.unwrapInjectedRef = true;
65
app.provide("mainStore", mainStore);
66
app.provide("navigationStore", navigationStore);
67
const IdentityProvidersStore = useIdentityProvidersStore(pinia);
68
app.provide("IdentityProvidersStore", IdentityProvidersStore);
69
70
app.mount("#identity-providers");
71
72
const { removeMessages } = mainStore;
73
router.beforeEach((to, from) => {
74
    navigationStore.$patch({ current: to.matched, params: to.params || {} });
75
    removeMessages();
76
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/identity-providers.js (+125 lines)
Line 0 Link Here
1
import { markRaw } from "vue";
2
3
import ResourceWrapper from "../components/ResourceWrapper.vue";
4
import { $__ } from "../i18n";
5
6
export const routes = [
7
    {
8
        path: "/cgi-bin/koha/admin/admin-home.pl",
9
        title: $__("Administration"),
10
        children: [
11
            {
12
                path: "/cgi-bin/koha/admin/identity_providers.pl",
13
                is_default: true,
14
                is_base: true,
15
                title: $__("Identity providers"),
16
                children: [
17
                    {
18
                        path: "",
19
                        redirect: { name: "ProvidersList" },
20
                    },
21
                    {
22
                        path: "/cgi-bin/koha/admin/identity_providers",
23
                        is_empty: true,
24
                        is_end_node: true,
25
                        resource: "IdentityProviders/ProviderResource.vue",
26
                        children: [
27
                            {
28
                                path: "",
29
                                name: "ProvidersList",
30
                                is_empty: true,
31
                                component: markRaw(ResourceWrapper),
32
                            },
33
                            {
34
                                path: ":identity_provider_id",
35
                                name: "ProviderShow",
36
                                component: markRaw(ResourceWrapper),
37
                                title: $__("Show provider"),
38
                            },
39
                            {
40
                                path: "add",
41
                                name: "ProviderFormAdd",
42
                                component: markRaw(ResourceWrapper),
43
                                title: $__("New identity provider"),
44
                            },
45
                            {
46
                                path: "edit/:identity_provider_id",
47
                                name: "ProviderFormEdit",
48
                                component: markRaw(ResourceWrapper),
49
                                title: $__("Edit identity provider"),
50
                            },
51
                            {
52
                                path: ":identity_provider_id/mappings",
53
                                title: $__("Field mappings"),
54
                                icon: "fa fa-exchange-alt",
55
                                is_end_node: true,
56
                                resource:
57
                                    "IdentityProviders/MappingResource.vue",
58
                                children: [
59
                                    {
60
                                        path: "",
61
                                        name: "MappingsList",
62
                                        is_empty: true,
63
                                        component: markRaw(ResourceWrapper),
64
                                    },
65
                                    {
66
                                        path: ":identity_provider_mapping_id",
67
                                        name: "MappingShow",
68
                                        component: markRaw(ResourceWrapper),
69
                                        title: $__("Show mapping"),
70
                                    },
71
                                    {
72
                                        path: "add",
73
                                        name: "MappingsFormAdd",
74
                                        component: markRaw(ResourceWrapper),
75
                                        title: $__("New field mapping"),
76
                                    },
77
                                    {
78
                                        path: "edit/:identity_provider_mapping_id",
79
                                        name: "MappingsFormEdit",
80
                                        component: markRaw(ResourceWrapper),
81
                                        title: $__("Edit field mapping"),
82
                                    },
83
                                ],
84
                            },
85
                            {
86
                                path: ":identity_provider_id/domains",
87
                                title: $__("Domains"),
88
                                icon: "fa fa-globe",
89
                                is_end_node: true,
90
                                resource:
91
                                    "IdentityProviders/DomainResource.vue",
92
                                children: [
93
                                    {
94
                                        path: "",
95
                                        name: "DomainsList",
96
                                        is_empty: true,
97
                                        component: markRaw(ResourceWrapper),
98
                                    },
99
                                    {
100
                                        path: ":identity_provider_domain_id",
101
                                        name: "DomainShow",
102
                                        component: markRaw(ResourceWrapper),
103
                                        title: $__("Show domain"),
104
                                    },
105
                                    {
106
                                        path: "add",
107
                                        name: "DomainsFormAdd",
108
                                        component: markRaw(ResourceWrapper),
109
                                        title: $__("New domain"),
110
                                    },
111
                                    {
112
                                        path: "edit/:identity_provider_domain_id",
113
                                        name: "DomainsFormEdit",
114
                                        component: markRaw(ResourceWrapper),
115
                                        title: $__("Edit domain"),
116
                                    },
117
                                ],
118
                            },
119
                        ],
120
                    },
121
                ],
122
            },
123
        ],
124
    },
125
];
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/identity-providers.js (+15 lines)
Line 0 Link Here
1
import { defineStore } from "pinia";
2
3
export const useIdentityProvidersStore = defineStore("identity_providers", {
4
    state: () => ({
5
        current_provider: null,
6
    }),
7
    actions: {
8
        setCurrentProvider(provider) {
9
            this.current_provider = provider;
10
        },
11
        clearCurrentProvider() {
12
            this.current_provider = null;
13
        },
14
    },
15
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/navigation.js (+3 lines)
Lines 190-195 export const useNavigationStore = defineStore("navigation", () => { Link Here
190
            }
190
            }
191
        }),
191
        }),
192
        leftNavigation: computed(() => {
192
        leftNavigation: computed(() => {
193
            if (!store.current || store.current.length === 0) {
194
                return _getNavigationElements(store.routeState);
195
            }
193
            const currentRoute = store.current[store.current.length - 1];
196
            const currentRoute = store.current[store.current.length - 1];
194
            if (currentRoute) {
197
            if (currentRoute) {
195
                const alternateMenuRequired =
198
                const alternateMenuRequired =
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (-1 / +1 lines)
Lines 136-142 Link Here
136
                            <a tabindex="0" role="menuitem" class="logout js-hide" href="/cgi-bin/koha/opac-main.pl?logout.x=1"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Log out</a>
136
                            <a tabindex="0" role="menuitem" class="logout js-hide" href="/cgi-bin/koha/opac-main.pl?logout.x=1"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Log out</a>
137
                        [% ELSE %]
137
                        [% ELSE %]
138
                            [% IF Koha.Preference('casAuthentication') %]
138
                            [% IF Koha.Preference('casAuthentication') %]
139
                                [%# CAS authentication is too complicated for modal window %]
139
                                [%# CAS authentication and Shibboleth force SSO are too complicated for modal window %]
140
                                <a class="nav-link login-link" href="/cgi-bin/koha/opac-user.pl" aria-label="Log in to your account"
140
                                <a class="nav-link login-link" href="/cgi-bin/koha/opac-user.pl" aria-label="Log in to your account"
141
                                    ><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a
141
                                    ><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a
142
                                >
142
                                >
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt (-87 / +81 lines)
Lines 97-206 Link Here
97
                                    <!-- This is what is displayed if shibboleth login has failed to match a koha user -->
97
                                    <!-- This is what is displayed if shibboleth login has failed to match a koha user -->
98
                                    <div class="alert alert-info">
98
                                    <div class="alert alert-info">
99
                                        <p aria-live="assertive" role="alert" class="shib_invalid">Sorry, your Shibboleth identity does not match a valid library identity.</p>
99
                                        <p aria-live="assertive" role="alert" class="shib_invalid">Sorry, your Shibboleth identity does not match a valid library identity.</p>
100
                                        [% UNLESS ( Koha.Preference('OPACShibOnly') ) %]
100
                                        [% IF ( casAuthentication ) %]
101
                                            [% IF ( casAuthentication ) %]
101
                                            [% IF ( invalidCasLogin ) %]
102
                                                [% IF ( invalidCasLogin ) %]
102
                                                <!-- This is what is displayed if cas login has failed -->
103
                                                    <!-- This is what is displayed if cas login has failed -->
103
                                                <p class="cas_invalid">Sorry, the CAS login also failed. If you have a local login you may use that below.</p>
104
                                                    <p class="cas_invalid">Sorry, the CAS login also failed. If you have a local login you may use that below.</p>
105
                                                [% ELSE %]
106
                                                    <p>If you have a CAS account, you may use that below.</p>
107
                                                [% END %]
108
                                            [% ELSE %]
104
                                            [% ELSE %]
109
                                                <p>If you have a local account, you may use that below.</p>
105
                                                <p>If you have a CAS account, you may use that below.</p>
110
                                            [% END %]
106
                                            [% END %]
107
                                        [% ELSE %]
108
                                            <p>If you have a local account, you may use that below.</p>
111
                                        [% END %]
109
                                        [% END %]
112
                                    </div>
110
                                    </div>
113
                                [% ELSE %]
111
                                [% ELSE %]
114
                                    <h2 class="shib_title">Shibboleth login</h2>
112
                                    <h2 class="shib_title">Shibboleth login</h2>
115
                                    <p><a class="shib_url" href="[% shibbolethLoginUrl | $raw %]">Log in using a Shibboleth account.</a></p>
113
                                    <p><a class="shib_url" href="[% shibbolethLoginUrl | $raw %]">Log in using a Shibboleth account.</a></p>
116
                                [% END # /IF invalidShibLogin %]
114
                                [% END # /IF invalidShibLogin %]
117
                                [% UNLESS ( Koha.Preference('OPACShibOnly') ) %]
115
                                [% IF ( casAuthentication ) %]
118
                                    [% IF ( casAuthentication ) %]
116
                                    <h2 class="cas_title">CAS login</h2>
119
                                        <h2 class="cas_title">CAS login</h2>
117
                                    <p>If you do not have a Shibboleth account, but you do have a CAS account, you can use CAS.</p>
120
                                        <p>If you do not have a Shibboleth account, but you do have a CAS account, you can use CAS.</p>
118
                                [% ELSE %]
121
                                    [% ELSE %]
119
                                    <h2 class="shib_local_title">Local login</h2>
122
                                        <h2 class="shib_local_title">Local login</h2>
120
                                    <p class="shib_local_text">If you do not have a Shibboleth account, but you do have a local login, then you may login below.</p>
123
                                        <p class="shib_local_text">If you do not have a Shibboleth account, but you do have a local login, then you may login below.</p>
124
                                    [% END %]
125
                                [% END %]
121
                                [% END %]
126
                            [% END # /IF shibbolethAuthentication %]
122
                            [% END # /IF shibbolethAuthentication %]
127
123
128
                            [% UNLESS ( Koha.Preference('OPACShibOnly') ) %]
124
                            [% IF ( casAuthentication ) %]
129
                                [% IF ( casAuthentication ) %]
125
                                [% IF ( shibbolethAuthentication ) %]
130
                                    [% IF ( shibbolethAuthentication ) %]
126
                                    [% IF ( casServerUrl ) %]
131
                                        [% IF ( casServerUrl ) %]
127
                                        <p><a class="cas_url" href="[% casServerUrl | $raw %]">Log in.</a></p>
132
                                            <p><a class="cas_url" href="[% casServerUrl | $raw %]">Log in.</a></p>
128
                                    [% END %]
133
                                        [% END %]
134
135
                                        [% IF ( casServersLoop ) %]
136
                                            <p>Please choose against which one you would like to authenticate: </p>
137
                                            <ul>
138
                                                [% FOREACH casServer IN casServersLoop %]
139
                                                    <li><a class="cas_url" href="[% casServer.value | $raw %]">[% casServer.name | html %]</a></li>
140
                                                [% END %]
141
                                            </ul>
142
                                        [% END %]
143
                                    [% ELSE %]
144
                                        [% IF ( invalidCasLogin ) %]
145
                                            <div class="alert alert-info">
146
                                                <!-- This is what is displayed if cas login has failed -->
147
                                                <p class="cas_invalid" role="alert" aria-live="assertive">Sorry, the CAS login failed.</p>
148
                                            </div>
149
                                        [% END %]
150
151
                                        <h2 class="cas_title">CAS login</h2>
152
129
153
                                        [% IF ( casServerUrl ) %]
130
                                    [% IF ( casServersLoop ) %]
154
                                            <p><a class="cas_url" href="[% casServerUrl | $raw %]">Log in using a CAS account.</a></p>
131
                                        <p>Please choose against which one you would like to authenticate: </p>
155
                                        [% END %]
132
                                        <ul>
133
                                            [% FOREACH casServer IN casServersLoop %]
134
                                                <li><a class="cas_url" href="[% casServer.value | $raw %]">[% casServer.name | html %]</a></li>
135
                                            [% END %]
136
                                        </ul>
137
                                    [% END %]
138
                                [% ELSE %]
139
                                    [% IF ( invalidCasLogin ) %]
140
                                        <div class="alert alert-info">
141
                                            <!-- This is what is displayed if cas login has failed -->
142
                                            <p class="cas_invalid" role="alert" aria-live="assertive">Sorry, the CAS login failed.</p>
143
                                        </div>
144
                                    [% END %]
156
145
157
                                        [% IF ( casServersLoop ) %]
146
                                    <h2 class="cas_title">CAS login</h2>
158
                                            <p>If you have a CAS account, please choose against which one you would like to authenticate:</p>
159
                                            <ul>
160
                                                [% FOREACH casServer IN casServersLoop %]
161
                                                    <li><a class="cas_url" href="[% casServer.value | $raw %]">[% casServer.name | html %]</a></li>
162
                                                [% END %]
163
                                            </ul>
164
                                        [% END %]
165
                                    [% END # /IF shibbolethAuthentication %]
166
147
167
                                    [% IF ( shibbolethAuthentication ) %]
148
                                    [% IF ( casServerUrl ) %]
168
                                        <p>Nothing</p>
149
                                        <p><a class="cas_url" href="[% casServerUrl | $raw %]">Log in using a CAS account.</a></p>
169
                                    [% ELSE %]
170
                                        <h2>Local login</h2>
171
                                        <p>If you do not have a CAS account, but do have a local account, you can still log in: </p>
172
                                    [% END %]
150
                                    [% END %]
173
                                [% END # / IF casAuthentication %]
174
151
175
                                [% SET identity_providers = AuthClient.get_providers('opac') %]
152
                                    [% IF ( casServersLoop ) %]
176
                                [% IF ( ! identity_providers.empty ) %]
153
                                        <p>If you have a CAS account, please choose against which one you would like to authenticate:</p>
177
                                    [% FOREACH provider IN identity_providers %]
154
                                        <ul>
178
                                        <p class="clearfix">
155
                                            [% FOREACH casServer IN casServersLoop %]
179
                                            <a href="[% provider.url | url %]" class="btn btn-light col-md-12" id="provider_[% provider.code | html %]">
156
                                                <li><a class="cas_url" href="[% casServer.value | $raw %]">[% casServer.name | html %]</a></li>
180
                                                [% IF provider.icon_url %]
157
                                            [% END %]
181
                                                    <img src="[% provider.icon_url | url %]" style="max-height: 20px; max-width: 20px;" />
158
                                        </ul>
182
                                                [% ELSE %]
183
                                                    <i class="fa fa-user" aria-hidden="true"></i>
184
                                                [% END %]
185
                                                Log in with [% provider.description | html %]
186
                                            </a>
187
                                        </p>
188
                                    [% END %]
159
                                    [% END %]
189
                                    <hr />
160
                                [% END # /IF shibbolethAuthentication %]
190
                                    <p>If you do not have an external account, but do have a local account, you can still log in: </p>
191
                                [% END # /IF  identity_providers %]
192
161
193
                                [% IF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %]
162
                                [% IF ( shibbolethAuthentication ) %]
194
                                    [% IF ( invalidGoogleOpenIDConnectLogin ) %]
163
                                    <p>Nothing</p>
195
                                        <h2>Google login</h2>
164
                                [% ELSE %]
196
                                        <p>Sorry, your Google login failed. <span class="error">[% invalidGoogleOpenIDConnectLogin | html %]</span></p>
165
                                    <h2>Local login</h2>
197
                                        <p>Please note that the Google login will only work if you are using the e-mail address registered with this library.</p>
166
                                    <p>If you do not have a CAS account, but do have a local account, you can still log in: </p>
198
                                        <p>If you want to, you can try to <a href="/cgi-bin/koha/svc/auth/googleopenidconnect?reauthenticate=select_account">log in using a different account</a> </p>
167
                                [% END %]
199
                                    [% END %]
168
                            [% END # / IF casAuthentication %]
200
                                    <a href="/cgi-bin/koha/svc/auth/googleopenidconnect" class="btn btn-light" id="openid_connect"><i class="fa-brands fa-google" aria-hidden="true"></i> Log in with Google</a>
169
201
                                    <p>If you do not have a Google account, but do have a local account, you can still log in: </p>
170
                            [% SET identity_providers = AuthClient.get_providers('opac') %]
202
                                [% END # /IF GoogleOpenIDConnect %]
171
                            [% IF ( ! identity_providers.empty ) %]
203
                            [% END # /UNLESS OPACShibOnly %]
172
                                [% FOREACH provider IN identity_providers %]
173
                                    <p class="clearfix">
174
                                        <a href="[% provider.url | url %]" class="btn btn-light col-md-12" id="provider_[% provider.code | html %]">
175
                                            [% IF provider.icon_url %]
176
                                                <img src="[% provider.icon_url | url %]" style="max-height: 20px; max-width: 20px;" />
177
                                            [% ELSE %]
178
                                                <i class="fa fa-user" aria-hidden="true"></i>
179
                                            [% END %]
180
                                            Log in with [% provider.description | html %]
181
                                        </a>
182
                                    </p>
183
                                [% END %]
184
                                <hr />
185
                                <p>If you do not have an external account, but do have a local account, you can still log in: </p>
186
                            [% END # /IF  identity_providers %]
187
188
                            [% IF ( Koha.Preference('GoogleOpenIDConnect') == 1 ) %]
189
                                [% IF ( invalidGoogleOpenIDConnectLogin ) %]
190
                                    <h2>Google login</h2>
191
                                    <p>Sorry, your Google login failed. <span class="error">[% invalidGoogleOpenIDConnectLogin | html %]</span></p>
192
                                    <p>Please note that the Google login will only work if you are using the e-mail address registered with this library.</p>
193
                                    <p>If you want to, you can try to <a href="/cgi-bin/koha/svc/auth/googleopenidconnect?reauthenticate=select_account">log in using a different account</a> </p>
194
                                [% END %]
195
                                <a href="/cgi-bin/koha/svc/auth/googleopenidconnect" class="btn btn-light" id="openid_connect"><i class="fa-brands fa-google" aria-hidden="true"></i> Log in with Google</a>
196
                                <p>If you do not have a Google account, but do have a local account, you can still log in: </p>
197
                            [% END # /IF GoogleOpenIDConnect %]
204
198
205
                            [% IF !(invalid_username_or_password || too_many_login_attempts) and is_anonymous_patron %]
199
                            [% IF !(invalid_username_or_password || too_many_login_attempts) and is_anonymous_patron %]
206
                                <div class="alert alert-info">
200
                                <div class="alert alert-info">
Lines 227-233 Link Here
227
                                [% ELSE %]
221
                                [% ELSE %]
228
                                    <p>You must contact the library to reset your password</p>
222
                                    <p>You must contact the library to reset your password</p>
229
                                [% END %]
223
                                [% END %]
230
                            [% ELSIF !Koha.Preference('OPACShibOnly') or SCO_login or SCI_login %]
224
                            [% ELSIF SCO_login or SCI_login %]
231
                                [% SET form_action = script_name %]
225
                                [% SET form_action = script_name %]
232
                                [% IF SCO_login %]
226
                                [% IF SCO_login %]
233
                                    [% form_action = "/cgi-bin/koha/sco/sco-main.pl" %]
227
                                    [% form_action = "/cgi-bin/koha/sco/sco-main.pl" %]
(-)a/opac/opac-user.pl (-3 / +4 lines)
Lines 21-28 use Modern::Perl; Link Here
21
use CGI qw ( -utf8 );
21
use CGI qw ( -utf8 );
22
use URI;
22
use URI;
23
23
24
use C4::Auth qw( get_template_and_user );
24
use C4::Auth                 qw( get_template_and_user );
25
use C4::Koha qw(
25
use C4::Auth_with_shibboleth qw( shib_ok );
26
use C4::Koha                 qw(
26
    getitemtypeimagelocation
27
    getitemtypeimagelocation
27
    GetNormalizedISBN
28
    GetNormalizedISBN
28
    GetNormalizedUPC
29
    GetNormalizedUPC
Lines 82-88 for ( C4::Context->preference("OPACShowHoldQueueDetails") ) { Link Here
82
my $patronupdate = $query->param('patronupdate');
83
my $patronupdate = $query->param('patronupdate');
83
my $canrenew     = 1;
84
my $canrenew     = 1;
84
85
85
$template->param( shibbolethAuthentication => C4::Context->config('useshibboleth') );
86
$template->param( shibbolethAuthentication => shib_ok() );
86
87
87
# get borrower information ....
88
# get borrower information ....
88
my $patron = Koha::Patrons->find($borrowernumber);
89
my $patron = Koha::Patrons->find($borrowernumber);
(-)a/rspack.config.js (-1 / +2 lines)
Lines 28-33 module.exports = [ Link Here
28
                "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/acquisitions.ts",
28
                "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/acquisitions.ts",
29
            islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts",
29
            islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts",
30
            sip2: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/sip2.ts",
30
            sip2: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/sip2.ts",
31
            "admin/identity_providers":
32
                "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/identity-providers.ts",
31
        },
33
        },
32
        output: {
34
        output: {
33
            filename: "[name].js",
35
            filename: "[name].js",
34
- 

Return to bug 39224