Notes from a Newbie 10: Authentication/Authorization
Notes from a Newbie document the creation and deployment of yardbirdfanclub.org with Perl Catalyst on shared hosting. They are intended for a Perl Catalyst Newbie who would like to study the creation and deployment of a simple Perl Catalyst application.
Now that we've experimented a little and are confident in what we're doing, we'll start over from scratch, beginning with authentication and authorization as explained in the Catalyst tutorials.
- Prerequisites
- Create New Catalyst Application
- Authentication
- Create New Database
- Create and Configure a Model and Schema
- Include Authentication and Session Plugins
- Configure Authentication
- Modify the 'password' Column to Use PassphraseColumn
- Load Hashed Password into the Database
- Create New Controllers
- Add Feature: Login
- Add Feature: Logout
- Add Feature: Create New Member
- Summary
- Yardbird/lib/Yardbird.pm
- Yardbird/Makefile.PL
- Yardbird/yardbird.sql
- Yardbird/set_hashed_passwords.pl
- Yardbird/lib/Yardbird/Controller/Login.pm
- Yardbird/lib/Yardbird/Controller/Logout.pm
- Yardbird/lib/Yardbird/Controller/Member.pm
- Yardbird/lib/Yardbird/Controller/Root.pm
- Yardbird/root/static/css/main.css
- Yardbird/root/src/footer.tt
- Yardbird/root/src/header.tt
- Yardbird/root/src/index.tt
- Yardbird/root/src/login.tt
- Yardbird/root/src/member/about.tt
- Yardbird/root/src/member/create.tt
- Yardbird/lib/Yardbird/Form/User.pm
- Yardbird/lib/Yardbird/Schema/Result/User.pm
Prerequisites
Notes from a Newbie are not an exhaustive, authoritative source for information, they augment Catalyst tutorials and other documentation and resources you may be using. Therefore you must study and be competent with the Catalyst tutorials and other documents mentioned in Experiment 01 for Notes from a Newbie to be of use to you.
People at irc.perl.org have been very generous and helpful in answering my questions, I see no reason for them not to do the same for you. Lurk about the catalyst channel at irc.perl.org and ask questions. mst's bite can sting a little so be careful, don't be a fool - but don't be too cool for school.
And please go now, right away, before you forget, sign-up and become a member of yardbirdfanclub.org, and be kind to each other.
Experiment! Create and edit your own blog entries, comments and personal information.
yardbirdfanclub.org is there for you to see how it works, please become a member and say hello.
As a member, you will be able to find my email address in the webmaster's personal information. I would be happy to correspond with you about Catalyst.
You may also correspond via the yardbirdfanclub.org blog, I am watching it closely.
Create New Catalyst Application
As I said previously, now that we've experimented a little we will start over from scratch. We begin by creating a new Catalyst application in a new directory as we did in our experiments:
> catalyst.pl Yardbird
Yardbird> perl Makefile.PL
Create and Configure a TT View
Yardbird> script/yardbird_create.pl view TT TT
In our experiment we used the default location for our templates, but I now want to configure the application to use my templates in a different location and for 'TT' to be my default view:
Yardbird/lib/Yardbird.pm:
__PACKAGE__->config(
# Configure the view
'View::TT' => {
#Set the location for TT files
INCLUDE_PATH => [
__PACKAGE__->path_to( 'root', 'src' ),
],
},
default_view => 'TT',
);
In our new application we can use the header.tt and footer.tt templates, and main.css stylesheet from our 03 experiment, they don't require any changes:
Yardbird/root/src/header.tt:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>[% title %]</title>
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/main.css') %]" />
</head>
<body>
<div id="wrapper">
<div id="main_menu">
<ul>
<div id="main_menu_left">
<li><a href="[% c.uri_for_action('/index') %]">Home</a></li>
<li>Blog</li>
<li>Events</li>
<li>Links</li>
<li>News</li>
<li>Photos</li>
<li>Video</li>
<li>Members</li>
</div>
<div id="main_menu_right">
<li>Login | Not a member?</li>
</div>
</ul>
</div> <!-- main_menu -->
Yardbird/root/src/footer.tt:
<div id="footer">
<p>Feedback Welcome | Webmaster<span style="float:right">The YARDBIRD Fan Club</span></p>
</div>
</div> <!-- wrapper -->
</body>
</html>
root/static/css/main.css:
#wrapper {
width: 960px;
margin-top: 0px;
margin-right: auto;
margin-left: auto;
}
#header {
height: 100px;
margin-left: 20px;
background: #004060;
background-image: url(../images/bird/bird.gif);
background-position: left bottom;
background-repeat: no-repeat;
}
#header_title {
margin-left: 70px;
padding-top: 22px;
color: #87ceeb;
font-size: 2.2em;
font-weight: bold;
}
#header_subtitle {
margin-left: 70px;
margin-top: 2px;
color: #87ceeb;
font-size: 0.9em;
}
#footer {
padding-top: 0.4em;
padding-bottom: 1.5em;
background: #004060;
color: #FFFFFF;
font-size: 0.9em;
}
body {
font-family: arial;
margin-top: 0px;
background: #004060;
}
#main_menu {
border-left: 1px solid #FFFFFF;
border-right: 1px solid #FFFFFF;
border-bottom: 1px solid #FFFFFF;
border-bottom-left-radius: 0.3em;
border-bottom-right-radius: 0.3em;
background: #003050;
font-size: 0.9em;
color: #FFFFFF;
}
#main_menu ul {
padding-top: 0.275em;
padding-bottom: 0.18em;
margin: auto; /* center horizontally */
list-style: none;
/* Menu background/border has no vertical height without
this, I don't know why it works. */
overflow: hidden;
}
#main_menu a {
color: #87ceeb;
text-decoration: none;
}
#main_menu a:hover {
color: #FFFFFF;
text-decoration: underline;
}
#main_menu_left li {
padding-right: 5em;
/*padding-left: 0em;*/
margin-left: -1.5em;
float: left;
/*width: 1em;*/
}
#main_menu_right li {
/*padding-right: 0em;*/
/*padding-left: 40px;*/
margin-right: 1em;
float: right;
/*width: 12.5em;*/
}
#content_wrapper {
border: 1px solid #000000;
border-radius: 0.3em;
background: #FFFFFF;
/*background: #A4D3EE;*/
/* Expand content area when sidebar is vertically longer than content. */
overflow: auto;
}
/* formhandler generates html with id selectors named 'content' for it's
* 'content' fields in the blog and blogcomments forms. For this reason I
* renamed the 'content' id style 'content_with_sidebar'. */
#content_with_sidebar {
margin-right: 225px;
margin-left: 10px;
margin-top: 0px;
margin-bottom: 20px;
}
#content_without_sidebar {
margin-right: 10px;
margin-left: 10px;
margin-top: 0px;
margin-bottom: 20px;
}
#sidebar_wrapper {
float: right;
}
.sidebar_item {
width: 200px;
margin-right: 10px;
margin-top: 13px;
margin-bottom: 10px;
border: 1px solid #000000;
border-radius: 0.3em;
/*background: #FFFFFF;*/
}
.sidebar_item_content {
padding-left: 12px;
padding-right: 12px;
}
You must also copy your image file of Bird from your 03 experiment and save it in your new app:
Yardbird/root/static/images/bird/bird.gif
Go to the blog section of yardbirdfanclub.org if you need a copy of the file, and right-click on the image of Bird (he will have a frisbee in his mouth) and save.
Copy index.tt from your 03 experiment, modify it to not use a database, update your new controller to use index.tt, run the application and see if you get anything decent.
Yardbird/root/src/index.tt:
[% INCLUDE header.tt title = 'The YARDBIRD Fan Club'; %]
<div id="header">
<div id="header_title">The YARDBIRD Fan Club</div>
<div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>
<div id="content_wrapper">
<div id="content_without_sidebar">
<h3>Homepage</h3>
</div>
</div>
[% INCLUDE footer.tt %]
Yardbird/lib/Yardbird/Controller/Root.pm:
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->detach($c->view("TT"));
}
We now have the following files in our new application:
Run it and you should get a decent page:
Yardbird> script/yardbird_server.pl -r
Authentication
We now have a decent page and will begin to add features.
We will need a way for people to become new members, and for members to be able to login and logout.
Logged-in users will be able to:
- Create, edit and delete their own blog entries and comments.
- Determine what information about themselves they want other members to be able to view.
- View other member's personal information.
Non-members will be unable to:
- Create blog entries or comments.
- Provide personal information to share with other members.
- View other members personal information.
Create New Database
Let's begin by creating a new database. The following yardbird.sql file will generate all tables and columns needed for yardbirdfanclub.org.
yardbird.sql:
--
-- Add user and role tables, along with a many-to-many join table
--
CREATE TABLE `user` (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(30) NOT NULL,
password TEXT NOT NULL,
email VARCHAR(50) NOT NULL,
email_visible INT UNSIGNED,
firstname VARCHAR(30),
lastname VARCHAR(30),
location VARCHAR(100),
about_me TEXT,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE role (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
-- Careful, don't create role's larger than 28 characters to be safe.
role VARCHAR(30) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE user_role (
userid INT UNSIGNED NOT NULL,
roleid INT UNSIGNED NOT NULL,
-- Delete user_role record or update userid when `user`(id) is deleted or updated.
FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (roleid) REFERENCES role(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (userid, roleid)
) ENGINE=InnoDB;
--
-- Add blog and blog_comment tables
--
CREATE TABLE blog (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
userid INT UNSIGNED NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created TIMESTAMP NOT NULL,
FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE blog_comment (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
blogid INT UNSIGNED NOT NULL,
-- userid of the commenter (NOT THE userid OF THE BLOG ENTRY)
userid INT UNSIGNED NOT NULL,
content TEXT NOT NULL,
created TIMESTAMP NOT NULL,
FOREIGN KEY (blogid) REFERENCES blog(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO `user` (id, username, email) VALUES (1, 'webmaster', 'webmaster@something.com');
INSERT INTO role (id, role) VALUES (1, 'user');
INSERT INTO role (id, role) VALUES (2, 'admin');
INSERT INTO user_role (userid, roleid) VALUES (1, 1); -- Assign user role to webmaster
INSERT INTO user_role (userid, roleid) VALUES (1, 2); -- Assign admin role to webmaster
INSERT INTO blog (userid, title, content) VALUES (1, 'title', 'content');
We are creating one user with id
1 and username
webmaster. We also assign both user
and admin
roles and create a simple blog entry for webmaster.
The user
, role
and user_role
tables are explained in the Catalyst tutorials:
- Catalyst::Manual::Tutorial::05_Authentication
- Catalyst::Manual::Tutorial::06_Authorization
Our user
table contains a few columns not used in the Catalyst tutorials:
email_visible
: Make email address visible to other members.location
: Whatever a user wants to describe where they live.about_me
: Whatever a user wants other members to know about themselves.
The blog
and blog_comment
tables are the same as we used in our experiments, with the addition of a created
column to store a blog entry or comment creation time and date.
Use yardbird.sql to create a new database:
> mysql -uusername -ppassword
mysql> create database yardbird;
mysql> quit;
> mysql -uusername -ppassword yardbird < yardbird.sql
Create and Configure a Model and Schema
Run the following command to create a model and schema for our application:
Yardbird> script/yardbird_create.pl model DB DBIC::Schema Yardbird::Schema create=static components=TimeStamp,PassphraseColumn dbi:mysql:yardbird username password
Note: There is no space between the comma and PassphraseColumn in the above command.
Our application should now have new Model and Schema classes:
Include Authentication and Session Plugins
Update the application class to use the following:
Authentication
Authorization::Roles
Session
Session::Store::File
Session::State::Cookie
I have this after adding the above:
Yardbird/lib/Yardbird.pm:
use Catalyst qw/
-Debug
ConfigLoader
Static::Simple
Authentication
Authorization::Roles
Session
Session::Store::File
Session::State::Cookie
/;
Update Makefile.PL to use the following:
Yardbird/Makefile.PL:
requires 'Catalyst::Plugin::Authentication';
requires 'Catalyst::Plugin::Authorization::Roles';
requires 'Catalyst::Plugin::Session';
requires 'Catalyst::Plugin::Session::Store::File';
requires 'Catalyst::Plugin::Session::State::Cookie';
Configure Authentication
We use Catalyst::Authentication::Realm::SimpleDB because it automatically sets a reasonable set of defaults for us.
Add the following before __PACKAGE__->setup();
in the application class to configure SimpleDB Authentication:
Yardbird/lib/Yardbird.pm:
# Configure SimpleDB Authentication
__PACKAGE__->config(
'Plugin::Authentication' => {
default => {
class => 'SimpleDB',
user_model => 'DB::User',
password_type => 'self_check',
},
},
);
Modify the 'password' Column to Use PassphraseColumn
Add to the user Result class below the "# DO NOT MODIFY THIS OR ANYTHING ABOVE!" line, but above the closing "1;":
Yardbird/lib/Yardbird/Schema/Result/User.pm:
# Have the 'password' column use a SHA-1 hash and 20-byte salt
# with RFC 2307 encoding; Generate the 'check_password" method
__PACKAGE__->add_columns(
'password' => {
passphrase => 'rfc2307',
passphrase_class => 'SaltedDigest',
passphrase_args => {
algorithm => 'SHA-1',
salt_random => 20.
},
passphrase_check_method => 'check_password',
},
);
Load Hashed Password into the Database
Initialize a hashed password for webmaster with the following script:
Yardbird/sethashedpasswords.pl:
#!/usr/bin/perl
use strict;
use warnings;
use Yardbird::Schema;
# $ perl -Ilib set_hashed_passwords.pl
my $schema = Yardbird::Schema->connect('dbi:mysql:yardbird', 'username', 'password');
my @users = $schema->resultset('User')->all;
foreach my $user (@users) {
$user->password('password');
$user->update;
}
Run the script:
Yardbird> perl -Ilib set_hashed_passwords.pl
We should be done creating and configuring the Model and Schema now. As a sanity check you may want to run the application and verify you still get the same decent page you had previously:
Yardbird> script/yardbird_server.pl -r
Create New Controllers
Create three new controllers for logging in and out and creating new members:
Yardbird> script/yardbird_create.pl controller Login
Yardbird> script/yardbird_create.pl controller Logout
Yardbird> script/yardbird_create.pl controller Member
Add Feature: Login
Replace the index method in the new Login controller:
Yardbird/lib/Yardbird/Controller/Login.pm:
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
if ($c->authenticate({ username => $username,
password => $password } )) {
# If successful, then let them use the application
$c->response->redirect($c->uri_for_action('/index'));
return;
} else {
# Set an error message
$c->stash(error_msg => "Bad username or password.");
}
} else {
# Set an error message
$c->stash(error_msg => "Empty username or password.")
unless ($c->user_exists);
}
# If either of above don't work out, send to the login page
$c->stash(template => 'login.tt');
}
Create a Login template for the view:
Yardbird/root/src/login.tt:
[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Login'; %]
<div id="header">
<div id="header_title">The YARDBIRD Fan Club</div>
<div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>
<div id="content_wrapper">
<div id="content_without_sidebar">
<h3>Login</h3>
<form method="post" action="[% c.uri_for_action('/login/index') %]">
<table>
<tr>
<td>Username:</td>
<td><input type="text" name="username" size="40" /></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="password" size="40" /></td>
</tr>
<tr>
<td> </td>
<td colspan="2"><input type="submit" name="submit" value="Submit" /></td>
</tr>
</table>
</form>
</div>
</div>
[% INCLUDE footer.tt %]
We need to modify header.tt to link to our new Login and Logout controllers.
Replace this:
Yardbird/root/src/header.tt:
<div id="main_menu_right">
<li>Login | Not a member?</li>
</div>
With this:
<div id="main_menu_right">
[% IF c.user_exists %]
<li>[% c.user.name %] | <a href="[% c.uri_for_action('/logout/index') %]">Logout</a></li>
[% ELSE %]
<li><a href="[% c.uri_for_action('/login/index') %]">Login</a> | Not a member?</li>
[% END %]
</div>
header.tt needs the user Result name
method we created in the experiments.
Add the following below the "# DO NOT MODIFY THIS OR ANYTHING ABOVE!" line, but above the closing "1;":
Yardbird/lib/Yardbird/Schema/Result/User.pm:
sub name {
my ($self) = @_;
my $bestname;
if ($self->firstname) {
$bestname = $self->firstname;
if ($self->lastname) {
$bestname .= ' ' . $self->lastname;
}
}
elsif ($self->lastname) {
$bestname = $self->lastname;
}
else {
$bestname = $self->username;
}
return $bestname;
}
Go to Experiment 02 and refresh your memory about Result class methods if you need to.
Add Feature: Logout
Replace the index method in the new Logout controller:
Yardbird/lib/Yardbird/Controller/Logout.pm:
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_action('/index'));
}
If we've done everything correctly, we should now be able to log the webmaster in and out with 'password'.
Add Feature: Create New Member
Not a Member?
If you are following along you may want to go to yardbirdfanclub.org and take the 'Not a Member?' link to see how a new member is created. When a user selects this feature, the following reasons for joining appear with a Sidebar Menu 'Join now!' link:
- Post blog entries and comments.
- Optionally provide your contact and other information to other users.
- View other member's contact information.
Add method to Member controller:
Controller/Member.pm:
sub about :Local :Args(0) {
my ( $self, $c ) = @_;
$c->detach($c->view("TT"));
}
Notice the use of the :Local
attribute.
Previously we've used the :Path
attribute in all our controller actions, which achieves the same results with a little more typing:
sub about :Path('about') :Args(0) {
my ( $self, $c ) = @_;
$c->detach($c->view("TT"));
}
:Path
actions let you map a method to an explicit URI path.
For example, :Path('create')
in lib/Yardbird/Controller/Member.pm would match on http://localhost:3000/member/create, but :Path('/create')
would match on http://localhost:3000/create because of the leading slash.
:Local
is shorthand for :Path('name_of_method')
.
For example, these are equivalent: sub create :Local {...}
and sub create :Path('create') {...}
.
See:
- Catalyst::Manual::Tutorial::03_MoreCatalystBasics
root/src/member/about.tt:
[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Membership' %]
<div id="header">
<div id="header_title">The YARDBIRD Fan Club</div>
<div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>
<div id="content_wrapper">
<div id="sidebar_wrapper">
<div class="sidebar_item">
<div class="sidebar_item_content">
<p>Join now!</p>
</div>
</div>
</div>
<div id="content_with_sidebar">
<h3>Why become a member?</h3>
<p>With membership you can:
<ul>
<li>Post blog entries and comments.</li>
<li>Optionally provide your contact and other information to other users.</li>
<li>View other member's contact information.</li>
</ul>
</div>
</div>
[% INCLUDE footer.tt %]
Modify header.tt to link to member/about.
Change this:
root/src/header.tt:
Not a member?
To this:
<a href="[% c.uri_for_action('/member/about') %]">Not a member?</a>
Have you noticed any relationships between controller/action names/paths, template names/paths and URI's?
Run the application and you should see our new 'Not a member?' link and 'Join now!' item in a Sidebar Menu:
Join Now!
Finally, we add the ability for yardbirdfanclub.org users to become members of The Yardbird Fanclub.
I used HTML::FormHandler to validate and process data.
HTML::FormHandler
I learned how to use HTML::FormHandler from these docs:
- HTML::FormHandler
- HTML::FormHandler::Manual::Tutorial
- HTML::FormHandler::Manual::Catalyst
- HTML::FormHandler::Manual::Intro
Create a Form
We will create a form for inputting new member information.
Yardbird/lib/Yardbird/Form/User.pm:
package Yardbird::Form::User;
use HTML::FormHandler::Moose;
use HTML::FormHandler::Types ('NoSpaces', 'WordChars', 'NotAllDigits', 'SimpleStr' );
extends 'HTML::FormHandler::Model::DBIC';
has '+item_class' => ( default => 'User' );
has_field 'username' => (
type => 'Text',
apply => [ NoSpaces, WordChars, NotAllDigits ],
required => 1,
unique => 1,
maxlength => 25,
);
has_field 'password' => (
type => 'Password',
apply => [ NoSpaces, WordChars, NotAllDigits ],
required => 1,
maxlength => 25,
);
has_field 'password_confirm' => (
type => 'PasswordConf',
tags => { label_after => ': ' },
);
has_field 'email' => (
type => 'Email',
required => 1,
unique => 1,
maxlength => 45,
);
has_field 'email_visible' => (
type => 'Checkbox',
);
has_field 'firstname' => (
type => 'Text',
apply => [ NoSpaces, WordChars, NotAllDigits ],
maxlength => 25,
);
has_field 'lastname' => (
type => 'Text',
apply => [ NoSpaces, WordChars, NotAllDigits ],
maxlength => 25,
);
has_field 'location' => (
type => 'Text',
maxlength => 95,
);
has_field 'about_me' => (
type => 'TextArea',
cols => 70,
rows => 10,
do_label => 0,
);
has_field 'submit' => (
type => 'Submit',
value => 'Submit',
);
no HTML::FormHandler::Moose;
1;
Use the Form in a Controller
In the controller you should now have:
Yardbird/lib/Yardbird/Controller/Member.pm:
package Yardbird::Controller::Member;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
Edit the controller to use your new form:
Yardbird/lib/Yardbird/Controller/Member.pm:
package Yardbird::Controller::Member;
use Moose;
use namespace::autoclean;
use Yardbird::Form::User;
BEGIN { extends 'Catalyst::Controller'; }
has 'form' => (
isa => 'Yardbird::Form::User',
is => 'rw',
lazy => 1,
default => sub { Yardbird::Form::User->new }
);
Create a controller action for creating a new member:
sub create :Local {
my ( $self, $c, $user_id ) = @_;
$c->stash(template => 'member/create.tt', form => $self->form );
# Validate and insert data into database
return unless $self->form->process(
item_id => $user_id,
params => $c->req->parameters,
schema => $c->model('DB')->schema
);
# Form validated, return to home
$c->res->redirect($c->uri_for_action('/index'));
}
Create a Template to View the Form
Yardbird/root/src/member/create.tt:
[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Join'; %]
<div id="header">
<div id="header_title">The YARDBIRD Fan Club</div>
<div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>
<div id="content_wrapper">
<div id="content_with_sidebar">
<h3>Join The YARDBIRD Fan Club</h3>
<form id="user_form" name="[% form.name %]" action="[% c.uri_for_action('/member/create') %]" method="post">
[% f = form.field('username') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
<br>
[% f = form.field('password') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="password" name="[% f.name %]" id="[% f.name %]" value="">
<br>
[% f = form.field('password_confirm') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="password" name="[% f.name %]" id="[% f.name %]" value="">
<br>
[% f = form.field('email') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="email" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
[% f = form.field('email_visible') %]
<input type="checkbox" name="[% f.name %]" id="[% f.name %]" value="1">
<label for="[% f.name %]">Visible</label>
<br>
[% f = form.field('firstname') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
<br>
[% f = form.field('lastname') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
<br>
[% f = form.field('location') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
<br>
About me:
<div id='user_form_textarea'>
[% form.field('about_me').render %]
</div>
<input id="user_form_button" class="button" name="submit" type="submit" value="Submit"/>
</form>
<p>Username and Password are required.</p>
<ul>
<li>Username must be unique among member usernames.</li>
<li>They may contain upper and lowercase letters, digits, and the underscore character.</li>
</ul>
<p>A valid, unique email address is required.</p>
<ul>
<li>If "Email visible" is checked, it will be visible only to members via the Members page.</li>
<li>If "Email visible" is not checked, it will be visible only to the YARDBIRD Fan Club Webmaster.</li>
</ul>
<p>Firstname and Lastname are optional.</p>
<ul>
<li>If either are provided, they will be used in page content and visible to both members and non-members alike.</li>
<li>If neither are provided, your Username will be used in-lieu of them.</li>
</ul>
<p>Location is optional.</p>
<ul>
<li>If provided, it will be visible only to members via the Members page.</li>
<li>For example, the YARDBIRD Fan Club Webmaster's location is: Dover-Foxcroft, Maine, USA</li>
</ul>
<p>"About me" is optional.</p>
<ul>
<li>If provided, it will be visible only to members via the Members page.</li>
<li>HTML Tags may be used for styling.</li>
</ul>
</div>
</div>
[% INCLUDE footer.tt %]
Notice above, we use three new styles we must add to our stylesheet.
Update our stylesheet:
Yardbird/root/static/css/main.css:
#user_form_button {
margin-left: 140px;
}
#user_form .label {
float: left;
width: 140px;
}
#user_form_textarea {
margin-top: -1.15em;
margin-left: 140px;
}
Finally, we can enable a link for creating a new member.
Enable a link:
root/src/member/about.tt:
<p><a href="[% c.uri_for_action('/member/create') %]">Join now!</a></p>
Run it and you should be able to create a new member with a decent looking page:
Summary
Yardbird/lib/Yardbird.pm
package Yardbird;
use Moose;
use namespace::autoclean;
use Catalyst::Runtime 5.80;
use Catalyst qw/
-Debug
ConfigLoader
Static::Simple
Authentication
Authorization::Roles
Session
Session::Store::File
Session::State::Cookie
/;
extends 'Catalyst';
our $VERSION = '0.01';
__PACKAGE__->config(
name => 'Yardbird',
# Disable deprecated behavior needed by old applications
disable_component_resolution_regex_fallback => 1,
enable_catalyst_header => 1, # Send X-Catalyst header
);
__PACKAGE__->config(
# Configure the view
'View::TT' => {
#Set the location for TT files
INCLUDE_PATH => [
__PACKAGE__->path_to( 'root', 'src' ),
],
},
default_view => 'TT',
);
# Configure SimpleDB Authentication
__PACKAGE__->config(
'Plugin::Authentication' => {
default => {
class => 'SimpleDB',
user_model => 'DB::User',
password_type => 'self_check',
},
},
);
# Start the application
__PACKAGE__->setup();
1;
Yardbird/Makefile.PL
#!/usr/bin/env perl
# IMPORTANT: if you delete this file your app will not work as
# expected. You have been warned.
use inc::Module::Install 1.02;
use Module::Install::Catalyst; # Complain loudly if you don't have
# Catalyst::Devel installed or haven't said
# 'make dist' to create a standalone tarball.
name 'Yardbird';
all_from 'lib/Yardbird.pm';
requires 'Catalyst::Runtime' => '5.90018';
requires 'Catalyst::Plugin::ConfigLoader';
requires 'Catalyst::Plugin::Static::Simple';
requires 'Catalyst::Action::RenderView';
requires 'Moose';
requires 'namespace::autoclean';
requires 'Config::General'; # This should reflect the config file format you've chosen
# See Catalyst::Plugin::ConfigLoader for supported formats
requires 'Catalyst::Plugin::Authentication';
requires 'Catalyst::Plugin::Authorization::Roles';
requires 'Catalyst::Plugin::Session';
requires 'Catalyst::Plugin::Session::Store::File';
requires 'Catalyst::Plugin::Session::State::Cookie';
test_requires 'Test::More' => '0.88';
catalyst;
install_script glob('script/*.pl');
auto_install;
WriteAll;
Yardbird/yardbird.sql
--
-- Add user and role tables, along with a many-to-many join table
--
CREATE TABLE `user` (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(30) NOT NULL,
password TEXT NOT NULL,
email VARCHAR(50) NOT NULL,
email_visible INT UNSIGNED,
firstname VARCHAR(30),
lastname VARCHAR(30),
location VARCHAR(100),
about_me TEXT,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE role (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
-- Careful, don't create role's larger than 28 characters to be safe.
role VARCHAR(30) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE user_role (
userid INT UNSIGNED NOT NULL,
roleid INT UNSIGNED NOT NULL,
-- Delete user_role record or update userid when `user`(id) is deleted or updated.
FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (roleid) REFERENCES role(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (userid, roleid)
) ENGINE=InnoDB;
--
-- Add blog and blog_comment tables
--
CREATE TABLE blog (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
userid INT UNSIGNED NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created TIMESTAMP NOT NULL,
FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE blog_comment (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
blogid INT UNSIGNED NOT NULL,
-- userid of the commenter (NOT THE userid OF THE BLOG ENTRY)
userid INT UNSIGNED NOT NULL,
content TEXT NOT NULL,
created TIMESTAMP NOT NULL,
FOREIGN KEY (blogid) REFERENCES blog(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO `user` (id, username, email) VALUES (1, 'webmaster', 'webmaster@something.com');
INSERT INTO role (id, role) VALUES (1, 'user');
INSERT INTO role (id, role) VALUES (2, 'admin');
INSERT INTO user_role (userid, roleid) VALUES (1, 1); -- Assign user role to webmaster
INSERT INTO user_role (userid, roleid) VALUES (1, 2); -- Assign admin role to webmaster
INSERT INTO blog (userid, title, content) VALUES (1, 'title', 'content');
Create Model/Schema:
Yardbird> script/yardbird_create.pl model DB DBIC::Schema Yardbird::Schema create=static components=TimeStamp,PassphraseColumn dbi:mysql:yardbird username password
Yardbird/set_hashed_passwords.pl
#!/usr/bin/perl
use strict;
use warnings;
use Yardbird::Schema;
# $ perl -Ilib set_hashed_passwords.pl
my $schema = Yardbird::Schema->connect('dbi:mysql:yardbird', 'username', 'password');
my @users = $schema->resultset('User')->all;
foreach my $user (@users) {
$user->password('password');
$user->update;
}
Yardbird/lib/Yardbird/Controller/Login.pm
package Yardbird::Controller::Login;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
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
if ($c->authenticate({ username => $username,
password => $password } )) {
# If successful, then let them use the application
$c->response->redirect($c->uri_for_action('/index'));
return;
} else {
# Set an error message
$c->stash(error_msg => "Bad username or password.");
}
} else {
# Set an error message
$c->stash(error_msg => "Empty username or password.")
unless ($c->user_exists);
}
# If either of above don't work out, send to the login page
$c->stash(template => 'login.tt');
}
__PACKAGE__->meta->make_immutable;
1;
Yardbird/lib/Yardbird/Controller/Logout.pm
package Yardbird::Controller::Logout;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
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_action('/index'));
}
__PACKAGE__->meta->make_immutable;
1;
Yardbird/lib/Yardbird/Controller/Member.pm
package Yardbird::Controller::Member;
use Moose;
use namespace::autoclean;
use Yardbird::Form::User;
BEGIN { extends 'Catalyst::Controller'; }
has 'form' => (
isa => 'Yardbird::Form::User',
is => 'rw',
lazy => 1,
default => sub { Yardbird::Form::User->new }
);
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched Yardbird::Controller::Member in Member.');
}
sub about :Local :Args(0) {
my ( $self, $c ) = @_;
$c->detach($c->view("TT"));
}
sub create :Local {
my ( $self, $c, $user_id ) = @_;
$c->stash(template => 'member/create.tt', form => $self->form );
# Validate and insert data into database
return unless $self->form->process(
item_id => $user_id,
params => $c->req->parameters,
schema => $c->model('DB')->schema
);
# Form validated, return to home
$c->res->redirect($c->uri_for_action('/index'));
}
__PACKAGE__->meta->make_immutable;
1;
Yardbird/lib/Yardbird/Controller/Root.pm
package Yardbird::Controller::Root;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' }
#
# Sets the actions in this controller to be registered with no prefix
# so they function identically to actions created in MyApp.pm
#
__PACKAGE__->config(namespace => '');
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->detach($c->view("TT"));
}
sub default :Path {
my ( $self, $c ) = @_;
$c->response->body( 'Page not found' );
$c->response->status(404);
}
sub end : ActionClass('RenderView') {}
__PACKAGE__->meta->make_immutable;
1;
Yardbird/root/static/css/main.css
#wrapper {
width: 960px;
margin-top: 0px;
margin-right: auto;
margin-left: auto;
}
#header {
height: 100px;
margin-left: 20px;
background: #004060;
background-image: url(../images/bird/bird.gif);
background-position: left bottom;
background-repeat: no-repeat;
}
#header_title {
margin-left: 70px;
padding-top: 22px;
color: #87ceeb;
font-size: 2.2em;
font-weight: bold;
}
#header_subtitle {
margin-left: 70px;
margin-top: 2px;
color: #87ceeb;
font-size: 0.9em;
}
#footer {
padding-top: 0.4em;
padding-bottom: 1.5em;
background: #004060;
color: #FFFFFF;
font-size: 0.9em;
}
body {
font-family: arial;
margin-top: 0px;
background: #004060;
}
#main_menu {
border-left: 1px solid #FFFFFF;
border-right: 1px solid #FFFFFF;
border-bottom: 1px solid #FFFFFF;
border-bottom-left-radius: 0.3em;
border-bottom-right-radius: 0.3em;
background: #003050;
font-size: 0.9em;
color: #FFFFFF;
}
#main_menu ul {
padding-top: 0.275em;
padding-bottom: 0.18em;
margin: auto; /* center horizontally */
list-style: none;
/* Menu background/border has no vertical height without
this, I don't know why it works. */
overflow: hidden;
}
#main_menu a {
color: #87ceeb;
text-decoration: none;
}
#main_menu a:hover {
color: #FFFFFF;
text-decoration: underline;
}
#main_menu_left li {
padding-right: 5em;
/*padding-left: 0em;*/
margin-left: -1.5em;
float: left;
/*width: 1em;*/
}
#main_menu_right li {
/*padding-right: 0em;*/
/*padding-left: 40px;*/
margin-right: 1em;
float: right;
/*width: 12.5em;*/
}
#content_wrapper {
border: 1px solid #000000;
border-radius: 0.3em;
background: #FFFFFF;
/*background: #A4D3EE;*/
/* Expand content area when sidebar is vertically longer than content. */
overflow: auto;
}
/* formhandler generates html with id selectors named 'content' for it's
* 'content' fields in the blog and blogcomments forms. For this reason I
* renamed the 'content' id style 'content_with_sidebar'. */
#content_with_sidebar {
margin-right: 225px;
margin-left: 10px;
margin-top: 0px;
margin-bottom: 20px;
}
#content_without_sidebar {
margin-right: 10px;
margin-left: 10px;
margin-top: 0px;
margin-bottom: 20px;
}
#sidebar_wrapper {
float: right;
}
.sidebar_item {
width: 200px;
margin-right: 10px;
margin-top: 13px;
margin-bottom: 10px;
border: 1px solid #000000;
border-radius: 0.3em;
/*background: #FFFFFF;*/
}
.sidebar_item_content {
padding-left: 12px;
padding-right: 12px;
}
#user_form_button {
margin-left: 140px;
}
#user_form .label {
float: left;
width: 140px;
}
#user_form_textarea {
margin-top: -1.15em;
margin-left: 140px;
}
Yardbird/root/src/footer.tt
<div id="footer">
<p>Feedback Welcome | Webmaster<span style="float:right">The YARDBIRD Fan Club</span></p>
</div>
</div> <!-- wrapper -->
</body>
</html>
Yardbird/root/src/header.tt
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>[% title %]</title>
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/main.css') %]" />
</head>
<body>
<div id="wrapper">
<div id="main_menu">
<ul>
<div id="main_menu_left">
<li><a href="[% c.uri_for_action('/index') %]">Home</a></li>
<li>Blog</li>
<li>Events</li>
<li>Links</li>
<li>News</li>
<li>Photos</li>
<li>Video</li>
<li>Members</li>
</div>
<div id="main_menu_right">
[% IF c.user_exists %]
<li>[% c.user.name %] | <a href="[% c.uri_for_action('/logout/index') %]">Logout</a></li>
[% ELSE %]
<li><a href="[% c.uri_for_action('/login/index') %]">Login</a> | <a href="[% c.uri_for_action('/member/about') %]">Not a member?</a></li>
[% END %]
</div>
</ul>
</div> <!-- main_menu -->
Yardbird/root/src/index.tt
[% INCLUDE header.tt title = 'The YARDBIRD Fan Club'; %]
<div id="header">
<div id="header_title">The YARDBIRD Fan Club</div>
<div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>
<div id="content_wrapper">
<div id="content_without_sidebar">
<h3>Homepage</h3>
</div>
</div>
[% INCLUDE footer.tt %]
Yardbird/root/src/login.tt
[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Login'; %]
<div id="header">
<div id="header_title">The YARDBIRD Fan Club</div>
<div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>
<div id="content_wrapper">
<div id="content_without_sidebar">
<h3>Login</h3>
<form method="post" action="[% c.uri_for_action('/login/index') %]">
<table>
<tr>
<td>Username:</td>
<td><input type="text" name="username" size="40" /></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="password" size="40" /></td>
</tr>
<tr>
<td> </td>
<td colspan="2"><input type="submit" name="submit" value="Submit" /></td>
</tr>
</table>
</form>
</div>
</div>
[% INCLUDE footer.tt %]
Yardbird/root/src/member/about.tt
[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Membership' %]
<div id="header">
<div id="header_title">The YARDBIRD Fan Club</div>
<div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>
<div id="content_wrapper">
<div id="sidebar_wrapper">
<div class="sidebar_item">
<div class="sidebar_item_content">
<p><a href="[% c.uri_for_action('/member/create') %]">Join now!</a></p>
</div>
</div>
</div>
<div id="content_with_sidebar">
<h3>Why become a member?</h3>
<p>With membership you can:
<ul>
<li>Post blog entries and comments.</li>
<li>Optionally provide your contact and other information to other users.</li>
<li>View other member's contact information.</li>
</ul>
</div>
</div>
[% INCLUDE footer.tt %]
Yardbird/root/src/member/create.tt
[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Join'; %]
<div id="header">
<div id="header_title">The YARDBIRD Fan Club</div>
<div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>
<div id="content_wrapper">
<div id="content_with_sidebar">
<h3>Join The YARDBIRD Fan Club</h3>
<form id="user_form" name="[% form.name %]" action="[% c.uri_for_action('/member/create') %]" method="post">
[% f = form.field('username') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
<br>
[% f = form.field('password') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="password" name="[% f.name %]" id="[% f.name %]" value="">
<br>
[% f = form.field('password_confirm') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="password" name="[% f.name %]" id="[% f.name %]" value="">
<br>
[% f = form.field('email') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="email" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
[% f = form.field('email_visible') %]
<input type="checkbox" name="[% f.name %]" id="[% f.name %]" value="1">
<label for="[% f.name %]">Visible</label>
<br>
[% f = form.field('firstname') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
<br>
[% f = form.field('lastname') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
<br>
[% f = form.field('location') %]
<label class="label" for="[% f.name %]">[% f.label %]:</label>
<input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
<br>
About me:
<div id='user_form_textarea'>
[% form.field('about_me').render %]
</div>
<input id="user_form_button" class="button" name="submit" type="submit" value="Submit"/>
</form>
<p>Username and Password are required.</p>
<ul>
<li>Username must be unique among member usernames.</li>
<li>They may contain upper and lowercase letters, digits, and the underscore character.</li>
</ul>
<p>A valid, unique email address is required.</p>
<ul>
<li>If "Email visible" is checked, it will be visible only to members via the Members page.</li>
<li>If "Email visible" is not checked, it will be visible only to the YARDBIRD Fan Club Webmaster.</li>
</ul>
<p>Firstname and Lastname are optional.</p>
<ul>
<li>If either are provided, they will be used in page content and visible to both members and non-members alike.</li>
<li>If neither are provided, your Username will be used in-lieu of them.</li>
</ul>
<p>Location is optional.</p>
<ul>
<li>If provided, it will be visible only to members via the Members page.</li>
<li>For example, the YARDBIRD Fan Club Webmaster's location is: Dover-Foxcroft, Maine, USA</li>
</ul>
<p>"About me" is optional.</p>
<ul>
<li>If provided, it will be visible only to members via the Members page.</li>
<li>HTML Tags may be used for styling.</li>
</ul>
</div>
</div>
[% INCLUDE footer.tt %]
Yardbird/lib/Yardbird/Form/User.pm
package Yardbird::Form::User;
use HTML::FormHandler::Moose;
use HTML::FormHandler::Types ('NoSpaces', 'WordChars', 'NotAllDigits', 'SimpleStr' );
extends 'HTML::FormHandler::Model::DBIC';
has '+item_class' => ( default => 'User' );
has_field 'username' => (
type => 'Text',
apply => [ NoSpaces, WordChars, NotAllDigits ],
required => 1,
unique => 1,
maxlength => 25,
);
has_field 'password' => (
type => 'Password',
apply => [ NoSpaces, WordChars, NotAllDigits ],
required => 1,
maxlength => 25,
);
has_field 'password_confirm' => (
type => 'PasswordConf',
tags => { label_after => ': ' },
);
has_field 'email' => (
type => 'Email',
required => 1,
unique => 1,
maxlength => 45,
);
has_field 'email_visible' => (
type => 'Checkbox',
);
has_field 'firstname' => (
type => 'Text',
apply => [ NoSpaces, WordChars, NotAllDigits ],
maxlength => 25,
);
has_field 'lastname' => (
type => 'Text',
apply => [ NoSpaces, WordChars, NotAllDigits ],
maxlength => 25,
);
has_field 'location' => (
type => 'Text',
maxlength => 95,
);
has_field 'about_me' => (
type => 'TextArea',
cols => 70,
rows => 10,
do_label => 0,
);
has_field 'submit' => (
type => 'Submit',
value => 'Submit',
);
no HTML::FormHandler::Moose;
1;
Yardbird/lib/Yardbird/Schema/Result/User.pm
use utf8;
package Yardbird::Schema::Result::User;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Yardbird::Schema::Result::User
=cut
use strict;
use warnings;
use Moose;
use MooseX::NonMoose;
use MooseX::MarkAsMethods autoclean => 1;
extends 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<DBIx::Class::InflateColumn::DateTime>
=item * L<DBIx::Class::TimeStamp>
=item * L<DBIx::Class::PassphraseColumn>
=back
=cut
__PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp", "PassphraseColumn");
=head1 TABLE: C<user>
=cut
__PACKAGE__->table("user");
=head1 ACCESSORS
=head2 id
data_type: 'integer'
extra: {unsigned => 1}
is_auto_increment: 1
is_nullable: 0
=head2 username
data_type: 'varchar'
is_nullable: 0
size: 30
=head2 password
data_type: 'text'
is_nullable: 0
=head2 email
data_type: 'varchar'
is_nullable: 0
size: 50
=head2 email_visible
data_type: 'integer'
extra: {unsigned => 1}
is_nullable: 1
=head2 firstname
data_type: 'varchar'
is_nullable: 1
size: 30
=head2 lastname
data_type: 'varchar'
is_nullable: 1
size: 30
=head2 location
data_type: 'varchar'
is_nullable: 1
size: 100
=head2 about_me
data_type: 'text'
is_nullable: 1
=cut
__PACKAGE__->add_columns(
"id",
{
data_type => "integer",
extra => { unsigned => 1 },
is_auto_increment => 1,
is_nullable => 0,
},
"username",
{ data_type => "varchar", is_nullable => 0, size => 30 },
"password",
{ data_type => "text", is_nullable => 0 },
"email",
{ data_type => "varchar", is_nullable => 0, size => 50 },
"email_visible",
{ data_type => "integer", extra => { unsigned => 1 }, is_nullable => 1 },
"firstname",
{ data_type => "varchar", is_nullable => 1, size => 30 },
"lastname",
{ data_type => "varchar", is_nullable => 1, size => 30 },
"location",
{ data_type => "varchar", is_nullable => 1, size => 100 },
"about_me",
{ data_type => "text", is_nullable => 1 },
);
=head1 PRIMARY KEY
=over 4
=item * L</id>
=back
=cut
__PACKAGE__->set_primary_key("id");
=head1 RELATIONS
=head2 blog_comments
Type: has_many
Related object: L<Yardbird::Schema::Result::BlogComment>
=cut
__PACKAGE__->has_many(
"blog_comments",
"Yardbird::Schema::Result::BlogComment",
{ "foreign.userid" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 blogs
Type: has_many
Related object: L<Yardbird::Schema::Result::Blog>
=cut
__PACKAGE__->has_many(
"blogs",
"Yardbird::Schema::Result::Blog",
{ "foreign.userid" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 user_roles
Type: has_many
Related object: L<Yardbird::Schema::Result::UserRole>
=cut
__PACKAGE__->has_many(
"user_roles",
"Yardbird::Schema::Result::UserRole",
{ "foreign.userid" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 roleids
Type: many_to_many
Composing rels: L</user_roles> -> roleid
=cut
__PACKAGE__->many_to_many("roleids", "user_roles", "roleid");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-02-25 12:45:10
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:s1r1mjXJqZzRdLk0PGpC9A
# You can replace this text with custom code or comments, and it will be preserved on regeneration
# Have the 'password' column use a SHA-1 hash and 20-byte salt
# with RFC 2307 encoding; Generate the 'check_password" method
__PACKAGE__->add_columns(
'password' => {
passphrase => 'rfc2307',
passphrase_class => 'SaltedDigest',
passphrase_args => {
algorithm => 'SHA-1',
salt_random => 20.
},
passphrase_check_method => 'check_password',
},
);
sub name {
my ($self) = @_;
my $bestname;
if ($self->firstname) {
$bestname = $self->firstname;
if ($self->lastname) {
$bestname .= ' ' . $self->lastname;
}
}
elsif ($self->lastname) {
$bestname = $self->lastname;
}
else {
$bestname = $self->username;
}
return $bestname;
}
__PACKAGE__->meta->make_immutable;
1;
Leave a comment