From 4b421f7c5d3f2b324b07a90b49afe78ce614926a Mon Sep 17 00:00:00 2001 From: dbs Date: Mon, 19 Apr 2010 12:47:12 +0000 Subject: [PATCH] Forward-port self-serve password reset implementation from rel_1_6 TODO: Add support for craftsman TODO: Add barcode + email input option TODO: Document how it works git-svn-id: svn://svn.open-ils.org/ILS/trunk@16269 dcc99617-32d9-48b4-a31d-7c20da2025e4 --- Open-ILS/examples/apache/eg.conf | 2 +- Open-ILS/examples/apache/eg_vhost.conf | 15 + Open-ILS/examples/apache/startup.pl | 1 + Open-ILS/examples/fm_IDL.xml | 12 + Open-ILS/examples/opensrf.xml.example | 1 + Open-ILS/src/Makefile.am | 1 + Open-ILS/src/extras/ils_events.xml | 10 + Open-ILS/src/perlmods/OpenILS/Application/Actor.pm | 241 +++++++++++- Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm | 418 +++++++++++++++++++++ Open-ILS/src/sql/Pg/002.schema.config.sql | 2 +- Open-ILS/src/sql/Pg/005.schema.actors.sql | 31 ++ Open-ILS/src/sql/Pg/950.data.seed-values.sql | 49 ++- .../sql/Pg/upgrade/0237.data.password-reset.sql | 55 +++ .../src/templates/password-reset/request-form.tt2 | 49 +++ .../src/templates/password-reset/reset-form.tt2 | 30 ++ .../src/templates/password-reset/strings.en-US | 28 ++ Open-ILS/web/js/dojo/openils/opac/nls/opac.js | 12 + .../web/opac/skin/default/js/password_reset.js | 107 ++++++ .../web/opac/skin/default/xml/common/login.xml | 3 +- 19 files changed, 1062 insertions(+), 5 deletions(-) create mode 100644 Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm create mode 100644 Open-ILS/src/sql/Pg/upgrade/0237.data.password-reset.sql create mode 100644 Open-ILS/src/templates/password-reset/request-form.tt2 create mode 100644 Open-ILS/src/templates/password-reset/reset-form.tt2 create mode 100644 Open-ILS/src/templates/password-reset/strings.en-US create mode 100644 Open-ILS/web/opac/skin/default/js/password_reset.js diff --git a/Open-ILS/examples/apache/eg.conf b/Open-ILS/examples/apache/eg.conf index a3890606ab..17eb787bc7 100644 --- a/Open-ILS/examples/apache/eg.conf +++ b/Open-ILS/examples/apache/eg.conf @@ -19,7 +19,7 @@ PerlRequire /etc/apache2/startup.pl PerlChildInitHandler OpenILS::WWW::Reporter::child_init PerlChildInitHandler OpenILS::WWW::SuperCat::child_init PerlChildInitHandler OpenILS::WWW::AddedContent::child_init - +PerlChildInitHandler OpenILS::WWW::PasswordReset::child_init # ---------------------------------------------------------------------------------- # Set some defaults for our working directories diff --git a/Open-ILS/examples/apache/eg_vhost.conf b/Open-ILS/examples/apache/eg_vhost.conf index f2ea1cf8d3..11879601f1 100644 --- a/Open-ILS/examples/apache/eg_vhost.conf +++ b/Open-ILS/examples/apache/eg_vhost.conf @@ -163,6 +163,21 @@ RewriteRule - - [E=locale:en-US] [L] allow from all +# ---------------------------------------------------------------------------------- +# Self-serve password interface +# ---------------------------------------------------------------------------------- + + SetHandler perl-script + PerlHandler OpenILS::WWW::PasswordReset::password_reset + Options +ExecCGI + PerlSendHeader On + allow from all + + # Force clients to use HTTPS + RewriteEngine On + RewriteCond %{HTTPS} !=on [NC] + RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [R,L] + # ---------------------------------------------------------------------------------- # Supercat feeds diff --git a/Open-ILS/examples/apache/startup.pl b/Open-ILS/examples/apache/startup.pl index 189f36b4ff..378aaa3174 100644 --- a/Open-ILS/examples/apache/startup.pl +++ b/Open-ILS/examples/apache/startup.pl @@ -6,6 +6,7 @@ use OpenILS::WWW::AddedContent qw( /openils/conf/opensrf_core.xml ); use OpenILS::WWW::Proxy ('/openils/conf/opensrf_core.xml'); use OpenILS::WWW::Vandelay qw( /openils/conf/opensrf_core.xml ); use OpenILS::WWW::EGWeb ('/openils/conf/oils_web.xml'); +use OpenILS::WWW::PasswordReset ('/openils/conf/opensrf_core.xml'); # - Uncoment the following 2 lines to make use of the IP redirection code # - The IP file should to contain a map with the following format: diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index dc39053bc9..3b758f5595 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -1222,6 +1222,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + + + + + + + diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index 20fbdc71a0..192e94fb8f 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -19,6 +19,7 @@ vim:et:ts=4:sw=4: LOCALSTATEDIR/xsl LOCALSTATEDIR + LOCALSTATEDIR/templates diff --git a/Open-ILS/src/Makefile.am b/Open-ILS/src/Makefile.am index 0daeebfcb9..690bd4f3e5 100644 --- a/Open-ILS/src/Makefile.am +++ b/Open-ILS/src/Makefile.am @@ -170,6 +170,7 @@ ilscore-install: $(MKDIR_P) $(TEMPLATEDIR) cp -r @srcdir@/perlmods/* $(perldir) cp -r @srcdir@/templates/marc $(TEMPLATEDIR) + cp -r @srcdir@/templates/password-reset $(TEMPLATEDIR) sed -i 's|SYSCONFDIR|@sysconfdir@|g' '$(DESTDIR)@libdir@/perl5/OpenILS/WWW/Web.pm' sed -i 's|SYSCONFDIR|@sysconfdir@|g' '$(DESTDIR)@libdir@/perl5/OpenILS/WWW/Method.pm' @echo "Installing string templates to $(TEMPLATEDIR)" diff --git a/Open-ILS/src/extras/ils_events.xml b/Open-ILS/src/extras/ils_events.xml index 0a74a95b1b..b4cb739d10 100644 --- a/Open-ILS/src/extras/ils_events.xml +++ b/Open-ILS/src/extras/ils_events.xml @@ -779,6 +779,16 @@ Permission Denied + + There are too many active password reset request sessions for this patron. + + + The user attempted to update their password using a stale or inactive password reset request session. + + + The user attempted to set their password to a weak value. + + diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/OpenILS/Application/Actor.pm index dca0ca9b66..ead067e122 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Actor.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Actor.pm @@ -36,6 +36,8 @@ use OpenILS::Utils::CStoreEditor qw/:funcs/; use OpenILS::Utils::Penalty; use List::Util qw/max/; +use UUID::Tiny qw/:std/; + sub initialize { OpenILS::Application::Actor::Container->initialize(); OpenILS::Application::Actor::UserGroups->initialize(); @@ -3693,6 +3695,243 @@ sub negative_balance_users { return undef; } +__PACKAGE__->register_method( + method => "request_password_reset", + api_name => "open-ils.actor.patron.password_reset.request", + signature => { + params => [ + { desc => 'user_id_type', type => 'string' }, + { desc => 'user_id', type => 'string' }, + ] + }, +); +sub request_password_reset { + my($self, $conn, $user_id_type, $user_id) = @_; -1; + # Check to see if password reset requests are already being throttled: + # 0. Check cache to see if we're in throttle mode (avoid hitting database) + + my $e = new_editor(xact => 1); + my $user; + + # Get the user, if any, depending on the input value + if ($user_id_type eq 'username') { + $user = $e->search_actor_user({usrname => $user_id})->[0]; + if (!$user) { + $e->die_event; + return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ); + } + } elsif ($user_id_type eq 'barcode') { + my $card = $e->search_actor_card([ + {barcode => $user_id}, + {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0]; + if (!$card) { + $e->die_event; + return OpenILS::Event->new('ACTOR_USER_NOT_FOUND'); + } + $user = $card->usr; + } + + # If the user doesn't have an email address, we can't help them + if (!$user->email) { + $e->die_event; + return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS'); + } + _reset_password_request($conn, $e, $user); +} + +# Once we have the user, we can issue the password reset request +# XXX Add a wrapper method that accepts barcode + email input +sub _reset_password_request { + my ($conn, $e, $user) = @_; + + # 1. Get throttle threshold and time-to-live from OU_settings + my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000; + my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60; + + my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601(); + + # 2. Get time of last request and number of active requests (num_active) + my $active_requests = $e->json_query({ + from => 'aupr', + select => { + aupr => [ + { + column => 'uuid', + transform => 'COUNT' + }, + { + column => 'request_time', + transform => 'MAX' + } + ] + }, + where => { + has_been_reset => { '=' => 'f' }, + request_time => { '>' => $threshold_time } + } + }); + + # Guard against no active requests + if ($active_requests->[0]->{'request_time'}) { + my $last_request = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($active_requests->[0]->{'request_time'})); + my $now = DateTime::Format::ISO8601->new(); + + # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute) + if (($active_requests->[0]->{'usr'} > $aupr_throttle) && + ($last_request->add_duration('1 minute') > $now)) { + $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60); + $e->die_event; + return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS'); + } + } + + # TODO Check to see if the user is in a password-reset-restricted group + + # Otherwise, go ahead and try to get the user. + + # Check the number of active requests for this user + $active_requests = $e->json_query({ + from => 'aupr', + select => { + aupr => [ + { + column => 'usr', + transform => 'COUNT' + } + ] + }, + where => { + usr => { '=' => $user->id }, + has_been_reset => { '=' => 'f' }, + request_time => { '>' => $threshold_time } + } + }); + + $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests."); + + # if less than or equal to per-user threshold, proceed; otherwise, return event + my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3; + if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) { + $e->die_event; + return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS'); + } + + # Create the aupr object and insert into the database + my $reset_request = Fieldmapper::actor::usr_password_reset->new; + my $uuid = create_uuid_as_string(UUID_V4); + $reset_request->uuid($uuid); + $reset_request->usr($user->id); + + my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event; + $e->commit; + + # Create an event to notify user of the URL to reset their password + # Can we stuff this in the user_data param for trigger autocreate? + my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost'; + + my $ses = OpenSRF::AppSession->create('open-ils.trigger'); + $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou); + + # Trunk only + # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou); + + return 1; +} + +__PACKAGE__->register_method( + method => "commit_password_reset", + api_name => "open-ils.actor.patron.password_reset.commit", + signature => { + params => [ + { desc => 'uuid', type => 'string' }, + { desc => 'password', type => 'string' }, + ] + }, +); +sub commit_password_reset { + my($self, $conn, $uuid, $password) = @_; + + # Check to see if password reset requests are already being throttled: + # 0. Check cache to see if we're in throttle mode (avoid hitting database) + $cache ||= OpenSRF::Utils::Cache->new("global", 0); + my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef; + if ($throttle) { + return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST'); + } + + my $e = new_editor(xact => 1); + + my $aupr = $e->search_actor_usr_password_reset({ + uuid => $uuid, + has_been_reset => 0 + }); + + if (!$aupr->[0]) { + $e->die_event; + return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST'); + } + my $user_id = $aupr->[0]->usr; + my $user = $e->retrieve_actor_user($user_id); + + # Ensure we're still within the TTL for the request + my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60; + my $threshold = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl); + if ($threshold > DateTime->now(time_zone => 'local')) { + $e->die_event; + return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST'); + } + + # Check complexity of password against OU-defined regex + my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex'); + + my $is_strong = 0; + if ($pw_regex) { + # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex + # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs; + $is_strong = check_password_strength_custom($password, $pw_regex); + } else { + $is_strong = check_password_strength_default($password); + } + + if (!$is_strong) { + $e->die_event; + return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG'); + } + + # All is well; update the password + $user->passwd($password); + $e->update_actor_user($user); + + # And flag that this password reset request has been honoured + $aupr->[0]->has_been_reset('t'); + $e->update_actor_usr_password_reset($aupr->[0]); + $e->commit; + + return 1; +} + +sub check_password_strength_default { + my $password = shift; + # Use the default set of checks + if ( (length($password) < 7) or + ($password !~ m/.*\d+.*/) or + ($password !~ m/.*[A-Za-z]+.*/) + ) { + return 0; + } + return 1; +} + +sub check_password_strength_custom { + my ($password, $pw_regex) = @_; + + $pw_regex = qr/$pw_regex/; + if ($password !~ /$pw_regex/) { + return 0; + } + return 1; +} + +1; diff --git a/Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm b/Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm new file mode 100644 index 0000000000..6b76ecd24d --- /dev/null +++ b/Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm @@ -0,0 +1,418 @@ +package OpenILS::WWW::PasswordReset; + +# Copyright (C) 2010 Laurentian University +# Dan Scott +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; use warnings; + +use Apache2::Log; +use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log); +use APR::Const -compile => qw(:error SUCCESS); +use Apache2::RequestRec (); +use Apache2::RequestIO (); +use Apache2::RequestUtil; +use CGI; +use Template; + +use OpenSRF::EX qw(:try); +use OpenSRF::Utils qw/:datetime/; +use OpenSRF::Utils::Cache; +use OpenSRF::System; +use OpenSRF::AppSession; + +use OpenILS::Utils::Fieldmapper; +use OpenSRF::Utils::Logger qw/$logger/; +use OpenILS::Application::AppUtils; +use OpenILS::Utils::CStoreEditor qw/:funcs/; + +my $log = 'OpenSRF::Utils::Logger'; +my $U = 'OpenILS::Application::AppUtils'; + +my ($bootstrap, $actor, $templates); +my $i18n = {}; + +sub child_init { + OpenSRF::System->bootstrap_client( config_file => $bootstrap ); + + my $conf = OpenSRF::Utils::SettingsClient->new(); + my $idl = $conf->config_value("IDL"); + Fieldmapper->import(IDL => $idl); + $templates = $conf->config_value("dirs", "templates"); + $actor = OpenSRF::AppSession->create('open-ils.actor'); + load_i18n(); +} + +sub password_reset { + my $apache = shift; + return Apache2::Const::DECLINED if (-e $apache->filename); + + $apache->content_type('text/html'); + + my $cgi = new CGI; + my $ctx = {}; + + $ctx->{'uri'} = $apache->uri; + + # Get our locale from the URL + (my $locale = $apache->path_info) =~ s{^.*?/([a-z]{2}-[A-Z]{2})/.*?$}{$1}; + if (!$locale) { + $locale = 'en-US'; + } + + # If locale exists, use it; otherwise fall back to en-US + if (exists $i18n->{$locale}) { + $ctx->{'i18n'} = $i18n->{$locale}; + } else { + $ctx->{'i18n'} = $i18n->{'en-US'}; + } + + my $tt = Template->new({ + INCLUDE_PATH => $templates + }) || die "$Template::ERROR\n"; + + # Get our UUID: if no UUID, then display barcode / username / email prompt + (my $uuid = $apache->path_info) =~ s{^/$locale/([^/]*?)$}{$1}; + $logger->info("Password reset: UUID = $uuid"); + + if (!$uuid) { + request_password_reset($apache, $cgi, $tt, $ctx); + } else { + reset_password($apache, $cgi, $tt, $ctx, $uuid); + } +} + +sub reset_password { + my ($apache, $cgi, $tt, $ctx, $uuid) = @_; + + my $password_1 = $cgi->param('pwd1'); + my $password_2 = $cgi->param('pwd2'); + + $ctx->{'title'} = $ctx->{'i18n'}{'TITLE'}; + $ctx->{'password_prompt'} = $ctx->{'i18n'}{'PASSWORD_PROMPT'}; + $ctx->{'password_prompt2'} = $ctx->{'i18n'}{'PASSWORD_PROMPT2'}; + + # In case non-matching passwords slip through our funky Web interface + if ($password_1 and $password_2 and ($password_1 ne $password_2)) { + $apache->status(Apache2::Const::DECLINED); + $ctx->{'status'} = { + style => 'error', + msg => $ctx->{'i18n'}{'NO_MATCH'} + }; + $tt->process('password-reset/reset-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } + + if ($password_1 and $password_2 and ($password_1 eq $password_2)) { + my $response = $actor->request('open-ils.actor.patron.password_reset.commit', $uuid, $password_1)->gather(); + if (ref($response) && $response->{'textcode'}) { + $apache->status(Apache2::Const::DECLINED); + + if ($response->{'textcode'} eq 'PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST') { + $ctx->{'status'} = { + style => 'error', + msg => $ctx->{'i18n'}{'NOT_ACTIVE'} + + }; + } + if ($response->{'textcode'} eq 'PATRON_PASSWORD_WAS_NOT_STRONG') { + $ctx->{'status'} = { + style => 'error', + msg => $ctx->{'i18n'}{'NOT_STRONG'} + + }; + } + $tt->process('password-reset/reset-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } + $ctx->{'status'} = { + style => 'success', + msg => $ctx->{'i18n'}{'SUCCESS'} + }; + } + + # Either the password change was successful, or this is their first time through + $tt->process('password-reset/reset-form.tt2', $ctx) + || die $tt->error(); + + return Apache2::Const::OK; +} + +# Load our localized strings - lame, need to convert to Locale::Maketext +sub load_i18n { + foreach my $string_bundle (glob("$templates/password-reset/strings.*")) { + open(I18NFH, '<', $string_bundle); + (my $locale = $string_bundle) =~ s/^.*\.([a-z]{2}-[A-Z]{2})$/$1/; + $logger->debug("Loaded locale [$locale] from file: [$string_bundle]"); + while() { + my ($string_id, $string) = ($_ =~ m/^(.+?)=(.*?)$/); + $i18n->{$locale}{$string_id} = $string; + } + close(I18NFH); + } +} + +sub request_password_reset { + my ($apache, $cgi, $tt, $ctx) = @_; + + my $barcode = $cgi->param('barcode'); + my $username = $cgi->param('username'); + my $email = $cgi->param('email'); + + if (!($barcode or $username or $email)) { + $apache->status(Apache2::Const::OK); + $ctx->{'status'} = { + style => 'plain', + msg => $ctx->{'i18n'}{'IDENTIFY_YOURSELF'} + }; + $tt->process('password-reset/request-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } elsif ($barcode) { + my $response = $actor->request('open-ils.actor.patron.password_reset.request', 'barcode', $barcode)->gather(); + $apache->status(Apache2::Const::OK); + $ctx->{'status'} = { + style => 'plain', + msg => $ctx->{'i18n'}{'REQUEST_SUCCESS'} + }; + # Hide form + $tt->process('password-reset/request-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } elsif ($username) { + my $response = $actor->request('open-ils.actor.patron.password_reset.request', 'username', $username)->gather(); + $apache->status(Apache2::Const::OK); + $ctx->{'status'} = { + style => 'plain', + msg => $ctx->{'i18n'}{'REQUEST_SUCCESS'} + }; + # Hide form + $tt->process('password-reset/request-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } +} + +1; + +# vim: et:ts=4:sw=4 +package OpenILS::WWW::PasswordReset; + +# Copyright (C) 2010 Laurentian University +# Dan Scott +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; use warnings; + +use Apache2::Log; +use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log); +use APR::Const -compile => qw(:error SUCCESS); +use Apache2::RequestRec (); +use Apache2::RequestIO (); +use Apache2::RequestUtil; +use CGI; +use Template; + +use OpenSRF::EX qw(:try); +use OpenSRF::Utils qw/:datetime/; +use OpenSRF::Utils::Cache; +use OpenSRF::System; +use OpenSRF::AppSession; + +use OpenILS::Utils::Fieldmapper; +use OpenSRF::Utils::Logger qw/$logger/; +use OpenILS::Application::AppUtils; +use OpenILS::Utils::CStoreEditor qw/:funcs/; + +my $log = 'OpenSRF::Utils::Logger'; +my $U = 'OpenILS::Application::AppUtils'; + +my ($bootstrap, $actor, $templates); +my $i18n = {}; + +sub child_init { + OpenSRF::System->bootstrap_client( config_file => $bootstrap ); + + my $conf = OpenSRF::Utils::SettingsClient->new(); + my $idl = $conf->config_value("IDL"); + Fieldmapper->import(IDL => $idl); + $templates = $conf->config_value("dirs", "templates"); + $actor = OpenSRF::AppSession->create('open-ils.actor'); + load_i18n(); +} + +sub password_reset { + my $apache = shift; + return Apache2::Const::DECLINED if (-e $apache->filename); + + $apache->content_type('text/html'); + + my $cgi = new CGI; + my $ctx = {}; + + $ctx->{'uri'} = $apache->uri; + + # Get our locale from the URL + (my $locale = $apache->path_info) =~ s{^.*?/([a-z]{2}-[A-Z]{2})/.*?$}{$1}; + if (!$locale) { + $locale = 'en-US'; + } + + # If locale exists, use it; otherwise fall back to en-US + if (exists $i18n->{$locale}) { + $ctx->{'i18n'} = $i18n->{$locale}; + } else { + $ctx->{'i18n'} = $i18n->{'en-US'}; + } + + my $tt = Template->new({ + INCLUDE_PATH => $templates + }) || die "$Template::ERROR\n"; + + # Get our UUID: if no UUID, then display barcode / username / email prompt + (my $uuid = $apache->path_info) =~ s{^/$locale/([^/]*?)$}{$1}; + $logger->info("Password reset: UUID = $uuid"); + + if (!$uuid) { + request_password_reset($apache, $cgi, $tt, $ctx); + } else { + reset_password($apache, $cgi, $tt, $ctx, $uuid); + } +} + +sub reset_password { + my ($apache, $cgi, $tt, $ctx, $uuid) = @_; + + my $password_1 = $cgi->param('pwd1'); + my $password_2 = $cgi->param('pwd2'); + + $ctx->{'title'} = $ctx->{'i18n'}{'TITLE'}; + $ctx->{'password_prompt'} = $ctx->{'i18n'}{'PASSWORD_PROMPT'}; + $ctx->{'password_prompt2'} = $ctx->{'i18n'}{'PASSWORD_PROMPT2'}; + + # In case non-matching passwords slip through our funky Web interface + if ($password_1 and $password_2 and ($password_1 ne $password_2)) { + $apache->status(Apache2::Const::DECLINED); + $ctx->{'status'} = { + style => 'error', + msg => $ctx->{'i18n'}{'NO_MATCH'} + }; + $tt->process('password-reset/reset-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } + + if ($password_1 and $password_2 and ($password_1 eq $password_2)) { + my $response = $actor->request('open-ils.actor.patron.password_reset.commit', $uuid, $password_1)->gather(); + if (ref($response) && + $response->{'textcode'} && + $response->{'textcode'} eq 'PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST') { + $apache->status(Apache2::Const::DECLINED); + $ctx->{'status'} = { + style => 'error', + msg => $ctx->{'i18n'}{'NOT_ACTIVE'} + + }; + $tt->process('password-reset/reset-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } + $ctx->{'status'} = { + style => 'success', + msg => $ctx->{'i18n'}{'SUCCESS'} + }; + } + + # Either the password change was successful, or this is their first time through + $tt->process('password-reset/reset-form.tt2', $ctx) + || die $tt->error(); + + return Apache2::Const::OK; +} + +# Load our localized strings - lame, need to convert to Locale::Maketext +sub load_i18n { + foreach my $string_bundle (glob("$templates/password-reset/strings.*")) { + open(I18NFH, '<', $string_bundle); + (my $locale = $string_bundle) =~ s/^.*\.([a-z]{2}-[A-Z]{2})$/$1/; + $logger->debug("Loaded locale [$locale] from file: [$string_bundle]"); + while() { + my ($string_id, $string) = ($_ =~ m/^(.+?)=(.*?)$/); + $i18n->{$locale}{$string_id} = $string; + } + close(I18NFH); + } +} + +sub request_password_reset { + my ($apache, $cgi, $tt, $ctx) = @_; + + my $barcode = $cgi->param('barcode'); + my $username = $cgi->param('username'); + my $email = $cgi->param('email'); + + if (!($barcode or $username or $email)) { + $apache->status(Apache2::Const::OK); + $ctx->{'status'} = { + style => 'plain', + msg => $ctx->{'i18n'}{'IDENTIFY_YOURSELF'} + }; + $tt->process('password-reset/request-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } elsif ($barcode) { + my $response = $actor->request('open-ils.actor.patron.password_reset.request', 'barcode', $barcode)->gather(); + $apache->status(Apache2::Const::OK); + $ctx->{'status'} = { + style => 'plain', + msg => $ctx->{'i18n'}{'REQUEST_SUCCESS'} + }; + # Hide form + $tt->process('password-reset/request-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } elsif ($username) { + my $response = $actor->request('open-ils.actor.patron.password_reset.request', 'username', $username)->gather(); + $apache->status(Apache2::Const::OK); + $ctx->{'status'} = { + style => 'plain', + msg => $ctx->{'i18n'}{'REQUEST_SUCCESS'} + }; + # Hide form + $tt->process('password-reset/request-form.tt2', $ctx) + || die $tt->error(); + return Apache2::Const::OK; + } +} + +1; + +# vim: et:ts=4:sw=4 diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql index 16b0bb97e1..e6f4e1373f 100644 --- a/Open-ILS/src/sql/Pg/002.schema.config.sql +++ b/Open-ILS/src/sql/Pg/002.schema.config.sql @@ -60,7 +60,7 @@ CREATE TABLE config.upgrade_log ( install_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); -INSERT INTO config.upgrade_log (version) VALUES ('0236'); -- berick +INSERT INTO config.upgrade_log (version) VALUES ('0237'); -- dbs CREATE TABLE config.bib_source ( id SERIAL PRIMARY KEY, diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql index d22b90d0d3..e7b171fe1d 100644 --- a/Open-ILS/src/sql/Pg/005.schema.actors.sql +++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql @@ -520,6 +520,37 @@ CREATE INDEX actor_usr_addr_city_idx ON actor.usr_address (lower(city)); CREATE INDEX actor_usr_addr_state_idx ON actor.usr_address (lower(state)); CREATE INDEX actor_usr_addr_post_code_idx ON actor.usr_address (lower(post_code)); +CREATE TABLE actor.usr_password_reset ( + id SERIAL PRIMARY KEY, + uuid TEXT NOT NULL, + usr BIGINT NOT NULL REFERENCES actor.usr(id) DEFERRABLE INITIALLY DEFERRED, + request_time TIMESTAMP NOT NULL DEFAULT NOW(), + has_been_reset BOOL NOT NULL DEFAULT false +); +COMMENT ON TABLE actor.usr_password_reset IS $$ +/* + * Copyright (C) 2010 Laurentian University + * Dan Scott + * + * Self-serve password reset requests + * + * **** + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +$$; +CREATE UNIQUE INDEX actor_usr_password_reset_uuid_idx ON actor.usr_password_reset (uuid); +CREATE INDEX actor_usr_password_reset_usr_idx ON actor.usr_password_reset (usr); +CREATE INDEX actor_usr_password_reset_request_time_idx ON actor.usr_password_reset (request_time); +CREATE INDEX actor_usr_password_reset_has_been_reset_idx ON actor.usr_password_reset (has_been_reset); CREATE TABLE actor.org_address ( id SERIAL PRIMARY KEY, diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index efd1618919..ae9189a1d1 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -1938,7 +1938,22 @@ INSERT into config.org_unit_setting_type ( 'circ.block_renews_for_holds', oils_i18n_gettext('circ.block_renews_for_holds', 'Holds: Block Renewal of Items Needed for Holds', 'coust', 'label'), oils_i18n_gettext('circ.block_renews_for_holds', 'When an item could fulfill a hold, do not allow the current patron to renew', 'coust', 'description'), - 'bool' ) + 'bool' ), + +( 'circ.password_reset_request_per_user_limit', + oils_i18n_gettext('circ.password_reset_request_per_user_limit', 'Circulation: Maximum concurrently active self-serve password reset requests per user', 'coust', 'label'), + oils_i18n_gettext('circ.password_reset_request_per_user_limit', 'When a user has more than this number of concurrently active self-serve password reset requests for their account, prevent the user from creating any new self-serve password reset requests until the number of active requests for the user drops back below this number.', 'coust', 'description'), + 'string'), + +( 'circ.password_reset_request_time_to_live', + oils_i18n_gettext('circ.password_reset_request_time_to_live', 'Circulation: Self-serve password reset request time-to-live', 'coust', 'label'), + oils_i18n_gettext('circ.password_reset_request_time_to_live', 'Length of time (in seconds) a self-serve password reset request should remain active.', 'coust', 'description'), + 'string'), + +( 'circ.password_reset_request_throttle', + oils_i18n_gettext('circ.password_reset_request_throttle', 'Circulation: Maximum concurrently active self-serve password reset requests', 'coust', 'label'), + oils_i18n_gettext('circ.password_reset_request_throttle', 'Prevent the creation of new self-serve password reset requests until the number of active requests drops back below this number.', 'coust', 'description'), + 'string') ; -- 0234.data.org-setting-ui.circ.suppress_checkin_popups.sql @@ -3630,6 +3645,38 @@ INSERT INTO action_trigger.environment ( ( 19, 'cancel_reason' ) ; +INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('password.reset_request','aupr','Patron has requested a self-serve password reset'); +INSERT INTO action_trigger.event_definition (id, active, owner, name, hook, validator, reactor, delay, template) + VALUES (20, 'f', 1, 'Password reset request notification', 'password.reset_request', 'NOOP_True', 'SendEmail', '00:00:01', +$$ +[%- USE date -%] +[%- user = target.usr -%] +To: [%- params.recipient_email || user.email %] +From: [%- params.sender_email || user.home_ou.email || default_sender %] +Subject: [% user.home_ou.name %]: library account password reset request + +You have received this message because you, or somebody else, requested a reset +of your library system password. If you did not request a reset of your library +system password, just ignore this message and your current password will +continue to work. + +If you did request a reset of your library system password, please perform +the following steps to continue the process of resetting your password: + +1. Open the following link in a web browser: https://[% params.hostname %]/opac/password/[% params.locale || 'en-US' %]/[% target.uuid %] +The browser displays a password reset form. + +2. Enter your new password in the password reset form in the browser. You must +enter the password twice to ensure that you do not make a mistake. If the +passwords match, you will then be able to log in to your library system account +with the new password. + +$$); +INSERT INTO action_trigger.environment ( event_def, path) VALUES + ( 20, 'usr' ); +INSERT INTO action_trigger.environment ( event_def, path) VALUES + ( 20, 'usr.home_ou' ); + SELECT SETVAL('action_trigger.event_definition_id_seq'::TEXT, 100); -- Org Unit Settings for configuring org unit weights and org unit max-loops for hold targeting diff --git a/Open-ILS/src/sql/Pg/upgrade/0237.data.password-reset.sql b/Open-ILS/src/sql/Pg/upgrade/0237.data.password-reset.sql new file mode 100644 index 0000000000..1b67110488 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0237.data.password-reset.sql @@ -0,0 +1,55 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0237'); + +INSERT into config.org_unit_setting_type +( name, label, description, datatype ) VALUES +( 'circ.password_reset_request_per_user_limit', + oils_i18n_gettext('circ.password_reset_request_per_user_limit', 'Circulation: Maximum concurrently active self-serve password reset requests per user', 'coust', 'label'), + oils_i18n_gettext('circ.password_reset_request_per_user_limit', 'When a user has more than this number of concurrently active self-serve password reset requests for their account, prevent the user from creating any new self-serve password reset requests until the number of active requests for the user drops back below this number.', 'coust', 'description'), + 'string'), + +( 'circ.password_reset_request_time_to_live', + oils_i18n_gettext('circ.password_reset_request_time_to_live', 'Circulation: Self-serve password reset request time-to-live', 'coust', 'label'), + oils_i18n_gettext('circ.password_reset_request_time_to_live', 'Length of time (in seconds) a self-serve password reset request should remain active.', 'coust', 'description'), + 'string'), + +( 'circ.password_reset_request_throttle', + oils_i18n_gettext('circ.password_reset_request_throttle', 'Circulation: Maximum concurrently active self-serve password reset requests', 'coust', 'label'), + oils_i18n_gettext('circ.password_reset_request_throttle', 'Prevent the creation of new self-serve password reset requests until the number of active requests drops back below this number.', 'coust', 'description'), + 'string') +; + +INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('password.reset_request','aupr','Patron has requested a self-serve password reset'); +INSERT INTO action_trigger.event_definition (id, active, owner, name, hook, validator, reactor, delay, template) + VALUES (20, 'f', 1, 'Password reset request notification', 'password.reset_request', 'NOOP_True', 'SendEmail', '00:00:01', +$$ +[%- USE date -%] +[%- user = target.usr -%] +To: [%- params.recipient_email || user.email %] +From: [%- params.sender_email || user.home_ou.email || default_sender %] +Subject: [% user.home_ou.name %]: library account password reset request + +You have received this message because you, or somebody else, requested a reset +of your library system password. If you did not request a reset of your library +system password, just ignore this message and your current password will +continue to work. + +If you did request a reset of your library system password, please perform +the following steps to continue the process of resetting your password: + +1. Open the following link in a web browser: https://[% params.hostname %]/opac/password/[% params.locale || 'en-US' %]/[% target.uuid %] +The browser displays a password reset form. + +2. Enter your new password in the password reset form in the browser. You must +enter the password twice to ensure that you do not make a mistake. If the +passwords match, you will then be able to log in to your library system account +with the new password. + +$$); +INSERT INTO action_trigger.environment ( event_def, path) VALUES + ( 20, 'usr' ); +INSERT INTO action_trigger.environment ( event_def, path) VALUES + ( 20, 'usr.home_ou' ); + +COMMIT; diff --git a/Open-ILS/src/templates/password-reset/request-form.tt2 b/Open-ILS/src/templates/password-reset/request-form.tt2 new file mode 100644 index 0000000000..4618775a3d --- /dev/null +++ b/Open-ILS/src/templates/password-reset/request-form.tt2 @@ -0,0 +1,49 @@ + + + [% i18n.REQUEST_TITLE %] + + + + +

[% i18n.REQUEST_TITLE %]

+

[% status.msg %]

+
+ + + + + + + + + +
+ + +
+ + + + + + + [% i18n.REQUEST_TITLE %] + + +

[% i18n.REQUEST_TITLE %]

+

[% status.msg %]

+
+
+
+
+ + +
+
+ + diff --git a/Open-ILS/src/templates/password-reset/reset-form.tt2 b/Open-ILS/src/templates/password-reset/reset-form.tt2 new file mode 100644 index 0000000000..c76d5d7052 --- /dev/null +++ b/Open-ILS/src/templates/password-reset/reset-form.tt2 @@ -0,0 +1,30 @@ + + + [% title %] + + +

[% title %]

+

[% status.msg %]

+
+
+
+
+
+
+ + + + + [% title %] + + +

[% title %]

+

[% status.msg %]

+
+
+
+
+
+
+ + diff --git a/Open-ILS/src/templates/password-reset/strings.en-US b/Open-ILS/src/templates/password-reset/strings.en-US new file mode 100644 index 0000000000..f19bc429be --- /dev/null +++ b/Open-ILS/src/templates/password-reset/strings.en-US @@ -0,0 +1,28 @@ +BUTTON_SUBMIT=Submit +REQUEST_TITLE=Library system password reset request form +IDENTIFY_YOURSELF=Please enter your user name or barcode to identify your library account and request a password reset. +REQUEST_SUCCESS=Your user name or barcode has been submitted for a password reset. If a matching account with an email address is found, you will soon receive an email at that address with further instructions for resetting your password. +BARCODE_PROMPT=Barcode: +USERNAME_PROMPT=User name: +EMAIL_PROMPT=Email address associated with the account: +NO_SESSION=Could not find the requested password reset session. +NO_MATCH=Passwords did not match. Please try again +NOT_ACTIVE=This was not an active password reset request. Your password has not been reset. +NOT_STRONG=The password you chose was not considered complex enough to protect your account. Your password has not been reset. +SUCCESS=Password has been reset. +TITLE=Library system password reset +PASSWORD_PROMPT=New password: +PASSWORD_PROMPT2=Re-enter new password: +REQUEST_TITLE=Library system password reset request form +IDENTIFY_YOURSELF=Please enter your user name or barcode to identify your library account and request a password reset. +REQUEST_SUCCESS=Your user name or barcode has been submitted for a password reset. If a matching account with an email address is found, you will soon receive an email at that address with further instructions for resetting your password. +BARCODE_PROMPT=Barcode: +USERNAME_PROMPT=User name: +EMAIL_PROMPT=Email address associated with the account: +NO_SESSION=Could not find the requested password reset session. +NO_MATCH=Passwords did not match. Please try again +NOT_ACTIVE=This was not an active password reset request. Your password has not been reset. +SUCCESS=Password has been reset. +TITLE=Library system password reset +PASSWORD_PROMPT=New password: +PASSWORD_PROMPT2=Re-enter new password: diff --git a/Open-ILS/web/js/dojo/openils/opac/nls/opac.js b/Open-ILS/web/js/dojo/openils/opac/nls/opac.js index c1978605e0..000d879192 100644 --- a/Open-ILS/web/js/dojo/openils/opac/nls/opac.js +++ b/Open-ILS/web/js/dojo/openils/opac/nls/opac.js @@ -1,4 +1,16 @@ { + "BARCODE_PROMPT": "Barcode: ", + "USERNAME_PROMPT": "User name: ", + "CANCEL_BUTTON_LABEL": "Cancel", + "SUBMIT_BUTTON_LABEL": "Submit", + "OK": "OK", + "PWD_RESET_RESPONSE_TITLE": "Password reset response", + "PWD_RESET_SUBMIT_SUCCESS": "Your request to begin the password reset process has been processed. If your account has a valid email address, you should soon receive an email containing further instructions for resetting your password.", + "PWD_RESET_SUBMIT_ERROR": "The system could not process your request for a password reset. Please try again, or contact circulation staff for assistance.", + "PWD_RESET_SUBMIT_STATUS": "Sending request...", + "PWD_RESET_FORGOT_PROMPT": "Forgot your password?", + "PWD_RESET_FORM_TITLE": "Request password reset", + "PWD_RESET_SUBMIT_PROMPT": "To begin the password reset process, enter either your barcode or user name in the form below and click 'Submit'", "CREATE_MFHD": "Add MFHD Record", "CREATED_MFHD_RECORD": "Created MFHD record for ${0}", "DELETE_MFHD": "Delete Record", diff --git a/Open-ILS/web/opac/skin/default/js/password_reset.js b/Open-ILS/web/opac/skin/default/js/password_reset.js new file mode 100644 index 0000000000..ba54469944 --- /dev/null +++ b/Open-ILS/web/opac/skin/default/js/password_reset.js @@ -0,0 +1,107 @@ +dojo.require('dojo.parser'); +dojo.require('dijit.Dialog'); +dojo.require('dijit.form.Button'); +dojo.require('dijit.form.TextBox'); + +dojo.requireLocalization("openils.opac", "opac"); +opac_strings = dojo.i18n.getLocalization("openils.opac", "opac"); + +dojo.addOnLoad(function() { + + // Create the password reset dialog + var pwResetFormDlg = createResetDialog(); + dojo.parser.parse(); + + // Connect the buttons to submit / cancel events that override + // the default actions associated with the buttons to do + // pleasing Ajax things + dojo.connect(dijit.byId("cancelButton"), "onClick", function(event) { + event.preventDefault(); + event.stopPropagation(); + pwResetFormDlg.hide(); + }); + dojo.connect(dijit.byId("submitButton"), "onClick", function(event) { + event.preventDefault(); + event.stopPropagation(); + var xhrArgs = { + form: dojo.byId("requestReset"), + handleAs: "text", + load: function(data) { + pwResetFormDlg.hide(); + passwordSubmission(opac_strings.PWD_RESET_SUBMIT_SUCCESS); + }, + error: function(error) { + pwResetFormDlg.hide(); + passwordSubmission(opac_strings.PWD_RESET_SUBMIT_ERROR); + } + } + var deferred = dojo.xhrPost(xhrArgs); + }); + dojo.place("", "login_tbody"); + dojo.query("#pwResetLink").attr("innerHTML", opac_strings.PWD_RESET_FORGOT_PROMPT); + +}); + +function passwordSubmission( msg ) { + var responseDialog = new dijit.Dialog({ + title: opac_strings.PWD_RESET_RESPONSE_TITLE, + style: "width: 35em" + }); + responseDialog.startup(); + var requestStatusDiv = dojo.create("div", { style: "width: 30em" }); + var requestStatusMsg = dojo.create("div", { innerHTML: msg }, requestStatusDiv); + var okButton = new dijit.form.Button({ + id: "okButton", + type: "submit", + label: opac_strings.OK + }).placeAt(requestStatusDiv); + responseDialog.attr("content", requestStatusDiv); + responseDialog.show(); + dojo.connect(dijit.byId("okButton"), "onClick", responseDialog, "hide"); +} + +function createResetDialog() { + var pwResetFormDlg = new dijit.Dialog({ + id: "pwResetFormDlg", + title: opac_strings.PWD_RESET_FORM_TITLE, + style: "width: 35em" + }); + pwResetFormDlg.startup(); + + // Instantiate the form + var pwResetFormDiv = dojo.create("form", { id: "requestReset", style: "width: 30em", method: "post", action: "/opac/password/en-US" }); + dojo.create("p", { innerHTML: opac_strings.PWD_RESET_SUBMIT_PROMPT }, pwResetFormDiv); + var pwResetFormTable = dojo.create("table", null, pwResetFormDiv); + var pwResetFormTbody = dojo.create("tbody", null, pwResetFormTable); + var pwResetFormRow = dojo.create("tr", null, pwResetFormTbody); + var pwResetFormCell = dojo.create("td", null, pwResetFormRow); + var pwResetFormLabel = dojo.create("label", null, pwResetFormCell); + dojo.attr(pwResetFormCell, { innerHTML: opac_strings.BARCODE_PROMPT }); + pwResetFormCell = dojo.create("td", null, pwResetFormRow); + var barcodeText = new dijit.form.TextBox({ + name: "barcode" + }).placeAt(pwResetFormCell); + pwResetFormRow = dojo.create("tr", {}, pwResetFormTbody); + pwResetFormCell = dojo.create("td", {}, pwResetFormRow); + dojo.attr(pwResetFormCell, { innerHTML: opac_strings.USERNAME_PROMPT }); + pwResetFormCell = dojo.create("td", {}, pwResetFormRow); + var usernameText = new dijit.form.TextBox({ + name: "username" + }).placeAt(pwResetFormCell); + dojo.create("br", null, pwResetFormDiv); + var submitButton = new dijit.form.Button({ + id: "submitButton", + type: "submit", + label: opac_strings.SUBMIT_BUTTON_LABEL + }).placeAt(pwResetFormDiv); + var cancelButton = new dijit.form.Button({ + id: "cancelButton", + type: "cancel", + label: opac_strings.CANCEL_BUTTON_LABEL + }).placeAt(pwResetFormDiv); + + // Set the content of the Dialog to the pwResetForm + pwResetFormDlg.attr("content", pwResetFormDiv); + return pwResetFormDlg; +} + diff --git a/Open-ILS/web/opac/skin/default/xml/common/login.xml b/Open-ILS/web/opac/skin/default/xml/common/login.xml index 81720896ff..b94cb5b2c4 100644 --- a/Open-ILS/web/opac/skin/default/xml/common/login.xml +++ b/Open-ILS/web/opac/skin/default/xml/common/login.xml @@ -11,6 +11,7 @@ config.ids.login.cancel = "login_cancel_button"; config.ids.altcanvas.login = config.ids.login.box; +
@@ -21,7 +22,7 @@
- +
-- 2.11.0