[Catalyst-commits] r14214 - in trunk/examples/CatalystAdvent/root/2011: . pen

dpetrov at dev.catalyst.perl.org dpetrov at dev.catalyst.perl.org
Thu Dec 15 07:28:25 GMT 2011


Author: dpetrov
Date: 2011-12-15 07:28:25 +0000 (Thu, 15 Dec 2011)
New Revision: 14214

Added:
   trunk/examples/CatalystAdvent/root/2011/15.pod
Removed:
   trunk/examples/CatalystAdvent/root/2011/15.pod
   trunk/examples/CatalystAdvent/root/2011/pen/login_authorization_and_user_admin.pod
Log:
Wrong article was pushed! Push the correct one

Deleted: trunk/examples/CatalystAdvent/root/2011/15.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2011/15.pod	2011-12-15 07:24:21 UTC (rev 14213)
+++ trunk/examples/CatalystAdvent/root/2011/15.pod	2011-12-15 07:28:25 UTC (rev 14214)
@@ -1,5 +0,0 @@
-=head1 Multiple chains ending in ONE action
-
-=head1 AUTHOR
-
-Dimitar Petrov <mitakaa at gmail.com>

Copied: trunk/examples/CatalystAdvent/root/2011/15.pod (from rev 14212, trunk/examples/CatalystAdvent/root/2011/pen/login_authorization_and_user_admin.pod)
===================================================================
--- trunk/examples/CatalystAdvent/root/2011/15.pod	                        (rev 0)
+++ trunk/examples/CatalystAdvent/root/2011/15.pod	2011-12-15 07:28:25 UTC (rev 14214)
@@ -0,0 +1,1067 @@
+=pod
+
+=head1 Login, Authorization and User Administration
+
+This tutorial will describe the latest tech in Catalyst authentication and
+authorization and describe a real-world user administration system.
+
+To follow along, first check out the sample code for chapter 4 of the Catalyst
+tutorial:
+
+    svn co http://dev.catalystframework.org/repos/Catalyst/trunk/examples/Tutorial/Final/Chapter04
+
+To get the complete working source for this article, download this
+L<file|http://dev.catalystframework.org/svnweb/Catalyst/checkout/trunk/examples/Tutorial/MyApp_Chapter4_advent_auth.tar.gz>.
+
+=head2 The Users Schema
+
+In C<users.sql> we'll put the tables for holding the user information of our
+app.
+
+If you're following along on PostgreSQL replace C<INTEGER PRIMARY KEY> with
+C<BIGSERIAL PRIMARY KEY>.
+
+    DROP TABLE IF EXISTS users;
+    DROP TABLE IF EXISTS roles;
+    DROP TABLE IF EXISTS user_roles;
+
+    CREATE TABLE users (
+        id INTEGER PRIMARY KEY,
+        active CHAR(1) NOT NULL,
+        username TEXT NOT NULL UNIQUE,
+        password TEXT NOT NULL,
+        password_expires TIMESTAMP,
+        name TEXT NOT NULL,
+        email_address TEXT NOT NULL,
+        phone_number TEXT,
+        mail_address TEXT
+    );
+
+    CREATE TABLE roles (
+        id INTEGER PRIMARY KEY,
+        name TEXT NOT NULL UNIQUE
+    );
+
+    CREATE TABLE user_roles (
+        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE,
+        role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE,
+        PRIMARY KEY (user_id, role_id)
+    );
+
+    INSERT INTO users (username, active, name, email_address, password) VALUES (
+        'admin', 'Y', 'Administrator', 'admin at myapp.org', 'dummy'
+    );
+    INSERT INTO roles (name) VALUES ('admin');
+    INSERT INTO roles (name) VALUES ('can_edit');
+    INSERT INTO user_roles (user_id, role_id) VALUES (
+        (SELECT id FROM users WHERE username = 'admin'),
+        (SELECT id FROM roles WHERE name     = 'admin')
+    );
+
+Load the schema into the db:
+
+    sqlite3 myapp.db < users.sql
+
+=head2 Password Hashing
+
+We will use L<DBIx::Class::PassphraseColumn> and
+L<Authen::Passphrase::BlowfishCrypt> for handling the password hash. This is
+currently the most secure hashing method.
+
+It is a good idea to add dependencies for your application to the
+C<Makefile.PL>, as follows:
+
+    requires 'DBIx::Class::PassphraseColumn';
+    requires 'Authen::Passphrase::BlowfishCrypt';
+
+Then when you deploy the app, you can simply do:
+
+    perl Makefile.PL
+    make listdeps | cpanm
+
+Or C<cpanm -n> to skip tests.
+
+First regenerate the L<DBIx::Class> schema with our new tables and this
+component (make sure you have the latest CPAN version of
+L<DBIx::Class::Schema::Loader>:)
+
+    perl script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
+        create=static components=TimeStamp,PassphraseColumn dbi:SQLite:myapp.db
+
+Now edit C<lib/MyApp/Schema/Result/User.pm> and below the C<DO NOT MODIFY> line
+add the following:
+
+    __PACKAGE__->add_columns(
+        '+password' => {
+            passphrase       => 'rfc2307',
+            passphrase_class => 'BlowfishCrypt',
+            passphrase_args  => {
+                cost        => 8,
+                salt_random => 20,
+            },
+            passphrase_check_method => 'check_password',
+        }
+    );
+
+If your L<DBIx::Class::Schema::Loader> did not generate a C<many_to_many> to
+roles, you will also need to add the following:
+
+    __PACKAGE__->many_to_many("roles", "user_roles", "role");
+
+=head2 Configuring Catalyst Authentication
+
+We will use L<CatalystX::SimpleLogin> as the entry point to the auth system.
+
+You will need to install a memcached server, on Debian run the following:
+
+    sudo aptitude install memcached
+
+You will need the following modules installed: L<Catalyst::Plugin::Session>,
+L<Catalyst::Plugin::Session::Store::Memcached>,
+L<Catalyst::Plugin::Session::State::Cookie>,
+L<Catalyst::Plugin::Authentication>,
+L<Catalyst::Authentication::Store::DBIx::Class>,
+L<Catalyst::Plugin::Authorization::Roles> and L<CatalystX::SimpleLogin>.
+
+Now edit C<lib/MyApp.pm> and change the C<use Catalyst> line to:
+
+    use Catalyst qw/
+        -Debug
+        ConfigLoader
+        Static::Simple
+        Session
+        Session::Store::Memcached
+        Session::State::Cookie
+        Authentication
+        Authorization::Roles
+        +CatalystX::SimpleLogin
+    /;
+
+Above the C<< __PACKAGE__->setup() >> call, put the following configuration:
+
+    __PACKAGE__->config(
+       authentication => {
+          default_realm => 'users',
+          realms        => {
+             users => {
+                credential => {
+                   class          => 'Password',
+                   password_field => 'password',
+                   password_type  => 'self_check'
+                },
+                store => {
+                   class         => 'DBIx::Class',
+                   user_model    => 'DB::User',
+                   role_relation => 'roles',
+                   role_field    => 'name',
+                }
+             }
+          },
+       },
+       'Controller::Login' => { traits => ['-RenderAsTTTemplate'], },
+    );
+
+Now we need a login template:
+
+    mkdir root/src/login
+    touch root/src/login/login.tt2
+
+Place the following into the C<root/src/login/login.tt2> file:
+
+    [% META title = 'Welcome to MyApp: Please Log In' %]
+    <div>
+        [% FOR field IN login_form.error_fields %]
+            [% FOR error IN field.errors %]
+                <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
+            [% END %]
+        [% END %]
+    </div>
+
+    <div>
+        <form id="login_form" method="post" action="[% c.req.uri %]">
+            <fieldset style="border: 0;">
+                <table>
+                    <tr>
+                        <td><label class="label" for="username">Username:</label></td>
+                        <td><input type="text" name="username" value="" /></td>
+                    </tr>
+                    <tr>
+                        <td><label class="label" for="password">Password:</label></td>
+                        <td><input type="password" name="password" value="" /></td>
+                    </tr>
+                    <tr><td><input type="submit" name="submit" value="Login" /></td></tr>
+                </table>
+            </fieldset>
+        </form>
+    </div>
+
+Now we need the auth to protect the app. Replace the contents of
+C<lib/MyApp/Controller/Root.pm> with the following:
+
+    package MyApp::Controller::Root;
+    use Moose;
+    use namespace::autoclean;
+
+    BEGIN { extends 'Catalyst::Controller' }
+
+    __PACKAGE__->config(namespace => '');
+
+    sub base : Chained('/login/required') PathPart('') CaptureArgs(0) {}
+
+    sub home : Chained('/base') PathPart('') Args(0) {
+        my ($self, $c) = @_;
+
+        $c->res->redirect($c->uri_for('/books/list'));
+    }
+
+    sub default : Chained('/base') PathPart('') Args {
+        my ($self, $c) = @_;
+        $c->res->body('Page not found');
+        $c->res->status(404);
+    }
+
+    sub end : ActionClass('RenderView') {}
+
+    __PACKAGE__->meta->make_immutable;
+
+    1;
+
+Now make sure all actions in the app chain from C</base>. Replace the C<Books>
+controller in C<lib/MyApp/Controller/Books.pm> with the following:
+
+    package MyApp::Controller::Books;
+    use Moose;
+    use namespace::autoclean;
+
+    BEGIN {extends 'Catalyst::Controller'; }
+
+    sub base : Chained('/base') PathPrefix CaptureArgs(0) {}
+
+    sub list : Chained('base') PathPart('list') Args(0) {
+        my ($self, $c) = @_;
+
+        $c->stash(books => [ $c->model('DB::Book')->all ]);
+    }
+
+    sub url_create : Chained('base') PathPart('url_create') Args(3) {
+        my ($self, $c, $title, $rating, $author_id) = @_;
+
+        my $book = $c->model('DB::Book')->create({
+            title  => $title,
+            rating => $rating
+        });
+
+        $book->add_to_book_authors({ author_id => $author_id });
+
+        $c->stash(
+            book     => $book,
+            template => 'books/create_done.tt2'
+        );
+
+        $c->response->header('Cache-Control' => 'no-cache');
+    }
+
+
+    sub form_create : Chained('base') PathPart('form_create') Args(0) {
+        my ($self, $c) = @_;
+
+        $c->stash(template => 'books/form_create.tt2');
+    }
+
+
+    sub form_create_do : Chained('base') PathPart('form_create_do') Args(0) {
+        my ($self, $c) = @_;
+
+        my $title     = $c->request->params->{title}     || 'N/A';
+        my $rating    = $c->request->params->{rating}    || 'N/A';
+        my $author_id = $c->request->params->{author_id} || '1';
+
+        my $book = $c->model('DB::Book')->create({
+            title   => $title,
+            rating  => $rating,
+        });
+
+        $book->add_to_book_authors({author_id => $author_id});
+
+        $c->stash(
+            book     => $book,
+            template => 'books/create_done.tt2'
+        );
+    }
+
+    sub object : Chained('base') PathPart('id') CaptureArgs(1) {
+        my ($self, $c, $id) = @_;
+
+        $c->stash(object => $c->model('DB::Book')->find($id));
+
+        die "Book $id not found!" if !$c->stash->{object};
+    }
+
+
+    sub delete : Chained('object') PathPart('delete') Args(0) {
+        my ($self, $c) = @_;
+
+        $c->stash->{object}->delete;
+
+        $c->res->redirect($c->uri_for($self->action_for('list'),
+            {status_msg => "Book deleted."}));
+    }
+
+
+    sub list_recent : Chained('base') PathPart('list_recent') Args(1) {
+        my ($self, $c, $mins) = @_;
+
+        $c->stash(books => [$c->model('DB::Book')
+                                ->created_after(DateTime->now->subtract(minutes => $mins))]);
+
+        $c->stash(template => 'books/list.tt2');
+    }
+
+
+    sub list_recent_tcp : Chained('base') PathPart('list_recent_tcp') Args(1) {
+        my ($self, $c, $mins) = @_;
+
+        $c->stash(books => [
+                $c->model('DB::Book')
+                    ->created_after(DateTime->now->subtract(minutes => $mins))
+                    ->title_like('TCP')
+            ]);
+
+        $c->stash(template => 'books/list.tt2');
+    }
+
+    __PACKAGE__->meta->make_immutable;
+
+    1;
+
+Now we need to set the admin password so we can log in and try it out, in
+C<script/set_admin_password.pl> place the following:
+
+    #!/usr/bin/env perl
+
+    use strict;
+    use warnings;
+    use lib 'lib';
+
+    BEGIN { $ENV{CATALYST_DEBUG} = 0 }
+
+    use MyApp;
+    use DateTime;
+
+    my $admin = MyApp->model('DB::User')->search({ username => 'admin' })
+        ->single;
+
+    $admin->update({ password => 'admin', password_expires => DateTime->now });
+
+Now run C<perl script/set_admin_password.pl> and the password should be set.
+
+Now run C<perl script/myapp_server.pl>, go to the server in your web browser and
+it should ask you to log in. Log in as C<admin/admin>, and it should take you to
+the books list.
+
+=head2 Authorization
+
+Let's protect our edit and delete actions for books by allowing only users with
+the C<can_edit> or C<admin> roles.
+
+You will need the following modules: L<Catalyst::Controller::ActionRole> and
+L<Catalyst::ActionRole::ACL>.
+
+Change the C<extends> line in the Books controller to:
+
+    BEGIN { extends 'Catalyst::Controller::ActionRole'; }
+
+Add the edit chain bases:
+
+    sub edit : Chained('base') PathPart('') CaptureArgs(0) Does('ACL') AllowedRole('admin') AllowedRole('can_edit') ACLDetachTo('denied') {}
+
+    sub edit_object : Chained('object') PathPart('') CaptureArgs(0) Does('ACL') AllowedRole('admin') AllowedRole('can_edit') ACLDetachTo('denied') {}
+
+Now make the actions C<url_create>, C<form_create>,
+C<form_create_do> chain from C<edit> and C<delete> chain from C<edit_object>.
+
+Finally, add a C<denied> action:
+
+    sub denied : Private {
+        my ($self, $c) = @_;
+
+        $c->res->redirect($c->uri_for($self->action_for('list'),
+            {status_msg => "Access Denied"}));
+    }
+
+Now try removing your roles:
+
+    % sqlite3 myapp.db
+    sqlite> DELETE FROM user_roles;
+    sqlite> .q
+
+Restart the server, log back into the app, and try hitting a delete link. You
+should get the message C<Access Denied>.
+
+We should really hide the delete link, to do so edit C<root/src/books/list.tt2>.
+and change the C<Delete> link code to:
+
+    [% IF c.check_any_user_role('can_edit', 'admin') %]
+    <td>
+      [% # Add a link to delete a book %]
+      <a href="[%
+        c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a>
+    </td>
+    [% END %]
+
+Now restart the server and go back to the book list, the delete link should no
+longer appear.
+
+Now recreate the role mapping:
+
+    % sqlite3 myapp.db
+    sqlite> INSERT INTO user_roles (user_id, role_id) VALUES (
+       ...>     (SELECT id FROM users WHERE username = 'admin'),
+       ...>     (SELECT id FROM roles WHERE name     = 'admin')
+       ...> );
+    sqlite> .q
+
+=head2 Password Expiry
+
+For the next section you will need the modules L<HTML::FormHandler> and
+L<Method::Signatures::Simple>.
+
+Create a skeleton C<User> controller with a C<change_password> action in
+C<lib/MyApp/Controller/User.pm>:
+
+    package MyApp::Controller::User;
+    use Moose;
+    use namespace::autoclean;
+    use MyApp::Form::ChangePassword ();
+
+    BEGIN { extends 'Catalyst::Controller::ActionRole'; }
+
+    sub base : Chained('/base') PathPrefix CaptureArgs(0) {}
+
+    sub admin : Chained('base') PathPart('') CaptureArgs(0) Does('ACL') RequiresRole('admin') ACLDetachTo('denied') {}
+
+    sub change_password : Chained('base') PathPart('change_password') Args(0) {
+        my ($self, $c) = @_;
+
+        my $form = MyApp::Form::ChangePassword->new;
+
+        $c->stash(form => $form);
+
+        return unless $form->process(
+            user   => $c->user,
+            params => $c->req->body_parameters,
+        );
+
+        $c->user->update({
+            password         => $form->field('new_password')->value,
+            password_expires => undef,
+        });
+
+        $c->res->redirect($c->uri_for('/books/list', {
+            status_msg => 'Password changed successfully'
+        }));
+    }
+
+    sub denied : Private {
+        my ($self, $c) = @_;
+
+        $c->res->redirect($c->uri_for('/books/list', {
+            status_msg => "Access Denied"
+        }));
+    }
+
+    __PACKAGE__->meta->make_immutable;
+
+    1;
+
+And the form class in C<lib/MyApp/Form/ChangePassword.pm>:
+
+    package MyApp::Form::ChangePassword;
+
+    use HTML::FormHandler::Moose;
+    extends 'HTML::FormHandler';
+    use namespace::autoclean;
+    use Method::Signatures::Simple;
+
+    has user => (is => 'rw');
+
+    has_field 'current_password' => (
+       type     => 'Password',
+       label    => 'Current Password',
+       required => 1,
+    );
+
+    method validate_current_password($field) {
+        $field->add_error('Incorrect password')
+            if not $self->user->check_password($field->value);
+    }
+
+    has_field 'new_password' => (
+        type      => 'Password',
+        label     => 'New Password',
+        required  => 1,
+        minlength => 5,
+    );
+
+    after validate => method {
+        if ($self->field('new_password')->value eq
+                $self->field('current_password')->value )
+        {
+            $self->field('new_password')
+                ->add_error('Must be different from current password');
+        }
+    };
+
+    has_field 'new_password_conf' => (
+       type           => 'PasswordConf',
+       label          => 'New Password (again)',
+       password_field => 'new_password',
+       required       => 1,
+       minlength      => 5,
+    );
+
+    has_field submit => (type => 'Submit', value => 'Change');
+
+    __PACKAGE__->meta->make_immutable;
+
+    1;
+
+And the template in C<root/src/user/change_password.tt2>:
+
+    [% META title = 'MyApp: Change Password' %]
+
+    <div>
+    <form name="[% form.name %]" action="[% c.req.uri %]" method="post">
+
+    [% FOR field IN form.error_fields %]
+        [% FOR error IN field.errors %]
+            <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
+        [% END %]
+    [% END %]
+
+    <fieldset style="border: 0;">
+    <table>
+    [% FOREACH field_name = ['current_password', 'new_password', 'new_password_conf'] %]
+    <tr>
+    [% f = form.field(field_name) %]
+    <td><label for="[% f.name %]">[% f.label %]:</label></td>
+    <td><input type="password" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]"></td>
+    </tr>
+    [% END %]
+    <tr><td><input type="submit" name="submit" value="Change" /></td></tr>
+    </fieldset>
+    </table>
+    </form>
+    </div>
+
+Now to enforce password expiry, we need to modify the base action in the root
+controller, edit C<lib/MyApp/Controller/Root.pm> and replace the first part with
+the following:
+
+    package MyApp::Controller::Root;
+    use Moose;
+    use namespace::autoclean;
+    use DateTime;
+
+    BEGIN { extends 'Catalyst::Controller' }
+
+    __PACKAGE__->config(namespace => '');
+
+    sub base : Chained('/login/required') PathPart('') CaptureArgs(0) {
+        my ($self, $c) = @_;
+
+        if ($c->user_exists && $c->user->active ne 'Y') {
+            $c->res->redirect($c->uri_for('/login', {
+                status_msg => 'User has been Deactivated',
+                username => $c->user->username,
+                password => 'xxx',
+            }));
+            $c->logout;
+            $c->detach;
+        }
+
+        if ($c->action ne $c->controller('User')->action_for('change_password')
+            && $c->user_exists
+            && $c->user->password_expires
+            && $c->user->password_expires <= DateTime->now)
+        {
+            $c->res->redirect($c->uri_for('/user/change_password', {
+                status_msg => 'Password Expired'
+            }));
+            $c->detach;
+        }
+    }
+
+We are also checking that the user is active.
+
+Now restart the server and open the site in your web browser, you will be asked
+to change your password then taken to the book list.
+
+=head2 User Profile
+
+Now we'll create an editable profile page.
+
+You will need the module L<HTML::FormHandler::Model::DBIC>.
+
+First, lets add a couple navigation links to the wrapper, edit
+C<root/src/wrapper.tt2> and change the menu to the following:
+
+    <div id="bodyblock">
+    <div id="menu">
+        <ul>
+            <li><strong>Navigation:</strong></li>
+            <li><a href="[% c.uri_for('/books/list') %]">Home</a></li>
+            <li><a href="[% c.uri_for('/user/profile') %]">Profile</a></li>
+            <li><a href="[% c.uri_for('/user/change_password') %]">Change Password</a></li>
+            [% IF c.check_user_roles('admin') %]
+            <li><a href="[% c.uri_for('/user/list') %]">Admin</a></li>
+            [% END %]
+            <li><a href="[% c.uri_for('/logout') %]">Logout</a></li>
+        </ul>
+    </div><!-- end menu -->
+
+Lets make a form class for the profile, create a
+C<lib/MyApp/Form/UserProfile.pm> with the following:
+
+    package MyApp::Form::UserProfile;
+
+    use HTML::FormHandler::Moose;
+    extends 'HTML::FormHandler::Model::DBIC';
+    use namespace::autoclean;
+
+    has '+item_class' => (default => 'User');
+
+    has_field 'name'          => ( type => 'Text',  required => 1 );
+    has_field 'email_address' => ( type => 'Email', required => 1 );
+    has_field 'phone_number'  => ( type => 'Text' );
+    has_field 'mail_address'  => ( type => 'Text' );
+
+    has_field submit => (
+       type  => 'Submit',
+       value => 'Update'
+    );
+
+    __PACKAGE__->meta->make_immutable;
+
+    1;
+
+Then we'll add a profile action to the User controller. At the top add:
+
+    use MyApp::Form::UserProfile    ();
+
+Then add the action:
+
+    sub profile : Chained('base') PathPart('profile') Args(0) {
+        my ($self, $c) = @_;
+
+        my $form = MyApp::Form::UserProfile->new;
+
+        $c->stash(form => $form);
+
+        return unless $form->process(
+            schema  => $c->model('DB')->schema,
+            item_id => $c->user->users_id,
+            params  => $c->req->body_parameters,
+        );
+
+        $c->res->redirect($c->uri_for('/books/list', {
+            status_msg => 'Profile Updated'
+        }));
+    }
+
+Add the template in C<root/src/user/profile.tt2>:
+
+    [% META title = 'MyApp: User Profile' %]
+
+    <div>
+    <form name="[% form.name %]" action="[% c.req.uri %]" method="post">
+
+    [% FOR field IN form.error_fields %]
+        [% FOR error IN field.errors %]
+            <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
+        [% END %]
+    [% END %]
+
+    <fieldset style="border: 0;">
+    <table>
+    [% FOREACH field_name = ['name', 'email_address',
+                             'phone_number', 'mail_address'] %]
+    <tr>
+    [% f = form.field(field_name) %]
+    <td><label for="[% f.name %]">[% f.label %]:</label></td>
+    <td><input type="text" size=30 name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]"></td>
+    </tr>
+    [% END %]
+    <tr><td><input type="submit" name="submit" id="submit" value="Update" /></td></tr>
+    </fieldset>
+    </table>
+    </form>
+    </div>
+
+Now restart the server and try editing your profile.
+
+=head2 User Administration
+
+Finally, we will create an interface to manage the users of the app.
+
+For this part you will need the modules L<Crypt::PassGen> and
+L<Catalyst::View::Email>.
+
+Create an email view:
+
+    perl script/myapp_create.pl view Email::Template Email::Template
+
+And add its configuration to C<myapp.conf>:
+
+    <View::Email::Template>
+        <sender>
+            mailer Sendmail
+        </sender>
+        template_prefix email
+        <default>
+            content_type text/html
+            charset utf-8
+            view HTML
+        </default>
+    </View::Email::Template>
+
+    default_view HTML
+
+In the C<User> controller, add these use statements at the top:
+
+    use Crypt::PassGen 'passgen';
+    use MyApp::Form::AddUser        ();
+    use MyApp::Form::EditUser       ();
+
+And these actions:
+
+    sub list : Chained('admin') PathPart('list') Args(0) {
+        my ($self, $c) = @_;
+
+        my $users = $c->model('DB::User')->search(
+            { active => 'Y'},
+            {
+                order_by => ['username'],
+                page     => ($c->req->param('page') || 1),
+                rows     => 20,
+            }
+        );
+
+        $c->stash(
+            users => $users,
+            pager => $users->pager,
+        );
+    }
+
+    sub add : Chained('admin') PathPart('add') Args(0) {
+        my ($self, $c) = @_;
+
+        my $form = MyApp::Form::AddUser->new;
+
+        $c->stash(form => $form);
+
+        my $user = $c->model('DB::User')->new_result({});
+
+        my ($temp_password) = passgen(NWORDS => 1, NLETT => 8);
+
+        $user->password($temp_password);
+        $user->password_expires(DateTime->now);
+        $user->active('Y');
+
+        return unless $form->process(
+            schema => $c->model('DB')->schema,
+            item   => $user,
+            params => $c->req->body_parameters,
+        );
+
+        $c->stash->{email} = {
+            to           => $user->email_address,
+            from         => 'admin at myapp.org',
+            subject      => 'Welcome to MyApp',
+            content_type => 'text/html',
+            template     => 'welcome.tt2',
+        };
+
+        $c->stash(
+            username => $user->username,
+            password => $temp_password,
+        );
+
+        $c->forward($c->view('Email::Template'));
+
+        $c->res->redirect($c->uri_for($self->action_for('list'), {
+            status_msg => 'User '
+                . $user->username
+                . ' created successfully'
+                . ', initial password emailed ' . 'to '
+                . $user->email_address
+        }));
+    }
+
+    sub user : Chained('admin') PathPart('') CaptureArgs(1) {
+        my ($self, $c, $user_id) = @_;
+
+        $c->stash(user => $c->model('DB::User')->find($user_id));
+    }
+
+    sub inactivate : Chained('user') PathPart('inactivate') Args(0) {
+        my ($self, $c) = @_;
+
+        my $user = $c->stash->{user};
+
+        $user->update({ active => 'N' });
+
+        my $username = $user->username;
+
+        $c->res->redirect($c->uri_for($self->action_for('list'), {
+            status_msg => "User $username inactivated"
+        }));
+    }
+
+    sub reset_password : Chained('user') PathPart('reset_password') Args(0) {
+        my ($self, $c) = @_;
+
+        my $user = $c->stash->{user};
+
+        my ($temp_password) = passgen(NWORDS => 1, NLETT => 8);
+
+        $user->password($temp_password);
+        $user->password_expires(DateTime->now);
+        $user->update;
+
+        $c->stash->{email} = {
+            to           => $user->email_address,
+            from         => 'admin at myapp.org',
+            subject      => 'Your MyApp Password has been Reset',
+            content_type => 'text/html',
+            template     => 'reset_password.tt2',
+        };
+
+        $c->stash(
+            username => $user->username,
+            password => $temp_password,
+        );
+
+        $c->forward($c->view('Email::Template'));
+
+        $c->res->redirect($c->uri_for($self->action_for('list'), {
+            status_msg => 'Password reset email for '
+                . $user->username
+                . ' sent to '
+                . $user->email_address
+        }));
+    }
+
+    sub edit : Chained('user') PathPart('edit') Args(0) {
+        my ($self, $c) = @_;
+
+        my $form = MyApp::Form::EditUser->new;
+
+        $c->stash(form => $form);
+
+        return unless $form->process(
+            schema  => $c->model('DB')->schema,
+            item_id => $c->stash->{user}->id,
+            params  => $c->req->body_parameters,
+        );
+
+        $c->res->redirect($c->uri_for($self->action_for('list'), {
+            status_msg => 'User '
+                . $c->stash->{user}->username
+                . ' updated successfully'
+        }));
+    }
+
+B<NOTE:> the C<from> email address must be a valid address, otherwise your MTA
+will reject the email, replace it with your email address before testing this
+controller.
+
+There are two email templates, one for the C<add> action which goes into
+C<root/src/email/welcome.tt2>:
+
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+    <html xmlns="http://www.w3.org/1999/xhtml"
+          xml:lang="en"
+          lang="en">
+    <head>
+    </head>
+    <body>
+    <h2 align="center">Welcome to the MyApp system.</h2>
+    <p>Your username is: <span style="color: green;">[% username %]</span></p>
+    <p>Your initial password is: <span style="color: red;">[% password %]</span></p>
+    <p>You will be asked to change your password on first login.</p>
+    </body>
+    </html>
+
+And one for C<password_reset> which goes into
+C<root/src/email/reset_password.tt2>:
+
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+    <html xmlns="http://www.w3.org/1999/xhtml"
+          xml:lang="en"
+          lang="en">
+    <head>
+    </head>
+    <body>
+    <h2 align="center">Your MyApp Password has been Reset</h2>
+    <p>Your username is: <span style="color: green;">[% username %]</span></p>
+    <p>Your password is: <span style="color: red;">[% password %]</span></p>
+    <p>You will be asked to change your password on first login.</p>
+    </body>
+    </html>
+
+Four web templates, C<root/src/user/list.tt2>:
+
+    [% META title = 'MyApp: User Admin' %]
+
+    <br />
+    <a class="button" href="[% c.uri_for('/user/add') %]" onclick='this.blur();'><span>Add User</span></a>
+    <br />
+    Displaying users [% pager.first %]-[% pager.last %] of [% pager.total_entries %]
+
+    <table>
+    <tr>
+        <th>Username</th>
+        <th>Name</th>
+        <th>Email Address</th>
+    </tr>
+    [% WHILE (u = users.next) %]
+    <tr>
+    <td><a href="[% c.uri_for('/user', u.id, 'edit') %]">[% u.username %]</a></td>
+    <td>[% u.name %]</td>
+    <td>[% u.email_address %]</td>
+    <td><a href="[% c.uri_for('/user', u.id, 'reset_password') %]">Reset Password</a></td>
+    <td><a href="[% c.uri_for('/user', u.id, 'inactivate') %]">Inactivate</a></td>
+    </tr>
+    [% END %]
+    </table>
+
+    &lt;&lt; 
+    <a href="[% c.req.uri_with({ page => pager.first_page }) %]">First</a>
+    <a href="[% c.req.uri_with({ page => pager.previous_page })%]">Previous</a>
+    |
+    <a href="[% c.req.uri_with({ page => pager.next_page })%]">Next</a>
+    <a href="[% c.req.uri_with({ page => pager.last_page }) %]">Last</a>
+    &gt;&gt;
+
+C<root/src/user/add.tt2>:
+
+    [% META title = 'MyApp: Add User' %]
+
+    <div>
+    <form name="[% form.name %]" action="[% c.req.uri %]" method="post">
+
+    [% FOR field IN form.error_fields %]
+        [% FOR error IN field.errors %]
+            <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
+        [% END %]
+    [% END %]
+
+    <fieldset style="border: 0;">
+    <table>
+    <tr>
+    [% f = form.field('username') %]
+    <td><label for="[% f.name %]">[% f.label %]:</label></td>
+    <td><input type="text" size=30 name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]"></td>
+    </tr>
+    [% PROCESS user/edit_details.tt2 %]
+    <tr>
+        <td><input type="submit" name="submit" id="submit" value="Add" /></td>
+        <td><a href="/user/list">Users List</a></td>
+    </tr>
+    </fieldset>
+    </table>
+    </form>
+    </div>
+
+C<root/src/user/edit.tt2>:
+
+    [% META title = 'MyApp: Edit User' %]
+
+    <div>
+    <form name="[% form.name %]" action="[% c.req.uri %]" method="post">
+
+    [% FOR field IN form.error_fields %]
+        [% FOR error IN field.errors %]
+            <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
+        [% END %]
+    [% END %]
+
+    <fieldset style="border: 0;">
+    <table>
+    [% PROCESS user/edit_details.tt2 %]
+    <tr>
+        <td><input type="submit" name="submit" id="submit" value="Update" /></td>
+        <td><a href="/user/list">Users List</a></td>
+    </tr>
+    </fieldset>
+    </table>
+    </form>
+    </div>
+
+and C<root/src/user/edit_details.tt2>:
+
+    [% FOREACH field_name = ['name', 'email_address',
+                             'phone_number', 'mail_address'] %]
+    <tr>
+    [% f = form.field(field_name) %]
+    <td><label class="text.label" for="[% f.name %]">[% f.label %]:</label></td>
+    <td><input class="text" type="text" size=30 name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]"></td>
+    </tr>
+    [% END %]
+    <tr>
+    [% f = form.field('roles') %]
+    <td><label for="[% f.name %]">Roles:</label></td>
+    <td>[% f.render %]</td>
+    </tr>
+
+And the two form classes, C<lib/MyApp/Form/AddUser.pm>:
+
+    package MyApp::Form::AddUser;
+
+    use HTML::FormHandler::Moose;
+    extends 'MyApp::Form::EditUser';
+    use namespace::autoclean;
+
+    has_field 'username' => (
+        type     => 'Text',
+        label    => 'User name',
+        required => 1,
+    );
+
+    __PACKAGE__->meta->make_immutable;
+
+    1;
+
+and C<lib/MyApp/Form/EditUser.pm>:
+
+    package MyApp::Form::EditUser;
+
+    use HTML::FormHandler::Moose;
+    extends 'MyApp::Form::UserProfile';
+    use namespace::autoclean;
+
+    has_field 'roles' => (
+        type         => 'Multiple',
+        widget       => 'checkbox_group',
+        label_column => 'name',
+        label        => '',
+    );
+
+    __PACKAGE__->meta->make_immutable;
+
+    1;
+
+Now try it out, launch the server and click the Admin link.
+
+I've taken you on a tour of a simple intranet auth system using modern
+L<Catalyst> technologies, hopefully you got some ideas about how to implement
+your own auth and administration systems.
+
+=head2 AUTHOR
+
+Caelum: Rafael Kitover <rkitover at cpan.org>
+
+=cut

Deleted: trunk/examples/CatalystAdvent/root/2011/pen/login_authorization_and_user_admin.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2011/pen/login_authorization_and_user_admin.pod	2011-12-15 07:24:21 UTC (rev 14213)
+++ trunk/examples/CatalystAdvent/root/2011/pen/login_authorization_and_user_admin.pod	2011-12-15 07:28:25 UTC (rev 14214)
@@ -1,1067 +0,0 @@
-=pod
-
-=head1 Login, Authorization and User Administration
-
-This tutorial will describe the latest tech in Catalyst authentication and
-authorization and describe a real-world user administration system.
-
-To follow along, first check out the sample code for chapter 4 of the Catalyst
-tutorial:
-
-    svn co http://dev.catalystframework.org/repos/Catalyst/trunk/examples/Tutorial/Final/Chapter04
-
-To get the complete working source for this article, download this
-L<file|http://dev.catalystframework.org/svnweb/Catalyst/checkout/trunk/examples/Tutorial/MyApp_Chapter4_advent_auth.tar.gz>.
-
-=head2 The Users Schema
-
-In C<users.sql> we'll put the tables for holding the user information of our
-app.
-
-If you're following along on PostgreSQL replace C<INTEGER PRIMARY KEY> with
-C<BIGSERIAL PRIMARY KEY>.
-
-    DROP TABLE IF EXISTS users;
-    DROP TABLE IF EXISTS roles;
-    DROP TABLE IF EXISTS user_roles;
-
-    CREATE TABLE users (
-        id INTEGER PRIMARY KEY,
-        active CHAR(1) NOT NULL,
-        username TEXT NOT NULL UNIQUE,
-        password TEXT NOT NULL,
-        password_expires TIMESTAMP,
-        name TEXT NOT NULL,
-        email_address TEXT NOT NULL,
-        phone_number TEXT,
-        mail_address TEXT
-    );
-
-    CREATE TABLE roles (
-        id INTEGER PRIMARY KEY,
-        name TEXT NOT NULL UNIQUE
-    );
-
-    CREATE TABLE user_roles (
-        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE,
-        role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE,
-        PRIMARY KEY (user_id, role_id)
-    );
-
-    INSERT INTO users (username, active, name, email_address, password) VALUES (
-        'admin', 'Y', 'Administrator', 'admin at myapp.org', 'dummy'
-    );
-    INSERT INTO roles (name) VALUES ('admin');
-    INSERT INTO roles (name) VALUES ('can_edit');
-    INSERT INTO user_roles (user_id, role_id) VALUES (
-        (SELECT id FROM users WHERE username = 'admin'),
-        (SELECT id FROM roles WHERE name     = 'admin')
-    );
-
-Load the schema into the db:
-
-    sqlite3 myapp.db < users.sql
-
-=head2 Password Hashing
-
-We will use L<DBIx::Class::PassphraseColumn> and
-L<Authen::Passphrase::BlowfishCrypt> for handling the password hash. This is
-currently the most secure hashing method.
-
-It is a good idea to add dependencies for your application to the
-C<Makefile.PL>, as follows:
-
-    requires 'DBIx::Class::PassphraseColumn';
-    requires 'Authen::Passphrase::BlowfishCrypt';
-
-Then when you deploy the app, you can simply do:
-
-    perl Makefile.PL
-    make listdeps | cpanm
-
-Or C<cpanm -n> to skip tests.
-
-First regenerate the L<DBIx::Class> schema with our new tables and this
-component (make sure you have the latest CPAN version of
-L<DBIx::Class::Schema::Loader>:)
-
-    perl script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
-        create=static components=TimeStamp,PassphraseColumn dbi:SQLite:myapp.db
-
-Now edit C<lib/MyApp/Schema/Result/User.pm> and below the C<DO NOT MODIFY> line
-add the following:
-
-    __PACKAGE__->add_columns(
-        '+password' => {
-            passphrase       => 'rfc2307',
-            passphrase_class => 'BlowfishCrypt',
-            passphrase_args  => {
-                cost        => 8,
-                salt_random => 20,
-            },
-            passphrase_check_method => 'check_password',
-        }
-    );
-
-If your L<DBIx::Class::Schema::Loader> did not generate a C<many_to_many> to
-roles, you will also need to add the following:
-
-    __PACKAGE__->many_to_many("roles", "user_roles", "role");
-
-=head2 Configuring Catalyst Authentication
-
-We will use L<CatalystX::SimpleLogin> as the entry point to the auth system.
-
-You will need to install a memcached server, on Debian run the following:
-
-    sudo aptitude install memcached
-
-You will need the following modules installed: L<Catalyst::Plugin::Session>,
-L<Catalyst::Plugin::Session::Store::Memcached>,
-L<Catalyst::Plugin::Session::State::Cookie>,
-L<Catalyst::Plugin::Authentication>,
-L<Catalyst::Authentication::Store::DBIx::Class>,
-L<Catalyst::Plugin::Authorization::Roles> and L<CatalystX::SimpleLogin>.
-
-Now edit C<lib/MyApp.pm> and change the C<use Catalyst> line to:
-
-    use Catalyst qw/
-        -Debug
-        ConfigLoader
-        Static::Simple
-        Session
-        Session::Store::Memcached
-        Session::State::Cookie
-        Authentication
-        Authorization::Roles
-        +CatalystX::SimpleLogin
-    /;
-
-Above the C<< __PACKAGE__->setup() >> call, put the following configuration:
-
-    __PACKAGE__->config(
-       authentication => {
-          default_realm => 'users',
-          realms        => {
-             users => {
-                credential => {
-                   class          => 'Password',
-                   password_field => 'password',
-                   password_type  => 'self_check'
-                },
-                store => {
-                   class         => 'DBIx::Class',
-                   user_model    => 'DB::User',
-                   role_relation => 'roles',
-                   role_field    => 'name',
-                }
-             }
-          },
-       },
-       'Controller::Login' => { traits => ['-RenderAsTTTemplate'], },
-    );
-
-Now we need a login template:
-
-    mkdir root/src/login
-    touch root/src/login/login.tt2
-
-Place the following into the C<root/src/login/login.tt2> file:
-
-    [% META title = 'Welcome to MyApp: Please Log In' %]
-    <div>
-        [% FOR field IN login_form.error_fields %]
-            [% FOR error IN field.errors %]
-                <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
-            [% END %]
-        [% END %]
-    </div>
-
-    <div>
-        <form id="login_form" method="post" action="[% c.req.uri %]">
-            <fieldset style="border: 0;">
-                <table>
-                    <tr>
-                        <td><label class="label" for="username">Username:</label></td>
-                        <td><input type="text" name="username" value="" /></td>
-                    </tr>
-                    <tr>
-                        <td><label class="label" for="password">Password:</label></td>
-                        <td><input type="password" name="password" value="" /></td>
-                    </tr>
-                    <tr><td><input type="submit" name="submit" value="Login" /></td></tr>
-                </table>
-            </fieldset>
-        </form>
-    </div>
-
-Now we need the auth to protect the app. Replace the contents of
-C<lib/MyApp/Controller/Root.pm> with the following:
-
-    package MyApp::Controller::Root;
-    use Moose;
-    use namespace::autoclean;
-
-    BEGIN { extends 'Catalyst::Controller' }
-
-    __PACKAGE__->config(namespace => '');
-
-    sub base : Chained('/login/required') PathPart('') CaptureArgs(0) {}
-
-    sub home : Chained('/base') PathPart('') Args(0) {
-        my ($self, $c) = @_;
-
-        $c->res->redirect($c->uri_for('/books/list'));
-    }
-
-    sub default : Chained('/base') PathPart('') Args {
-        my ($self, $c) = @_;
-        $c->res->body('Page not found');
-        $c->res->status(404);
-    }
-
-    sub end : ActionClass('RenderView') {}
-
-    __PACKAGE__->meta->make_immutable;
-
-    1;
-
-Now make sure all actions in the app chain from C</base>. Replace the C<Books>
-controller in C<lib/MyApp/Controller/Books.pm> with the following:
-
-    package MyApp::Controller::Books;
-    use Moose;
-    use namespace::autoclean;
-
-    BEGIN {extends 'Catalyst::Controller'; }
-
-    sub base : Chained('/base') PathPrefix CaptureArgs(0) {}
-
-    sub list : Chained('base') PathPart('list') Args(0) {
-        my ($self, $c) = @_;
-
-        $c->stash(books => [ $c->model('DB::Book')->all ]);
-    }
-
-    sub url_create : Chained('base') PathPart('url_create') Args(3) {
-        my ($self, $c, $title, $rating, $author_id) = @_;
-
-        my $book = $c->model('DB::Book')->create({
-            title  => $title,
-            rating => $rating
-        });
-
-        $book->add_to_book_authors({ author_id => $author_id });
-
-        $c->stash(
-            book     => $book,
-            template => 'books/create_done.tt2'
-        );
-
-        $c->response->header('Cache-Control' => 'no-cache');
-    }
-
-
-    sub form_create : Chained('base') PathPart('form_create') Args(0) {
-        my ($self, $c) = @_;
-
-        $c->stash(template => 'books/form_create.tt2');
-    }
-
-
-    sub form_create_do : Chained('base') PathPart('form_create_do') Args(0) {
-        my ($self, $c) = @_;
-
-        my $title     = $c->request->params->{title}     || 'N/A';
-        my $rating    = $c->request->params->{rating}    || 'N/A';
-        my $author_id = $c->request->params->{author_id} || '1';
-
-        my $book = $c->model('DB::Book')->create({
-            title   => $title,
-            rating  => $rating,
-        });
-
-        $book->add_to_book_authors({author_id => $author_id});
-
-        $c->stash(
-            book     => $book,
-            template => 'books/create_done.tt2'
-        );
-    }
-
-    sub object : Chained('base') PathPart('id') CaptureArgs(1) {
-        my ($self, $c, $id) = @_;
-
-        $c->stash(object => $c->model('DB::Book')->find($id));
-
-        die "Book $id not found!" if !$c->stash->{object};
-    }
-
-
-    sub delete : Chained('object') PathPart('delete') Args(0) {
-        my ($self, $c) = @_;
-
-        $c->stash->{object}->delete;
-
-        $c->res->redirect($c->uri_for($self->action_for('list'),
-            {status_msg => "Book deleted."}));
-    }
-
-
-    sub list_recent : Chained('base') PathPart('list_recent') Args(1) {
-        my ($self, $c, $mins) = @_;
-
-        $c->stash(books => [$c->model('DB::Book')
-                                ->created_after(DateTime->now->subtract(minutes => $mins))]);
-
-        $c->stash(template => 'books/list.tt2');
-    }
-
-
-    sub list_recent_tcp : Chained('base') PathPart('list_recent_tcp') Args(1) {
-        my ($self, $c, $mins) = @_;
-
-        $c->stash(books => [
-                $c->model('DB::Book')
-                    ->created_after(DateTime->now->subtract(minutes => $mins))
-                    ->title_like('TCP')
-            ]);
-
-        $c->stash(template => 'books/list.tt2');
-    }
-
-    __PACKAGE__->meta->make_immutable;
-
-    1;
-
-Now we need to set the admin password so we can log in and try it out, in
-C<script/set_admin_password.pl> place the following:
-
-    #!/usr/bin/env perl
-
-    use strict;
-    use warnings;
-    use lib 'lib';
-
-    BEGIN { $ENV{CATALYST_DEBUG} = 0 }
-
-    use MyApp;
-    use DateTime;
-
-    my $admin = MyApp->model('DB::User')->search({ username => 'admin' })
-        ->single;
-
-    $admin->update({ password => 'admin', password_expires => DateTime->now });
-
-Now run C<perl script/set_admin_password.pl> and the password should be set.
-
-Now run C<perl script/myapp_server.pl>, go to the server in your web browser and
-it should ask you to log in. Log in as C<admin/admin>, and it should take you to
-the books list.
-
-=head2 Authorization
-
-Let's protect our edit and delete actions for books by allowing only users with
-the C<can_edit> or C<admin> roles.
-
-You will need the following modules: L<Catalyst::Controller::ActionRole> and
-L<Catalyst::ActionRole::ACL>.
-
-Change the C<extends> line in the Books controller to:
-
-    BEGIN { extends 'Catalyst::Controller::ActionRole'; }
-
-Add the edit chain bases:
-
-    sub edit : Chained('base') PathPart('') CaptureArgs(0) Does('ACL') AllowedRole('admin') AllowedRole('can_edit') ACLDetachTo('denied') {}
-
-    sub edit_object : Chained('object') PathPart('') CaptureArgs(0) Does('ACL') AllowedRole('admin') AllowedRole('can_edit') ACLDetachTo('denied') {}
-
-Now make the actions C<url_create>, C<form_create>,
-C<form_create_do> chain from C<edit> and C<delete> chain from C<edit_object>.
-
-Finally, add a C<denied> action:
-
-    sub denied : Private {
-        my ($self, $c) = @_;
-
-        $c->res->redirect($c->uri_for($self->action_for('list'),
-            {status_msg => "Access Denied"}));
-    }
-
-Now try removing your roles:
-
-    % sqlite3 myapp.db
-    sqlite> DELETE FROM user_roles;
-    sqlite> .q
-
-Restart the server, log back into the app, and try hitting a delete link. You
-should get the message C<Access Denied>.
-
-We should really hide the delete link, to do so edit C<root/src/books/list.tt2>.
-and change the C<Delete> link code to:
-
-    [% IF c.check_any_user_role('can_edit', 'admin') %]
-    <td>
-      [% # Add a link to delete a book %]
-      <a href="[%
-        c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a>
-    </td>
-    [% END %]
-
-Now restart the server and go back to the book list, the delete link should no
-longer appear.
-
-Now recreate the role mapping:
-
-    % sqlite3 myapp.db
-    sqlite> INSERT INTO user_roles (user_id, role_id) VALUES (
-       ...>     (SELECT id FROM users WHERE username = 'admin'),
-       ...>     (SELECT id FROM roles WHERE name     = 'admin')
-       ...> );
-    sqlite> .q
-
-=head2 Password Expiry
-
-For the next section you will need the modules L<HTML::FormHandler> and
-L<Method::Signatures::Simple>.
-
-Create a skeleton C<User> controller with a C<change_password> action in
-C<lib/MyApp/Controller/User.pm>:
-
-    package MyApp::Controller::User;
-    use Moose;
-    use namespace::autoclean;
-    use MyApp::Form::ChangePassword ();
-
-    BEGIN { extends 'Catalyst::Controller::ActionRole'; }
-
-    sub base : Chained('/base') PathPrefix CaptureArgs(0) {}
-
-    sub admin : Chained('base') PathPart('') CaptureArgs(0) Does('ACL') RequiresRole('admin') ACLDetachTo('denied') {}
-
-    sub change_password : Chained('base') PathPart('change_password') Args(0) {
-        my ($self, $c) = @_;
-
-        my $form = MyApp::Form::ChangePassword->new;
-
-        $c->stash(form => $form);
-
-        return unless $form->process(
-            user   => $c->user,
-            params => $c->req->body_parameters,
-        );
-
-        $c->user->update({
-            password         => $form->field('new_password')->value,
-            password_expires => undef,
-        });
-
-        $c->res->redirect($c->uri_for('/books/list', {
-            status_msg => 'Password changed successfully'
-        }));
-    }
-
-    sub denied : Private {
-        my ($self, $c) = @_;
-
-        $c->res->redirect($c->uri_for('/books/list', {
-            status_msg => "Access Denied"
-        }));
-    }
-
-    __PACKAGE__->meta->make_immutable;
-
-    1;
-
-And the form class in C<lib/MyApp/Form/ChangePassword.pm>:
-
-    package MyApp::Form::ChangePassword;
-
-    use HTML::FormHandler::Moose;
-    extends 'HTML::FormHandler';
-    use namespace::autoclean;
-    use Method::Signatures::Simple;
-
-    has user => (is => 'rw');
-
-    has_field 'current_password' => (
-       type     => 'Password',
-       label    => 'Current Password',
-       required => 1,
-    );
-
-    method validate_current_password($field) {
-        $field->add_error('Incorrect password')
-            if not $self->user->check_password($field->value);
-    }
-
-    has_field 'new_password' => (
-        type      => 'Password',
-        label     => 'New Password',
-        required  => 1,
-        minlength => 5,
-    );
-
-    after validate => method {
-        if ($self->field('new_password')->value eq
-                $self->field('current_password')->value )
-        {
-            $self->field('new_password')
-                ->add_error('Must be different from current password');
-        }
-    };
-
-    has_field 'new_password_conf' => (
-       type           => 'PasswordConf',
-       label          => 'New Password (again)',
-       password_field => 'new_password',
-       required       => 1,
-       minlength      => 5,
-    );
-
-    has_field submit => (type => 'Submit', value => 'Change');
-
-    __PACKAGE__->meta->make_immutable;
-
-    1;
-
-And the template in C<root/src/user/change_password.tt2>:
-
-    [% META title = 'MyApp: Change Password' %]
-
-    <div>
-    <form name="[% form.name %]" action="[% c.req.uri %]" method="post">
-
-    [% FOR field IN form.error_fields %]
-        [% FOR error IN field.errors %]
-            <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
-        [% END %]
-    [% END %]
-
-    <fieldset style="border: 0;">
-    <table>
-    [% FOREACH field_name = ['current_password', 'new_password', 'new_password_conf'] %]
-    <tr>
-    [% f = form.field(field_name) %]
-    <td><label for="[% f.name %]">[% f.label %]:</label></td>
-    <td><input type="password" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]"></td>
-    </tr>
-    [% END %]
-    <tr><td><input type="submit" name="submit" value="Change" /></td></tr>
-    </fieldset>
-    </table>
-    </form>
-    </div>
-
-Now to enforce password expiry, we need to modify the base action in the root
-controller, edit C<lib/MyApp/Controller/Root.pm> and replace the first part with
-the following:
-
-    package MyApp::Controller::Root;
-    use Moose;
-    use namespace::autoclean;
-    use DateTime;
-
-    BEGIN { extends 'Catalyst::Controller' }
-
-    __PACKAGE__->config(namespace => '');
-
-    sub base : Chained('/login/required') PathPart('') CaptureArgs(0) {
-        my ($self, $c) = @_;
-
-        if ($c->user_exists && $c->user->active ne 'Y') {
-            $c->res->redirect($c->uri_for('/login', {
-                status_msg => 'User has been Deactivated',
-                username => $c->user->username,
-                password => 'xxx',
-            }));
-            $c->logout;
-            $c->detach;
-        }
-
-        if ($c->action ne $c->controller('User')->action_for('change_password')
-            && $c->user_exists
-            && $c->user->password_expires
-            && $c->user->password_expires <= DateTime->now)
-        {
-            $c->res->redirect($c->uri_for('/user/change_password', {
-                status_msg => 'Password Expired'
-            }));
-            $c->detach;
-        }
-    }
-
-We are also checking that the user is active.
-
-Now restart the server and open the site in your web browser, you will be asked
-to change your password then taken to the book list.
-
-=head2 User Profile
-
-Now we'll create an editable profile page.
-
-You will need the module L<HTML::FormHandler::Model::DBIC>.
-
-First, lets add a couple navigation links to the wrapper, edit
-C<root/src/wrapper.tt2> and change the menu to the following:
-
-    <div id="bodyblock">
-    <div id="menu">
-        <ul>
-            <li><strong>Navigation:</strong></li>
-            <li><a href="[% c.uri_for('/books/list') %]">Home</a></li>
-            <li><a href="[% c.uri_for('/user/profile') %]">Profile</a></li>
-            <li><a href="[% c.uri_for('/user/change_password') %]">Change Password</a></li>
-            [% IF c.check_user_roles('admin') %]
-            <li><a href="[% c.uri_for('/user/list') %]">Admin</a></li>
-            [% END %]
-            <li><a href="[% c.uri_for('/logout') %]">Logout</a></li>
-        </ul>
-    </div><!-- end menu -->
-
-Lets make a form class for the profile, create a
-C<lib/MyApp/Form/UserProfile.pm> with the following:
-
-    package MyApp::Form::UserProfile;
-
-    use HTML::FormHandler::Moose;
-    extends 'HTML::FormHandler::Model::DBIC';
-    use namespace::autoclean;
-
-    has '+item_class' => (default => 'User');
-
-    has_field 'name'          => ( type => 'Text',  required => 1 );
-    has_field 'email_address' => ( type => 'Email', required => 1 );
-    has_field 'phone_number'  => ( type => 'Text' );
-    has_field 'mail_address'  => ( type => 'Text' );
-
-    has_field submit => (
-       type  => 'Submit',
-       value => 'Update'
-    );
-
-    __PACKAGE__->meta->make_immutable;
-
-    1;
-
-Then we'll add a profile action to the User controller. At the top add:
-
-    use MyApp::Form::UserProfile    ();
-
-Then add the action:
-
-    sub profile : Chained('base') PathPart('profile') Args(0) {
-        my ($self, $c) = @_;
-
-        my $form = MyApp::Form::UserProfile->new;
-
-        $c->stash(form => $form);
-
-        return unless $form->process(
-            schema  => $c->model('DB')->schema,
-            item_id => $c->user->users_id,
-            params  => $c->req->body_parameters,
-        );
-
-        $c->res->redirect($c->uri_for('/books/list', {
-            status_msg => 'Profile Updated'
-        }));
-    }
-
-Add the template in C<root/src/user/profile.tt2>:
-
-    [% META title = 'MyApp: User Profile' %]
-
-    <div>
-    <form name="[% form.name %]" action="[% c.req.uri %]" method="post">
-
-    [% FOR field IN form.error_fields %]
-        [% FOR error IN field.errors %]
-            <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
-        [% END %]
-    [% END %]
-
-    <fieldset style="border: 0;">
-    <table>
-    [% FOREACH field_name = ['name', 'email_address',
-                             'phone_number', 'mail_address'] %]
-    <tr>
-    [% f = form.field(field_name) %]
-    <td><label for="[% f.name %]">[% f.label %]:</label></td>
-    <td><input type="text" size=30 name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]"></td>
-    </tr>
-    [% END %]
-    <tr><td><input type="submit" name="submit" id="submit" value="Update" /></td></tr>
-    </fieldset>
-    </table>
-    </form>
-    </div>
-
-Now restart the server and try editing your profile.
-
-=head2 User Administration
-
-Finally, we will create an interface to manage the users of the app.
-
-For this part you will need the modules L<Crypt::PassGen> and
-L<Catalyst::View::Email>.
-
-Create an email view:
-
-    perl script/myapp_create.pl view Email::Template Email::Template
-
-And add its configuration to C<myapp.conf>:
-
-    <View::Email::Template>
-        <sender>
-            mailer Sendmail
-        </sender>
-        template_prefix email
-        <default>
-            content_type text/html
-            charset utf-8
-            view HTML
-        </default>
-    </View::Email::Template>
-
-    default_view HTML
-
-In the C<User> controller, add these use statements at the top:
-
-    use Crypt::PassGen 'passgen';
-    use MyApp::Form::AddUser        ();
-    use MyApp::Form::EditUser       ();
-
-And these actions:
-
-    sub list : Chained('admin') PathPart('list') Args(0) {
-        my ($self, $c) = @_;
-
-        my $users = $c->model('DB::User')->search(
-            { active => 'Y'},
-            {
-                order_by => ['username'],
-                page     => ($c->req->param('page') || 1),
-                rows     => 20,
-            }
-        );
-
-        $c->stash(
-            users => $users,
-            pager => $users->pager,
-        );
-    }
-
-    sub add : Chained('admin') PathPart('add') Args(0) {
-        my ($self, $c) = @_;
-
-        my $form = MyApp::Form::AddUser->new;
-
-        $c->stash(form => $form);
-
-        my $user = $c->model('DB::User')->new_result({});
-
-        my ($temp_password) = passgen(NWORDS => 1, NLETT => 8);
-
-        $user->password($temp_password);
-        $user->password_expires(DateTime->now);
-        $user->active('Y');
-
-        return unless $form->process(
-            schema => $c->model('DB')->schema,
-            item   => $user,
-            params => $c->req->body_parameters,
-        );
-
-        $c->stash->{email} = {
-            to           => $user->email_address,
-            from         => 'admin at myapp.org',
-            subject      => 'Welcome to MyApp',
-            content_type => 'text/html',
-            template     => 'welcome.tt2',
-        };
-
-        $c->stash(
-            username => $user->username,
-            password => $temp_password,
-        );
-
-        $c->forward($c->view('Email::Template'));
-
-        $c->res->redirect($c->uri_for($self->action_for('list'), {
-            status_msg => 'User '
-                . $user->username
-                . ' created successfully'
-                . ', initial password emailed ' . 'to '
-                . $user->email_address
-        }));
-    }
-
-    sub user : Chained('admin') PathPart('') CaptureArgs(1) {
-        my ($self, $c, $user_id) = @_;
-
-        $c->stash(user => $c->model('DB::User')->find($user_id));
-    }
-
-    sub inactivate : Chained('user') PathPart('inactivate') Args(0) {
-        my ($self, $c) = @_;
-
-        my $user = $c->stash->{user};
-
-        $user->update({ active => 'N' });
-
-        my $username = $user->username;
-
-        $c->res->redirect($c->uri_for($self->action_for('list'), {
-            status_msg => "User $username inactivated"
-        }));
-    }
-
-    sub reset_password : Chained('user') PathPart('reset_password') Args(0) {
-        my ($self, $c) = @_;
-
-        my $user = $c->stash->{user};
-
-        my ($temp_password) = passgen(NWORDS => 1, NLETT => 8);
-
-        $user->password($temp_password);
-        $user->password_expires(DateTime->now);
-        $user->update;
-
-        $c->stash->{email} = {
-            to           => $user->email_address,
-            from         => 'admin at myapp.org',
-            subject      => 'Your MyApp Password has been Reset',
-            content_type => 'text/html',
-            template     => 'reset_password.tt2',
-        };
-
-        $c->stash(
-            username => $user->username,
-            password => $temp_password,
-        );
-
-        $c->forward($c->view('Email::Template'));
-
-        $c->res->redirect($c->uri_for($self->action_for('list'), {
-            status_msg => 'Password reset email for '
-                . $user->username
-                . ' sent to '
-                . $user->email_address
-        }));
-    }
-
-    sub edit : Chained('user') PathPart('edit') Args(0) {
-        my ($self, $c) = @_;
-
-        my $form = MyApp::Form::EditUser->new;
-
-        $c->stash(form => $form);
-
-        return unless $form->process(
-            schema  => $c->model('DB')->schema,
-            item_id => $c->stash->{user}->id,
-            params  => $c->req->body_parameters,
-        );
-
-        $c->res->redirect($c->uri_for($self->action_for('list'), {
-            status_msg => 'User '
-                . $c->stash->{user}->username
-                . ' updated successfully'
-        }));
-    }
-
-B<NOTE:> the C<from> email address must be a valid address, otherwise your MTA
-will reject the email, replace it with your email address before testing this
-controller.
-
-There are two email templates, one for the C<add> action which goes into
-C<root/src/email/welcome.tt2>:
-
-    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-    <html xmlns="http://www.w3.org/1999/xhtml"
-          xml:lang="en"
-          lang="en">
-    <head>
-    </head>
-    <body>
-    <h2 align="center">Welcome to the MyApp system.</h2>
-    <p>Your username is: <span style="color: green;">[% username %]</span></p>
-    <p>Your initial password is: <span style="color: red;">[% password %]</span></p>
-    <p>You will be asked to change your password on first login.</p>
-    </body>
-    </html>
-
-And one for C<password_reset> which goes into
-C<root/src/email/reset_password.tt2>:
-
-    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-    <html xmlns="http://www.w3.org/1999/xhtml"
-          xml:lang="en"
-          lang="en">
-    <head>
-    </head>
-    <body>
-    <h2 align="center">Your MyApp Password has been Reset</h2>
-    <p>Your username is: <span style="color: green;">[% username %]</span></p>
-    <p>Your password is: <span style="color: red;">[% password %]</span></p>
-    <p>You will be asked to change your password on first login.</p>
-    </body>
-    </html>
-
-Four web templates, C<root/src/user/list.tt2>:
-
-    [% META title = 'MyApp: User Admin' %]
-
-    <br />
-    <a class="button" href="[% c.uri_for('/user/add') %]" onclick='this.blur();'><span>Add User</span></a>
-    <br />
-    Displaying users [% pager.first %]-[% pager.last %] of [% pager.total_entries %]
-
-    <table>
-    <tr>
-        <th>Username</th>
-        <th>Name</th>
-        <th>Email Address</th>
-    </tr>
-    [% WHILE (u = users.next) %]
-    <tr>
-    <td><a href="[% c.uri_for('/user', u.id, 'edit') %]">[% u.username %]</a></td>
-    <td>[% u.name %]</td>
-    <td>[% u.email_address %]</td>
-    <td><a href="[% c.uri_for('/user', u.id, 'reset_password') %]">Reset Password</a></td>
-    <td><a href="[% c.uri_for('/user', u.id, 'inactivate') %]">Inactivate</a></td>
-    </tr>
-    [% END %]
-    </table>
-
-    &lt;&lt; 
-    <a href="[% c.req.uri_with({ page => pager.first_page }) %]">First</a>
-    <a href="[% c.req.uri_with({ page => pager.previous_page })%]">Previous</a>
-    |
-    <a href="[% c.req.uri_with({ page => pager.next_page })%]">Next</a>
-    <a href="[% c.req.uri_with({ page => pager.last_page }) %]">Last</a>
-    &gt;&gt;
-
-C<root/src/user/add.tt2>:
-
-    [% META title = 'MyApp: Add User' %]
-
-    <div>
-    <form name="[% form.name %]" action="[% c.req.uri %]" method="post">
-
-    [% FOR field IN form.error_fields %]
-        [% FOR error IN field.errors %]
-            <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
-        [% END %]
-    [% END %]
-
-    <fieldset style="border: 0;">
-    <table>
-    <tr>
-    [% f = form.field('username') %]
-    <td><label for="[% f.name %]">[% f.label %]:</label></td>
-    <td><input type="text" size=30 name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]"></td>
-    </tr>
-    [% PROCESS user/edit_details.tt2 %]
-    <tr>
-        <td><input type="submit" name="submit" id="submit" value="Add" /></td>
-        <td><a href="/user/list">Users List</a></td>
-    </tr>
-    </fieldset>
-    </table>
-    </form>
-    </div>
-
-C<root/src/user/edit.tt2>:
-
-    [% META title = 'MyApp: Edit User' %]
-
-    <div>
-    <form name="[% form.name %]" action="[% c.req.uri %]" method="post">
-
-    [% FOR field IN form.error_fields %]
-        [% FOR error IN field.errors %]
-            <p><span style="color: red;">[% field.label _ ': ' _ error %]</span></p>
-        [% END %]
-    [% END %]
-
-    <fieldset style="border: 0;">
-    <table>
-    [% PROCESS user/edit_details.tt2 %]
-    <tr>
-        <td><input type="submit" name="submit" id="submit" value="Update" /></td>
-        <td><a href="/user/list">Users List</a></td>
-    </tr>
-    </fieldset>
-    </table>
-    </form>
-    </div>
-
-and C<root/src/user/edit_details.tt2>:
-
-    [% FOREACH field_name = ['name', 'email_address',
-                             'phone_number', 'mail_address'] %]
-    <tr>
-    [% f = form.field(field_name) %]
-    <td><label class="text.label" for="[% f.name %]">[% f.label %]:</label></td>
-    <td><input class="text" type="text" size=30 name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]"></td>
-    </tr>
-    [% END %]
-    <tr>
-    [% f = form.field('roles') %]
-    <td><label for="[% f.name %]">Roles:</label></td>
-    <td>[% f.render %]</td>
-    </tr>
-
-And the two form classes, C<lib/MyApp/Form/AddUser.pm>:
-
-    package MyApp::Form::AddUser;
-
-    use HTML::FormHandler::Moose;
-    extends 'MyApp::Form::EditUser';
-    use namespace::autoclean;
-
-    has_field 'username' => (
-        type     => 'Text',
-        label    => 'User name',
-        required => 1,
-    );
-
-    __PACKAGE__->meta->make_immutable;
-
-    1;
-
-and C<lib/MyApp/Form/EditUser.pm>:
-
-    package MyApp::Form::EditUser;
-
-    use HTML::FormHandler::Moose;
-    extends 'MyApp::Form::UserProfile';
-    use namespace::autoclean;
-
-    has_field 'roles' => (
-        type         => 'Multiple',
-        widget       => 'checkbox_group',
-        label_column => 'name',
-        label        => '',
-    );
-
-    __PACKAGE__->meta->make_immutable;
-
-    1;
-
-Now try it out, launch the server and click the Admin link.
-
-I've taken you on a tour of a simple intranet auth system using modern
-L<Catalyst> technologies, hopefully you got some ideas about how to implement
-your own auth and administration systems.
-
-=head2 AUTHOR
-
-Caelum: Rafael Kitover <rkitover at cpan.org>
-
-=cut




More information about the Catalyst-commits mailing list