[open-ils-commits] r16269 - in trunk/Open-ILS: examples examples/apache src src/extras src/perlmods/OpenILS/Application src/perlmods/OpenILS/WWW src/sql/Pg src/sql/Pg/upgrade src/templates src/templates/password-reset web/js/dojo/openils/opac/nls web/opac/skin/default/js web/opac/skin/default/xml/common (dbs)

svn at svn.open-ils.org svn at svn.open-ils.org
Mon Apr 19 08:47:13 EDT 2010


Author: dbs
Date: 2010-04-19 08:47:12 -0400 (Mon, 19 Apr 2010)
New Revision: 16269

Added:
   trunk/Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm
   trunk/Open-ILS/src/sql/Pg/upgrade/0237.data.password-reset.sql
   trunk/Open-ILS/src/templates/password-reset/
   trunk/Open-ILS/src/templates/password-reset/request-form.tt2
   trunk/Open-ILS/src/templates/password-reset/reset-form.tt2
   trunk/Open-ILS/src/templates/password-reset/strings.en-US
   trunk/Open-ILS/web/opac/skin/default/js/password_reset.js
Modified:
   trunk/Open-ILS/examples/apache/eg.conf
   trunk/Open-ILS/examples/apache/eg_vhost.conf
   trunk/Open-ILS/examples/apache/startup.pl
   trunk/Open-ILS/examples/fm_IDL.xml
   trunk/Open-ILS/examples/opensrf.xml.example
   trunk/Open-ILS/src/Makefile.am
   trunk/Open-ILS/src/extras/ils_events.xml
   trunk/Open-ILS/src/perlmods/OpenILS/Application/Actor.pm
   trunk/Open-ILS/src/sql/Pg/002.schema.config.sql
   trunk/Open-ILS/src/sql/Pg/005.schema.actors.sql
   trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql
   trunk/Open-ILS/web/js/dojo/openils/opac/nls/opac.js
   trunk/Open-ILS/web/opac/skin/default/xml/common/login.xml
Log:
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


Modified: trunk/Open-ILS/examples/apache/eg.conf
===================================================================
--- trunk/Open-ILS/examples/apache/eg.conf	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/examples/apache/eg.conf	2010-04-19 12:47:12 UTC (rev 16269)
@@ -19,8 +19,8 @@
 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
 # ----------------------------------------------------------------------------------

Modified: trunk/Open-ILS/examples/apache/eg_vhost.conf
===================================================================
--- trunk/Open-ILS/examples/apache/eg_vhost.conf	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/examples/apache/eg_vhost.conf	2010-04-19 12:47:12 UTC (rev 16269)
@@ -163,7 +163,22 @@
     allow from all
 </LocationMatch>
 
+# ----------------------------------------------------------------------------------
+# Self-serve password interface
+# ----------------------------------------------------------------------------------
+<Location /opac/password>
+    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]
+</Location>
+
 # ----------------------------------------------------------------------------------
 # Supercat feeds
 # ----------------------------------------------------------------------------------

Modified: trunk/Open-ILS/examples/apache/startup.pl
===================================================================
--- trunk/Open-ILS/examples/apache/startup.pl	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/examples/apache/startup.pl	2010-04-19 12:47:12 UTC (rev 16269)
@@ -6,6 +6,7 @@
 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:

Modified: trunk/Open-ILS/examples/fm_IDL.xml
===================================================================
--- trunk/Open-ILS/examples/fm_IDL.xml	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/examples/fm_IDL.xml	2010-04-19 12:47:12 UTC (rev 16269)
@@ -1222,6 +1222,18 @@
 			<link field="creator" reltype="has_a" key="id" map="" class="au"/>
 		</links>
 	</class>
+	<class id="aupr" controller="open-ils.cstore" oils_obj:fieldmapper="actor::usr_password_reset" oils_persist:tablename="actor.usr_password_reset" reporter:label="User password reset requests">
+		<fields oils_persist:primary="id" oils_persist:sequence="actor.usr_password_reset_id_seq">
+			<field reporter:label="Request ID" name="id" reporter:datatype="id"/>
+			<field reporter:label="UUID" name="uuid" reporter:datatype="text"/>
+			<field reporter:label="User" name="usr" reporter:datatype="link"/>
+			<field reporter:label="Request Time" name="request_time" reporter:datatype="timestamp"/>
+			<field reporter:label="Was Reset?" name="has_been_reset" reporter:datatype="bool"/>
+		</fields>
+		<links>
+			<link field="usr" reltype="has_a" key="id" class="au"/>
+		</links>
+	</class>
 	<class id="aus" controller="open-ils.cstore" oils_obj:fieldmapper="actor::user_setting" oils_persist:tablename="actor.usr_setting" reporter:label="User Setting">
 		<fields oils_persist:primary="id" oils_persist:sequence="actor.usr_setting_id_seq">
 			<field reporter:label="Setting ID" name="id" reporter:datatype="id" />

Modified: trunk/Open-ILS/examples/opensrf.xml.example
===================================================================
--- trunk/Open-ILS/examples/opensrf.xml.example	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/examples/opensrf.xml.example	2010-04-19 12:47:12 UTC (rev 16269)
@@ -19,6 +19,7 @@
             <xsl>LOCALSTATEDIR/xsl</xsl>
             <script>LOCALSTATEDIR</script>
             <script_lib>LOCALSTATEDIR</script_lib>
+            <templates>LOCALSTATEDIR/templates</templates>
         </dirs>
 
         <!-- global data visibility settings -->

Modified: trunk/Open-ILS/src/Makefile.am
===================================================================
--- trunk/Open-ILS/src/Makefile.am	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/src/Makefile.am	2010-04-19 12:47:12 UTC (rev 16269)
@@ -170,6 +170,7 @@
 	$(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)"

Modified: trunk/Open-ILS/src/extras/ils_events.xml
===================================================================
--- trunk/Open-ILS/src/extras/ils_events.xml	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/src/extras/ils_events.xml	2010-04-19 12:47:12 UTC (rev 16269)
@@ -779,7 +779,17 @@
 	<event code='5000' textcode='PERM_FAILURE'>
 		<desc xml:lang="en-US">Permission Denied</desc>
 	</event>
+    <event code='7025' textcode='PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS'>
+        <desc xml:lang='en-US'>There are too many active password reset request sessions for this patron.</desc>
+    </event>
+    <event code='7026' textcode='PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST'>
+        <desc xml:lang='en-US'>The user attempted to update their password using a stale or inactive password reset request session.</desc>
+    </event>
+    <event code='7027' textcode='PATRON_PASSWORD_WAS_NOT_STRONG'>
+        <desc xml:lang='en-US'>The user attempted to set their password to a weak value.</desc>
+    </event>
 
+
 	<!-- ================================================================ -->
 	<!-- CIRC EVENTS -->
 	<event code='7000' textcode='ROUTE_ITEM'>

Modified: trunk/Open-ILS/src/perlmods/OpenILS/Application/Actor.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/OpenILS/Application/Actor.pm	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/src/perlmods/OpenILS/Application/Actor.pm	2010-04-19 12:47:12 UTC (rev 16269)
@@ -36,6 +36,8 @@
 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 @@
     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) = @_;
 
+    # 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;
-

Added: trunk/Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm	                        (rev 0)
+++ trunk/Open-ILS/src/perlmods/OpenILS/WWW/PasswordReset.pm	2010-04-19 12:47:12 UTC (rev 16269)
@@ -0,0 +1,418 @@
+package OpenILS::WWW::PasswordReset;
+
+# Copyright (C) 2010 Laurentian University
+# Dan Scott <dscott at laurentian.ca>
+# 
+# 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(<I18NFH>) {
+            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 <dscott at laurentian.ca>
+# 
+# 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(<I18NFH>) {
+            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

Modified: trunk/Open-ILS/src/sql/Pg/002.schema.config.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/002.schema.config.sql	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/src/sql/Pg/002.schema.config.sql	2010-04-19 12:47:12 UTC (rev 16269)
@@ -60,7 +60,7 @@
     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,

Modified: trunk/Open-ILS/src/sql/Pg/005.schema.actors.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/005.schema.actors.sql	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/src/sql/Pg/005.schema.actors.sql	2010-04-19 12:47:12 UTC (rev 16269)
@@ -520,6 +520,37 @@
 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 <dscott at laurentian.ca>
+ *
+ * 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,

Modified: trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql	2010-04-19 12:47:12 UTC (rev 16269)
@@ -1938,7 +1938,22 @@
 ( '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 @@
         ( 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

Added: trunk/Open-ILS/src/sql/Pg/upgrade/0237.data.password-reset.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/upgrade/0237.data.password-reset.sql	                        (rev 0)
+++ trunk/Open-ILS/src/sql/Pg/upgrade/0237.data.password-reset.sql	2010-04-19 12:47:12 UTC (rev 16269)
@@ -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;

Added: trunk/Open-ILS/src/templates/password-reset/request-form.tt2
===================================================================
--- trunk/Open-ILS/src/templates/password-reset/request-form.tt2	                        (rev 0)
+++ trunk/Open-ILS/src/templates/password-reset/request-form.tt2	2010-04-19 12:47:12 UTC (rev 16269)
@@ -0,0 +1,49 @@
+<html>
+<head>
+  <title>[% i18n.REQUEST_TITLE %]</title>
+  <link rel="stylesheet" type="text/css" href="/js/dojo/dijit/themes/tundra/tundra.css" />
+  <style type="text/css">
+    body, html { font-family:helvetica,arial,sans-serif; }
+  </style>
+</head>
+<body class="tundra">
+  <h1>[% i18n.REQUEST_TITLE %]</h1>
+  <p class='[% status.style %]'>[% status.msg %]</p>
+  <form method="post" action="[% uri %]" id="requestPwdReset">
+    <table>
+      <tr>
+        <td><label for="barcode">[% i18n.BARCODE_PROMPT %] </label></td>
+        <td><input type="text" id="barcode" name="barcode" dojoType="dijit.form.TextBox"/></td>
+      </tr>
+      <tr>
+        <td><label for="username">[% i18n.USERNAME_PROMPT %] </label></td>
+        <td><input type="text" id="barcode" name="username" dojoType="dijit.form.TextBox"/></td>
+      </tr>
+    </table>
+    <!--<label for="email">[% i18n.EMAIL_PROMPT %] </label><input type="text" name="email"/></br>-->
+    <button name="submit" id="submitButton" type="submit" dojoType="dijit.form.Button">[% i18n.BUTTON_SUBMIT %]</button>
+  </form>
+</body>
+<script type="text/javascript" src="/js/dojo/dojo/dojo.js" djConfig="parseOnLoad: true"></script>
+<script type="text/javascript">
+  dojo.require("dijit.form.Button");
+  dojo.require("dijit.form.ValidationTextBox");
+</script>
+</html>
+<html>
+<head>
+  <title>[% i18n.REQUEST_TITLE %]</title>
+</head>
+<body>
+  <h1>[% i18n.REQUEST_TITLE %]</h1>
+<p class='[% status.style %]'>[% status.msg %]</p>
+<form method="post" action="[% uri %]">
+    <div>
+        <label for="barcode">[% i18n.BARCODE_PROMPT %] </label><input type="text" name="barcode"/></br>
+        <label for="username">[% i18n.USERNAME_PROMPT %]  </label><input type="text" name="username"/></br>
+        <!--<label for="email">[% i18n.EMAIL_PROMPT %]  </label><input type="text" name="email"/></br>-->
+        <input type="submit"/>
+    </div>
+</form>
+</body>
+</html>

Added: trunk/Open-ILS/src/templates/password-reset/reset-form.tt2
===================================================================
--- trunk/Open-ILS/src/templates/password-reset/reset-form.tt2	                        (rev 0)
+++ trunk/Open-ILS/src/templates/password-reset/reset-form.tt2	2010-04-19 12:47:12 UTC (rev 16269)
@@ -0,0 +1,30 @@
+<html>
+<head>
+  <title>[% title %]</title>
+</head>
+<body>
+  <h1>[% title %]</h1>
+<p class='[% status.style %]'>[% status.msg %]</p>
+<form method="post" action="[% uri %]">
+    <div>
+        <label for="pwd1">[% password_prompt %] </label><input type="password" name="pwd1"/></br>
+        <label for="pwd2">[% password_prompt2 %]  </label><input type="password" name="pwd2"/></br>
+    </div>
+</form>
+</body>
+</html>
+<html>
+<head>
+  <title>[% title %]</title>
+</head>
+<body>
+  <h1>[% title %]</h1>
+<p class='[% status.style %]'>[% status.msg %]</p>
+<form method="post" action="[% uri %]">
+    <div>
+        <label for="pwd1">[% password_prompt %] </label><input type="password" name="pwd1"/></br>
+        <label for="pwd2">[% password_prompt2 %]  </label><input type="password" name="pwd2"/></br>
+    </div>
+</form>
+</body>
+</html>

Added: trunk/Open-ILS/src/templates/password-reset/strings.en-US
===================================================================
--- trunk/Open-ILS/src/templates/password-reset/strings.en-US	                        (rev 0)
+++ trunk/Open-ILS/src/templates/password-reset/strings.en-US	2010-04-19 12:47:12 UTC (rev 16269)
@@ -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: 

Modified: trunk/Open-ILS/web/js/dojo/openils/opac/nls/opac.js
===================================================================
--- trunk/Open-ILS/web/js/dojo/openils/opac/nls/opac.js	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/web/js/dojo/openils/opac/nls/opac.js	2010-04-19 12:47:12 UTC (rev 16269)
@@ -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",

Added: trunk/Open-ILS/web/opac/skin/default/js/password_reset.js
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/js/password_reset.js	                        (rev 0)
+++ trunk/Open-ILS/web/opac/skin/default/js/password_reset.js	2010-04-19 12:47:12 UTC (rev 16269)
@@ -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("<tr><td colspan='2' align='center'><a class='classic_link' id='pwResetLink' onClick='dijit.byId(\"pwResetFormDlg\").show();'</a></td></tr>", "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;
+}
+

Modified: trunk/Open-ILS/web/opac/skin/default/xml/common/login.xml
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/xml/common/login.xml	2010-04-19 00:04:26 UTC (rev 16268)
+++ trunk/Open-ILS/web/opac/skin/default/xml/common/login.xml	2010-04-19 12:47:12 UTC (rev 16269)
@@ -11,6 +11,7 @@
 		config.ids.login.cancel		= "login_cancel_button";
 		config.ids.altcanvas.login			= config.ids.login.box;
 	</script>
+	<script type='text/javascript' src='<!--#echo var="OILS_OPAC_JS_HOST"-->/skin/default/js/password_reset.js'></script>
 
 	<br/>
 
@@ -21,7 +22,7 @@
 	<br/>
 
 	<table id='login_table' class='data_grid' style='margin-left: 20px;' width='95%'>
-		<tbody>
+		<tbody id='login_tbody'>
 			<tr>
 				<td><span class='login_text'>&login.username;</span></td>
 				<td>



More information about the open-ils-commits mailing list