The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# @(#)Ident: DBIC.pm 2014-01-10 21:25 pjf ;

package CatalystX::Usul::Users::DBIC;

use strict;
use version; our $VERSION = qv( sprintf '0.17.%d', q$Rev: 1 $ =~ /\d+/gmx );

use CatalystX::Usul::Constants;
use CatalystX::Usul::Functions qw( emit sub_name io throw );
use CatalystX::Usul::Moose;
use Class::Usul::Time;
use TryCatch;

extends q(CatalystX::Usul::Users);

has 'dbic_role_model'       => is => 'ro', isa => Object, required => TRUE;

has 'dbic_user_model'       => is => 'ro', isa => Object, required => TRUE;

has 'dbic_user_roles_model' => is => 'ro', isa => Object, required => TRUE;

has 'get_features'          => is => 'ro', isa => HashRef,
   default                  => sub { { roles   => [ q(roles) ],
                                       session => TRUE, } };


has '_field_map' => is => 'ro', isa => HashRef,
   default       => sub { { password => q(crypted_password), id => q(uid), } };

# Interface methods
sub activate_account {
   my ($self, $file) = @_;

   my $username = $self->dequeue_activation_file( $file );

   $self->_execute( sub {
      my $user = $self->assert_user( $username ); $user->active( TRUE );

      $user->update; $self->_update_cache( $user ); return;
   } );

   return ('User [_1] account activated', $username);
}

sub assert_user {
   my $self     = shift;
   my $username = shift or throw 'User not specified';
   my $rs       = $self->dbic_user_model->search( { username => $username } );
   my $user     = $rs->first
      or throw error => 'User [_1] unknown', args => [ $username ];

   return $user;
}

sub create {
   my ($self, $fields) = @_;

   $self->_execute( sub {
      my $username = $fields->{username};
      my $p_name   = delete $fields->{profile};
      my $profile  = $self->profiles->find( $p_name );
      my $passwd   = $fields->{password}
                  || $profile->passwd || $self->def_passwd;
      my $src      = $self->dbic_user_model->result_source;
      my $cols;

      $passwd !~ m{ [*!] }msx
         and $fields->{password} = $self->_encrypt_password( $passwd );

      for ($src->columns) {
         defined $fields->{ $_ } and $cols->{ $_ } = $fields->{ $_ };
      }

      $self->dbic_user_model->create( $cols );

      $self->roles->is_member_of_role( $p_name, $username )
         or $self->roles->add_user_to_role( $p_name, $username );

      if ($profile->roles) {
         for my $role (split m{ , }mx, $profile->roles) {
            $self->roles->is_member_of_role( $role, $username )
               or $self->roles->add_user_to_role( $role, $username );
         }
      }

      $self->_update_cache( $self->assert_user( $username ) );
      return;
   } );

   return ('User [_1] account created', $fields->{username});
}

sub delete {
   my ($self, $username) = @_;

   $self->_execute( sub {
      $self->assert_user( $username )->delete;
      $self->_delete_user_from_cache( $username );
      return;
   } );

   return ('User [_1] account deleted', $username);
}

sub update {
   my ($self, $fields)  = @_;

   $self->_execute( sub {
      my $username = $fields->{username};
      my $user     = $self->assert_user( $username );
      my $src      = $self->dbic_user_model->result_source;

      for my $col ($src->columns) {
         defined $fields->{ $col } and $user->$col( $fields->{ $col } );
      }

      $user->update; $self->_update_cache( $user );
      return;
   } );

   return ('User [_1] account updated', $fields->{username});
}

sub update_password {
   my ($self, @rest) = @_; my ($force, $username) = @rest;

   $self->_execute( sub {
      my $user = $self->assert_user( $username );

      $user->password( $self->encrypt_password( @rest ) );
      $user->pwlast( $force ? 0 : int time / 86_400 );
      $user->update; $self->_update_cache( $user );
      return;
   } );

   return ('User [_1] password updated', $username);
}

sub user_report {
   my ($self, $args) = @_; my @lines = (); my (@flds, $line);

   my $fmt = $args && $args->{type} ? $args->{type} : q(text);

   for my $username (@{ $self->list }) {
      my $user_ref = $self->_get_user_ref( $username );
      my $passwd   = $user_ref->{crypted_password} || NUL;

      @flds = ( q(C) );
   TRY: {
      if ($passwd =~ m{ DISABLED }imsx) { $flds[ 0 ] = q(D); last TRY }
      if ($passwd =~ m{ EXPIRED }imsx)  { $flds[ 0 ] = q(E); last TRY }
      if ($passwd =~ m{ LEFT }imsx)     { $flds[ 0 ] = q(L); last TRY }
      if ($passwd =~ m{ NOLOGIN }imsx)  { $flds[ 0 ] = q(N); last TRY }
      if ($passwd =~ m{ [*!] }msx)      { $flds[ 0 ] = q(N); last TRY }
      } # TRY

      $flds[ 1 ] = $username;
      $flds[ 2 ] = $user_ref->{first_name}.q( ).$user_ref->{last_name};
      $flds[ 3 ] = $user_ref->{location};
      $flds[ 4 ] = $user_ref->{work_phone};
      $flds[ 5 ] = $user_ref->{project};
      $flds[ 6 ] = 'Never Logged In';

      unless ($fmt eq q(csv)) {
         $line = sprintf '%s %-8.8s %-20.20s %-10.10s %5.5s %-14.14s %-16.16s',
                         map { defined $_ ? $_ : q(~) } @flds[ 0 .. 6 ];
      }
      else { $line = join q(,), map { defined $_ ? $_ : NUL } @flds }

      push @lines, $line;
   }

   @lines = sort @lines; my $count = @lines;

   if ($fmt eq q(csv)) {
      unshift @lines, '#S,Login,Full Name,Location,Extn,Role,Last Login';
   }
   else {
      # Prepend header
      unshift @lines, q(_) x 80;
      $line  = 'S Login    Full Name            Location    ';
      $line .= 'Extn Role           Last Login';
      unshift @lines, $line;
      unshift @lines, 'Host: '.$self->host.' Printed: '.time2str;

      # Append footer
      push @lines, NUL, NUL;
      $line  = 'Status field key: C = Current, D = Disabled, ';
      $line .= 'E = Expired, L = Left, N = NOLOGIN';
      push @lines, $line;
      push @lines, '                  U = Unused';
      push @lines, "Total users $count";
   }

   unless ($fmt eq q(csv)) { emit @lines }
   else { io( $args->{path} )->println( join "\n", @lines  ) }

   return 'Here ends the user report';
}

# Private methods
sub _delete_user_from_cache {
   return delete $_[ 0 ]->cache->{users}->{ $_[ 1 ] };
}

sub _execute {
   my ($self, $f) = @_; my $key = __PACKAGE__.q(::_execute); my $res;

   $self->debug and $self->log->debug( __PACKAGE__.q(::).(sub_name 1) );
   $self->lock->set( k => $key );

   try        { $res = $f->() }
   catch ($e) { $self->lock->reset( k => $key ); throw $e }

   $self->lock->reset( k => $key );
   return $res;
}

sub _load {
   my ($self, $wanted) = @_;

   my $key; $self->lock->set( k => $key = __PACKAGE__.q(::_load) );

   my $cache = $self->cache; my $users = $cache->{users} ||= {};

   if ($wanted) {
      exists $users->{ $wanted } and defined $users->{ $wanted }
         and return $self->_cache_results( $key );

      try { $self->_update_cache( $self->assert_user( $wanted ) ) } catch {}
   }
   elsif (delete $cache->{_dirty}) {
      $cache->{users} = {};

      try {
         my $rs = $self->dbic_user_model->search( undef, {
            columns => [ qw(username) ] } );

         for my $username (map { $_->username } $rs->all) {
            $cache->{users}->{ $username } = $users->{ $username };
         }
      }
      catch ($e) { $self->lock->reset( k => $key ); throw $e }
   }

   return $self->_cache_results( $key );
}

sub _update_cache {
   my ($self, $user) = @_;

   my $map = $self->_field_map;
   my $src = $self->dbic_user_model->result_source;
   my $mcu = $self->cache->{users}->{ $user->username } ||= {};

   for my $col ($src->columns) {
      $mcu->{ $map->{ $col } || $col } = $user->$col;
   }

   return;
}

__PACKAGE__->meta->make_immutable;

1;

__END__

=pod

=head1 Name

CatalystX::Usul::Users::DBIC - Database user storage

=head1 Version

Describes v0.17.$Rev: 1 $

=head1 Synopsis

   use CatalystX::Usul::Users::DBIC;

   my $class = CatalystX::Usul::Users::DBIC;

   my $user = $class->new( $attr );

=head1 Description

User storage model for relational databases. This model makes use of
L<DBIx::Class>. It inherits from L<CatalystX::Usul::Users> and
implements the required list of factory methods

=head1 Configuration and Environment

Defines the following list of attributes

=over 3

=item dbic_role_model

Required schema object which represents roles

=item dbic_user_model

Required schema object which represents users

=item dbic_user_roles_model

Required schema object which represents the user / roles join table

=item field_map

A hash ref which maps the field names used by the user model onto the field
names used by this data store

=item get_features

A hash ref which details the features supported by the DBIC user data store

=back

=head1 Subroutines/Methods

=head2 activate_account

Searches the user store for the supplied user name and if it exists sets
the active column to true

=head2 assert_user

Returns a DBIC user object for the specified user or throws an exception
if the user does not exist

=head2 change_password

Calls C<update_password> in L<CatalystX::Usul::Identity::Users> with
the authenticate flag set to I<false>, thereby forcing the user to
authenticate. Passes the supplied arguments through

=head2 check_password

Calls C<authenticate> in L<CatalystX::Usul::Identity::Users>. Returns I<true>
if the authentication succeeded, I<false> otherwise

=head2 create

Creates a new user object on the user model. Adds the user to the list of
roles appropriate to the user profile

=head2 delete

Deletes a user object from the user model

=head2 get_primary_rid

Returns I<undef> as primary role ids are not supported by this storage
backend

=head2 get_user

Returns a hash ref of fields for the request user

=head2 get_users_by_rid

Returns an empty list as primary role ids are not supported by this storage
backend

=head2 is_user

Returns I<true> if the supplied user exists, I<false> otherwise

=head2 list

Returns a list reference of users in the database

=head2 set_password

Calls C<update_password> in L<CatalystX::Usul::Identity::Users> with
the authenticate flag set to I<true>, which bypasses user
authentication. Passes the supplied arguments through

=head2 update

Updates columns on the user object for the supplied user

=head2 update_password

Updates the users password in the database

=head2 user_report

Generate a report from the data in the user database

=head1 Diagnostics

None

=head1 Dependencies

=over 3

=item L<CatalystX::Usul::Users>

=item L<CatalystX::Usul::Moose>

=item L<Class::Usul::Time>

=item L<TryCatch>

=back

=head1 Incompatibilities

There are no known incompatibilities in this module

=head1 Bugs and Limitations

There are no known bugs in this module.
Please report problems to the address below.
Patches are welcome

=head1 Author

Peter Flanigan, C<< <Support at RoxSoft.co.uk> >>

=head1 License and Copyright

Copyright (c) 2014 Peter Flanigan. All rights reserved

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself. See L<perlartistic>

This program is distributed in the hope that it will be useful,
but WITHOUT WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE

=cut

# Local Variables:
# mode: perl
# tab-width: 3
# End: