the new tables added in the previous step, let's use the C<create=static>
option on the DBIC model helper to do most of the work for us:
- $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema create=static dbi:SQLite:myapp.db
+ $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
+ create=static components=TimeStamp dbi:SQLite:myapp.db
exists "/root/dev/MyApp/script/../lib/MyApp/Model"
exists "/root/dev/MyApp/script/../t"
Dumping manual schema for MyApp::Schema to directory /root/dev/MyApp/script/../lib ...
Schema dump completed.
exists "/root/dev/MyApp/script/../lib/MyApp/Model/DB.pm"
$
- $ ls lib/MyApp/Schema
+ $ ls lib/MyApp/Schema/Result
Authors.pm BookAuthors.pm Books.pm Roles.pm UserRoles.pm Users.pm
Notice how the helper has added three new table-specific result source
-files to the C<lib/MyApp/Schema/MyApp> directory. And, more
+files to the C<lib/MyApp/Schema/Result> directory. And, more
importantly, even if there were changes to the existing result source
files, those changes would have only been written above the C<# DO NOT
MODIFY THIS OR ANYTHING ABOVE!> comment and your hand-edited
each of these files and add the following information between the C<#
DO NOT MODIFY THIS OR ANYTHING ABOVE!> comment and the closing C<1;>:
-C<lib/MyApp/Schema/Users.pm>:
+C<lib/MyApp/Schema/Result/Users.pm>:
#
# Set relationships:
#
-
+
# has_many():
# args:
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of the model class referenced by this relationship
# 3) Column name in *foreign* table (aka, foreign key in peer table)
- __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::UserRoles', 'user_id');
-
+ __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::Result::UserRoles', 'user_id');
+
# many_to_many():
# args:
# 1) Name of relationship, DBIC will create accessor with this name
__PACKAGE__->many_to_many(roles => 'map_user_role', 'role');
-C<lib/MyApp/Schema/Roles.pm>:
+C<lib/MyApp/Schema/Result/Roles.pm>:
#
# Set relationships:
#
-
+
# has_many():
# args:
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of the model class referenced by this relationship
# 3) Column name in *foreign* table (aka, foreign key in peer table)
- __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::UserRoles', 'role_id');
+ __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::Result::UserRoles', 'role_id');
-C<lib/MyApp/Schema/UserRoles.pm>:
+C<lib/MyApp/Schema/Result/UserRoles.pm>:
#
# Set relationships:
#
-
+
# belongs_to():
# args:
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of the model class referenced by this relationship
# 3) Column name in *this* table
- __PACKAGE__->belongs_to(user => 'MyApp::Schema::Users', 'user_id');
-
+ __PACKAGE__->belongs_to(user => 'MyApp::Schema::Result::Users', 'user_id');
+
# belongs_to():
# args:
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of the model class referenced by this relationship
# 3) Column name in *this* table
- __PACKAGE__->belongs_to(role => 'MyApp::Schema::Roles', 'role_id');
+ __PACKAGE__->belongs_to(role => 'MyApp::Schema::Result::Roles', 'role_id');
The code for these three sets of updates is obviously very similar to
classes created in Part 3.
Note that we do not need to make any change to the
-C<lib/MyApp/Schema.pm> schema file. It simply tells DBIC to
-load all of the result class files it finds in below the
-C<lib/MyApp/Schema> directory, so it will automatically pick
-up our new table information.
+C<lib/MyApp/Schema.pm> schema file. It simply tells DBIC to load all
+of the Result Class and ResultSet Class files it finds in below the
+C<lib/MyApp/Schema> directory, so it will automatically pick up our
+new table information.
=head2 Sanity-Check Reload of Development Server
'-------------------------------------------------------------------+----------'
...
-Again, notice that your "result class" classes have been "re-loaded"
+Again, notice that your "Result Class" classes have been "re-loaded"
by Catalyst under C<MyApp::Model>.
Edit C<lib/MyApp.pm> and update it as follows (everything below
C<StackTrace> is new):
- __PACKAGE__->setup(qw/
- -Debug
- ConfigLoader
- Static::Simple
-
- StackTrace
-
- Authentication
-
- Session
- Session::Store::FastMmap
- Session::State::Cookie
- /);
-
-B<Note:> As discussed in MoreCatalystBasics, different versions of
-C<Catalyst::Devel> have used a variety of methods to load the plugins.
+ # Load plugins
+ use Catalyst qw/-Debug
+ ConfigLoader
+ Static::Simple
+
+ StackTrace
+
+ Authentication
+
+ Session
+ Session::Store::FastMmap
+ Session::State::Cookie
+ /;
+
+B<Note:> As discussed in MoreCatalystBasics, different versions of
+C<Catalyst::Devel> have used a variety of methods to load the plugins.
You can put the plugins in the C<use Catalyst> statement if you prefer.
The C<Authentication> plugin supports Authentication while the
=head2 Configure Authentication
-Although C<__PACKAGE__-E<gt>config(name =E<gt> 'value');> is still
-supported, newer Catalyst applications tend to place all configuration
-information in C<myapp.conf> and automatically load this information
-into C<MyApp-E<gt>config> using the
+Although C<__PACKAGE__-E<gt>config(name =E<gt> 'value');> is still
+supported, newer Catalyst applications tend to place all configuration
+information in C<myapp.conf> and automatically load this information
+into C<MyApp-E<gt>config> using the
L<ConfigLoader|Catalyst::Plugin::ConfigLoader> plugin.
-As discussed in Part 3 of the tutorial, Catalyst has recently
-switched from a default config file format of YAML to
-L<Config::General|Config::General> (an apache-like format). In case
-you are using a version of Catalyst earlier than v5.7014, delete the
-C<myapp.yml>, or convert it to .conf format using the TIP in
-L<Catalyst::Manual::MoreCatalystBasics/EDIT THE LIST OF CATALYST PLUGINS>
-then simply follow the directions below to create a new C<myapp.conf>
+As discussed in Part 3 of the tutorial, Catalyst has recently
+switched from a default config file format of YAML to
+L<Config::General|Config::General> (an apache-like format). In case
+you are using a version of Catalyst earlier than v5.7014, delete the
+C<myapp.yml>, or convert it to .conf format using the TIP in
+L<Catalyst::Manual::MoreCatalystBasics/EDIT THE LIST OF CATALYST PLUGINS>
+then simply follow the directions below to create a new C<myapp.conf>
file. Although we will use the C<Config::General> format here because
-YAML files can be difficult to cut and paste in certain environments,
-you are free to use any format supported by
+YAML files can be difficult to cut and paste in certain environments,
+you are free to use any format supported by
L<Catalyst::Plugin::ConfigLoader|Catalyst::Plugin::ConfigLoader> and
L<Config::Any|Config::Any> -- Catalyst will transparently handle the
different formats.
# Use DBIC to retrieve username, password & role information
class DBIx::Class
# This is the model object created by Catalyst::Model::DBIC
- # from your schema (you created 'MyApp::Schema::User' but as
- # the Catalyst startup debug messages show, it was loaded as
- # 'MyApp::Model::DB::Users').
+ # from your schema (you created 'MyApp::Schema::Result::User'
+ # but as the Catalyst startup debug messages show, it was
+ # loaded as 'MyApp::Model::DB::Users').
# NOTE: Omit 'MyApp::Model' here just as you would when using
# '$c->model("DB::Users)'
user_class DB::Users
$ script/myapp_create.pl controller Login
$ script/myapp_create.pl controller Logout
-You could easily use a single controller here. For example, you could
-have a C<User> controller with both C<login> and C<logout> actions.
-Remember, Catalyst is designed to be very flexible, and leaves such
+You could easily use a single controller here. For example, you could
+have a C<User> controller with both C<login> and C<logout> actions.
+Remember, Catalyst is designed to be very flexible, and leaves such
matters up to you, the designer and programmer.
-Then open C<lib/MyApp/Controller/Login.pm>, locate the
-C<sub index :Path :Args(0)> method (or C<sub index : Private> if you
-are using an older version of Catalyst) that was automatically
-inserted by the helpers when we created the Login controller above,
+Then open C<lib/MyApp/Controller/Login.pm>, locate the
+C<sub index :Path :Args(0)> method (or C<sub index : Private> if you
+are using an older version of Catalyst) that was automatically
+inserted by the helpers when we created the Login controller above,
and update the definition of C<sub index> to match:
=head2 index
-
+
Login logic
-
+
=cut
-
+
sub index :Path :Args(0) {
my ($self, $c) = @_;
-
+
# Get the username and password from form
my $username = $c->request->params->{username} || "";
my $password = $c->request->params->{password} || "";
-
+
# If the username and password values were found in form
if ($username && $password) {
# Attempt to log the user in
$c->stash->{error_msg} = "Bad username or password.";
}
}
-
+
# If either of above don't work out, send to the login page
$c->stash->{template} = 'login.tt2';
}
C<username> and C<password> values are not present in the form, the
user will be taken to the empty login form.
-Note that we could have used something like "C<sub default :Path>",
-however, it is generally recommended (partly for historical reasons,
-and partly for code clarity) only to use C<default> in
-C<MyApp::Controller::Root>, and then mainly to generate the 404 not
+Note that we could have used something like "C<sub default :Path>",
+however, it is generally recommended (partly for historical reasons,
+and partly for code clarity) only to use C<default> in
+C<MyApp::Controller::Root>, and then mainly to generate the 404 not
found page for the application.
Instead, we are using "C<sub somename :Path :Args(0) {...}>" here to
C<lib/MyApp/Controller/Logout.pm> to match:
=head2 index
-
+
Logout logic
-
+
=cut
-
+
sub index :Path :Args(0) {
my ($self, $c) = @_;
-
+
# Clear the user's state
$c->logout;
-
+
# Send the user to the starting point
$c->response->redirect($c->uri_for('/'));
}
Create a login form by opening C<root/src/login.tt2> and inserting:
[% META title = 'Login' %]
-
+
<!-- Login form -->
<form method="post" action="[% c.uri_for('/login') %]">
<table>
the following method:
=head2 auto
-
+
Check if there is a user and, if not, forward to login page
-
+
=cut
-
+
# Note that 'auto' runs after 'begin' but before your actions and that
# 'auto's "chain" (all from application path to most specific class are run)
# See the 'Actions' section of 'Catalyst::Manual::Intro' for more info.
sub auto : Private {
my ($self, $c) = @_;
-
+
# Allow unauthenticated users to reach the login page. This
# allows unauthenticated users to reach any action in the Login
# controller. To lock it down to a single action, we could use:
if ($c->controller eq $c->controller('Login')) {
return 1;
}
-
+
# If a user doesn't exist, force login
if (!$c->user_exists) {
# Dump a log message to the development server debug output
# Return 0 to cancel 'post-auto' processing and prevent use of application
return 0;
}
-
+
# User found, so return 1 to continue with processing after this 'auto'
return 1;
}
-As discussed in
-L<Catalyst::Manual::Tutorial::MoreCatalystBasics/CREATE A CATALYST CONTROLLER>,
-every C<auto> method from the application/root controller down to the
-most specific controller will be called. By placing the
-authentication enforcement code inside the C<auto> method of
-C<lib/MyApp/Controller/Root.pm> (or C<lib/MyApp.pm>), it will be
-called for I<every> request that is received by the entire
+As discussed in
+L<Catalyst::Manual::Tutorial::MoreCatalystBasics/CREATE A CATALYST CONTROLLER>,
+every C<auto> method from the application/root controller down to the
+most specific controller will be called. By placing the
+authentication enforcement code inside the C<auto> method of
+C<lib/MyApp/Controller/Root.pm> (or C<lib/MyApp.pm>), it will be
+called for I<every> request that is received by the entire
application.
this, open C<root/src/login.tt2> in your editor and add the following
lines to the bottom of the file:
+ ...
<p>
[%
# This code illustrates how certain parts of the TT
$ script/myapp_server.pl
-B<IMPORTANT NOTE:> If you are having issues with authentication on
-Internet Explorer, be sure to check the system clocks on both your
-server and client machines. Internet Explorer is very picky about
-timestamps for cookies. You can quickly sync an Ubuntu system with
-the following command:
+B<IMPORTANT NOTE:> If you are having issues with authentication on
+Internet Explorer, be sure to check the system clocks on both your
+server and client machines. Internet Explorer is very picky about
+timestamps for cookies. You can quickly sync a Debian system by
+installing the "ntpdate" package:
+
+ sudo aptitude -y install ntpdate
+
+And then run the following command:
+
+ sudo ntpdate-debian
- sudo ntpdate ntp.ubuntu.com
+Or, depending on your firewall configuration:
-Or possibly try C<sudo ntpdate -u ntp.ubuntu.com> (to us an
-unpriviledged port) or C<sudo ntpdate pool.ntp.org> (to try a
-different server in case the Ubuntu NTP server is down).
+ sudo ntpdate-debian -u
-Now trying going to L<http://localhost:3000/books/list> and you should
-be redirected to the login page, hitting Shift+Reload or Ctrl+Reload
-if necessary (the "You are already logged in" message should I<not>
-appear -- if it does, click the C<logout> button and try again). Note
-the C<***Root::auto User not found...> debug message in the
-development server output. Enter username C<test01> and password
+Note: NTP can be a little more finicky about firewalls because it uses
+UDP vs. the more common TCP that you see with most Internet protocols.
+Worse case, you might have to manually set the time on your development
+box instead of using NTP.
+
+Now trying going to L<http://localhost:3000/books/list> and you should
+be redirected to the login page, hitting Shift+Reload or Ctrl+Reload
+if necessary (the "You are already logged in" message should I<not>
+appear -- if it does, click the C<logout> button and try again). Note
+the C<***Root::auto User not found...> debug message in the
+development server output. Enter username C<test01> and password
C<mypass>, and you should be taken to the Book List page.
Open C<root/src/books/list.tt2> and add the following lines to the
using a SHA-1 hash. If you are concerned about cleartext passwords
between the browser and your application, consider using SSL/TLS, made
easy with the Catalyst plugin Catalyst::Plugin:RequireSSL. You should
-also consider adding a "salt" mechanism to your hashed passwords to
+also consider adding a "salt" mechanism to your hashed passwords to
mitigate the risk of a "rainbow table" crack against your passwords (see
L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password>
for more information on using a salt value).
$ perl -MDigest::SHA -e 'print Digest::SHA::sha1_hex("mypass"), "\n"'
e727d1464ae12436e899a726da5b2f11d8381b26
- $
-
-B<Note:> If you are following along in Ubuntu, you will need to install
-C<Digest::SHA> with the following command to run the example code above:
-
- sudo aptitude install libdigest-sha-perl
B<Note:> You should probably modify this code for production use to
not read the password from the command line. By having the script
$ sqlite3 myapp.db < myapp03.sql
-B<Note:> We are using SHA-1 hashes here, but many other hashing
+B<Note:> We are using SHA-1 hashes here, but many other hashing
algorithms are supported. See C<Digest> for more information.
# Use DBIC to retrieve username, password & role information
class DBIx::Class
# This is the model object created by Catalyst::Model::DBIC
- # from your schema (you created 'MyApp::Schema::User' but as
- # the Catalyst startup debug messages show, it was loaded as
- # 'MyApp::Model::DB::Users').
+ # from your schema (you created 'MyApp::Schema::Result::User'
+ # but as the Catalyst startup debug messages show, it was
+ # loaded as 'MyApp::Model::DB::Users').
# NOTE: Omit 'MyApp::Model' here just as you would when using
# '$c->model("DB::Users)'
user_class DB::Users
=head1 USING THE SESSION FOR FLASH
-As discussed in Part 3 of the tutorial, C<flash> allows you to set
-variables in a way that is very similar to C<stash>, but it will
+As discussed in the previous part of the tutorial, C<flash> allows you to
+set variables in a way that is very similar to C<stash>, but it will
remain set across multiple requests. Once the value is read, it
is cleared (unless reset). Although C<flash> has nothing to do with
authentication, it does leverage the same session plugins. Now that
has changed):
=head2 delete
-
+
Delete a book
-
+
=cut
-
+
sub delete :Chained('object') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
-
+
# Use the book object saved by 'object' and delete it along
# with related 'book_authors' entries
$c->stash->{object}->delete;
-
+
# Use 'flash' to save information across requests until it's read
$c->flash->{status_msg} = "Book deleted";
-
+
# Redirect the user back to the list page
$c->response->redirect($c->uri_for($self->action_for('list')));
}
</div><!-- end content -->
...
-Although the sample above only shows the C<content> div, leave the
+Although the sample above only shows the C<content> div, leave the
rest of the file intact -- the only change we made to the C<wrapper.tt2>
-was to add "C<|| c.request.params.status_msg>" to the
+was to add "C<|| c.request.params.status_msg>" to the
C<E<lt>span class="message"E<gt>> line.
=head2 Try Out Flash
-Restart the development server, log in, and then point your browser to
-L<http://localhost:3000/books/url_create/Test/1/4> to create an extra
-several books. Click the "Return to list" link and delete one of the
-"Test" books you just added. The C<flash> mechanism should retain our
+Restart the development server, log in, and then point your browser to
+L<http://localhost:3000/books/url_create/Test/1/4> to create an extra
+several books. Click the "Return to list" link and delete one of the
+"Test" books you just added. The C<flash> mechanism should retain our
"Book deleted" status message across the redirect.
B<NOTE:> While C<flash> will save information across multiple requests,
=head2 Switch To Flash-To-Stash
-Although the a use of flash above works well, the
+Although the a use of flash above works well, the
C<status_msg || c.flash.status_msg> statement is a little ugly. A nice
alternative is to use the C<flash_to_stash> feature that automatically
copies the content of flash to stash. This makes your controller