API call to get the age of a patron's password, this is displayed in the user editor...
authorLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Thu, 9 Dec 2021 22:00:44 +0000 (17:00 -0500)
committerLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Wed, 20 Apr 2022 18:14:19 +0000 (14:14 -0400)
note for patron with expired pword

get password age in staff splash

check user perms for user self edit, provide button to update password if they do

default config tt2 uses org unit setting for password age

password changed hook

fixed issues with the templates.

Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/templates-bootstrap/opac/myopac/main.tt2
Open-ILS/src/templates-bootstrap/opac/parts/config.tt2
Open-ILS/src/templates/opac/parts/config.tt2
Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
Open-ILS/src/templates/staff/t_splash.tt2
Open-ILS/web/js/ui/default/staff/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js

index 674ace4..013ee90 100644 (file)
@@ -2409,6 +2409,19 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        </actions>
                </permacrud>
        </class>
+       <class id="aupsd" controller="open-ils.cstore" oils_obj:fieldmapper="action::usr_password_set_date" reporter:label="User Password Set Date" oils_persist:readonly="true">
+               <oils_persist:source_definition><![CDATA[
+                       SELECT usr,create_date,edit_date FROM actor.passwd
+               ]]></oils_persist:source_definition>
+               <fields oils_persist:primary="id">
+            <field reporter:label="User ID" name="usr" reporter:datatype="id" />
+                       <field reporter:label="Create Date" name="create_date" reporter:datatype="timestamp"/>
+                       <field reporter:label="Last Edit Date" name="edit_date" reporter:datatype="timestamp"/>
+               </fields>
+               <links>
+                       <link field="usr" 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"/>
index 4298f28..46dda80 100644 (file)
@@ -525,6 +525,7 @@ sub update_patron {
     my $old_patron;
     my $barred_hook = '';
     my $renew_hook = '';
+    my $password_hook = '';
 
     if($patron->isnew()) {
         ( $new_patron, $evt ) = _add_patron($e, _clone_patron($patron));
@@ -555,6 +556,8 @@ sub update_patron {
             modify_migrated_user_password($e, $patron->id, $patron->passwd);
             $new_patron->passwd(''); # subsequent update will set
                                      # actor.usr.passwd to MD5('')
+            #$U->create_events_for_hook('au.passwd_changed', $db_user, $e->requestor->ws_ou);
+            $password_hook = 'au.passwd_changed';
         }
     }
 
@@ -603,6 +606,9 @@ sub update_patron {
 
         $tses->request('open-ils.trigger.event.autocreate', $barred_hook,
             $new_patron, $new_patron->home_ou) if $barred_hook;
+
+        $tses->request('open-ils.trigger.event.autocreate', $password_hook,
+            $new_patron, $new_patron->home_ou) if $password_hook;
     }
 
     $e->xact_begin; # $e->rollback is called in new_flesh_user
@@ -1650,9 +1656,9 @@ sub update_passwd {
         # NOTE: with access to the plain text password we could crypt
         # the password without the extra MD5 pre-hashing.  Other changes
         # would be required.  Noting here for future reference.
-        modify_migrated_user_password($e, $db_user->id, $new_val);
+        modify_migrated_user_password($e, $db_user->id, $new_val);     
         $db_user->passwd('');
-
+        $U->create_events_for_hook('au.passwd_changed', $db_user, $e->requestor->ws_ou);
     } else {
 
         # if we don't clear the password, the user will be updated with
@@ -4427,7 +4433,8 @@ sub commit_password_reset {
 
     # All is well; update the password
     modify_migrated_user_password($e, $user->id, $password);
-
+    $U->create_events_for_hook('au.passwd_changed', $user, $user->home_ou);
+    
     # And flag that this password reset request has been honoured
     $aupr->[0]->has_been_reset('t');
     $e->update_actor_usr_password_reset($aupr->[0]);
@@ -4987,6 +4994,49 @@ sub get_barcodes {
         return $db_result;
     }
 }
+
+__PACKAGE__->register_method(
+    method   => "get_password_last_edit_age",
+    api_name => "open-ils.actor.get_password_age"
+);
+
+sub get_password_last_edit_age {
+    my( $self, $client, $auth, $patron_id ) = @_;
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+    my $patron = $e->retrieve_actor_user($patron_id);
+    
+    #return unless the requestor is either the patron in question, or can view users at that patron's home ou
+    unless($patron && ($patron_id == $e->requestor->id || $e->allowed('VIEW_USER', $patron->home_ou))) {
+         return $e->event;
+    }
+    
+    #get the password dates from the virtual table
+    my $aupsds = $e->json_query({
+        select => {aupsd => ['create_date','edit_date']},
+        from => 'aupsd',
+        where => {
+            usr => $patron_id
+        }
+    });
+
+    if(defined $aupsds){
+        my $pwd = $aupsds->[0];
+        #convert the dates with the DateTime module
+        if($pwd){
+            my $edit_datetime = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($pwd->{'edit_date'}));
+            #get time in days since last password update
+            #my $now = DateTime->today()->iso8601();
+            my $now = DateTime->now();
+            
+            my $duration = $now->subtract_datetime_absolute($edit_datetime)->delta_seconds / (24*60*60);
+            return int($duration);            
+        }
+    }
+    #no password entry, return -1 days
+    return -1;    
+}
+
 __PACKAGE__->register_method(
     method   => 'address_alert_test',
     api_name => 'open-ils.actor.address_alert.test',
index 5763cb8..5b9b320 100644 (file)
@@ -15,6 +15,7 @@ use OpenILS::Utils::Fieldmapper;
 use OpenSRF::Utils::Cache;
 use OpenILS::Event;
 use DateTime::Format::ISO8601;
+use Data::Dumper;
 use CGI qw(:all -utf8);
 use Time::HiRes;
 
@@ -381,6 +382,9 @@ sub load_common {
             $ctx->{authtoken} = $e->authtoken;
             $ctx->{authtime} = $e->authtime;
             $ctx->{user} = $e->requestor;
+            $ctx->{password_age} = int($U->simplereq(
+                'open-ils.actor',
+                'open-ils.actor.get_password_age', $e->authtoken, $ctx->{user}->id));
             my $card = $self->editor->retrieve_actor_card($ctx->{user}->card);
             $ctx->{active_card} = (ref $card) ? $card->barcode : undef;
             $ctx->{place_unfillable} = 1 if $e->requestor->wsid && $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
index 29b8198..1454b4d 100755 (executable)
         <div id="acct_sum_block" class="container">
             <h3>[% l('My Account Summary') %]</h3>
                 <div class="row">
+                    <br>
+                    [% IF ctx.password_age && ctx.password_age_reminder && ctx.disable_password_change != 'true' %]
+                    [% need_password_change = ctx.password_age == -1 || ctx.password_age >= ctx.password_age_reminder %]
+                    <div class="col-12">
+                            [% IF ctx.password_age == -1 %]
+                            You have never changed your password. Please consider updating your password.
+                            [% ELSIF ctx.password_age >= (ctx.password_age_reminder - 7) %]
+                            Your password is <b>[% ctx.password_age %]</b> days old[%- IF !need_password_change %], you will be asked to change it soon.[%- ELSE %]. We recommend passwords be updated every <b>[% ctx.password_age_reminder %]</b> days. Please consider updating your password.[% END %]
+                            [% END %]
+                            [% IF need_password_change %]
+                            <br>
+                            <a class="btn btn-sm btn-action" href='update_password'
+                            title="[% l('Change Password') %]"><i class="fas fa-user-cog"></i> [% l("Change Password") %]</a>
+                            <br>
+                            [% END %]
+                    </div>
+                    <br>
+                    [% END %]  
                     <div class="col-12">
                      <span [% IF ctx.expired_card %]class="danger"[% END %]>
                         [% l("Account Expiration Date - ") %]
@@ -29,7 +47,7 @@
                         </span>
                         [% END %]
                     </div>
-                    <br>
+                    <br>               
                     <div class="col-12">
                      <a href="[% mkurl(ctx.opac_root _ '/myopac/circs') %]"
                             title="[% l('View My Checked Out Items') %]">
index 7409a4b..698f2a1 100755 (executable)
@@ -280,4 +280,11 @@ ctx.max_cart_size = 500;
 ##############################################################################
 ctx.show_reservations_tab = 'false';
 
+##############################################################################
+# Password Reminder Settings
+##############################################################################
+
+# days since last password change to start reminding a patron to change their password
+# commenting out this line will disable the reminder
+ctx.password_age_reminder = 15;
 %]
index eda51ad..9de990e 100644 (file)
@@ -285,4 +285,12 @@ contents_truncate_length = 50;
 # Edit parts/record/contents.tt2 to designate character length on a field-by-
 # field basis for notes.
 
+
+##############################################################################
+# Password Reminder Settings
+##############################################################################
+
+# days since last password change to start reminding a patron to change their password
+# commenting out this line will disable the reminder
+ctx.password_age_reminder = ctx.get_org_setting(ctx.physical_loc || 1, 'global.password_reset_age');
 %]
index 820e346..d56e7c8 100644 (file)
@@ -222,7 +222,14 @@ within the "form" by name for validation.
       [% l('Generate Password') %]</button>
   </div>
 </div>
-
+<div class="row reg-field-row">
+    <div class="col-md-3">
+    </div>
+    <div class="col-md-9">
+        <div ng-show="password_age !== undefined && password_age !== '' && password_age !== '-1'"><i>Password last changed <b>{{password_age}}</b> day(s) ago</i></div>
+        <div ng-show="password_age === '-1'"><i>User has never changed their password</i></div>
+    </div>
+</div>
 <div class="row reg-field-row">
   <div class="col-md-6">
     <ul class="nav nav-pills nav-pills-like-tabs">
index 598f9ff..b7184a2 100644 (file)
@@ -5,6 +5,17 @@
       <h1 class="sr-only" i18n>Evergreen Staff Client Home Page</h1>
     </div>
   </div>
+  <div ng-if="password_reset_age && password_age >= password_reset_age" class="alert alert-danger row">Your password is <b>{{password_age}} days</b> old. It is recommended that passwords be updated every <b>{{password_reset_age}} days</b>. 
+      <p ng-if="!can_self_update">
+        Please contact an adminstrator to have your password changed.
+      </p>
+      <div ng-if="can_self_update">
+      <br>
+        <a class="btn btn-warning btn-block" target="_top" href="{{self_update_link}}">
+        Update Password
+        </a>
+      </div>
+  </div>
   <br/>
   <div class="row" id="splash-nav">
 
index 58afd06..aa3ba9b 100644 (file)
@@ -157,10 +157,26 @@ function($routeProvider , $locationProvider) {
     function($scope, $window,egCore) {
                
     $scope.focus_search = true;
-       
+       $scope.can_self_update = false;
+    
        egCore.strings.setPageTitle(
         egCore.strings['PAGE_TITLE_SPLASH']);
-
+    egCore.org.settingsFromServer(['global.password_reset_age','global.password_regex']).then(function(settings) {
+            $scope.password_reset_age = parseInt(settings['global.password_reset_age']);
+        });
+    egCore.net.request(
+                'open-ils.actor', 
+                'open-ils.actor.get_password_age',
+                egCore.auth.token(),
+                egCore.auth.user().id()
+            ).then(function(age) {
+                $scope.password_age = parseInt(age);
+            });
+    egCore.perm.hasPermHere('EDIT_SELF_IN_CLIENT')
+    .then(function(bool){
+        $scope.can_self_update = bool;
+        $scope.self_update_link = './circ/patron/' + egCore.auth.user().id() + '/edit'
+        });
     $scope.catalog_search = function($event) {
         $scope.focus_search = true;
         if (!$scope.cat_query) return;
index ac1894c..de023c3 100644 (file)
@@ -30,6 +30,7 @@ angular.module('egCoreMod')
         // These are fetched with every instance of the page.
         var page_data = [
             service.get_user_settings(),
+            service.get_user_password_age(),
             service.get_clone_user(),
             service.get_stage_user()
         ];
@@ -638,6 +639,18 @@ angular.module('egCoreMod')
         });
     }
 
+    service.get_user_password_age = function() {
+        return egCore.net.request(
+            'open-ils.actor', 
+            'open-ils.actor.get_password_age',
+            egCore.auth.token(),
+            service.patron_id
+        ).then(function(age) {
+            console.log("retrieved from AUSPD: "+age)
+            service.password_age = age;
+        });  
+    }
+
     service.invalidate_field = function(patron, field) {
         console.log('Invalidating patron field ' + field);
 
@@ -1394,6 +1407,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         $scope.stat_cat_entry_maps = prs.stat_cat_entry_maps;
         $scope.stage_user = prs.stage_user;
         $scope.stage_user_requestor = prs.stage_user_requestor;
+        $scope.password_age = prs.password_age;
 
         $scope.user_settings = prs.user_settings;
         prs.user_settings = {};