Commit | Line | Data |
2857be0d |
1 | =head1 NAME |
d442cc9f |
2 | |
3ab6187c |
3 | Catalyst::Manual::Tutorial::05_Authentication - Catalyst Tutorial - Chapter 5: Authentication |
d442cc9f |
4 | |
d442cc9f |
5 | =head1 OVERVIEW |
6 | |
4b4d3884 |
7 | This is B<Chapter 5 of 10> for the Catalyst tutorial. |
d442cc9f |
8 | |
9 | L<Tutorial Overview|Catalyst::Manual::Tutorial> |
10 | |
11 | =over 4 |
12 | |
13 | =item 1 |
14 | |
3ab6187c |
15 | L<Introduction|Catalyst::Manual::Tutorial::01_Intro> |
d442cc9f |
16 | |
17 | =item 2 |
18 | |
3ab6187c |
19 | L<Catalyst Basics|Catalyst::Manual::Tutorial::02_CatalystBasics> |
d442cc9f |
20 | |
21 | =item 3 |
22 | |
3ab6187c |
23 | L<More Catalyst Basics|Catalyst::Manual::Tutorial::03_MoreCatalystBasics> |
d442cc9f |
24 | |
25 | =item 4 |
26 | |
3ab6187c |
27 | L<Basic CRUD|Catalyst::Manual::Tutorial::04_BasicCRUD> |
d442cc9f |
28 | |
29 | =item 5 |
30 | |
3ab6187c |
31 | B<05_Authentication> |
d442cc9f |
32 | |
33 | =item 6 |
34 | |
3ab6187c |
35 | L<Authorization|Catalyst::Manual::Tutorial::06_Authorization> |
d442cc9f |
36 | |
37 | =item 7 |
38 | |
3ab6187c |
39 | L<Debugging|Catalyst::Manual::Tutorial::07_Debugging> |
d442cc9f |
40 | |
41 | =item 8 |
42 | |
3ab6187c |
43 | L<Testing|Catalyst::Manual::Tutorial::08_Testing> |
d442cc9f |
44 | |
45 | =item 9 |
46 | |
3ab6187c |
47 | L<Advanced CRUD|Catalyst::Manual::Tutorial::09_AdvancedCRUD> |
d442cc9f |
48 | |
3533daff |
49 | =item 10 |
d442cc9f |
50 | |
3ab6187c |
51 | L<Appendices|Catalyst::Manual::Tutorial::10_Appendices> |
d442cc9f |
52 | |
3533daff |
53 | =back |
2d0526d1 |
54 | |
2d0526d1 |
55 | |
d442cc9f |
56 | =head1 DESCRIPTION |
57 | |
905a3a26 |
58 | Now that we finally have a simple yet functional application, we can |
59 | focus on providing authentication (with authorization coming next in |
e18d15c9 |
60 | L<Chapter 6|Catalyst::Manual::Tutorial::06_Authorization>). |
d442cc9f |
61 | |
e18d15c9 |
62 | This chapter of the tutorial is divided into two main sections: 1) |
63 | basic, cleartext authentication and 2) hash-based authentication. |
d442cc9f |
64 | |
477a6d5b |
65 | Source code for the tutorial in included in the F</home/catalyst/Final> directory |
b1b6582a |
66 | of the Tutorial Virtual machine (one subdirectory per chapter). There |
67 | are also instructions for downloading the code in |
2217b252 |
68 | L<Catalyst::Manual::Tutorial::01_Intro>. |
d442cc9f |
69 | |
70 | =head1 BASIC AUTHENTICATION |
71 | |
72 | This section explores how to add authentication logic to a Catalyst |
73 | application. |
74 | |
75 | |
76 | =head2 Add Users and Roles to the Database |
77 | |
78 | First, we add both user and role information to the database (we will |
79 | add the role information here although it will not be used until the |
e18d15c9 |
80 | authorization section, Chapter 6). Create a new SQL script file by |
f4e9de4a |
81 | opening F<myapp02.sql> in your editor and insert: |
d442cc9f |
82 | |
83 | -- |
861a0cdd |
84 | -- Add users and role tables, along with a many-to-many join table |
d442cc9f |
85 | -- |
3c700304 |
86 | PRAGMA foreign_keys = ON; |
861a0cdd |
87 | CREATE TABLE users ( |
d442cc9f |
88 | id INTEGER PRIMARY KEY, |
89 | username TEXT, |
90 | password TEXT, |
91 | email_address TEXT, |
92 | first_name TEXT, |
93 | last_name TEXT, |
94 | active INTEGER |
95 | ); |
3b1fa91b |
96 | CREATE TABLE role ( |
d442cc9f |
97 | id INTEGER PRIMARY KEY, |
98 | role TEXT |
99 | ); |
3b1fa91b |
100 | CREATE TABLE user_role ( |
bbdce044 |
101 | user_id INTEGER REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, |
b66dd084 |
102 | role_id INTEGER REFERENCES role(id) ON DELETE CASCADE ON UPDATE CASCADE, |
d442cc9f |
103 | PRIMARY KEY (user_id, role_id) |
104 | ); |
105 | -- |
106 | -- Load up some initial test data |
107 | -- |
861a0cdd |
108 | INSERT INTO users VALUES (1, 'test01', 'mypass', 't01@na.com', 'Joe', 'Blow', 1); |
109 | INSERT INTO users VALUES (2, 'test02', 'mypass', 't02@na.com', 'Jane', 'Doe', 1); |
110 | INSERT INTO users VALUES (3, 'test03', 'mypass', 't03@na.com', 'No', 'Go', 0); |
3b1fa91b |
111 | INSERT INTO role VALUES (1, 'user'); |
112 | INSERT INTO role VALUES (2, 'admin'); |
113 | INSERT INTO user_role VALUES (1, 1); |
114 | INSERT INTO user_role VALUES (1, 2); |
115 | INSERT INTO user_role VALUES (2, 1); |
116 | INSERT INTO user_role VALUES (3, 1); |
d442cc9f |
117 | |
f4e9de4a |
118 | Then load this into the F<myapp.db> database with the following command: |
d442cc9f |
119 | |
120 | $ sqlite3 myapp.db < myapp02.sql |
121 | |
444d6b27 |
122 | |
d442cc9f |
123 | =head2 Add User and Role Information to DBIC Schema |
124 | |
3533daff |
125 | Although we could manually edit the DBIC schema information to include |
e18d15c9 |
126 | the new tables added in the previous step, let's use the |
127 | C<create=static> option on the DBIC model helper to do most of the work |
128 | for us: |
d442cc9f |
129 | |
acbd7bdd |
130 | $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \ |
b6e53c1c |
131 | create=static components=TimeStamp dbi:SQLite:myapp.db \ |
b66dd084 |
132 | on_connect_do="PRAGMA foreign_keys = ON" |
477a6d5b |
133 | exists "/home/catalyst/dev/MyApp/script/../lib/MyApp/Model" |
134 | exists "/home/catalyst/dev/MyApp/script/../t" |
135 | Dumping manual schema for MyApp::Schema to directory /home/catalyst/dev/MyApp/script/../lib ... |
1390ef0e |
136 | Schema dump completed. |
477a6d5b |
137 | exists "/home/catalyst/dev/MyApp/script/../lib/MyApp/Model/DB.pm" |
1390ef0e |
138 | $ |
acbd7bdd |
139 | $ ls lib/MyApp/Schema/Result |
3b1fa91b |
140 | Author.pm BookAuthor.pm Book.pm Role.pm User.pm UserRole.pm |
d442cc9f |
141 | |
3c700304 |
142 | Notice how the helper has added three new table-specific Result Source |
f4e9de4a |
143 | files to the F<lib/MyApp/Schema/Result> directory. And, more |
905a3a26 |
144 | importantly, even if there were changes to the existing result source |
d8e9b469 |
145 | files, those changes would have only been written above the |
146 | C<# DO NOT MODIFY THIS OR ANYTHING ABOVE!> comment and your hand-edited |
3533daff |
147 | enhancements would have been preserved. |
d442cc9f |
148 | |
e18d15c9 |
149 | Speaking of "hand-edited enhancements," we should now add the |
861a0cdd |
150 | C<many_to_many> relationship information to the User Result Source file. |
d8e9b469 |
151 | As with the Book, BookAuthor, and Author files in |
152 | L<Chapter 3|Catalyst::Manual::Tutorial::03_MoreCatalystBasics>, |
e18d15c9 |
153 | L<DBIx::Class::Schema::Loader> has automatically created the C<has_many> |
154 | and C<belongs_to> relationships for the new User, UserRole, and Role |
155 | tables. However, as a convenience for mapping Users to their assigned |
156 | roles (see L<Chapter 6|Catalyst::Manual::Tutorial::06_Authorization>), |
157 | we will also manually add a C<many_to_many> relationship. Edit |
f4e9de4a |
158 | F<lib/MyApp/Schema/Result/User.pm> add the following information between |
861a0cdd |
159 | the C<# DO NOT MODIFY THIS OR ANYTHING ABOVE!> comment and the closing |
160 | C<1;>: |
d442cc9f |
161 | |
3533daff |
162 | # many_to_many(): |
163 | # args: |
164 | # 1) Name of relationship, DBIC will create accessor with this name |
905a3a26 |
165 | # 2) Name of has_many() relationship this many_to_many() is shortcut for |
166 | # 3) Name of belongs_to() relationship in model class of has_many() above |
3533daff |
167 | # You must already have the has_many() defined to use a many_to_many(). |
bd8f28e0 |
168 | __PACKAGE__->many_to_many(roles => 'user_roles', 'role'); |
d442cc9f |
169 | |
861a0cdd |
170 | The code for this update is obviously very similar to the edits we made |
d8e9b469 |
171 | to the C<Book> and C<Author> classes created in |
172 | L<Chapter 3|Catalyst::Manual::Tutorial::03_MoreCatalystBasics> with one |
861a0cdd |
173 | exception: we only defined the C<many_to_many> relationship in one |
174 | direction. Whereas we felt that we would want to map Authors to Books |
175 | B<AND> Books to Authors, here we are only adding the convenience |
176 | C<many_to_many> in the Users to Roles direction. |
3533daff |
177 | |
636ba9f7 |
178 | Note that we do not need to make any change to the |
f4e9de4a |
179 | F<lib/MyApp/Schema.pm> schema file. It simply tells DBIC to load all of |
d8e9b469 |
180 | the Result Class and ResultSet Class files it finds below the |
f4e9de4a |
181 | F<lib/MyApp/Schema> directory, so it will automatically pick up our new |
e18d15c9 |
182 | table information. |
d442cc9f |
183 | |
184 | |
3c700304 |
185 | =head2 Sanity-Check of the Development Server Reload |
d442cc9f |
186 | |
861a0cdd |
187 | We aren't ready to try out the authentication just yet; we only want to |
188 | do a quick check to be sure our model loads correctly. Assuming that you |
f9ce2976 |
189 | are following along and using the "-r" option on F<myapp_server.pl>, |
861a0cdd |
190 | then the development server should automatically reload (if not, press |
191 | C<Ctrl-C> to break out of the server if it's running and then enter |
f9ce2976 |
192 | F<script/myapp_server.pl> to start it). Look for the three new model |
861a0cdd |
193 | objects in the startup debug output: |
d442cc9f |
194 | |
195 | ... |
f9ce2976 |
196 | .-------------------------------------------------------------------+----------. |
d442cc9f |
197 | | Class | Type | |
198 | +-------------------------------------------------------------------+----------+ |
199 | | MyApp::Controller::Books | instance | |
200 | | MyApp::Controller::Root | instance | |
d0496197 |
201 | | MyApp::Model::DB | instance | |
202 | | MyApp::Model::DB::Author | class | |
3b1fa91b |
203 | | MyApp::Model::DB::Book | class | |
204 | | MyApp::Model::DB::BookAuthor | class | |
205 | | MyApp::Model::DB::Role | class | |
206 | | MyApp::Model::DB::User | class | |
207 | | MyApp::Model::DB::UserRole | class | |
1edbdee6 |
208 | | MyApp::View::HTML | instance | |
d442cc9f |
209 | '-------------------------------------------------------------------+----------' |
210 | ... |
211 | |
e18d15c9 |
212 | Again, notice that your "Result Class" classes have been "re-loaded" by |
213 | Catalyst under C<MyApp::Model>. |
d442cc9f |
214 | |
215 | |
216 | =head2 Include Authentication and Session Plugins |
217 | |
f4e9de4a |
218 | Edit F<lib/MyApp.pm> and update it as follows (everything below |
3533daff |
219 | C<StackTrace> is new): |
d442cc9f |
220 | |
acbd7bdd |
221 | # Load plugins |
2a6eb5f9 |
222 | use Catalyst qw/ |
3c700304 |
223 | -Debug |
224 | ConfigLoader |
225 | Static::Simple |
7ce05098 |
226 | |
3c700304 |
227 | StackTrace |
7ce05098 |
228 | |
3c700304 |
229 | Authentication |
7ce05098 |
230 | |
3c700304 |
231 | Session |
95455c74 |
232 | Session::Store::File |
3c700304 |
233 | Session::State::Cookie |
234 | /; |
d442cc9f |
235 | |
d8e9b469 |
236 | B<Note:> As discussed in |
237 | L<Chapter 3|Catalyst::Manual::Tutorial::03_MoreCatalystBasics>, |
238 | different versions of C<Catalyst::Devel> have used a variety of methods |
239 | to load the plugins, but we are going to use the current Catalyst 5.9 |
240 | practice of putting them on the C<use Catalyst> line. |
94d8da41 |
241 | |
905a3a26 |
242 | The C<Authentication> plugin supports Authentication while the |
243 | C<Session> plugins are required to maintain state across multiple HTTP |
244 | requests. |
6d0971ad |
245 | |
905a3a26 |
246 | Note that the only required Authentication class is the main one. This |
d8e9b469 |
247 | is a change that occurred in version 0.09999_01 of the |
248 | L<Authentication|Catalyst::Plugin::Authentication> plugin. You |
249 | B<do not need> to specify a particular |
250 | L<Authentication::Store|Catalyst::Authentication::Store> or |
251 | C<Authentication::Credential> you want to use. Instead, indicate the |
252 | Store and Credential you want to use in your application configuration |
253 | (see below). |
6d0971ad |
254 | |
e18d15c9 |
255 | Make sure you include the additional plugins as new dependencies in the |
256 | Makefile.PL file something like this: |
3b1fa91b |
257 | |
e12f8011 |
258 | requires 'Catalyst::Plugin::Authentication'; |
259 | requires 'Catalyst::Plugin::Session'; |
95455c74 |
260 | requires 'Catalyst::Plugin::Session::Store::File'; |
e12f8011 |
261 | requires 'Catalyst::Plugin::Session::State::Cookie'; |
3b1fa91b |
262 | |
905a3a26 |
263 | Note that there are several options for |
3c700304 |
264 | L<Session::Store|Catalyst::Plugin::Session::Store>. |
e18d15c9 |
265 | L<Session::Store::Memcached|Catalyst::Plugin::Session::Store::Memcached> |
266 | is generally a good choice if you are on Unix. If you are running on |
267 | Windows L<Session::Store::File|Catalyst::Plugin::Session::Store::File> |
268 | is fine. Consult L<Session::Store|Catalyst::Plugin::Session::Store> and |
269 | its subclasses for additional information and options (for example to |
d8e9b469 |
270 | use a database-backed session store). |
d442cc9f |
271 | |
272 | |
273 | =head2 Configure Authentication |
274 | |
3b1fa91b |
275 | There are a variety of ways to provide configuration information to |
e18d15c9 |
276 | L<Catalyst::Plugin::Authentication>. Here we will use |
277 | L<Catalyst::Authentication::Realm::SimpleDB> because it automatically |
d8e9b469 |
278 | sets a reasonable set of defaults for us. (Note: the C<SimpleDB> here |
279 | has nothing to do with the SimpleDB offered in Amazon's web services |
280 | offerings -- here we are only talking about a "simple" way to use your |
f4e9de4a |
281 | DB as an authentication backend.) Open F<lib/MyApp.pm> and place the |
429d1caf |
282 | following text above the call to C<< __PACKAGE__->setup(); >>: |
efdaddec |
283 | |
284 | # Configure SimpleDB Authentication |
19a5b486 |
285 | __PACKAGE__->config( |
286 | 'Plugin::Authentication' => { |
efdaddec |
287 | default => { |
288 | class => 'SimpleDB', |
3b1fa91b |
289 | user_model => 'DB::User', |
efdaddec |
290 | password_type => 'clear', |
291 | }, |
19a5b486 |
292 | }, |
293 | ); |
efdaddec |
294 | |
f4e9de4a |
295 | We could have placed this configuration in F<myapp.conf>, but placing it |
296 | in F<lib/MyApp.pm> is probably a better place since it's not likely |
861a0cdd |
297 | something that users of your application will want to change during |
e18d15c9 |
298 | deployment (or you could use a mixture: leave C<class> and C<user_model> |
f4e9de4a |
299 | defined in F<lib/MyApp.pm> as we show above, but place C<password_type> |
300 | in F<myapp.conf> to allow the type of password to be easily modified |
e18d15c9 |
301 | during deployment). We will stick with putting all of the |
f4e9de4a |
302 | authentication-related configuration in F<lib/MyApp.pm> for the |
303 | tutorial, but if you wish to use F<myapp.conf>, just convert to the |
e18d15c9 |
304 | following code: |
c3cf3bc3 |
305 | |
306 | <Plugin::Authentication> |
c3cf3bc3 |
307 | <default> |
43707053 |
308 | password_type clear |
3b1fa91b |
309 | user_model DB::User |
c3cf3bc3 |
310 | class SimpleDB |
311 | </default> |
312 | </Plugin::Authentication> |
313 | |
861a0cdd |
314 | B<TIP:> Here is a short script that will dump the contents of |
f4e9de4a |
315 | C<MyApp->config> to L<Config::General> format in F<myapp.conf>: |
c3cf3bc3 |
316 | |
861a0cdd |
317 | $ CATALYST_DEBUG=0 perl -Ilib -e 'use MyApp; use Config::General; |
c3cf3bc3 |
318 | Config::General->new->save_file("myapp.conf", MyApp->config);' |
d442cc9f |
319 | |
3c700304 |
320 | B<HOWEVER>, if you try out the command above, be sure to delete the |
321 | "myapp.conf" command. Otherwise, you will wind up with duplicate |
322 | configurations. |
323 | |
d8e9b469 |
324 | B<NOTE:> Because we are using |
89a65964 |
325 | L<SimpleDB|Catalyst::Authentication::Realm::SimpleDB> along with a |
d8e9b469 |
326 | database layout that complies with its default assumptions: we don't |
327 | need to specify the names of the columns where our username and password |
328 | information is stored (hence, the "Simple" part of "SimpleDB"). That |
329 | being said, SimpleDB lets you specify that type of information if you |
330 | need to. Take a look at C<Catalyst::Authentication::Realm::SimpleDB> |
c4fa597d |
331 | for details. |
332 | |
1390ef0e |
333 | |
d442cc9f |
334 | =head2 Add Login and Logout Controllers |
335 | |
fa59770d |
336 | Use the Catalyst create script to create two stub controller files: |
d442cc9f |
337 | |
fa59770d |
338 | $ script/myapp_create.pl controller Login |
339 | $ script/myapp_create.pl controller Logout |
d442cc9f |
340 | |
fa59770d |
341 | You could easily use a single controller here. For example, you could |
342 | have a C<User> controller with both C<login> and C<logout> actions. |
636ba9f7 |
343 | Remember, Catalyst is designed to be very flexible, and leaves such |
fbbb9084 |
344 | matters up to you, the designer and programmer. |
d442cc9f |
345 | |
f4e9de4a |
346 | Then open F<lib/MyApp/Controller/Login.pm>, and update the definition of |
d8e9b469 |
347 | C<sub index> to match: |
d442cc9f |
348 | |
fa59770d |
349 | =head2 index |
7ce05098 |
350 | |
d442cc9f |
351 | Login logic |
7ce05098 |
352 | |
d442cc9f |
353 | =cut |
7ce05098 |
354 | |
fa59770d |
355 | sub index :Path :Args(0) { |
d442cc9f |
356 | my ($self, $c) = @_; |
7ce05098 |
357 | |
d442cc9f |
358 | # Get the username and password from form |
ab0bd0bb |
359 | my $username = $c->request->params->{username}; |
360 | my $password = $c->request->params->{password}; |
7ce05098 |
361 | |
d442cc9f |
362 | # If the username and password values were found in form |
ab0bd0bb |
363 | if ($username && $password) { |
d442cc9f |
364 | # Attempt to log the user in |
905a3a26 |
365 | if ($c->authenticate({ username => $username, |
5fefca35 |
366 | password => $password } )) { |
d442cc9f |
367 | # If successful, then let them use the application |
0416017e |
368 | $c->response->redirect($c->uri_for( |
369 | $c->controller('Books')->action_for('list'))); |
fa59770d |
370 | return; |
d442cc9f |
371 | } else { |
fa59770d |
372 | # Set an error message |
0ed3df53 |
373 | $c->stash(error_msg => "Bad username or password."); |
d442cc9f |
374 | } |
ab0bd0bb |
375 | } else { |
fa59770d |
376 | # Set an error message |
b6ff4050 |
377 | $c->stash(error_msg => "Empty username or password.") |
378 | unless ($c->user_exists); |
d442cc9f |
379 | } |
7ce05098 |
380 | |
d442cc9f |
381 | # If either of above don't work out, send to the login page |
0ed3df53 |
382 | $c->stash(template => 'login.tt2'); |
d442cc9f |
383 | } |
384 | |
385 | This controller fetches the C<username> and C<password> values from the |
905a3a26 |
386 | login form and attempts to authenticate the user. If successful, it |
387 | redirects the user to the book list page. If the login fails, the user |
388 | will stay at the login page and receive an error message. If the |
e18d15c9 |
389 | C<username> and C<password> values are not present in the form, the user |
390 | will be taken to the empty login form. |
d442cc9f |
391 | |
636ba9f7 |
392 | Note that we could have used something like "C<sub default :Path>", |
e18d15c9 |
393 | however, it is generally recommended (partly for historical reasons, and |
394 | partly for code clarity) only to use C<default> in |
636ba9f7 |
395 | C<MyApp::Controller::Root>, and then mainly to generate the 404 not |
85d49fb6 |
396 | found page for the application. |
ae492862 |
397 | |
fa59770d |
398 | Instead, we are using "C<sub somename :Path :Args(0) {...}>" here to |
399 | specifically match the URL C</login>. C<Path> actions (aka, "literal |
e18d15c9 |
400 | actions") create URI matches relative to the namespace of the controller |
401 | where they are defined. Although C<Path> supports arguments that allow |
402 | relative and absolute paths to be defined, here we use an empty C<Path> |
403 | definition to match on just the name of the controller itself. The |
404 | method name, C<index>, is arbitrary. We make the match even more |
405 | specific with the C<:Args(0)> action modifier -- this forces the match |
406 | on I<only> C</login>, not C</login/somethingelse>. |
d442cc9f |
407 | |
905a3a26 |
408 | Next, update the corresponding method in |
f4e9de4a |
409 | F<lib/MyApp/Controller/Logout.pm> to match: |
d442cc9f |
410 | |
411 | =head2 index |
7ce05098 |
412 | |
d442cc9f |
413 | Logout logic |
7ce05098 |
414 | |
d442cc9f |
415 | =cut |
7ce05098 |
416 | |
ae492862 |
417 | sub index :Path :Args(0) { |
d442cc9f |
418 | my ($self, $c) = @_; |
7ce05098 |
419 | |
d442cc9f |
420 | # Clear the user's state |
421 | $c->logout; |
7ce05098 |
422 | |
d442cc9f |
423 | # Send the user to the starting point |
424 | $c->response->redirect($c->uri_for('/')); |
425 | } |
426 | |
d442cc9f |
427 | |
428 | =head2 Add a Login Form TT Template Page |
429 | |
f4e9de4a |
430 | Create a login form by opening F<root/src/login.tt2> and inserting: |
d442cc9f |
431 | |
432 | [% META title = 'Login' %] |
7ce05098 |
433 | |
d442cc9f |
434 | <!-- Login form --> |
8a7c5151 |
435 | <form method="post" action="[% c.uri_for('/login') %]"> |
d442cc9f |
436 | <table> |
437 | <tr> |
438 | <td>Username:</td> |
439 | <td><input type="text" name="username" size="40" /></td> |
440 | </tr> |
441 | <tr> |
442 | <td>Password:</td> |
443 | <td><input type="password" name="password" size="40" /></td> |
444 | </tr> |
445 | <tr> |
446 | <td colspan="2"><input type="submit" name="submit" value="Submit" /></td> |
447 | </tr> |
448 | </table> |
449 | </form> |
450 | |
451 | |
452 | =head2 Add Valid User Check |
453 | |
454 | We need something that provides enforcement for the authentication |
455 | mechanism -- a I<global> mechanism that prevents users who have not |
456 | passed authentication from reaching any pages except the login page. |
861a0cdd |
457 | This is generally done via an C<auto> action/method in |
f4e9de4a |
458 | F<lib/MyApp/Controller/Root.pm>. |
d442cc9f |
459 | |
f4e9de4a |
460 | Edit the existing F<lib/MyApp/Controller/Root.pm> class file and insert |
d442cc9f |
461 | the following method: |
462 | |
463 | =head2 auto |
7ce05098 |
464 | |
d442cc9f |
465 | Check if there is a user and, if not, forward to login page |
7ce05098 |
466 | |
d442cc9f |
467 | =cut |
7ce05098 |
468 | |
d442cc9f |
469 | # Note that 'auto' runs after 'begin' but before your actions and that |
905a3a26 |
470 | # 'auto's "chain" (all from application path to most specific class are run) |
d442cc9f |
471 | # See the 'Actions' section of 'Catalyst::Manual::Intro' for more info. |
ddfbd850 |
472 | sub auto :Private { |
d442cc9f |
473 | my ($self, $c) = @_; |
7ce05098 |
474 | |
d442cc9f |
475 | # Allow unauthenticated users to reach the login page. This |
191dee29 |
476 | # allows unauthenticated users to reach any action in the Login |
d442cc9f |
477 | # controller. To lock it down to a single action, we could use: |
478 | # if ($c->action eq $c->controller('Login')->action_for('index')) |
905a3a26 |
479 | # to only allow unauthenticated access to the 'index' action we |
d442cc9f |
480 | # added above. |
481 | if ($c->controller eq $c->controller('Login')) { |
482 | return 1; |
483 | } |
7ce05098 |
484 | |
d442cc9f |
485 | # If a user doesn't exist, force login |
486 | if (!$c->user_exists) { |
487 | # Dump a log message to the development server debug output |
488 | $c->log->debug('***Root::auto User not found, forwarding to /login'); |
489 | # Redirect the user to the login page |
490 | $c->response->redirect($c->uri_for('/login')); |
491 | # Return 0 to cancel 'post-auto' processing and prevent use of application |
492 | return 0; |
493 | } |
7ce05098 |
494 | |
d442cc9f |
495 | # User found, so return 1 to continue with processing after this 'auto' |
496 | return 1; |
497 | } |
498 | |
636ba9f7 |
499 | As discussed in |
3ab6187c |
500 | L<Catalyst::Manual::Tutorial::03_MoreCatalystBasics/CREATE A CATALYST CONTROLLER>, |
636ba9f7 |
501 | every C<auto> method from the application/root controller down to the |
e18d15c9 |
502 | most specific controller will be called. By placing the authentication |
503 | enforcement code inside the C<auto> method of |
f4e9de4a |
504 | F<lib/MyApp/Controller/Root.pm> (or F<lib/MyApp.pm>), it will be called |
e18d15c9 |
505 | for I<every> request that is received by the entire application. |
d442cc9f |
506 | |
507 | |
508 | =head2 Displaying Content Only to Authenticated Users |
509 | |
510 | Let's say you want to provide some information on the login page that |
511 | changes depending on whether the user has authenticated yet. To do |
f4e9de4a |
512 | this, open F<root/src/login.tt2> in your editor and add the following |
d442cc9f |
513 | lines to the bottom of the file: |
514 | |
acbd7bdd |
515 | ... |
d442cc9f |
516 | <p> |
517 | [% |
905a3a26 |
518 | # This code illustrates how certain parts of the TT |
d442cc9f |
519 | # template will only be shown to users who have logged in |
520 | %] |
8a7c5151 |
521 | [% IF c.user_exists %] |
522 | Please Note: You are already logged in as '[% c.user.username %]'. |
523 | You can <a href="[% c.uri_for('/logout') %]">logout</a> here. |
d442cc9f |
524 | [% ELSE %] |
525 | You need to log in to use this application. |
526 | [% END %] |
527 | [%# |
528 | Note that this whole block is a comment because the "#" appears |
905a3a26 |
529 | immediate after the "[%" (with no spaces in between). Although it |
530 | can be a handy way to temporarily "comment out" a whole block of |
531 | TT code, it's probably a little too subtle for use in "normal" |
d442cc9f |
532 | comments. |
533 | %] |
3533daff |
534 | </p> |
d442cc9f |
535 | |
536 | Although most of the code is comments, the middle few lines provide a |
537 | "you are already logged in" reminder if the user returns to the login |
538 | page after they have already authenticated. For users who have not yet |
539 | authenticated, a "You need to log in..." message is displayed (note the |
540 | use of an IF-THEN-ELSE construct in TT). |
541 | |
542 | |
543 | =head2 Try Out Authentication |
544 | |
861a0cdd |
545 | The development server should have reloaded each time we edited one of |
3e1a2240 |
546 | the Controllers in the previous section. Now try going to |
861a0cdd |
547 | L<http://localhost:3000/books/list> and you should be redirected to the |
548 | login page, hitting Shift+Reload or Ctrl+Reload if necessary (the "You |
549 | are already logged in" message should I<not> appear -- if it does, click |
550 | the C<logout> button and try again). Note the C<***Root::auto User not |
551 | found...> debug message in the development server output. Enter username |
552 | C<test01> and password C<mypass>, and you should be taken to the Book |
553 | List page. |
d442cc9f |
554 | |
636ba9f7 |
555 | B<IMPORTANT NOTE:> If you are having issues with authentication on |
d8e9b469 |
556 | Internet Explorer (or potentially other browsers), be sure to check the |
557 | system clocks on both your server and client machines. Internet |
558 | Explorer is very picky about timestamps for cookies. You can use the |
559 | C<ntpq -p> command on the Tutorial Virtual Machine to check time sync |
560 | and/or use the following command to force a sync: |
25ed8f40 |
561 | |
acbd7bdd |
562 | sudo ntpdate-debian |
d442cc9f |
563 | |
d8e9b469 |
564 | Or, depending on your firewall configuration, try it with "-u": |
acbd7bdd |
565 | |
566 | sudo ntpdate-debian -u |
567 | |
636ba9f7 |
568 | Note: NTP can be a little more finicky about firewalls because it uses |
acbd7bdd |
569 | UDP vs. the more common TCP that you see with most Internet protocols. |
570 | Worse case, you might have to manually set the time on your development |
571 | box instead of using NTP. |
1390ef0e |
572 | |
f4e9de4a |
573 | Open F<root/src/books/list.tt2> and add the following lines to the |
3533daff |
574 | bottom (below the closing </table> tag): |
d442cc9f |
575 | |
aa7ff325 |
576 | ... |
d442cc9f |
577 | <p> |
8a7c5151 |
578 | <a href="[% c.uri_for('/login') %]">Login</a> |
0416017e |
579 | <a href="[% c.uri_for(c.controller.action_for('form_create')) %]">Create</a> |
d442cc9f |
580 | </p> |
581 | |
905a3a26 |
582 | Reload your browser and you should now see a "Login" and "Create" links |
583 | at the bottom of the page (as mentioned earlier, you can update template |
e18d15c9 |
584 | files without a development server reload). Click the first link to |
585 | return to the login page. This time you I<should> see the "You are |
d442cc9f |
586 | already logged in" message. |
587 | |
588 | Finally, click the C<You can logout here> link on the C</login> page. |
589 | You should stay at the login page, but the message should change to "You |
590 | need to log in to use this application." |
591 | |
592 | |
593 | =head1 USING PASSWORD HASHES |
594 | |
861a0cdd |
595 | In this section we increase the security of our system by converting |
e18d15c9 |
596 | from cleartext passwords to SHA-1 password hashes that include a random |
d8e9b469 |
597 | "salt" value to make them extremely difficult to crack, even with |
598 | dictionary and "rainbow table" attacks. |
d442cc9f |
599 | |
600 | B<Note:> This section is optional. You can skip it and the rest of the |
601 | tutorial will function normally. |
602 | |
e18d15c9 |
603 | Be aware that even with the techniques shown in this section, the |
604 | browser still transmits the passwords in cleartext to your application. |
605 | We are just avoiding the I<storage> of cleartext passwords in the |
606 | database by using a salted SHA-1 hash. If you are concerned about |
607 | cleartext passwords between the browser and your application, consider |
d8e9b469 |
608 | using SSL/TLS, made easy with modules such as |
609 | L<Catalyst::Plugin:RequireSSL> and L<Catalyst::ActionRole::RequireSSL>. |
d442cc9f |
610 | |
611 | |
436f45da |
612 | =head2 Re-Run the DBIC::Schema Model Helper to Include DBIx::Class::PassphraseColumn |
d442cc9f |
613 | |
d8e9b469 |
614 | Let's re-run the model helper to have it include |
e18d15c9 |
615 | L<DBIx::Class::PassphraseColumn> in all of the Result Classes it |
616 | generates for us. Simply use the same command we saw in Chapters 3 and |
617 | 4, but add C<,PassphraseColumn> to the C<components> argument: |
d442cc9f |
618 | |
efdaddec |
619 | $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \ |
b6e53c1c |
620 | create=static components=TimeStamp,PassphraseColumn dbi:SQLite:myapp.db \ |
b66dd084 |
621 | on_connect_do="PRAGMA foreign_keys = ON" |
d442cc9f |
622 | |
861a0cdd |
623 | If you then open one of the Result Classes, you will see that it |
e18d15c9 |
624 | includes PassphraseColumn in the C<load_components> line. Take a look |
f4e9de4a |
625 | at F<lib/MyApp/Schema/Result/User.pm> since that's the main class where |
e18d15c9 |
626 | we want to use hashed and salted passwords: |
efdaddec |
627 | |
436f45da |
628 | __PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp", "PassphraseColumn"); |
efdaddec |
629 | |
630 | |
436f45da |
631 | =head2 Modify the "password" Column to Use PassphraseColumn |
efdaddec |
632 | |
f4e9de4a |
633 | Open the file F<lib/MyApp/Schema/Result/User.pm> and enter the following |
efdaddec |
634 | text below the "# DO NOT MODIFY THIS OR ANYTHING ABOVE!" line but above |
635 | the closing "1;": |
636 | |
436f45da |
637 | # Have the 'password' column use a SHA-1 hash and 20-byte salt |
638 | # with RFC 2307 encoding; Generate the 'check_password" method |
efdaddec |
639 | __PACKAGE__->add_columns( |
640 | 'password' => { |
436f45da |
641 | passphrase => 'rfc2307', |
642 | passphrase_class => 'SaltedDigest', |
643 | passphrase_args => { |
644 | algorithm => 'SHA-1', |
682c50f1 |
645 | salt_random => 20, |
436f45da |
646 | }, |
647 | passphrase_check_method => 'check_password', |
efdaddec |
648 | }, |
649 | ); |
650 | |
e18d15c9 |
651 | This redefines the automatically generated definition for the password |
652 | fields at the top of the Result Class file to now use PassphraseColumn |
653 | logic, storing passwords in RFC 2307 format (C<passphrase> is set to |
654 | C<rfc2307>). C<passphrase_class> can be set to the name of any |
655 | C<Authen::Passphrase::*> class, such as C<SaltedDigest> to use |
656 | L<Authen::Passphrase::SaltedDigest>, or C<BlowfishCrypt> to use |
657 | L<Authen::Passphrase::BlowfishCrypt>. C<passphrase_args> is then used |
658 | to customize the passphrase class you selected. Here we specified the |
659 | digest algorithm to use as C<SHA-1> and the size of the salt to use, but |
660 | we could have also specified any other option the selected passphrase |
661 | class supports. |
662 | |
efdaddec |
663 | |
664 | =head2 Load Hashed Passwords in the Database |
665 | |
e18d15c9 |
666 | Next, let's create a quick script to load some hashed and salted |
667 | passwords into the C<password> column of our C<users> table. Open the |
f4e9de4a |
668 | file F<set_hashed_passwords.pl> in your editor and enter the following |
e18d15c9 |
669 | text: |
efdaddec |
670 | |
671 | #!/usr/bin/perl |
7ce05098 |
672 | |
efdaddec |
673 | use strict; |
674 | use warnings; |
7ce05098 |
675 | |
efdaddec |
676 | use MyApp::Schema; |
7ce05098 |
677 | |
efdaddec |
678 | my $schema = MyApp::Schema->connect('dbi:SQLite:myapp.db'); |
7ce05098 |
679 | |
3b1fa91b |
680 | my @users = $schema->resultset('User')->all; |
7ce05098 |
681 | |
efdaddec |
682 | foreach my $user (@users) { |
683 | $user->password('mypass'); |
684 | $user->update; |
685 | } |
686 | |
436f45da |
687 | PassphraseColumn lets us simply call C<$user->check_password($password)> |
861a0cdd |
688 | to see if the user has supplied the correct password, or, as we show |
689 | above, call C<$user->update($new_password)> to update the hashed |
efdaddec |
690 | password stored for this user. |
691 | |
692 | Then run the following command: |
693 | |
2a6eb5f9 |
694 | $ DBIC_TRACE=1 perl -Ilib set_hashed_passwords.pl |
efdaddec |
695 | |
bd8f28e0 |
696 | We had to use the C<-Ilib> argument to tell Perl to look under the |
f4e9de4a |
697 | F<lib> directory for our C<MyApp::Schema> model. |
efdaddec |
698 | |
2a6eb5f9 |
699 | The DBIC_TRACE output should show that the update worked: |
700 | |
701 | $ DBIC_TRACE=1 perl -Ilib set_hashed_passwords.pl |
861a0cdd |
702 | SELECT me.id, me.username, me.password, me.email_address, |
703 | me.first_name, me.last_name, me.active FROM users me: |
704 | UPDATE users SET password = ? WHERE ( id = ? ): |
436f45da |
705 | '{SSHA}esgz64CpHMo8pMfgIIszP13ft23z/zio04aCwNdm0wc6MDeloMUH4g==', '1' |
861a0cdd |
706 | UPDATE users SET password = ? WHERE ( id = ? ): |
436f45da |
707 | '{SSHA}FpGhpCJus+Ea9ne4ww8404HH+hJKW/fW+bAv1v6FuRUy2G7I2aoTRQ==', '2' |
861a0cdd |
708 | UPDATE users SET password = ? WHERE ( id = ? ): |
436f45da |
709 | '{SSHA}ZyGlpiHls8qFBSbHr3r5t/iqcZE602XLMbkSVRRNl6rF8imv1abQVg==', '3' |
2a6eb5f9 |
710 | |
711 | But we can further confirm our actions by dumping the users table: |
efdaddec |
712 | |
861a0cdd |
713 | $ sqlite3 myapp.db "select * from users" |
436f45da |
714 | 1|test01|{SSHA}esgz64CpHMo8pMfgIIszP13ft23z/zio04aCwNdm0wc6MDeloMUH4g==|t01@na.com|Joe|Blow|1 |
715 | 2|test02|{SSHA}FpGhpCJus+Ea9ne4ww8404HH+hJKW/fW+bAv1v6FuRUy2G7I2aoTRQ==|t02@na.com|Jane|Doe|1 |
716 | 3|test03|{SSHA}ZyGlpiHls8qFBSbHr3r5t/iqcZE602XLMbkSVRRNl6rF8imv1abQVg==|t03@na.com|No|Go|0 |
efdaddec |
717 | |
e18d15c9 |
718 | As you can see, the passwords are much harder to steal from the database |
719 | (not only are the hashes stored, but every hash is different even though |
720 | the passwords are the same because of the added "salt" value). Also |
721 | note that this demonstrates how to use a DBIx::Class model outside of |
722 | your web application -- a very useful feature in many situations. |
efdaddec |
723 | |
724 | |
725 | =head2 Enable Hashed and Salted Passwords |
726 | |
f4e9de4a |
727 | Edit F<lib/MyApp.pm> and update the config() section for |
d8e9b469 |
728 | C<Plugin::Authentication> it to match the following text (the only |
e18d15c9 |
729 | change is to the C<password_type> field): |
efdaddec |
730 | |
731 | # Configure SimpleDB Authentication |
19a5b486 |
732 | __PACKAGE__->config( |
733 | 'Plugin::Authentication' => { |
efdaddec |
734 | default => { |
735 | class => 'SimpleDB', |
3b1fa91b |
736 | user_model => 'DB::User', |
efdaddec |
737 | password_type => 'self_check', |
738 | }, |
19a5b486 |
739 | }, |
740 | ); |
efdaddec |
741 | |
861a0cdd |
742 | The use of C<self_check> will cause |
9c5abba4 |
743 | Catalyst::Plugin::Authentication::Store::DBIx::Class to call the |
efdaddec |
744 | C<check_password> method we enabled on our C<password> columns. |
d442cc9f |
745 | |
746 | =head2 Try Out the Hashed Passwords |
747 | |
861a0cdd |
748 | The development server should restart as soon as your save the |
f4e9de4a |
749 | F<lib/MyApp.pm> file in the previous section. You should now be able to |
861a0cdd |
750 | go to L<http://localhost:3000/books/list> and login as before. When |
751 | done, click the "logout" link on the login page (or point your browser |
752 | at L<http://localhost:3000/logout>). |
d442cc9f |
753 | |
d442cc9f |
754 | |
755 | =head1 USING THE SESSION FOR FLASH |
756 | |
861a0cdd |
757 | As discussed in the previous chapter of the tutorial, C<flash> allows |
758 | you to set variables in a way that is very similar to C<stash>, but it |
e18d15c9 |
759 | will remain set across multiple requests. Once the value is read, it is |
760 | cleared (unless reset). Although C<flash> has nothing to do with |
861a0cdd |
761 | authentication, it does leverage the same session plugins. Now that |
762 | those plugins are enabled, let's go back and update the "delete and |
e18d15c9 |
763 | redirect with query parameters" code seen at the end of the |
764 | L<Basic CRUD|Catalyst::Manual::Tutorial::04_BasicCRUD> chapter of the |
765 | tutorial to take advantage of C<flash>. |
d442cc9f |
766 | |
f4e9de4a |
767 | First, open F<lib/MyApp/Controller/Books.pm> and modify C<sub delete> to |
e18d15c9 |
768 | match the following (everything after the model search line of code has |
769 | changed): |
d442cc9f |
770 | |
905a3a26 |
771 | =head2 delete |
7ce05098 |
772 | |
d442cc9f |
773 | Delete a book |
7ce05098 |
774 | |
d442cc9f |
775 | =cut |
7ce05098 |
776 | |
fbbb9084 |
777 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
778 | my ($self, $c) = @_; |
7ce05098 |
779 | |
fbbb9084 |
780 | # Use the book object saved by 'object' and delete it along |
781 | # with related 'book_authors' entries |
782 | $c->stash->{object}->delete; |
7ce05098 |
783 | |
d442cc9f |
784 | # Use 'flash' to save information across requests until it's read |
785 | $c->flash->{status_msg} = "Book deleted"; |
7ce05098 |
786 | |
3533daff |
787 | # Redirect the user back to the list page |
0416017e |
788 | $c->response->redirect($c->uri_for($self->action_for('list'))); |
d442cc9f |
789 | } |
790 | |
f4e9de4a |
791 | Next, open F<root/src/wrapper.tt2> and update the TT code to pull from |
d442cc9f |
792 | flash vs. the C<status_msg> query parameter: |
793 | |
1390ef0e |
794 | ... |
d442cc9f |
795 | <div id="content"> |
1390ef0e |
796 | [%# Status and error messages %] |
797 | <span class="message">[% status_msg || c.flash.status_msg %]</span> |
798 | <span class="error">[% error_msg %]</span> |
799 | [%# This is where TT will stick all of your template's contents. -%] |
800 | [% content %] |
801 | </div><!-- end content --> |
802 | ... |
905a3a26 |
803 | |
e18d15c9 |
804 | Although the sample above only shows the C<content> div, leave the rest |
805 | of the file intact -- the only change we made to replace "|| |
806 | c.request.params.status_msg" with "c.flash.status_msg" in the |
429d1caf |
807 | C<< <span class="message"> >> line. |
d442cc9f |
808 | |
809 | |
810 | =head2 Try Out Flash |
811 | |
3c700304 |
812 | Authenticate using the login screen and then point your browser to |
636ba9f7 |
813 | L<http://localhost:3000/books/url_create/Test/1/4> to create an extra |
814 | several books. Click the "Return to list" link and delete one of the |
815 | "Test" books you just added. The C<flash> mechanism should retain our |
3533daff |
816 | "Book deleted" status message across the redirect. |
d442cc9f |
817 | |
818 | B<NOTE:> While C<flash> will save information across multiple requests, |
819 | I<it does get cleared the first time it is read>. In general, this is |
e18d15c9 |
820 | exactly what you want -- the C<flash> message will get displayed on the |
821 | next screen where it's appropriate, but it won't "keep showing up" after |
822 | that first time (unless you reset it). Please refer to |
823 | L<Catalyst::Plugin::Session> for additional information. |
d442cc9f |
824 | |
d8e9b469 |
825 | B<Note:> There is also a C<flash-to-stash> feature that will |
826 | automatically load the contents the contents of flash into stash, |
827 | allowing us to use the more typical C<c.flash.status_msg> in our TT |
828 | template in lieu of the more verbose C<status_msg || c.flash.status_msg> |
829 | we used above. Consult L<Catalyst::Plugin::Session> for additional |
830 | information. |
831 | |
832 | |
7ce05098 |
833 | =head2 Switch To Catalyst::Plugin::StatusMessages |
d8e9b469 |
834 | |
835 | Although the query parameter technique we used in |
836 | L<Chapter 4|Catalyst::Manual::Tutorial::04_BasicCRUD> and the C<flash> |
837 | approach we used above will work in most cases, they both have their |
838 | drawbacks. The query parameters can leave the status message on the |
839 | screen longer than it should (for example, if the user hits refresh). |
840 | And C<flash> can display the wrong message on the wrong screen (flash |
841 | just shows the message on the next page for that user... if the user |
842 | has multiple windows or tabs open, then the wrong one can get the |
843 | status message). |
844 | |
845 | L<Catalyst::Plugin::StatusMessage> is designed to address these |
846 | shortcomings. It stores the messages in the user's session (so they are |
847 | available across multiple requests), but ties each status message to a |
848 | random token. By passing this token across the redirect, we are no |
849 | longer relying on a potentially ambiguous "next request" like we do with |
850 | flash. And, because the message is deleted the first time it's |
851 | displayed, the user can hit refresh and still only see the message a |
852 | single time (even though the URL may continue to reference the token, |
853 | it's only displayed the first time). The use of C<StatusMessage> |
854 | or a similar mechanism is recommended for all Catalyst applications. |
855 | |
f4e9de4a |
856 | To enable C<StatusMessage>, first edit F<lib/MyApp.pm> and add |
d8e9b469 |
857 | C<StatusMessage> to the list of plugins: |
1390ef0e |
858 | |
d8e9b469 |
859 | use Catalyst qw/ |
860 | -Debug |
861 | ConfigLoader |
862 | Static::Simple |
7ce05098 |
863 | |
3e29729b |
864 | StackTrace |
7ce05098 |
865 | |
d8e9b469 |
866 | Authentication |
7ce05098 |
867 | |
d8e9b469 |
868 | Session |
869 | Session::Store::File |
870 | Session::State::Cookie |
7ce05098 |
871 | |
d8e9b469 |
872 | StatusMessage |
873 | /; |
3533daff |
874 | |
f4e9de4a |
875 | Then edit F<lib/MyApp/Controller/Books.pm> and modify the C<delete> |
d8e9b469 |
876 | action to match the following: |
3533daff |
877 | |
d8e9b469 |
878 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
879 | my ($self, $c) = @_; |
7ce05098 |
880 | |
d8e9b469 |
881 | # Saved the PK id for status_msg below |
882 | my $id = $c->stash->{object}->id; |
7ce05098 |
883 | |
d8e9b469 |
884 | # Use the book object saved by 'object' and delete it along |
885 | # with related 'book_authors' entries |
886 | $c->stash->{object}->delete; |
7ce05098 |
887 | |
d8e9b469 |
888 | # Redirect the user back to the list page |
889 | $c->response->redirect($c->uri_for($self->action_for('list'), |
890 | {mid => $c->set_status_msg("Deleted book $id")})); |
891 | } |
892 | |
893 | This uses the C<set_status_msg> that the plugin added to C<$c> to save |
894 | the message under a random token. (If we wanted to save an error |
895 | message, we could have used C<set_error_msg>.) Because |
896 | C<set_status_msg> and C<set_error_msg> both return the random token, we |
897 | can assign that value to the "C<mid>" query parameter via C<uri_for> as |
898 | shown above. |
899 | |
900 | Next, we need to make sure that the list page will load display the |
901 | message. The easiest way to do this is to take advantage of the chained |
902 | dispatch we implemented in |
903 | L<Chapter 4|Catalyst::Manual::Tutorial::04_BasicCRUD>. Edit |
f4e9de4a |
904 | F<lib/MyApp/Controller/Books.pm> again and update the C<base> action to |
d8e9b469 |
905 | match: |
906 | |
907 | sub base :Chained('/') :PathPart('books') :CaptureArgs(0) { |
908 | my ($self, $c) = @_; |
7ce05098 |
909 | |
d8e9b469 |
910 | # Store the ResultSet in stash so it's available for other methods |
911 | $c->stash(resultset => $c->model('DB::Book')); |
7ce05098 |
912 | |
d8e9b469 |
913 | # Print a message to the debug log |
914 | $c->log->debug('*** INSIDE BASE METHOD ***'); |
7ce05098 |
915 | |
d8e9b469 |
916 | # Load status messages |
917 | $c->load_status_msgs; |
918 | } |
3533daff |
919 | |
d8e9b469 |
920 | That way, anything that chains off C<base> will automatically get any |
921 | status or error messages loaded into the stash. Let's convert the |
922 | C<list> action to take advantage of this. Modify the method signature |
923 | for C<list> from: |
3533daff |
924 | |
d8e9b469 |
925 | sub list :Local { |
3533daff |
926 | |
d8e9b469 |
927 | to: |
3533daff |
928 | |
8f19d4dd |
929 | sub list :Chained('base') :PathPart('list') :Args(0) { |
3533daff |
930 | |
d8e9b469 |
931 | Finally, let's clean up the status/error message code in our wrapper |
f4e9de4a |
932 | template. Edit F<root/src/wrapper.tt2> and change the "content" div |
d8e9b469 |
933 | to match the following: |
934 | |
935 | <div id="content"> |
936 | [%# Status and error messages %] |
937 | <span class="message">[% status_msg %]</span> |
938 | <span class="error">[% error_msg %]</span> |
939 | [%# This is where TT will stick all of your template's contents. -%] |
940 | [% content %] |
941 | </div><!-- end content --> |
3533daff |
942 | |
861a0cdd |
943 | Now go to L<http://localhost:3000/books/list> in your browser. Delete |
d8e9b469 |
944 | another of the "Test" books you added in the previous step. You should |
945 | get redirection from the C<delete> action back to the C<list> action, |
946 | but with a "mid=########" message ID query parameter. The screen should |
947 | say "Deleted book #" (where # is the PK id of the book you removed). |
948 | However, if you hit refresh in your browser, the status message is no |
949 | longer displayed (even though the URL does still contain the message ID |
950 | token, it is ignored -- thereby keeping the state of our status/error |
951 | messages in sync with the users actions). |
3533daff |
952 | |
24acc5d7 |
953 | You can jump to the next chapter of the tutorial here: |
954 | L<Authorization|Catalyst::Manual::Tutorial::06_Authorization> |
955 | |
d442cc9f |
956 | =head1 AUTHOR |
957 | |
958 | Kennedy Clark, C<hkclark@gmail.com> |
959 | |
53243324 |
960 | Feel free to contact the author for any errors or suggestions, but the |
961 | best way to report issues is via the CPAN RT Bug system at |
bb0999d3 |
962 | L<https://rt.cpan.org/Public/Dist/Display.html?Name=Catalyst-Manual>. |
53243324 |
963 | |
bb0999d3 |
964 | Copyright 2006-2011, Kennedy Clark, under the |
ec3ef4ad |
965 | Creative Commons Attribution Share-Alike License Version 3.0 |
95674086 |
966 | (L<http://creativecommons.org/licenses/by-sa/3.0/us/>). |