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

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:

yfc10b.png

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.

yfc10a.png

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:

yfc10c.png

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'.

yfc10d.png

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:

yfc10e.png

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:

yfc10f.png

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

About j0e

user-pic I have experience and am skilled at maintaining old school Perl 5 and am seeking opportunities to use modern Perl.