Dancer2

 view release on metacpan or  search on metacpan

lib/Dancer2/Manual/Tutorial.pod  view on Meta::CPAN


At minimum, we need to create a table to contain our blog entries. Create
a new directory for your database and SQL files:

    $ mkdir db

Then create a new file, F<db/entries.sql>, with the following:

    CREATE TABLE entries (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        summary TEXT NOT NULL,
        content TEXT NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

Later, we'll add an additional table for users.

Let's create our blog database with the above table. From our project
directory:

    $ sqlite3 db/dlblog.db < db/entries.sql

=head2 Using Dancer2::Plugin::DBIx::Class

L<Dancer2::Plugin::DBIx::Class> is a plugin that integrates the
L<DBIx::Class> Object Relational Mapper (ORM) and Dancer2. It maps tables,
rows, and columns into classes, objects, and methods in Perl. L<DBIx::Class>
(DBIC for short) makes it convenient to work with databases in your Perl
applications and reduces a lot of manual SQL creation.

DBIC is also a large and complex beast, and can take some time to become
proficient with it. This tutorial merely scrapes the surface of what you
can do with it; for more information, check out
L<DBIx::Class::Manual::DocMap>.

You'll also need to install two other dependencies, L<DBD::SQLite> (the
SQLite database driver), L<DBIx::Class::Schema::Loader> (for automatically
generating database classes from tables), and L<DateTime::Format::SQLite>
(to interact with dates in DBIC objects).

Install them using C<cpan> or C<cpanm>:

    cpanm DBD::SQLite Dancer2::Plugin::DBIx::Class \
        DBIx::Class::Schema::Loader DateTime::Format::SQLite

Add these to your F<cpanfile>:

    requires "DBD::SQLite";
    requires "Dancer2::Plugin::DBIx::Class";
    requires "DBIx::Class::Schema::Loader";
    requires "DateTime::Format::SQLite";

And then add it to the top of F<lib/DLBlog.pm> after C<use Dancer2;>:

    use Dancer2::Plugin::DBIx::Class;

We need to add configuration to tell our plugin where to find the SQLite
database. For this project, it's sufficient to put configuration for
the database in F<config.yml>. In a production application, you'd
have different database credentials in your development and staging
environments than you would in your production environment (we'd hope you
would anyhow!). And this is where environment config files are handy.

By default, Dancer2 runs your application in the development environment.
To that end, we'll add plugin configuration appropriately. Add the following
to your F<environments/development.yml> file:

    plugins:
      DBIx::Class:
        default:
          dsn: "dbi:SQLite:dbname=db/dlblog.db"
          schema_class: "DLBlog::Schema"
          dbi_params:
            RaiseError: 1
            AutoCommit: 1

Note that we only provided C<DBIx::Class> for the plugin name; Dancer2
automatically infers the C<Dancer2::Plugin::> prefix.

As SQLite databases are a local file, we don't need to provide login
credentials for the database. The two settings in the C<dbi_params>
section tell L<DBIx::Class> to raise an error automatically to our code
(should one occur), and to automatically manage transactions for us (so
we don't have to).

=head1 Generating Schema Classes

L<DBIx::Class> relies on class definitions to map database tables to
Perl constructs. Thankfully, L<DBIx::Class::Schema::Loader> can do much
of this work for us.

To generate the schema object, and a class that represents the C<entries>
table, run the following from your shell:

    dbicdump -o dump_directory=./lib \
        -o components='["InflateColumn::DateTime"]' \
        DLBlog::Schema dbi:SQLite:db/dlblog.db '{ quote_char => "\"" }'

This creates two new files in your application:

=over

=item * F<lib/DLBlog/Schema.pm>

This is a class that represents all database schema.

=item * F<lib/DLBlog/Schema/Result/Entry.pm>

This is a class representing a single row in the C<entries> table.

=back

=head1 Implementing the Danceyland Blog

Let's start by creating an entry and saving it to the database; all other
routes rely on us having at least one entry (in some form).

=head2 Performing Queries

L<Dancer2::Plugin::DBIx::Class> lets us easily perform SQL queries against
a database. It does this by providing methods to interact with data, such
as C<find>, C<search>, C<create>, and C<update>. These methods make for
simpler maintenance of code, as they do all the work of writing and executing
SQL in the background.

For example, let's use a convenience method to create a new blog entry.
Here's the form we created for entering a blog post:

    <div id="create">
        <form method="post" action="<% request.uri_for('/create') %>">
            <label for="title">Title</label>
            <input type="text" name="title" id="title"><br>
            <label for="summary">Summary</label>
            <input type="text" name="summary" id="summary"><br>
            <label for="content">Content</label>
            <textarea name="content" id="content" cols="50" rows="10"></textarea><br>
            <button type="submit">Save Entry</button>
        </form>
    </div>

We can take values submitted via this form and turn them into a row in

lib/Dancer2/Manual/Tutorial.pod  view on Meta::CPAN

Retrieving session data can also be done with the C<session> keyword:

    my $user = session 'user';

You can verify the username was written to the session by looking at the
session file created on disk. If you look in your project directory, you'll
notice a new F<sessions/> directory. There should now be exactly one file
there: your current session. Run the following:

    $ cat sessions/<some session id>.yml

You'll have a file that looks like:

    ---
    user: admin

The session filename matches the session ID, which is stored in a cookie
that is delivered to the client browser when your application is accessed.
If you have the browser developer tools open when you access your
development site, you can inspect the cookie and see for yourself.

YAML files are great for sessions while developing, but they are not a
good choice for production. We'll examine some other options when we
discuss deploying to production later in this tutorial.

=head2 Storing application users

Since we're already using a database to store our blog contents, it only
makes sense to track our application users there, too. Let's create a
simplistic table to store user data in F<db/users.sql>:

    CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username VARCHAR NOT NULL UNIQUE,
        password VARCHAR NOT NULL
    );

Then run this from our shell:

    sqlite3 db/dlblog.db < db/users.sql

By declaring password to be an unbounded varchar field, we allow for
passwords or passphrases of any length. Notice we don't track admin
status, rights, or anything of the like - if you can log in, you can
administer the blog.

We'll need to regenerate the L<DBIx::Class::Result> classes so we can
create objects that represent users. Run the following in your shell from
the project directory:

    dbicdump -o dump_directory=./lib \
        -o components='["InflateColumn::DateTime"]' \
        DLBlog::Schema dbi:SQLite:db/dlblog.db '{ quote_char => "\"" }'

You should have an additional source file in your project directory now,
F<lib/DLBlog/Schema/Result/User.pm>.

=head2 Password management with Dancer2::Plugin::CryptPassphrase

It is best practice to store passwords encrypted, less someone with database
access look at your C<users> table and steal account credentials. Rather than
roll our own, we'll use one of the many great options on CPAN.

L<Dancer2::Plugin::CryptPassphrase> provides convenient access to
L<Crypt::Passphrase> in your Dancer2 applications. We'll use the latter
to generate a password hash for any new users we create.

Install the above modules:

    cpanm Dancer2::Plugin::CryptPassphrase

and add the module to your F<cpanfile>:

    requires "Dancer2::Plugin::CryptPassphrase";

This extension requires configuration as to which password encoder to
use. Add this to the C<plugins> section of the F<environments/development.yml>
file:

  CryptPassphrase:
    encoder:
      module: Argon2
      parallelism: 2

From your shell, running the following will produce an encrypted password
string:

    perl -MCrypt::Passphrase -E \
        'my $auth=Crypt::Passphrase->new(encoder=>"Argon2"); say $auth->hash_password("test")'

(substitute any other password for C<test> you'd rather use)

That can then be filled in as the password value in the below SQL. From your shell:

    sqlite3 db/dlblog.db

    sqlite> INSERT INTO users (username, password)
    VALUES (
        'admin',
        '$argon2id$v=19$m=262144,t=3,p=1$07krd3DaNn3b9JplNPSjnA$CiFKqjqgecDiYoV0qq0QsZn2GXmORkia2YIhgn/dbBo'
    ); -- admin/test

    sqlite> .quit

=head2 Dancer2::Plugin::Auth::Tiny

We also need to install L<Dancer2::Plugin::Auth::Tiny> From your shell:

    cpanm Dancer2::Plugin::Auth::Tiny

Then add this to your F<cpanfile>:

    requires "Dancer2::Plugin::Auth::Tiny";

=head2 Implementing Login

We need two routes to implement the login process: a GET route to display
a login form, and a POST route to process the form data. Before that, we
need to include the plugins we need for authentication. Below your other
C<use> statements, add the following:

lib/Dancer2/Manual/Tutorial.pod  view on Meta::CPAN

        default:
          dsn: "dbi:SQLite:dbname=t/db/test.db"
          schema_class: "DLBlog::Schema"
          dbi_params:
            RaiseError: 1
            AutoCommit: 1
      CryptPassphrase:
        encoder:
          module: Argon2
          parallelism: 2

This is a hybrid of your production and development configs:

=over

=item * It still logs to the console, but at a higher level so only errors are seen

=item * We hide any stacktraces and disable server tokens/headers

=item * We tell DBIx::Class to look at our test database

=back

To run the default tests from your shell:

    DANCER_ENVIRONMENT=test prove -lv

This runs all tests in the F<t/> subdirectory with maximum verbosity, and
runs with our test environment configuration.

Dancer2 creates applications with two default tests. The first,
F<t/001_base.t>, ensures that your Dancer2 application compiles successfully
without errors. The other test, F<t/002_index_route.t>, ensures that the
default route of your application is accessible. These are the two most
basic tests an application can have, and validate very little about the
functionality of your app. We're going to make two new tests: one just to
test the nuances of the login process (as security is critical), and another
to test basic blog functionality.

=head2 Login Tests

Let's verify the login process behaves as intended. Edit F<t/003_login.t>
and add the following:

    use strict;
    use warnings;
    use Test::More;
    use Test::WWW::Mechanize::PSGI;

    use DLBlog;
    my $mech = Test::WWW::Mechanize::PSGI->new(
        app => DLBlog->to_app,
    );

    $mech->get_ok('/create', 'Got /create while not logged in');
    $mech->content_contains('Password', '...and was presented with a login page');
    $mech->submit_form_ok({
        fields => {
            username => 'admin',
            password => 'foobar',
        }}, '...which we gave invalid credentials');
    $mech->content_contains('Invalid username or password', '...and gave us an appropriate error');
    $mech->submit_form_ok({
        fields => {
            username => 'admin',
            password => 'test',
        }}, '...so we give it real credentials');
    $mech->content_contains('form', '...and get something that looks like the create form' );
    $mech->content_contains('Content', 'Confirmed this is the create form');

    done_testing;

We load two test modules: L<Test::More>, which provides a basic set of test
functionality, and L<Test::WWW::Mechanize::PSGI>, which will do all our heavy
lifting.

To start, we need to create an instance of a Mechanize object:

    my $mech = Test::WWW::Mechanize::PSGI->new(
        app => DLBlog->to_app,
    );

This creates an instance of the Mechanize user agent, and points it at an
instance of the Danceyland blog app (C<DLBlog>).

We've specified that some routes can't be accessed by unauthorized/non-logged
in users. Let's test this:

    $mech->get_ok('/create', 'Got /create while not logged in');
    $mech->content_contains('Password', '...and was presented with a login page');

This tests the C<needs login> condition on the C</create> route. We should
be taken to a login page if we haven't logged in. C<get_ok> ensures the
route is accessible, and C<content_contains> looks for a password field.

We should get an error message for a failed login attempt. Let's stuff
the form with invalid credentials and verify that:

    $mech->submit_form_ok({
        fields => {
            username => 'admin',
            password => 'foobar',
        }}, '...which we gave invalid credentials');
    $mech->content_contains('Invalid username or password', '...and gave us an appropriate error');

C<submit_form_ok> takes a hashref of fields and puts the specified values
into them, then clicks the appropriate submit button. We then check the
resulting page content to confirm that we do, in fact, see the invalid
username/password error message.

We know that login handles failed attempts ok now. How about a login with
valid credentials>

    $mech->submit_form_ok({
        fields => {
            username => 'admin',
            password => 'test',
        }}, '...so we give it real credentials');
    $mech->content_contains('form', '...and get something that looks like the create form' );
    $mech->content_contains('Content', 'Confirmed this is the create form');

We pass the default admin/test credentials, then look at the page we're
taken to. The create page should have a form, and one of the fields should
be named Content. C<content_contains> looks for both of these on the
resulting page, and passes if they are present.

Finally, we need to say we're done running tests:

    done_testing;

Now that it's done, let's run just this one test. From your shell:

    DANCER_ENVIRONMENT=test prove -lv t/003_login.t

And you should see the following output:

    t/003_login.t ..
    ok 1 - Got /create while not logged in
    ok 2 - ...and was presented with a login page
    [DLBlog:2287783] warning @2025-02-06 22:34:25> Failed login attempt for admin in /path/to/DLBlog/lib/DLBlog.pm l. 134
    ok 3 - ...which we gave invalid credentials
    ok 4 - ...and gave us an appropriate error
    ok 5 - ...so we give it real credentials
    ok 6 - ...and get something that looks like the create form
    ok 7 - Confirmed this is the create form
    1..7
    ok
    All tests successful.
    Files=1, Tests=7,  1 wallclock secs ( 0.02 usr  0.00 sys +  0.92 cusr  0.10 csys =  1.04 CPU)
    Result: PASS

The labels help us see which tests are running, making it easier to examine
failures when they happen. You'll see a log message produced when our login
attempt failed, and a C<PASS> at the end showing all tests were successfully
run.

=head2 Blog Tests

Using the same techniques we learned writing the login test, we'll make
a test for basic blog functionality. The complete test will run through
a basic workflow of the Danceyland blog:

=over

=item * Login

=item * Create an entry

=item * Edit an entry

=item * Delete an entry

=back

Add the following to F<t/004_blog.t> in your project directory:

    use strict;
    use warnings;
    use Test::More;
    use Test::WWW::Mechanize::PSGI;

    use DLBlog;
    my $mech = Test::WWW::Mechanize::PSGI->new(
        app => DLBlog->to_app,
    );

    subtest 'Landing page' => sub {
        $mech->get_ok('/', 'Got landing page');
        $mech->title_is('Danceyland Blog', '...for our blog');
        $mech->content_contains('Test Blog Post','...and it has a test post');
    };

    subtest 'Login' => sub {
        $mech->get_ok('/login', 'Visit login page to make some changes');
        $mech->submit_form_ok({
            fields => {
                username => 'admin',
                password => 'test',
            }}, '...so we give it a user');
    };

    subtest 'Create' => sub {
        $mech->get_ok('/create', 'Write a new blog post');

lib/Dancer2/Manual/Tutorial.pod  view on Meta::CPAN


    t/004_blog.t ..
    # Subtest: Landing page
        ok 1 - Got landing page
        ok 2 - ...for our blog
        ok 3 - ...and it has a test post
        1..3
    ok 1 - Landing page
    # Subtest: Login
        ok 1 - Visit login page to make some changes
        ok 2 - ...so we give it a user
        1..2
    ok 2 - Login
    # Subtest: Create
        ok 1 - Write a new blog post
        ok 2 - ...then write another post
        ok 3 - ...and get redirected to the new entry
        1..3
    ok 3 - Create
    # Subtest: Update
        ok 1 - Navigating to the update page for this post
        ok 2 - ...then update yet another post
        ok 3 - ...and get redirected to the entry page
        ok 4 - ...and it has the updated title
        1..4
    ok 4 - Update
    # Subtest: Delete
        ok 1 - Go delete page for new entry
        ok 2 - ...then delete it!
        ok 3 - ...then try to navigate to the entry
        ok 4 - ...and see the post is no longer there
        1..4
    ok 5 - Delete
    1..5
    ok
    All tests successful.
    Files=1, Tests=5,  1 wallclock secs ( 0.01 usr  0.01 sys +  0.77 cusr  0.10 csys =  0.89 CPU)
    Result: PASS

You'll notice that not only is the code conveniently grouped by subtest,
but so is the output.

There's a lot more you can test still. Look for some additional ideas at
the end of this tutorial.

=head1 Deployment

We've built the application, and written some basic tests to ensure the
application can function properly. Now, let's put it on a server and
make it available to the public!

=head2 Creating Production Configuration

The default Dancer2 configuration provides a lot of information to the
developer to assist in debugging while creating an application. In a
production environment, there's too much information being given that can
be used by someone trying to compromise your application. Let's create
an environment specifically for production to turn the level of detail down.

If you were using a database server instead of SQLite, you'd want to update
database credentials in your production configuration to point to your
production database server.

Replace your F<environments/production.yml> file with the following:

    # configuration file for production environment
    behind_proxy: 1

    # only log info, warning and error messsages
    log: "info"

    # log message to a file in logs/
    logger: "file"

    # hide errors
    show_stacktrace: 0

    # disable server tokens in production environments
    no_server_tokens: 1

    # Plugin configuration
    plugins:
      DBIx::Class:
        default:
          dsn: "dbi:SQLite:dbname=db/dlblog.db"
          schema_class: "DLBlog::Schema"
          dbi_params:
            RaiseError: 1
            AutoCommit: 1
      CryptPassphrase:
        encoder:
          module: Argon2
          parallelism: 2

Changes include:

=over

=item * Running behind a reverse proxy

We're going to deploy our application in conjunction with NGINX; running
in this manner requires Dancer2 to interact and set HTTP headers
differently than it would running standalone. This setting tells
Dancer2 to behave as it should behind an NGINX (or other) reverse proxy.

=item * Logging only informational or more severe messages

In a production environment, logging debugging and core Dancer2 messages
is rarely needed.

=item * Logging to a file

Servers will be running in the background, not in a console window. As
such, a place to catch log messages will be needed. File logs can also be
shipped to another service (such as Kibana) for analysis.

=item * No stacktraces

If a fatal error occurs, the stacktraces produced by Dancer2 provide a
potential attacker with information about the insides of your application.
By setting C<show_stacktrace> to C<0>, all errors show only the



( run in 1.683 second using v1.01-cache-2.11-cpan-39bf76dae61 )