RSS Git Download  Clone
Raw Blame History
package LIMS::Controller::Admin::User;

use strict;

use base 'LIMS::Controller::Admin';
use LIMS::Local::Utils;

use Moose;
with (
	'LIMS::Controller::Roles::User', # generate_new_password
    'LIMS::Controller::Roles::FormData', # validate_form_params
);

__PACKAGE__->meta->make_immutable(inline_constructor => 0);

__PACKAGE__->authz->authz_runmodes( ':all' => 'do_admin' );

use Data::Dumper;

#-------------------------------------------------------------------------------
sub default : StartRunmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift; # $self->stash( errs => $errs );

	my $model = $self->model('User');

    my %data = (
        users     => $model->get_all_users({sort_by => 'username'}),
        groups    => $model->get_user_groups({sort_by => 'group_label'}),
        locations => $model->get_user_locations({sort_by => 'location_name'}),
    );

    if ( my $id = $self->param('id') || $self->query->param('id') ) {
        # get user details:
        my $this_user_details = $self->_get_user_details($id);
			# $self->debug([keys %$user_details]);

        # load this users details into %data:
        map {
            $data{$_} = $this_user_details->{$_};
        } keys %$this_user_details;
    }

    map $self->tt_params($_ => $data{$_}), keys %data;
		# $self->debug([ keys %data ]);

    # get js validation foo_onsubmit & foo_dfv_js vars into template:
    $self->js_validation_profile('user_details');

    return $self->tt_process($errs);
}

#-------------------------------------------------------------------------------
sub get_user : Runmode {
    my $self = shift;
    
    my $username = $self->query->param('username'); # warn $username;

    my $user = $self->model('User')->get_user_by_username($username);

    # add user.id as param for default():
    $self->param( id => $user->id ); 
    return $self->forward('default');
}

#-------------------------------------------------------------------------------
# used to create new user and to update existing user details:
sub update_user_details : Runmode { 
    my $self = shift; $self->_debug_path($self->get_current_runmode);

	my $user_id = $self->param('id');
	
	# put id (if submitted) into params() for validate('user_details') profile:	
    if ( $user_id ) { # warn $user_id;
        $self->query->param( _record_id => $user_id );
    }

	my $dfv = $self->check_rm('default', $self->validate('user_details') )
	|| return $self->dfv_error_page;

	my $data = $dfv->valid
	|| return $self->forward('default'); # eg if empty param; # $self->debug($data);

	if ($user_id) {
		# provide 'id' if supplied, so record updated, otherwise new one created: 
		$data->{id} = $user_id;
		
		# load existing user (if it's an 'update user' action):
		my $user = $self->model('User')->get_user_profile($user_id)
    	|| return $self->error("Cannot retreive user details for id=$user_id");
		
		# check if password has been changed - if form pwd not same as retrieved
		# pwd, must have been changed & will need encrypting:
		if ($data->{password} ne $user->password) {
			# encrypt password using SHA1 (or MD5):
			my $sha1 = LIMS::Local::Utils::sha1_digest($data->{password});
			$data->{password} = $sha1; # $self->debug('sha1:'.$sha1);
		}
		# set message:
		$self->stash( user_update_action => 'edit_success' );
	}
	# it's a 'create new user' action, so encrypt password:
	else {
		my $sha1 = LIMS::Local::Utils::sha1_digest($data->{password});
		$data->{password} = $sha1; # $self->debug('sha1:'.$sha1);
		# set message:
		$self->stash( user_update_action => 'create_success' );
	}

    my $rtn = $self->model('User')->update_user($data);

	if ( $rtn ) {
        $self->error($rtn);
	}
	else { # redirect after db edit:
		# set MessageStack msg set above:
		my $action = $self->stash->{user_update_action};
		$self->flash( info => $self->messages('admin')->{user}->{$action} );
        
        if (! $user_id) { # use new 'username' to get new record id:
            my $username = $data->{username};
            my $user = $self->model('User')->get_user_by_username($username);
            $user_id = $user->id;
        }
        $self->redirect( $self->query->url . '/admin/user/default/'.$user_id );
	}
}

#-------------------------------------------------------------------------------
sub edit_permissions : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    my $id = $self->param('id')
	|| return $self->error( 'no id passed to '.$self->get_current_runmode );

    my $data = $self->_get_user_details($id);

    map $self->tt_params($_ => $data->{$_}), keys %$data;

    return $self->tt_process('admin/user/edit_permissions.tt');
}

#-------------------------------------------------------------------------------
sub reset_user_permissions : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    my $id = $self->param('id')
	|| return $self->error( 'no id passed to '.$self->get_current_runmode );

    my $user = $self->model('User')->get_user_profile($id);

    $self->tt_params( user => $user );

    # need confirmation before resetting permissions:
    return $self->tt_process('admin/user/reset_permissions.tt')
        unless $self->query->param('confirm_reset');

    my $rtn = $self->model('User')->delete_user_permissions($id);

    return $rtn ?
        $self->error($rtn) : # redirect after db edit:
            $self->redirect( $self->query->url . '/admin/user/default/'.$id );
}

#-------------------------------------------------------------------------------
sub update_user_permissions : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    my $user_id = $self->param('id')
	|| return $self->error('no id passed to '.$self->get_current_runmode);

    my @function_ids = $self->query->param('function_id'); # $self->debug(\@function_ids);

    my %args = (
        function_ids => \@function_ids,
        user_id      => $user_id,
    );

    my $rtn = $self->model('User')->update_user_permissions(\%args);

    return $rtn  ?
        $self->error($rtn) :
            $self->redirect( $self->query->url . '/admin/user/default/'.$user_id );
}

#-------------------------------------------------------------------------------
sub delete_user : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    my $user_id = $self->param('id')
	|| return $self->error('no id passed to '.$self->get_current_runmode);

    my $user = $self->model('User')->get_user_profile($user_id);

    $self->tt_params( user => $user );

    # need confirmation before deleting user:
    return $self->tt_process('admin/user/delete_user.tt')
        unless $self->query->param('confirm_reset');

    my $rtn = $self->model('User')->delete_user($user_id);

    return $rtn ?
        $self->error($rtn) : # redirect after db edit:
            $self->redirect( $self->query->url . '/admin/user' );
}

#-------------------------------------------------------------------------------
sub new_user : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
	
	my $vars = $self->query->Vars(); # warn Dumper $vars;

    # extract user details from textfield:
	my $src_data = $self->_parse_message($vars->{src_data});
    return $self->tt_process() unless $src_data; # ie 1st call to method
    
	if ($vars->{preview}) {
        return $self->_preview_new_user($src_data);
	}
	elsif ($vars->{post}) {
        # generate new plain text password:
        my $password = $self->generate_new_password();
        # encrypt password using SHA1 (or MD5) for model:
        my $sha1_pwd = LIMS::Local::Utils::sha1_digest($password); 
        
        # store as plain_txt_pwd for .tt and email:
        $src_data->{plain_txt_pwd} = $password;
        # store sha1-encrypted password for model/db:
        $src_data->{password} = $sha1_pwd; # $self->debug($sha1_pwd);    
        
        # check we have user group id or return to form:
		unless ( $vars->{group_id} ) {
			my $message = $self->_get_msg_body($src_data);
			$src_data->{message} = $message; # warn Dumper $src_data;
			return $self->_preview_new_user($src_data); # return to 'preview' page
		}
        $src_data->{group_id} = $vars->{group_id};
        
		{ # set last_login timestamp to 1 month before auto-expiry:
			my $today = LIMS::Local::Utils::today();
			$src_data->{last_login} = $today->clone->subtract( months => 5 );
            # add epoch time as token to msg for 1st login link:
            $src_data->{epoch} = $src_data->{last_login}->epoch;		
		}		
        { # execute update_user method & get last_insert_id:
            my $rtn = $self->model('User')->update_user($src_data);    
            return $self->error($rtn) if $rtn;

            $src_data->{uid} = # add new user ID to msg for 1st login link:
				$self->lims_db->dbh->last_insert_id(undef, undef, 'users', 'id');
        }
        { # send new registration details to new user:
			my $message = $self->_get_msg_body($src_data);
			$src_data->{message} = $message; # warn Dumper $src_data;
            my $rtn = $self->_email_registration_details($src_data);
            return $self->error($rtn->string) if $rtn->type ne 'success';
        }
        { # set success flash message:
            my $msg = $self->messages('admin')->{user}->{create_success};
            $self->flash( info => $msg );
            # set flag for tt to avoid loading warning msgs:
            $self->tt_params( new_user_create_success => 1 );
        }
	}
	
	return $self->tt_process();	
}

#-------------------------------------------------------------------------------
sub _get_msg_body {
	my $self = shift;
	my $data = shift;

	my $message_body = do { # turn off POST_CHOMP to control formatting in .tt:
        local $self->cfg('tt_config')->{TEMPLATE_OPTIONS}->{POST_CHOMP} = undef;
        $self->tt_params( user_details => $data );
		$self->tt_process('admin/user/new_user_msg.tt');
	}; # warn Dumper $message_body;
	return ${$message_body}; # return deref'ed string		
}

#-------------------------------------------------------------------------------
sub _preview_new_user {
    my $self = shift; $self->_debug_path();
    my $data = shift;
    
    # get user groups:
    my $groups
        = $self->model('User')->get_user_groups({sort_by => 'group_label'});
    # check role/designation:
    my $has_role = $self->model('User')->check_user_roles($data->{designation});    
    
	$self->tt_params(
        user_details => $data,
        user_groups  => $groups,
        has_role     => $has_role,
    );
    
    { # validate data:
        my $profile = $self->validate('user_details');
        # add dummy vals for validation profile:
        @{$data}{qw(password group_id active)} = (1,1,1); # just needs to exist
        my $dfv = $self->validate_form_params($profile, $data); # warn Dumper $dfv;        
        if ($dfv->has_invalid || $dfv->has_missing) {
            $self->tt_params( errs => $dfv->msgs );
        }
    }
    return $self->tt_process(); # will use caller's template
}

#-------------------------------------------------------------------------------
sub _email_registration_details {
    my $self = shift; $self->_debug_path();
    my $data = shift;
    
    my $subject = sprintf 'HILIS registration details (%s, %s)',
        uc $data->{last_name}, ucfirst lc $data->{first_name};

    my $cfg = $self->cfg('settings');

    my %mail_data = (
        recipient => $data->{email},
        subject   => $subject,
        message   => $data->{message},
        config    => $cfg,
    ); # $self->debug(\%mail_data);

    my $rtn = $self->model('Email')->send_message(\%mail_data); # Return::Value object
    
    # confirmation to admin:
    {
        $mail_data{recipient} = $cfg->{admin_contact};
        $self->model('Email')->send_message(\%mail_data);
    }
    
    return $rtn;
}

#-------------------------------------------------------------------------------
# returns hashref of selected_user (users details), permissions_type
# (custom/default), user_permissions (list)
sub _get_user_details {
    my $self    = shift; $self->_debug_path();
    my $user_id = shift;

    my %data;

    my $this_user = $data{selected_user}
		= $self->model('User')->get_user_profile($user_id); # hashref

    # load custom user permissions (if any):
    my $this_user_permissions
		= $self->model('User')->get_user_permissions($user_id); # arrayref

    # flag for template (if permissions set here - it's custom):
    $data{permissions_type} = @$this_user_permissions ? 'custom' : 'default';

    # if no user_permissions, load default settings for this users' group:
    if (! @$this_user_permissions) {
        $this_user_permissions
			= $self->model('User')->get_user_group_functions($this_user->group_id);
    }

    # get user_permissions, with active => 1 if function.id also in user_group_functions table:
    $data{user_permissions} = $self->get_user_functions($this_user_permissions); # in LIMS::Admin::Controller

    return \%data;
}

#-------------------------------------------------------------------------------
sub _parse_message {
	my ($self, $msg) = @_; $self->_debug_path(); # warn Dumper $msg;
    
    $msg = LIMS::Local::Utils::trim($msg);
    return 0 if ! $msg; # warn Dumper $msg; # in case submitted empty except spaces
	
	my $details = {
		first_name  => $msg =~ /First Name: (.*)/,
		last_name   => $msg =~ /Last Name: (.*)/,
		email       => $msg =~ /E-mail: (.*)/,
		location    => $msg =~ /Source: (.*)/,
		designation => $msg =~ /Position: (.*)/,
	}; # warn Dumper $details;

	map { # trim white-space:
		$details->{$_} = LIMS::Local::Utils::trim($details->{$_})
	} keys %$details; # warn Dumper $details;

	# add user_location_id if user location exist:
    unless ($details->{user_location_id}) { # unless already set in 'preview':
        my $location = $details->{location} || return 0; # unlikely, but check
        my $o = $self->model('User')->get_location_by_name($location);
        if ($o) { $details->{user_location_id} = $o->id }
    }
    
	# enforce case on names:
	$details->{last_name}  = lc $details->{last_name};
	$details->{first_name} = lc $details->{first_name};
	$details->{email}      = lc $details->{email};
  
	my $max_tries = length($details->{first_name}) - 1;
  
	my $username = $details->{last_name};
	
	# loop through each first_name character, appending to last_name
	# until we have a unique user_id: 
	CHAR: for my $i ( 0 .. $max_tries ) {
		# if already used all first_name chars:
		if ($i == $max_tries) {
			$username = '**** FULL NAME ALREADY IN USE ****';
			last CHAR;
		}
    
		my $next_character = (split '', $details->{first_name})[$i];
    
		# if username exists, append successive letters from first_name & try again
		if ( $self->model('User')->get_user_by_username($username) ) {
			$username .= $next_character;
			next CHAR;
		}
		# else username is unique so exit loop:
		last CHAR;
	}
	
	$details->{username} = $username;
    
	return $details;
}

=begin # needs a bit more work - eg tt can't access c.cfg or app_url, requires
cpan modules MIME::Base64 & Authen::SASL for smtp authentication 
#-------------------------------------------------------------------------------
sub _email_registration_details {
    my $self = shift; $self->_debug_path();
    my $data = shift;

	use Email::Template;
	
	my $path_to_app_root = $self->cfg('path_to_app_root');

    my $subject = sprintf 'HILIS registration details (%s, %s)',
        uc $data->{last_name}, ucfirst lc $data->{first_name};

	my @smtp = ('smtp', 'smtp.hmds.org.uk', AuthUser => 'ra.jones@hmds.org.uk',
		AuthPass => 'tetramor' );

	my %args = (
		From    => $self->cfg('settings')->{email_from} || 'hmds.lth@nhs.net',
		To      => $data->{email},
		Subject => $subject,

		tt_new  => { INCLUDE_PATH => $path_to_app_root.'/templates' },
		tt_vars => { user_details => $data },
		convert => { rightmargin => 80, no_rowspacing => 1 },
		mime_lite_send => \@smtp,
	);
	
	Email::Template->send( 'admin/user/new_user_msg.htm', \%args )
	or warn "could not send the email";
}
=cut

1;