updated log format
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Manual / Cookbook.pod
index ff80c19..db58529 100644 (file)
@@ -74,16 +74,16 @@ Catalyst Controller module 'upload' action:
         if ( $c->request->parameters->{form_submit} eq 'yes' ) {
 
             if ( my $upload = $c->request->upload('my_file') ) {
-            
+
                 my $filename = $upload->filename;
                 my $target   = "/tmp/upload/$filename";
-                
+
                 unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
                     die( "Failed to copy '$filename' to '$target': $!" );
                 }
             }
         }
-        
+
         $c->stash->{template} = 'file_upload.html';
     }
 
@@ -113,7 +113,7 @@ And in the controller:
                 my $upload   = $c->req->upload($field);
                 my $filename = $upload->filename;
                 my $target   = "/tmp/upload/$filename";
-                
+
                 unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
                     die( "Failed to copy '$filename' to '$target': $!" );
                 }
@@ -135,135 +135,166 @@ displaying this message.
 For more information about uploads and usable methods look at
 L<Catalyst::Request::Upload> and L<Catalyst::Request>.
 
-=head2 Authentication with Catalyst::Plugin::Authentication::CDBI
+=head2 Authentication with Catalyst::Plugin::Authentication
+
+In this example, we'll use the
+L<Catalyst::Plugin::Authentication::Store::DBIC> store and the
+L<Catalyst::Plugin::Authentication::Credential::Password> credentials.
+
+In the lib/MyApp.pm package, we'll need to change the C<use Catalyst;>
+line to include the following modules:
+
+    use Catalyst qw/
+        ConfigLoader 
+        Authentication
+        Authentication::Store::DBIC
+        Authentication::Credential::Password
+        Session
+        Session::Store::FastMmap
+        Session::State::Cookie
+        HTML::Widget
+        Static::Simple
+        /;
+
+The Session, Session::Store::* and Session::State::* modules listed above
+ensure that we stay logged-in across multiple page-views.
+
+In our MyApp.yml configuration file, we'll need to add:
+
+    authentication:
+      dbic:
+        user_class: MyApp::Model::DBIC::User
+        user_field: username
+        password_field: password
+        password_type: hashed
+        password_hash_type: SHA-1
+
+'user_class' is a DBIx::Class package for your users table.
+'user_field' tells which field (column) is used for username lookup.
+'password_field' is the password field in your table.
+The above settings for 'password_type' and 'password_hash_type' ensure that
+the password won't be stored in the database in clear text.
+
+In SQLite, the users table might be something like:
+
+    CREATE TABLE user (
+        id       INTEGER PRIMARY KEY,
+        username VARCHAR(100),
+        password VARCHAR(100)
+    );
+
+Now we need to create a DBIC::SchemaLoader component for this database
+(changing "myapp.db" to wherever your SQLite database is).
+
+    script/myapp_create.pl model DBIC DBIC::SchemaLoader 'dbi:SQLite:myapp.db'
+
+Now we can start creating our page controllers and templates.
+For our homepage, we create the file "root/index.tt" containing:
+
+    <html>
+    <body>
+    [% IF c.user %]
+        <p>hello [% c.user.username %]</p>
+        <p><a href="[% c.uri_for( '/logout' ) %]">logout</a></p>
+    [% ELSE %]
+        <p><a href="[% c.uri_for( '/login' ) %]">login</a></p>
+    [% END %]
+    </body>
+    </html>
+
+If the user is logged in, they will be shown their name, and a logout link.
+Otherwise, they will be shown a login link.
+
+To display the homepage, we can uncomment the C<default> and C<end>
+subroutines in lib/MyApp/Controller/Root.pm and populate them as so:
+
+    sub default : Private {
+        my ( $self, $c ) = @_;
+    
+        $c->stash->{template} = 'index.tt';
+    }
 
-There are (at least) two ways to implement authentication with this plugin:
-1) only checking username and password;
-2) checking username, password, and the roles the user has
+    sub end : Private {
+        my ( $self, $c ) = @_;
+    
+        $c->forward( $c->view('TT') )
+            unless $c->response->body || $c->response->redirect;
+    }
 
-For both variants you'll need the following code in your MyApp package:
+The login template is very simple, as L<HTML::Widget> will handle the
+HTML form creation for use. This is saved as "root/login.tt".
 
-    use Catalyst qw/Session::FastMmap Static Authentication::CDBI/;
+    <html>
+    <head>
+    <link href="[% c.uri_for('/static/simple.css') %]" rel="stylesheet" type="text/css">
+    </head>
+    <body>
+    [% result %]
+    </body>
+    </html>
 
-    MyApp->config( authentication => { user_class => 'MyApp::M::MyApp::Users',
-                                       user_field => 'email',
-                                       password_field => 'password' });
+For the HTML form to look correct, we also copy the C<simple.css> file
+from the L<HTML::Widget> distribution into our "root/static" folder.
+This file is automatically server by the L<Catalyst::Plugin::Static::Simple>
+module which we loaded in our lib/MyApp.pm package.
 
-'user_class' is a Class::DBI class for your users table.
-'user_field' tells which field is used for username lookup (might be 
-email, first name, surname etc.).
-'password_field' is, well, password field in your table and by default 
-password is stored in plain text. Authentication::CDBI looks for 'user' 
-and 'password' fields in table, if they're not defined in the config.
+To handle login requests, we first create a controller, like so:
 
-In PostgreSQL, the users table might be something like:
+    script/myapp_create.pl controller Login
 
- CREATE TABLE users (
-   user_id   serial,
-   name      varchar(100),
-   surname   varchar(100),
-   password  varchar(100),
-   email     varchar(100),
-   primary key(user_id)
- );
+In the lib/MyApp/Controller/Login.pm package, we can then uncomment the
+C<default> subroutine, and populate it, as below.
 
-We'll discuss the first variant for now:
-1. user:password login/auth without roles
+First the widget is created, it needs the 'action' set, and 'username' and
+'password' fields and a submit button added.
 
-To log in a user you might use an action like this:
+Then, if we've received a username and password in the request, we attempt
+to login. If successful, we redirect to the homepage; if not the login form
+will be displayed again.
 
-    sub login : Local {
-        my ($self, $c) = @_;
-        if ($c->req->params->{username}) {
-            $c->session_login($c->req->params->{username}, 
-                              $c->req->params->{password} );
-            if ($c->req->{user}) {
-                $c->forward('/restricted_area');
+    sub default : Private {
+        my ( $self, $c ) = @_;
+    
+        $c->widget->method('POST')->action( $c->uri_for('/login') );
+        $c->widget->element( 'Textfield', 'username' )->label( 'Username' );
+        $c->widget->element( 'Password', 'password' )->label( 'Password' );
+        $c->widget->element( 'Submit' )->value( 'Login' );
+        
+        my $result = $c->widget->process( $c->req );
+        
+        if ( my $user = $result->param('username')
+            and my $pass = $result->param('password') )
+        {    
+            if ( $c->login( $user, $pass ) ) {
+                $c->response->redirect( $c->uri_for( "/" ) );
+                return;
             }
         }
+        
+        $c->stash->{template} = 'login.tt';
+        $c->stash->{result}   = $result;
     }
 
-This action should not go in your MyApp class...if it does, it will
-conflict with the built-in method of the same name.  Instead, put it
-in a Controller class.
-
-$c->req->params->{username} and $c->req->params->{password} are html 
-form parameters from a login form. If login succeeds, then 
-$c->req->{user} contains the username of the authenticated user.
-
-If you want to remember the user's login status in between further 
-requests, then just use the C<$c-E<gt>session_login> method. Catalyst will 
-create a session id and session cookie and automatically append session 
-id to all urls. So all you have to do is just check $c->req->{user} 
-where needed.
-
-To log out a user, just call $c->session_logout.
-
-Now let's take a look at the second variant:
-2. user:password login/auth with roles
-
-To use roles you need to add the following parameters to  MyApp->config in the 'authentication' section:
-
-    role_class      => 'MyApp::M::MyApp::Roles',
-    user_role_class => 'MyApp::M::MyApp::UserRoles',
-    user_role_user_field => 'user_id',
-    user_role_role_field => 'role_id',
-
-Corresponding tables in PostgreSQL could look like this:
-
- CREATE TABLE roles (
-   role_id  serial,
-   name     varchar(100),
-   primary key(role_id)
- );
+To handle logout's, we create a new controller:
 
- CREATE TABLE user_roles (
-   user_role_id  serial,
-   user_id       int,
-   role_id       int,
-   primary key(user_role_id),
-   foreign key(user_id) references users(user_id),
-   foreign key(role_id) references roles(role_id)
- );
+    script/myapp_create.pl controller Logout
 
-The 'roles' table is a list of role names and the 'user_role' table is 
-used for the user -> role lookup.
+Then in the lib/MyApp/Controller/Logout.pm package, we change the
+C<default> subroutine, to logout and then redirect back to the
+homepage.
 
-Now if a logged-in user wants to see a location which is allowed only 
-for people with an 'admin' role, in your controller you can check it 
-with:
-
-    sub add : Local {
-        my ($self, $c) = @_;
-        if ($c->roles(qw/admin/)) {
-            $c->res->output("Your account has the role 'admin.'");
-        } else {
-            $c->res->output("You're not allowed to be here.");
-        }
-    }
-
-One thing you might need is to forward non-authenticated users to a login 
-form if they try to access restricted areas. If you want to do this 
-controller-wide (if you have one controller for your admin section) then it's 
-best to add a user check to a 'begin' action:
-
-    sub begin : Private {
-        my ($self, $c) = @_;
-        unless ($c->req->{user}) {
-            $c->req->action(undef);  ## notice this!!
-            $c->forward('/user/login');
-        }
+    sub default : Private {
+        my ( $self, $c ) = @_;
+    
+        $c->logout;
+        
+        $c->response->redirect( $c->uri_for( "/" ) );
     }
 
-Pay attention to $c->req->action(undef). This is needed because of the
-way $c->forward works - C<forward> to C<login> gets called, but after
-that Catalyst will still execute the action defined in the URI (e.g. if
-you tried to go to C</add>, then first 'begin' will forward to 'login',
-but after that 'add' will nonetheless be executed). So
-$c->req->action(undef) undefines any actions that were to be called and
-forwards the user where we want him/her to be.
+Remember that to test this, we would first need to add a user to the
+database, ensuring that the password field is saved as the SHA1 hash
+of our desired password.
 
-And this is all you need to do. 
 
 =head2 Pass-through login (and other actions)
 
@@ -321,7 +352,7 @@ output and  drops the first line if it is an HTTP status line (note:
 this may change).
 
 The Apache C<mod_fastcgi> module is provided by a number of Linux 
-distros and is straightforward to compile for most Unix-like systems.  
+distro's and is straightforward to compile for most Unix-like systems.  
 The module provides a FastCGI Process Manager, which manages FastCGI 
 scripts.  You configure your script as a FastCGI script with the 
 following Apache configuration directives:
@@ -343,7 +374,7 @@ authentication, authorization, and access check phases.
 
 For more information see the FastCGI documentation, the C<FCGI> module 
 and L<http://www.fastcgi.com/>.
+
 =head2 Serving static content
 
 Serving static content in Catalyst can be somewhat tricky; this recipe
@@ -410,7 +441,7 @@ Edit the file and add the following methods:
     # serve all files under /static as static files
     sub default : Path('/static') {
         my ( $self, $c ) = @_;
-    
+
         # Optional, allow the browser to cache the content
         $c->res->headers->header( 'Cache-Control' => 'max-age=86400' );
 
@@ -420,7 +451,7 @@ Edit the file and add the following methods:
     # also handle requests for /favicon.ico
     sub favicon : Path('/favicon.ico') {
         my ( $self, $c ) = @_;
-    
+
         $c->serve_static;
     }
 
@@ -461,7 +492,7 @@ config for this application under mod_perl 1.x:
         use lib qw(/var/www/MyApp/lib);
     </Perl>
     PerlModule MyApp
-    
+
     <VirtualHost *>
         ServerName myapp.example.com
         DocumentRoot /var/www/MyApp/root
@@ -495,8 +526,8 @@ the Catalyst Request object:
   $c->req->args([qw/arg1 arg2 arg3/]);
   $c->forward('/wherever');
 
-(See L<Catalyst::Manual::Intro#Flow_Control> for more information on
-passing arguments via C<forward>.)
+(See the L<Catalyst::Manual::Intro> Flow_Control section for more 
+information on passing arguments via C<forward>.)
 
 =head2 Configure your application
 
@@ -523,16 +554,6 @@ Now create C<myapp.yml> in your application home:
   # DO NOT USE TABS FOR INDENTATION OR label/value SEPARATION!!!
   name:     MyApp
 
-  # authentication; perldoc Catalyst::Plugin::Authentication::CDBI
-  authentication:
-    user_class:           'MyApp::M::MyDB::Customer'
-    user_field:           'username'
-    password_field:       'password'
-    password_hash:        'md5'
-    role_class:           'MyApp::M::MyDB::Role'
-    user_role_class:      'MyApp::M::MyDB::PersonRole'
-    user_role_user_field: 'person'
-
   # session; perldoc Catalyst::Plugin::Session::FastMmap
   session:
     expires:        '3600'
@@ -564,27 +585,31 @@ This is equivalent to:
 
 See also L<YAML>.
 
-=head2 Using existing CDBI (etc.) classes with Catalyst
+=head2 Using existing DBIC (etc.) classes with Catalyst
 
 Many people have existing Model classes that they would like to use with
 Catalyst (or, conversely, they want to write Catalyst models that can be
 used outside of Catalyst, e.g.  in a cron job). It's trivial to write a
 simple component in Catalyst that slurps in an outside Model:
 
-    package MyApp::M::Catalog;
-    use base qw/Catalyst::Base Some::Other::CDBI::Module::Catalog/;
+    package MyApp::Model::DB;
+    use base qw/Catalyst::Model::DBIC::Schema/;
+    __PACKAGE__->config(
+        schema_class => 'Some::DBIC::Schema',
+        connect_info => ['dbi:SQLite:foo.db', '', '', {AutoCommit=>1}];
+    );
     1;
 
-and that's it! Now C<Some::Other::CDBI::Module::Catalog> is part of your
-Cat app as C<MyApp::M::Catalog>.
+and that's it! Now C<Some::DBIC::Schema> is part of your
+Cat app as C<MyApp::Model::DB>.
 
 =head2 Delivering a Custom Error Page
 
 By default, Catalyst will display its own error page whenever it
 encounters an error in your application. When running under C<-Debug>
 mode, the error page is a useful screen including the error message and
-a full Data::Dumper output of the C<$c> context object. When not in
-C<-Debug>, users see a simple "Please come back later" screen.
+L<Data::Dump> output of the relevant parts of the C<$c> context object. 
+When not in C<-Debug>, users see a simple "Please come back later" screen.
 
 To use a custom error page, use a special C<end> method to short-circuit
 the error processing. The following is an example; you might want to
@@ -594,21 +619,21 @@ C<end> method; see L<Catalyst::Plugin::FillInForm>).
 
     sub end : Private {
         my ( $self, $c ) = @_;
-    
+
         if ( scalar @{ $c->error } ) {
             $c->stash->{errors}   = $c->error;
             $c->stash->{template} = 'errors.tt';
             $c->forward('MyApp::View::TT');
-            $c->{error} = [];
+            $c->error(0);
         }
-    
+
         return 1 if $c->response->status =~ /^3\d\d$/;
         return 1 if $c->response->body;
-    
+
         unless ( $c->response->content_type ) {
             $c->response->content_type('text/html; charset=utf-8');
         }
-    
+
         $c->forward('MyApp::View::TT');
     }
 
@@ -616,6 +641,164 @@ You can manually set errors in your code to trigger this page by calling
 
     $c->error( 'You broke me!' );
 
+=head2 Require user logins
+
+It's often useful to restrict access to your application to a set of
+registered users, forcing everyone else to the login page until they're
+signed in.
+
+To implement this in your application make sure you have a customer
+table with username and password fields and a corresponding Model class
+in your Catalyst application, then make the following changes:
+
+=head3 lib/MyApp.pm
+
+  use Catalyst qw/
+      Authentication
+      Authentication::Store::DBIC
+      Authentication::Credential::Password
+  /;
+
+  __PACKAGE__->config->{authentication}->{dbic} = {
+    'user_class'        => 'My::Model::DBIC::User',
+    'user_field'        => 'username',
+    'password_field'    => 'password'
+    'password_type'     => 'hashed',
+    'password_hash_type'=> 'SHA-1'
+  };
+
+  sub auto : Private {
+    my ($self, $c) = @_;
+    my $login_path = 'user/login';
+
+    # allow people to actually reach the login page!
+    if ($c->request->path eq $login_path) {
+      return 1;
+    }
+
+    # if a user doesn't exist, force login
+    if ( !$c->user_exists ) {
+      # force the login screen to be shown
+      $c->response->redirect($c->request->base . $login_path);
+    }
+
+    # otherwise, we have a user - continue with the processing chain
+    return 1;
+  }
+
+=head3 lib/MyApp/C/User.pm
+
+  sub login : Path('/user/login') {
+    my ($self, $c) = @_;
+
+    # default template
+    $c->stash->{'template'} = "user/login.tt";
+    # default form message
+    $c->stash->{'message'} = 'Please enter your username and password';
+
+    if ( $c->request->param('username') ) {
+      # try to log the user in
+      # login() is provided by ::Authentication::Credential::Password
+      if( $c->login(
+        $c->request->param('username'),
+        $c->request->param('password'),
+        );
+
+        # if login() returns 1, user is now logged in
+        $c->response->redirect('/some/page');
+      }
+
+      # otherwise we failed to login, try again!
+      $c->stash->{'message'} = 
+         'Unable to authenticate the login details supplied';
+    }
+  }
+
+  sub logout : Path('/user/logout') {
+    my ($self, $c) = @_;
+    # log the user out
+    $c->logout;
+
+    # do the 'default' action
+    $c->response->redirect($c->request->base);
+  }
+
+
+=head3 root/base/user/login.tt
+
+ [% INCLUDE header.tt %]
+ <form action="/user/login" method="POST" name="login_form">
+    [% message %]<br />
+    <label for="username">username:</label><br />
+    <input type="text" id="username" name="username" /><br />
+
+    <label for="password">password:</label><br />
+    <input type="password" id="password" name="password" /><br />
+
+    <input type="submit" value="log in" name="form_submit" />
+  </form>
+  [% INCLUDE footer.tt %]
+
+=head2 Role-based Authorization
+
+For more advanced access control, you may want to consider using role-based
+authorization. This means you can assign different roles to each user, e.g.
+"user", "admin", etc.
+
+The C<login> and C<logout> methods and view template are exactly the same as
+in the previous example.
+
+The L<Catalyst::Plugin::Authorization::Roles> plugin is required when
+implementing roles:
+
+ use Catalyst qw/
+    Authentication
+    Authentication::Credential::Password
+    Authentication::Store::Htpasswd
+    Authorization::Roles
+  /;
+
+Roles are implemented automatically when using
+L<Catalyst::Authentication::Store::Htpasswd>:
+
+  # no additional role configuration required
+  __PACKAGE__->config->{authentication}{htpasswd} = "passwdfile";
+
+Or can be set up manually when using L<Catalyst::Authentication::Store::DBIC>:
+
+  # Authorization using a many-to-many role relationship
+  __PACKAGE__->config->{authorization}{dbic} = {
+    'role_class'           => 'My::Model::DBIC::Role',
+    'role_field'           => 'name',
+    'user_role_user_field' => 'user',
+
+    # DBIx::Class only (omit if using Class::DBI)
+    'role_rel'             => 'user_role',
+
+    # Class::DBI only, (omit if using DBIx::Class)
+    'user_role_class'      => 'My::Model::CDBI::UserRole'
+    'user_role_role_field' => 'role',
+  };
+
+To restrict access to any action, you can use the C<check_user_roles> method:
+
+  sub restricted : Local {
+     my ( $self, $c ) = @_;
+
+     $c->detach("unauthorized")
+       unless $c->check_user_roles( "admin" );
+
+     # do something restricted here
+  }
+
+You can also use the C<assert_user_roles> method. This just gives an error if
+the current user does not have one of the required roles:
+
+  sub also_restricted : Global {
+    my ( $self, $c ) = @_;
+    $c->assert_user_roles( qw/ user admin / );
+  }
+
 =head1 AUTHOR
 
 Sebastian Riedel, C<sri@oook.de>
@@ -625,6 +808,9 @@ Marcus Ramberg, C<mramberg@cpan.org>
 Jesse Sheidlower, C<jester@panix.com>
 Andy Grundman, C<andy@hybridized.org> 
 Chisel Wright, C<pause@herlpacker.co.uk>
+Will Hawes, C<info@whawes.co.uk>
+Gavin Henry, C<ghenry@cpan.org> (Spell checking)
+
 
 =head1 COPYRIGHT