#TODO: send password: parse customizable mail with template #TODO: currently only working on apache mod_perl! (r->{...}) =head1 NAME Konstrukt::Plugin::usermanagement::basic - Basic user management. =head1 SYNOPSIS =head2 Tag interface <& usermanagement::basic show="login, register, sendpass, changepass" / &> <& usermanagement::basic show="login" / &> =head2 Perl interface my $user_basic = use_plugin 'usermanagement::basic' or die; my $id = $user_basic->id(); #will return the user's id, if logged in. 0 otherwise $user_basic->data($id); #will return { email => ..} #of the user with the given id $user_basic->email($id); #will return the users email address #... =head1 DESCRIPTION This Konstrukt plugin provides basic user management functionality. It will care for persistence by using the Konstrukt session management and it allows other plugins to access basic user data like the users id, if logged in. my $user_basic = use_plugin 'usermanagement::basic' or die; my $id = $user_basic->id(); #will return the user's id, if logged in. 0 otherwise $user_basic->data($id); #will return { email => ..} #of the user with the given id $user_basic->email($id); #will return the users email address It may also be used for user athentication on your website to provide log-in/ log-out-screens etc: <& usermanagement::basic show="login, register, sendpass, changepass" / &> The "login"-form will enable the user to log in with its email/password if not logged in or a log-off button otherwise. The "register"-form will enable the user to register itself with its email for the service. A random password will be generated and sent to the given email address. The "sendpass"-form will enable the user to get its password sent to its email address. The "changepass"-form will enable the user to change its password. Disabling some forms will let you split the forms into several pages. =head1 EVENTS This plugin triggers these L: =over =item * C =item * C =back Each with the user id as an argument. So your plugin might register for those events, if it wants to react on these events. =head1 CONFIGURATION You have to do some konstrukt.settings-configuration to let the plugin know where to get its data and which layout to use: #backend usermanagement/basic/backend DBI See the documentation of the backend modules (e.g. L) for their configuration. #layout usermanagement/basic/template_path /templates/usermanagement/basic/ =cut package Konstrukt::Plugin::usermanagement::basic; use strict; use warnings; use base 'Konstrukt::Plugin'; #inheritance use Konstrukt::Plugin; #import use_plugin use Konstrukt::Debug; use Konstrukt::Session; use Digest::SHA; =head1 METHODS =head2 init Initializes this object. Sets $self->{backend} and $self->{template_path}. C will be called the first time this plugin is needed each request. =cut sub init { my ($self) = @_; #depends on session management unless ($Konstrukt::Session->activated()) { $Konstrukt::Debug->error_message("Cannot initialize because the session management seems to be deactivated"); return undef; } #set default settings $Konstrukt::Settings->default("usermanagement/basic/backend" => 'DBI'); $Konstrukt::Settings->default("usermanagement/basic/template_path" => '/templates/usermanagement/basic/'); $self->{backend} = use_plugin "usermanagement::basic::" . $Konstrukt::Settings->get("usermanagement/basic/backend") or return undef; $self->{template_path} = $Konstrukt::Settings->get('usermanagement/basic/template_path'); return 1; } #= /init =head2 install Installs the templates. B none =cut sub install { my ($self) = @_; return $Konstrukt::Lib->plugin_file_install_helper($self->{template_path}); } # /install =head2 execute_again Yes, this plugin may return dynamic nodes (i.e. template nodes). =cut sub execute_again { return 1; } #= /execute_again =head2 prepare Prepare method B: =over =item * $tag - Reference to the tag (and its children) that shall be handled. =back =cut sub prepare { my ($self, $tag) = @_; #Don't do anything beside setting the dynamic-flag $tag->{dynamic} = 1; return undef; } #= /prepare =head2 execute Execute method B: =over =item * $tag - Reference to the tag (and its children) that shall be handled. =back =cut sub execute { my ($self, $tag) = @_; #reset the collected nodes $self->reset_nodes(); my $action = $Konstrukt::CGI->param('action'); my $uid = $self->id(); #default: show all $tag->{tag}->{attributes}->{show} = 'login, register, sendpass, changepass' unless defined $tag->{tag}->{attributes}->{show}; #grab "show" tag attributes my $show = {}; foreach my $key (split /\s*,\s*/, $tag->{tag}->{attributes}->{show}) { $show->{lc($key)} = 1; } #phase 1: work #process the passed action. show the messages if (defined $action) { if ($action eq 'logout' and $show->{login} and $uid) { $self->logout(); } elsif ($action eq 'login' and $show->{login}) { $self->login(); } elsif ($action eq 'deregister' and $show->{register} and $uid) { $self->deregister(); } elsif ($action eq 'register' and $show->{register}) { $self->register(); } elsif ($action eq 'sendpass' and $show->{sendpass} and !$uid) { $self->sendpass(); } elsif ($action eq 'changepass' and $show->{changepass} and $uid) { $self->changepass(); } } #this one may have changed within the processes above $uid = $self->id(); #phase 2: show #display the forms if ($show->{login}) { if ($uid) { $self->logout_show(); } else { $self->login_show(); } } if ($show->{register}) { if ($uid) { $self->deregister_show(); } else { $self->register_show(); } } if ($show->{sendpass} and !$uid) { $self->sendpass_show(); } if ($show->{changepass} and $uid) { $self->changepass_show(); } return $self->get_nodes(); } #= /execute =head2 register_show Displays the register form. =cut sub register_show { my ($self) = @_; my $template = use_plugin 'template'; $self->add_node($template->node("$self->{template_path}layout/registration_form.template")); return 1; } #= &/register_show =head2 register Takes the HTTP form input and tries to register a new user. The user will be added to the database and an email will be sent to the users email address. Returns a confirmation of the successful registration or error messages otherwise =cut sub register { my ($self) = @_; my $form = use_plugin 'formvalidator'; $form->load("$self->{template_path}layout/registration_form.form"); $form->retrieve_values('cgi'); my $log = use_plugin 'log'; if ($form->validate()) { my $email = $form->get_value('email'); my $rv = $self->{backend}->register($email); my $template = use_plugin 'template'; if ($rv > 0) { #success. $rv is the user id $log->put(__PACKAGE__ . '->register', "$email registered with user id $rv.", $email, $rv); $Konstrukt::Event->trigger("Konstrukt::Plugin::usermanagement::basic::registered", $rv); $self->add_node($template->node("$self->{template_path}messages/registration_successful.template")); $self->sendpass($email); return 1; } else { #failed if ($rv == -1) { #email address already exists in the database $self->add_node($template->node("$self->{template_path}messages/registration_failed_email_exists.template", { email => $email })); return 1; } else { $self->add_node($template->node("$self->{template_path}messages/registration_failed.template")); return undef; } } } else { $self->add_node($form->errors()); return undef; } } #= /register =head2 deregister_show Displays the deregister form. =cut sub deregister_show { my ($self) = @_; my $template = use_plugin 'template'; $self->add_node($template->node("$self->{template_path}layout/deregistration_form.template")); return 1; } #= &/deregister_show =head2 deregister Takes the HTTP form input and tries to deregister an existing user. The user will be removed from the database. Returns a confirmation of the successful deregistration or error messages otherwise =cut sub deregister { my ($self) = @_; my $template = use_plugin 'template'; my $log = use_plugin 'log'; if ($Konstrukt::CGI->param('confirmation')) { my $uid = $self->id(); my $email = $self->email($uid); my $success = $self->{backend}->deregister($uid); if ($success) { $Konstrukt::Session->set('user_id', 0); $log->put(__PACKAGE__ . '->deregister', $email . " with user $uid deregistered.", $email, $uid); $Konstrukt::Event->trigger("Konstrukt::Plugin::usermanagement::basic::deregistered", $uid); $self->add_node($template->node("$self->{template_path}messages/deregistration_successful.template")); return 1; } else { $self->add_node($template->node("$self->{template_path}messages/deregistration_failed.template")); return undef; } } else { $self->add_node($template->node("$self->{template_path}messages/deregistration_failed_not_confirmed.template")); return undef; } } #= /deregister =head2 login_show Displays the login form. =cut sub login_show { my ($self) = @_; my $template = use_plugin 'template'; $self->add_node($template->node("$self->{template_path}layout/login_form.template")); return 1; } #= /login_show =head2 login Takes the HTTP form input and tries to login the user. The user id will be saved inside the session. Returns a confirmation of the successful login or error messages otherwise. B: =over =item * $email - Optional: Username =item * $pass - Optional: Password =back =cut sub login { my ($self, $email, $pass) = @_; my $template = use_plugin 'template'; my $log = use_plugin 'log'; if (!$email or !$pass) { #get data from input form my $form = use_plugin 'formvalidator'; $form->load("$self->{template_path}layout/login_form.form"); $form->retrieve_values('cgi'); if (!$form->validate()) { $self->add_node($form->errors()); return undef; } else { $email = $form->get_value('email'); $pass = $form->get_value('pass'); } } if ($email and $pass) { #check login my $pass = Digest::SHA->new(256)->add($pass)->hexdigest(); my $uid = $self->{backend}->check_login($email, $pass); if ($uid) { #login successful $Konstrukt::Session->set('user_id', $uid); $Konstrukt::Session->set('failed_logins', 0); $log->put(__PACKAGE__ . '->login', $email . ' logged in.', $email, $uid); $self->add_node($template->node("$self->{template_path}messages/login_successful.template")); return 1; } else { #login failed $Konstrukt::Session->set('failed_logins', ($Konstrukt::Session->get('failed_logins') || 0) + 1); $log->put(__PACKAGE__.'->login', $email . ' entered the wrong password 3 times in a row.', $email, $uid) if $Konstrukt::Session->get('failed_logins') == 3; $self->add_node($template->node("$self->{template_path}messages/login_failed.template")); return undef; } } else { #login failed $self->add_node($template->node("$self->{template_path}messages/login_failed.template")); return undef; } } #= /login =head2 logout_show Displays the logout form. =cut sub logout_show { my ($self) = @_; my $template = use_plugin 'template'; $self->add_node($template->node("$self->{template_path}layout/logout_form.template", { email => $self->email() })); return 1; } =head2 logout Logs out the current user. The user id will be removed from the session. =cut sub logout { my ($self) = @_; my $template = use_plugin 'template'; $Konstrukt::Session->set('user_id', 0); $self->add_node($template->node("$self->{template_path}messages/logout_successful.template", { email => $self->email() })); return 1; } #= /logout =head2 sendpass_show Displays the "send password" form. =cut sub sendpass_show { my ($self) = @_; my $template = use_plugin 'template'; $self->add_node($template->node("$self->{template_path}layout/send_password_form.template")); return 1; } #= /sendpass_show =head2 sendpass Generates a new password for the user and sends an email with the password to the user. The email address may be passed as a parameter to this sub. If not passed, it will be received from the HTTP parametes. Returns a confirmation of the successfully sent email or error messages otherwise. B: =over =item * $email - Optional: The email address of the user to whom the pass should be sent =back =cut sub sendpass { my ($self, $email) = @_; my $template = use_plugin 'template'; my $log = use_plugin 'log'; if (!$email) { #get email from input form my $form = use_plugin 'formvalidator'; $form->load("$self->{template_path}layout/send_password_form.form"); $form->retrieve_values('cgi'); if (!$form->validate()) { $self->add_node($form->errors()); return undef; } else { $email = $form->get_value('email'); } } my $uid = $self->{backend}->get_id_from_email($email); if ($uid) { #get some user info my $email = $self->email($uid); #generate new password my $plain_pass = $Konstrukt::Lib->random_password(12); my $pass = Digest::SHA->new(256)->add($plain_pass)->hexdigest(); $self->{backend}->set_password($uid, $pass); #generate and send mail my $mailfile = $Konstrukt::File->read("$self->{template_path}layout/send_password_mail.email"); if (defined($mailfile)) { my $mail; eval($mailfile); #Check for errors if ($@) { #Errors in eval chomp($@); $Konstrukt::Debug->error_message("Error while loading mail template '$self->{template_path}layout/send_password_mail.email'! $@") if Konstrukt::Debug::ERROR; $self->add_node($template->node("$self->{template_path}messages/send_password_failed.template")); return 0; } else { $mail->{body} =~ s/\$pass\$/$plain_pass/gi; $mail->{body} =~ s/\$email\$/$email/gi; if ($Konstrukt::Lib->mail($mail->{subject}, $mail->{body}, $email)) { $log->put(__PACKAGE__ . '->sendpass', "$email got a new password sent.", $email); $self->add_node($template->node("$self->{template_path}messages/send_password_successful.template", { email => $email })); return 1; } else { $self->add_node($template->node("$self->{template_path}messages/send_password_failed.template")); return 0; } } } else { $self->add_node($template->node("$self->{template_path}messages/send_password_failed.template")); return 0; } } else { $self->add_node($template->node("$self->{template_path}messages/send_password_failed.template")); return 0; } } #= /sendpass =head2 changepass_show Displays the "change password" form. =cut sub changepass_show { my ($self) = @_; my $template = use_plugin 'template'; $self->add_node($template->node("$self->{template_path}layout/change_password_form.template")); return 1; } =head2 changepass Changes the password of the user that is currently logged in. The old password will be checked and the new ones must be identical. The parameters are optional. They will be received from HTTP parameters, if not specified. Returns a confirmation of the successful password change or error messages otherwise. B (optional): =over =item * $old_pass - The old password. =item * $new_pass - The new password. =item * $new_pass2 - The new password (confirmation). =back =cut sub changepass { my ($self, $old_pass, $new_pass, $new_pass2) = @_; my $template = use_plugin 'template'; my $log = use_plugin 'log'; if (!($old_pass and $new_pass and $new_pass2)) { my $form = use_plugin 'formvalidator'; $form->load("$self->{template_path}layout/change_password_form.form"); $form->retrieve_values('cgi'); if (!$form->validate()) { $self->add_node($form->errors()); return undef; } else { $old_pass = $form->get_value('old_pass'); $new_pass = $form->get_value('new_pass'); $new_pass2 = $form->get_value('new_pass2'); } } $old_pass = Digest::SHA->new(256)->add($old_pass)->hexdigest(); $new_pass = Digest::SHA->new(256)->add($new_pass)->hexdigest(); $new_pass2 = Digest::SHA->new(256)->add($new_pass2)->hexdigest(); my $uid = $self->id(); my $userdata = $self->{backend}->get_data($uid); if ($old_pass eq $userdata->{password}) { if ($new_pass eq $new_pass2) { if ($self->{backend}->set_password($uid, $new_pass)) { $log->put(__PACKAGE__.'->changepass', $userdata->{email} . " changed its password.", $userdata->{email}); $self->add_node($template->node("$self->{template_path}messages/change_password_successful.template")); return 1; } else { $self->add_node($template->node("$self->{template_path}messages/change_password_failed.template")); return undef; } } else { $self->add_node($template->node("$self->{template_path}messages/change_password_failed_unmatched.template")); return undef; } } else { $self->add_node($template->node("$self->{template_path}messages/change_password_failed_wrong_pass.template")); return undef; } } #= /changepass =head2 id Returns the user id of the user, which is currently logged in, 0 if not logged in. =cut sub id { my ($self) = @_; return $Konstrukt::Session->get('user_id') || 0; } #= /id =head2 email Returns the user's email address, if uid exists, undef otherwise. =cut sub email { my ($self, $id) = @_; $id ||= $self->id(); my $data = $self->data($id); return (exists($data->{email}) ? $data->{email} : undef); } #= /email =head2 data Returns all relevant user data as an anonymous hash, if uid exists: { email => 'a@b.c', pass => '' } Returns an empty hash if the uid doesn't exist. B (optional): =over =item * $uid - The user id (optional) =back =cut sub data { my ($self, $id) = @_; $id ||= $self->id(); return $self->{backend}->get_data($id); } #= /data 1; =head1 AUTHOR Copyright 2006 Thomas Wittek (mail at gedankenkonstrukt dot de). All rights reserved. This document is free software. It is distributed under the same terms as Perl itself. =head1 SEE ALSO L, L, L =cut __DATA__ -- 8< -- textfile: layout/change_password_form.form -- >8 -- $form_name = 'changepass'; $form_specification = { old_pass => { name => 'Old password (not empty)' , minlength => 1, maxlength => 256, match => '' }, new_pass => { name => 'New password (not empty)' , minlength => 1, maxlength => 256, match => '' }, new_pass2 => { name => 'New password (confirmation)', minlength => 1, maxlength => 256, match => '' }, }; -- 8< -- textfile: layout/change_password_form.template -- >8 -- <& formvalidator form="change_password_form.form" / &>

Change password

Empty passwords are not allowed!

For the technically minded: The passwords are stored as a SHA-256 hash. There is not much reason for paranoia. And no, the passwords don't get logged...





-- 8< -- textfile: layout/deregistration_form.form -- >8 -- $form_name = 'deregister'; $form_specification = { confirmation => { name => 'Confirmation of deregistration', minlength => 0, maxlength => 1, match => '1' }, }; -- 8< -- textfile: layout/deregistration_form.template -- >8 -- <& formvalidator form="deregistration_form.form" / &>

Deregistration

If you don't need your user account anymore, you can deregister yourself of course. All your user data will be deleted.



-- 8< -- textfile: layout/login_form.form -- >8 -- $form_name = 'login'; $form_specification = { email => { name => 'Email address (name@domain.tld)', minlength => 1, maxlength => 256, match => '^.+?\@.+\..+$' }, pass => { name => 'password (not empty)' , minlength => 1, maxlength => 256, match => '' }, }; -- 8< -- textfile: layout/login_form.template -- >8 -- <& formvalidator form="login_form.form" / &>

Log in




-- 8< -- textfile: layout/logout_form.template -- >8 --

Log out

You are logged in with the email address '<+$ email / $+>'.


-- 8< -- textfile: layout/registration_form.form -- >8 -- $form_name = 'register'; $form_specification = { email => { name => 'Email address (name@domain.tld)', minlength => 1, maxlength => 256, match => '^.+?\@.+\..+$' }, }; -- 8< -- textfile: layout/registration_form.template -- >8 -- <& formvalidator form="registration_form.form" / &>

Register

You may create a user account, to use some advanced functions of this site. For example you can write blog entries or manage bookmarks and appointments when your account has been activated for this functions.

All you need for registration is a valid email address.

After the registration you'll get an initial password sent to your email address. After the login you can change your password and your personal data.

Note that this email address will only be used internally and will not be published! If you want to publish your email address to other users, you can add it to your personal data page.



-- 8< -- textfile: layout/send_password_form.form -- >8 -- $form_name = 'sendpass'; $form_specification = { email => { name => 'Email address (name@domain.tld)', minlength => 1, maxlength => 256, match => '^.+?\@.+\..+$' }, }; -- 8< -- textfile: layout/send_password_form.template -- >8 -- <& formvalidator form="send_password_form.form" / &>

Send password

If you lost your password, you can get a new one sent to your email address

This new password can be changed later, of course



-- 8< -- textfile: layout/send_password_mail.email -- >8 -- $mail = { subject => 'Your password', body => "With this password you have access to the advanced features of this website\nYour temporary password for the email address \$email\$ is: \$pass\$\nYou can change the password after your login.", } -- 8< -- textfile: messages/change_password_failed.template -- >8 --

Password not changed

An internal error occurred while updating the password

-- 8< -- textfile: messages/change_password_failed_unmatched.template -- >8 --

Password not changed

The passwords don't match.

The password has not been changed.

-- 8< -- textfile: messages/change_password_failed_wrong_pass.template -- >8 --

Password not changed

The entered password is wrong.

The password has not been changed.

-- 8< -- textfile: messages/change_password_successful.template -- >8 --

Password updated

The password has been updated successfully.

-- 8< -- textfile: messages/deregistration_failed.template -- >8 --

Deregistration failed

An internal error occurred during the deregistration.

-- 8< -- textfile: messages/deregistration_failed_not_confirmed.template -- >8 --

Deregistration failed

The deregistration has failed.

The confirmation checkbox was not checked!

-- 8< -- textfile: messages/deregistration_successful.template -- >8 --

Deregistration successful

Your account has been removed from the database.

-- 8< -- textfile: messages/login_failed.template -- >8 --

Login failed

The login has failed.

Either a wrong username password has been entered.

-- 8< -- textfile: messages/login_successful.template -- >8 --

Login successful

You have logged in successfully.

-- 8< -- textfile: messages/logout_successful.template -- >8 --

Logout successful

You have logged out successful.

-- 8< -- textfile: messages/registration_failed.template -- >8 --

Registration failed

An internal error occurred while registering.

-- 8< -- textfile: messages/registration_failed_email_exists.template -- >8 --

Registration failed

The registration failed!

The specified email address <+$ email / $+> does already exist.

-- 8< -- textfile: messages/registration_successful.template -- >8 --

Registration successful

The registration was successful!

A temporary password will be sent to your email address.

You can change your password after your login.

-- 8< -- textfile: messages/send_password_failed.template -- >8 --

Password not sent

An error occurred while sending the password.

Maybe the specified email address is not registered here or an internal error occurred.

-- 8< -- textfile: messages/send_password_failed_email_not_exists.template -- >8 --

Password not sent

The password has not been sent.

The email address <+$ email / $+> is not registered on this site.

-- 8< -- textfile: messages/send_password_successful.template -- >8 --

Password sent

A new password has been sent to the email address <+$ email / $+>.