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 file.
The Users Schema
In users.sql
we'll put the tables for holding the user information of our
app.
If you're following along on PostgreSQL replace INTEGER PRIMARY KEY
with
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@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
Password Hashing
We will use DBIx::Class::PassphraseColumn and 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
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 cpanm -n
to skip tests.
First regenerate the DBIx::Class schema with our new tables and this component (make sure you have the latest CPAN version of 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 lib/MyApp/Schema/Result/User.pm
and below the DO NOT MODIFY
line
add the following:
__PACKAGE__->add_columns( '+password' => { passphrase => 'rfc2307', passphrase_class => 'BlowfishCrypt', passphrase_args => { cost => 14, salt_random => 20, }, passphrase_check_method => 'check_password', } );
If your DBIx::Class::Schema::Loader did not generate a many_to_many
to
roles, you will also need to add the following:
__PACKAGE__->many_to_many("roles", "user_roles", "role");
Configuring Catalyst Authentication
We will use 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: Catalyst::Plugin::Session, Catalyst::Plugin::Session::Store::Memcached, Catalyst::Plugin::Session::State::Cookie, Catalyst::Plugin::Authentication, Catalyst::Authentication::Store::DBIx::Class, Catalyst::Plugin::Authorization::Roles and CatalystX::SimpleLogin.
Now edit lib/MyApp.pm
and change the 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 __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'], login_form_args => { authenticate_args => { active => 'Y' }, }, }, );
Now we need a login template:
mkdir root/src/login touch root/src/login/login.tt2
Place the following into the 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
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 /base
. Replace the Books
controller in 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
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 perl script/set_admin_password.pl
and the password should be set.
Now run perl script/myapp_server.pl
, go to the server in your web browser and
it should ask you to log in. Log in as admin/admin
, and it should take you to
the books list.
Authorization
Let's protect our edit and delete actions for books by allowing only users with
the can_edit
or admin
roles.
You will need the following modules: Catalyst::Controller::ActionRole and Catalyst::ActionRole::ACL.
Change the 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 url_create
, form_create
,
form_create_do
chain from edit
and delete
chain from edit_object
.
Finally, add a 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 Access Denied
.
We should really hide the delete link, to do so edit root/src/books/list.tt2
.
and change the 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
Password Expiry
For the next section you will need the modules HTML::FormHandler and Method::Signatures::Simple.
Create a skeleton User
controller with a change_password
action in
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 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 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 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->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; } }
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.
User Profile
Now we'll create an editable profile page.
You will need the module HTML::FormHandler::Model::DBIC.
First, lets add a couple navigation links to the wrapper, edit
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
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->id, params => $c->req->body_parameters, ); $c->res->redirect($c->uri_for('/books/list', { status_msg => 'Profile Updated' })); }
Add the template in 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.
User Administration
Finally, we will create an interface to manage the users of the app.
For this part you will need the modules Crypt::PassGen and Catalyst::View::Email.
Create an email view:
perl script/myapp_create.pl view Email::Template Email::Template
And add its configuration to 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 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@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@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' })); }
NOTE: the 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 add
action which goes into
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 password_reset
which goes into
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, 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> << <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> >>
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>
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 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, 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 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 Catalyst technologies, hopefully you got some ideas about how to implement your own auth and administration systems.
AUTHOR
Caelum: Rafael Kitover <rkitover@cpan.org>
- Previous
- Next