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
allow from all
</LocationMatch>
+# ----------------------------------------------------------------------------------
+# Self-serve password interface
+# ----------------------------------------------------------------------------------
+<Location /opac/extras/password>
+ SetHandler perl-script
+ PerlHandler OpenILS::WWW::PasswordReset::password_reset
+ Options +ExecCGI
+ PerlSendHeader On
+ allow from all
+</Location>
# ----------------------------------------------------------------------------------
# Supercat feeds
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:
<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" />
<xsl>LOCALSTATEDIR/xsl</xsl>
<script>LOCALSTATEDIR</script>
<script_lib>LOCALSTATEDIR</script_lib>
+ <templates>LOCALSTATEDIR/templates</templates>
</dirs>
<!-- global data visibility settings -->
<desc xml:lang='en-US'>The requested config_circ_matrix_ruleset_not_found was not found</desc>
</event>
+ <event code='1700' 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='1701' 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='1841' textcode='ACQ_PICKLIST_NOT_FOUND'>
<desc xml:lang='en-US'>The requested acq.picklist was not found</desc>
use OpenILS::Utils::CStoreEditor qw/:funcs/;
use OpenILS::Utils::Penalty;
+use UUID::Tiny qw/:std/;
+
sub initialize {
OpenILS::Application::Actor::Container->initialize();
OpenILS::Application::Actor::UserGroups->initialize();
return {complete => 1};
}
+__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)
+ # we use the weird test of usr = -1000 to generate a FALSE condition
+ my $active_requests = $e->json_query({
+ from => 'aupr',
+ select => {
+ aupr => [
+ {
+ column => 'uuid',
+ transform => 'COUNT'
+ }
+ ]
+ },
+ where => {
+ has_been_reset => { '=' => { 'usr' => { '=' => -1000 } } },
+ request_time => { '>' => $threshold_time }
+ }
+ });
+
+ # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
+ # ... delay - set cache - return event correspondingly ...
+ #
+ # 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 => { '=' => { 'usr' => { '=' => -1000 } } },
+ 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)
+
+ 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_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl);
+ my $request_time = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($aupr->[0]->request_time));
+ if ($request_time < $threshold_time) {
+ $e->die_event;
+ return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
+ }
+
+ # 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;
+}
1;
--- /dev/null
+package OpenILS::WWW::PasswordReset;
+
+# Copyright (C) 2010 Laurentian University
+# Dan Scott <dscott@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
CREATE INDEX actor_usr_standing_penalty_usr_idx ON actor.usr_standing_penalty (usr);
+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@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);
COMMIT;
(5, 'usr'),
(5, 'pickup_lib.billing_address');
+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 (15, '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 %]/password-reset/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
+ ( 15, 'usr' );
+INSERT INTO action_trigger.environment ( event_def, path) VALUES
+ ( 15, 'usr.home_ou' );
SELECT SETVAL('action_trigger.event_definition_id_seq'::TEXT, 100);
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+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:
<!ENTITY staff.server.admin.org_settings.cat.bib.alert_on_empty.desc "Alert staff when the last copy for a record is being deleted">
<!ENTITY staff.server.admin.org_settings.patron.password.use_phone "Patron: password from phone #">
<!ENTITY staff.server.admin.org_settings.patron.password.use_phone.desc "Use the last 4 digits of the patrons phone number as the default password when creating new users">
+<!ENTITY staff.server.admin.org_settings.circ.password_reset_request_time_to_live "Circulation: Self-serve password reset request time-to-live">
+<!ENTITY staff.server.admin.org_settings.circ.password_reset_request_time_to_live.desc "Length of time (in seconds) a self-serve password reset request should remain active.">
+<!ENTITY staff.server.admin.org_settings.circ.password_reset_request_per_user_limit "Circulation: Maximum concurrently active self-serve password reset requests per user'">
+<!ENTITY staff.server.admin.org_settings.circ.password_reset_request_per_user_limit.desc "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.">
+<!ENTITY staff.server.admin.org_settings.circ.password_reset_request_throttle "Circulation: Maximum concurrently active self-serve password reset requests">
+<!ENTITY staff.server.admin.org_settings.circ.password_reset_request_throttle.desc "Prevent the creation of new self-serve password reset requests until the number of active requests drops back below this number.">
<!ENTITY staff.server.admin.org_settings.circ.charge_on_damaged "Charge item price when marked damaged">
<!ENTITY staff.server.admin.org_settings.circ.charge_on_damaged.desc "Charge item price when marked damaged">
<!ENTITY staff.server.admin.org_settings.circ.damaged_item_processing_fee "Charge processing fee for damaged items">
desc : '&staff.server.admin.org_settings.patron.password.use_phone.desc;',
type : 'bool'
},
+ 'circ.password_reset_request_time_to_live': {
+ label: '&staff.server.admin.org_settings.circ.password_reset_request_time_to_live;',
+ desc: '&staff.server.admin.org_settings.circ.password_reset_request_time_to_live.desc;',
+ type : 'integer'
+ },
+ 'circ.password_reset_request_per_user_limit': {
+ label: '&staff.server.admin.org_settings.circ.password_reset_request_per_user_limit;',
+ desc: '&staff.server.admin.org_settings.circ.password_reset_request_per_user_limit.desc;',
+ type : 'integer'
+ },
+ 'circ.password_reset_request_throttle': {
+ label: '&staff.server.admin.org_settings.circ.password_reset_request_throttle;',
+ desc: '&staff.server.admin.org_settings.circ.password_reset_request_throttle.desc;',
+ type : 'integer'
+ },
'ui.circ.patron_summary.horizontal' : {
label : '&ui.circ.patron_summary.horizontal;',
desc : '&ui.circ.patron_summary.horizontal.desc;',