</actions>
</permacrud>
</class>
+ <class id="aupsd" controller="open-ils.cstore" oils_obj:fieldmapper="actor::usr_password_set_date" reporter:label="User Password Set Date" oils_persist:readonly="true">
+ <oils_persist:source_definition><![CDATA[
+ SELECT ac.id, ac.usr as usr, au.home_ou as home_ou, ac.create_date as create_date, ac.edit_date as edit_date
+ FROM actor.passwd ac
+ JOIN actor.usr au on au.id = ac.usr
+ ]]></oils_persist:source_definition>
+ <fields oils_persist:primary="id">
+ <field reporter:label="Password ID" name="id" reporter:datatype="id" />
+ <field reporter:label="User ID" name="usr" reporter:datatype="id" />
+ <field reporter:label="User Home OU" name="home_ou" 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"/>
+ <link field="home_ou" reltype="has_a" key="id" map="" class="aou"/>
+ </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"/>
</div>
</div>
+ <div *ngIf="passwordExpireAge && passwordAge >= (passwordExpireAge - 7)" class="alert alert-danger row">
+ <p i18n>Your password is <b>{{passwordAge}} days</b> old. It is recommended that passwords be updated every <b>{{passwordExpireAge}} days</b>.</p>
+ <p *ngIf="!canSelfUpdate" i18n>
+ Please contact an administrator to have your password changed or change your password through the OPAC.
+ </p>
+ <div *ngIf="canSelfUpdate">
+ <br>
+ <a class="btn btn-warning btn-block" target="_top" href="{{selfUpdateLink}}" i18n>
+ Update Password
+ </a>
+ </div>
+ </div>
+
<div class="row" id="splash-nav">
<div class="col-lg-4" *ngFor="let header of portalHeaders; index as i">
<div class="card">
import {AuthService} from '@eg/core/auth.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {ToastService} from '@eg/share/toast/toast.service';
+import {PermService} from '@eg/core/perm.service';
+import {NetService} from '@eg/core/net.service';
import {StringComponent} from '@eg/share/string/string.component';
import {Router} from '@angular/router';
@ViewChild('noPermissionString', { static: true }) noPermissionString: StringComponent;
catSearchQuery: string;
+ passwordExpireAge: number;
+ passwordAge: number;
+ canSelfUpdate: boolean;
+ selfUpdateLink: string;
portalEntries: any[][] = [];
portalHeaders: any[] = [];
private auth: AuthService,
private org: OrgService,
private router: Router,
- private toast: ToastService
+ private toast: ToastService,
+ private net: NetService,
+ private perm: PermService
) {}
ngOnInit() {
}
);
+ //get the maximum password age
+ this.org.settings(['auth.password_expire_age'])
+ .then(settings => {
+ this.passwordExpireAge = Number(settings['auth.password_expire_age']);
+ console.log('password expire age: ', settings);
+ });
+ //get the age of the current user's password
+ this.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.get_password_age',
+ this.auth.token(),
+ this.auth.user().id()).subscribe(
+ (res) => {
+ console.log('password age: ', res);
+ this.passwordAge = Number(res);
+ },
+ (err) => {
+ console.error('splash', err);
+ }
+ );
+ //check if user can change their own password
+ this.perm.hasWorkPermAt(
+ ['EDIT_SELF_IN_CLIENT'],
+ true).then(perms => {
+ if(perms['EDIT_SELF_IN_CLIENT']){
+ this.canSelfUpdate = true;
+ //create a link to the user's profile
+ this.selfUpdateLink = '/eg/staff/circ/patron/' + this.auth.user().id() + '/edit';
+ }
+ });
if (this.router.url === '/staff/no_permission') {
this.noPermissionString.current()
.then(str => {
return $db_result;
}
}
+
+__PACKAGE__->register_method(
+ method => "get_password_last_edit_age",
+ api_name => "open-ils.actor.get_password_age",
+ signature => {
+ desc => "Finds the number of days since a user's password was last updated.",
+ params => [
+ {desc => 'Authentication token', type => 'string'},
+ {desc => 'Patron ID', type => 'number'},
+ {desc => 'Reference Time', type => 'string'},
+ ],
+ return => {desc => 'Number of days since password update'}
+ }
+);
+
+sub get_password_last_edit_age {
+ my( $self, $client, $auth, $patron_id, $ref_time ) = @_;
+ 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];
+ if($pwd){
+ #convert the dates with the DateTime module
+ my $edit_datetime = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($pwd->{'edit_date'}));
+ #get the time we are subtracting from, use ref_time if it's defined or the current datetime otherwise
+ my $now = defined($ref_time) ? DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($ref_time)) : 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',
$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);
return Apache2::Const::OK;
}
+ if($current_pw eq $new_pw) {
+ $ctx->{password_duplicate} = 1;
+ return Apache2::Const::OK;
+ }
+
my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
if(!$pw_regex) {
--- /dev/null
+#!perl
+use constant FUTURE_DAYS => 150;
+use strict; use warnings;
+use Test::More tests => 4;
+use OpenILS::Utils::TestUtils;
+use OpenILS::Const qw(:const);
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use DateTime;
+use DateTime::Format::ISO8601;
+
+diag("test password age");
+
+my $U = 'OpenILS::Application::AppUtils';
+
+my $script = OpenILS::Utils::TestUtils->new();
+$script->bootstrap;
+
+$script->authenticate({
+ username => 'admin',
+ password => 'demo123',
+ type => 'staff'
+});
+
+my $authtoken = $script->authtoken;
+ok($authtoken, 'was able to authenticate');
+
+my $new_user = Fieldmapper::actor::user->new();
+my $new_card = Fieldmapper::actor::card->new();
+
+$new_card->barcode("lew_$$");
+$new_card->id(-1); # virtual ID
+$new_card->usr(undef);
+$new_card->isnew(1);
+
+$new_user->cards([ $new_card ]);
+$new_user->card($new_card);
+$new_user->usrname("lew_$$");
+$new_user->passwd('lew_$$');
+$new_user->family_name('Marshall');
+$new_user->first_given_name('Llewellyn');
+$new_user->profile(2);
+$new_user->home_ou(4);
+$new_user->ident_type(1);
+$new_user->isnew(1);
+
+my $resp = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.patron.update',
+ $authtoken,
+ $new_user
+);
+
+isa_ok($resp, 'Fieldmapper::actor::user', 'new patron');
+
+my $new_id = $resp->id();
+
+$resp = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.get_password_age',
+ $authtoken,
+ $new_id
+);
+
+cmp_ok($resp, '==', 0, 'Password age on new user is 0 days');
+
+my $dt = DateTime->now();
+
+$dt->add( days => FUTURE_DAYS );
+
+$resp = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.get_password_age',
+ $authtoken,
+ $new_id,
+ $dt->iso8601()
+);
+
+cmp_ok($resp, '==', FUTURE_DAYS, FUTURE_DAYS." days from now, Password age on new user is ".FUTURE_DAYS." days");
+
+# clean up
+$U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.delete',
+ $authtoken,
+ $new_id
+);
\ No newline at end of file
'integer'
);
+-- Password age reset
+
+INSERT INTO config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+ VALUES (
+ 'auth.password_expire_age',
+ 'sec',
+ oils_i18n_gettext(
+ 'auth.password_expire_age',
+ 'Password Reset Age',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'auth.password_expire_age',
+ 'The number of days after a password has been changed before ' ||
+ 'users will be alerted that they should update it.',
+ 'coust',
+ 'description'
+ ),
+ 'integer'
+ );
+
+INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES (
+ 'aupsd.passwd_changed',
+ 'aupsd',
+ oils_i18n_gettext(
+ 'au.passwd_changed',
+ 'An account password was updated',
+ 'ath',
+ 'description'
+ ),
+ true
+);
+
+-- Sample Password Update Notice --
+
+INSERT INTO action_trigger.event_definition (active, owner, name, delay_field, delay, max_delay, repeat_delay, hook, validator, reactor, template)
+ VALUES ('f', 1, 'Password Update Notice', 'edit_date','90 days', '91 days','90 days' 'aupsd.passwd_changed', 'NOOP_True', 'SendEmail',
+$$
+[%- USE date -%]
+[%- user = target.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender || helpers.get_org_setting(user.home_ou, 'org.bounced_emails') %]
+Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
+Subject: Password Update Required
+Auto-Submitted: auto-generated
+
+Dear [% user.family_name %], [% user.first_given_name %]
+Regularly updating your password is an essential part of maintaining the security of your account. At the time of writing, your password is 90 days old. Please log in to the system or contact a system administrator to update your password.
+
+$$);
+
+INSERT INTO action_trigger.environment (
+ event_def,
+ path
+) VALUES (
+ currval('action_trigger.event_definition_id_seq'),
+ 'usr'
+);
+
------------------- Disabled example A/T defintions ------------------------------
-- Create a "dummy" slot when applicable, and trigger the "offer curbside" events
--- /dev/null
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+-- password age display setting
+
+INSERT INTO config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+ VALUES (
+ 'auth.password_expire_age',
+ 'sec',
+ oils_i18n_gettext(
+ 'auth.password_expire_age',
+ 'Password Reset Age',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'auth.password_expire_age',
+ 'The number of days after a password has been changed before ' ||
+ 'users will be alerted that they should update it.',
+ 'coust',
+ 'description'
+ ),
+ 'integer'
+ );
+
+INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES (
+ 'aupsd.passwd_changed',
+ 'aupsd',
+ oils_i18n_gettext(
+ 'au.passwd_changed',
+ 'An account password was updated',
+ 'ath',
+ 'description'
+ ),
+ true
+);
+
+-- Sample Password Update Notice --
+
+INSERT INTO action_trigger.event_definition (active, owner, name, delay_field, delay, max_delay, repeat_delay, hook, validator, reactor, template)
+ VALUES ('f', 1, 'Password Update Notice', 'edit_date','90 days', '91 days','90 days' 'aupsd.passwd_changed', 'NOOP_True', 'SendEmail',
+$$
+[%- USE date -%]
+[%- user = target.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender || helpers.get_org_setting(user.home_ou, 'org.bounced_emails') %]
+Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
+Subject: Password Update Required
+Auto-Submitted: auto-generated
+
+Dear [% user.family_name %], [% user.first_given_name %]
+Regularly updating your password is an essential part of maintaining the security of your account. At the time of writing, your password is 90 days old. Please log in to the system or contact a system administrator to update your password.
+
+$$);
+
+INSERT INTO action_trigger.environment (
+ event_def,
+ path
+) VALUES (
+ currval('action_trigger.event_definition_id_seq'),
+ 'usr'
+);
+--ROLLBACK;
+COMMIT;
\ No newline at end of file
<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 %]
+ [% l('You have never changed your password. Please consider updating your password.') %]
+ [% ELSIF ctx.password_age >= (ctx.password_age_reminder - 7) %]
+ [% l('Your password is <b>[_1]</b> days old.',ctx.password_age) %][%- IF !need_password_change %] [% l('You will be asked to change your password soon.') %][%- ELSE %] [% l('It is recommended to update your password every <b>[_1]</b> days. Please consider updating your password.',ctx.password_age_reminder) %][% 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 - ") %]
</span>
[% END %]
</div>
- <br>
+ <br>
<div class="col-12">
<a href="[% mkurl(ctx.opac_root _ '/myopac/circs') %]"
title="[% l('View My Checked Out Items') %]">
[% |l %]Passwords do not match.[% END %]
</div>
+[% ELSIF ctx.password_duplicate %]
+ <div id='account-update-email-error'>
+ [% |l %]New password can not be the same as current password.[% END %]
+ </div>
+
[% ELSIF ctx.password_incorrect %]
<div id='account-update-email-error'>
[% |l %]Your current password was not correct.[% END %]
##############################################################################
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 = ctx.get_org_setting(ctx.physical_loc || 1, 'auth.password_expire_age');
%]
[% |l %]Passwords do not match.[% END %]
</div>
+[% ELSIF ctx.password_duplicate %]
+ <div id='account-update-email-error'>
+ [% |l %]New password can not be the same as current password.[% END %]
+ </div>
+
[% ELSIF ctx.password_incorrect %]
<div id='account-update-email-error'>
[% |l %]Your current password was not correct.[% END %]
# 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, 'auth.password_expire_age');
%]
[% l('Send Password Reset Link') %]</button>
</div>
</div>
-
+<div class="row reg-field-row" ng-show="!patron.isnew">
+ <div class="col-md-3">
+ </div>
+ <div class="col-md-9">
+ <div ng-show="password_age !== undefined && password_age !== '' && password_age !== '-1'"><i>[% l('Password last changed <b>[_1]</b> day(s) ago', '{{password_age}}') %]</i></div>
+ <div ng-show="password_age === '-1'"><i>[% l('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">
<h1 class="sr-only" i18n>Evergreen Staff Client Home Page</h1>
</div>
</div>
+ <div ng-if="password_reset_age && password_age >= (password_reset_age - 7)" class="alert alert-danger row">[% l('Your password is <b>[_1] days</b> old. It is recommended that passwords be updated every <b>[_2] days</b>.','{{password_age}}','{{password_reset_age}}') %]
+ <p ng-if="!can_self_update">
+ [% l('Please contact an administrator 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}}">
+ [% l('Update Password') %]
+ </a>
+ </div>
+ </div>
<br/>
<div class="row" id="splash-nav">
// 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()
];
});
}
+ 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);
$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 = {};