+sub cust_main {
+ my $self = shift;
+ qsearchs('cust_main', { 'custnum' => $self->custnum } );
+}
+
+sub cust_pkg {
+ my $self = shift;
+ qsearch('cust_pkg', { 'contactnum' => $self->contactnum } );
+}
+
+=item by_selfservice_email EMAILADDRESS
+
+Alternate search constructor (class method). Given an email address,
+returns the contact for that address, or the empty string if no contact
+has that email address.
+
+=cut
+
+sub by_selfservice_email {
+ my($class, $email) = @_;
+
+ my $contact_email = qsearchs({
+ 'table' => 'contact_email',
+ 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
+ 'hashref' => { 'emailaddress' => $email, },
+ 'extra_sql' => " AND selfservice_access = 'Y' ".
+ " AND ( disabled IS NULL OR disabled = '' )",
+ }) or return '';
+
+ $contact_email->contact;
+
+}
+
+#these three functions are very much false laziness w/FS/FS/Auth/internal.pm
+# and should maybe be libraried in some way for other password needs
+
+use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
+
+sub authenticate_password {
+ my($self, $check_password) = @_;
+
+ if ( $self->_password_encoding eq 'bcrypt' ) {
+
+ my( $cost, $salt, $hash ) = split(',', $self->_password);
+
+ my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
+ cost => $cost,
+ salt => de_base64($salt),
+ },
+ $check_password
+ )
+ );
+
+ $hash eq $check_hash;
+
+ } else {
+
+ return 0 if $self->_password eq '';
+
+ $self->_password eq $check_password;
+
+ }
+
+}
+
+=item change_password NEW_PASSWORD
+
+Changes the contact's selfservice access password to NEW_PASSWORD. This does
+not check password policy rules (see C<is_password_allowed>) and will return
+an error only if editing the record fails for some reason.
+
+If NEW_PASSWORD is the same as the existing password, this does nothing.
+
+=cut
+
+sub change_password {
+ my($self, $new_password) = @_;
+
+ # do nothing if the password is unchanged
+ return if $self->authenticate_password($new_password);
+
+ $self->change_password_fields( $new_password );
+
+ $self->replace;
+
+}
+
+sub change_password_fields {
+ my($self, $new_password) = @_;
+
+ $self->_password_encoding('bcrypt');
+
+ my $cost = 8;
+
+ my $salt = pack( 'C*', map int(rand(256)), 1..16 );
+
+ my $hash = bcrypt_hash( { key_nul => 1,
+ cost => $cost,
+ salt => $salt,
+ },
+ $new_password,
+ );
+
+ $self->_password(
+ join(',', $cost, en_base64($salt), en_base64($hash) )
+ );
+
+}
+
+# end of false laziness w/FS/FS/Auth/internal.pm
+
+
+#false laziness w/ClientAPI/MyAccount/reset_passwd
+use Digest::SHA qw(sha512_hex);
+use FS::Conf;
+use FS::ClientAPI_SessionCache;
+sub send_reset_email {
+ my( $self, %opt ) = @_;
+
+ my @contact_email = $self->contact_email or return '';
+
+ my $reset_session = {
+ 'contactnum' => $self->contactnum,
+ 'svcnum' => $opt{'svcnum'},
+ };
+
+
+ my $conf = new FS::Conf;
+ my $timeout =
+ ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours';
+
+ my $reset_session_id;
+ do {
+ $reset_session_id = sha512_hex(time(). {}. rand(). $$)
+ } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
+ #just in case
+
+ $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
+
+ #email it
+
+ my $conf = new FS::Conf;
+
+ my $cust_main = $self->cust_main
+ or die "no customer"; #reset a password for a prospect contact? someday
+
+ my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
+ #die "selfservice-password_reset_msgnum unset" unless $msgnum;
+ return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
+ my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
+ my %msg_template = (
+ 'to' => join(',', map $_->emailaddress, @contact_email ),
+ 'cust_main' => $cust_main,
+ 'object' => $self,
+ 'substitutions' => { 'session_id' => $reset_session_id }
+ );
+
+ if ( $opt{'queue'} ) { #or should queueing just be the default?
+
+ my $queue = new FS::queue {
+ 'job' => 'FS::Misc::process_send_email',
+ 'custnum' => $cust_main->custnum,
+ };
+ $queue->insert( $msg_template->prepare( %msg_template ) );
+
+ } else {
+
+ $msg_template->send( %msg_template );
+
+ }
+
+}
+
+use vars qw( $myaccount_cache );
+sub myaccount_cache {
+ #my $class = shift;
+ $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
+ 'namespace' => 'FS::ClientAPI::MyAccount',
+ } );
+}
+
+=item cgi_contact_fields
+
+Returns a list reference containing the set of contact fields used in the web
+interface for one-line editing (i.e. excluding contactnum, prospectnum, custnum
+and locationnum, as well as password fields, but including fields for
+contact_email and contact_phone records.)
+
+=cut
+
+sub cgi_contact_fields {
+ #my $class = shift;
+
+ my @contact_fields = qw(
+ classnum first last title comment emailaddress selfservice_access
+ );
+
+ push @contact_fields, 'phonetypenum'. $_->phonetypenum
+ foreach qsearch({table=>'phone_type', order_by=>'weight'});
+
+ \@contact_fields;
+
+}
+
+use FS::phone_type;
+