1. Introduction
In this tutorial, we will learn about the features of the signin
plugin, by creating a simple application allowing users to subscribe and create a profile page.
We will be using a SQL database, meaning you should already be familiar with the ODB plugin (see Getting started tutorial).
1.1 Preparations
Start by creating a crails application using the following command:
$ crails new -n userspace -d sqlite -p html $ cd userspace
Add the signin
plugin that we are going to use in this tutorial:
$ crails plugins signin install
And prepare a layout for our html views:
$ crails scaffold layout
We'll now make some changes to the ApplicationController
:
- we want our new layout to be the default layout,
- we want it to inherit from Crails::Odb::Controller
.
Here's how it should look like:
app/controllers/application.hpp
#pragma once
#include <crails/controller.hpp>
#include <crails/odb/controller.hpp>
class ApplicationController :
public Crails::Odb::Controller<Crails::Controller>
{
public:
ApplicationController(Crails::Context& context) :
Crails::Odb::Controller<Crails::Controller>(context)
{
vars["layout"] = std::string("application");
}
};
2. Authenticable models
The first step we need to take is to create a model which can be used to store a user's data. It'll need to provide at least a username and a password, so we'll use the following command to generate the model, controller and views for user management:
$ crails scaffold resource -m user -p std::string/name Crails::Password/password
Crails::Password
type is provided by the signin plugin. It behaves
as an std::string
, but encrypts any value set into it using the AES-256-CBC
algorithm. Optionally, it can be overloaded to implement other encryption strategies.
We will now enable authentication from this model, by having it inherit from Crails::AuthenticableModel
,
using multiple inheritance:
app/models/user.hpp
#pragma once
#include <crails/odb/model.hpp>
#include <crails/datatree.hpp>
#include <crails/password.hpp>
#include <crails/signin/model.hpp> // header for AuthenticableModel
#pragma db object
class User :
public Crails::Odb::Model,
public Crails::AuthenticableModel
{
odb_instantiable()
public:
...
private:
std::string name;
std::string password;
};
The AuthenticableModel
object will generate and store session data for a user, including an authentication
token and an expire time. The session duration can be configured in config/signin.cpp
by customizing the
AuthenticationModel::session_duration
variable.
3. Opening a session
3.1 The SessionController
At this point, we can already create new users, but we cannot connect as one of the users. In order to remedy that, we'll have to create a session controller. Create the following file:
app/controllers/session.hpp
#pragma once
#include <crails/signin/session_controller.hpp>
#include "application.hpp" // ApplicationController
#include "app/models/user.hpp" // our model
#include "lib/odb/application-odb.hxx" // query implementations
class SessionController :
public Crails::SessionController<User, ApplicationController>
{
public:
SessionController(Crails::Context& context) :
Crails::SessionController<User, ApplicationController>(context)
{
}
void on_session_created() override
{
redirect_to("/user");
}
void on_session_destroy() override
{
redirect_to("/user");
}
void new_()
{
render("session/new");
}
std::shared_ptr<User> find_user() override
{
std::shared_ptr<User> result;
std::string name = params["name"].as<std::string>();
Crails::Password password = params["password"].as<std::string>();
database.find_one(result,
odb::query<User>::name == name &&
odb::query<User>::password == password
);
return result;
}
};
The Crails::SessionController
class will do most of the heavy-lifting for us: we just need to implement:
- hooks for when sessions are created and destroyed: on_session_created
and on_session_destroyed
,
- an action to display the sign-in form: here it is the new_
method
- a find_user
method which will find a user in database matching a set of username and password provided by a query's parameters.
3.2 Sign in routes
The signin plugin also provides a helper to connect our session controller to the router:
app/routes.cpp
#include <crails/router.hpp>
#include "controllers/user.hpp"
#include "controllers/session.hpp"
void Crails::Router::initialize()
{
resource_actions(user, UserController);
// adds POST and DELETE routes for connecting and disconnected
signin_actions("/session", SessionController);
// adds a route to display our sign in form
match_action("GET", "/session/new", SessionController, new_);
}
The signin_actions
macro generates routes for creating and destroying a session. We added another
route to handle the new_
action that will display our sign in view.
3.3 Sign in form
Let's now create the form that will display when going to http://localhost:3001/session/new. Create the following file:
app/views/session/new.html
<%= form({{"action","/session"},{"method","post"}}) yields %>
<!-- Name input -->
<div class="form-outline mb-4">
<input type="text" name="name" class="form-control" />
<label class="form-label" for="name">Username</label>
</div>
<!-- Password input -->
<div class="form-outline mb-4">
<input type="password" name="password" class="form-control" />
<label class="form-label" for="password">Password</label>
</div>
<!-- Submit button -->
<button type="button" class="btn btn-primary btn-block mb-4">
Sign in
</button>
<!-- Register buttons -->
<div class="text-center">
<p>Not a member? <a href="/user/new">Register</a></p>
</div>
<% yields-end %>
4. Session and current user
4.1 Fetch the current user
The users can connect, so we probably want to display data relative to each user.
The signin plugin provides the AuthController
class to help
you interact with a session. Let's update our ApplicationController
so that all our controllers may be aware of the current user:
app/controllers/application.hpp
#pragma once
#include <crails/controller.hpp>
#include <crails/odb/controller.hpp>
#include <crails/signin/auth_controller.hpp> // AuthController
#include "app/models/user.hpp"
/*
* This is a chain of controller components: it's something you'll
* use when you want to combine components from multiple crails
* plugins.
* In this case, we use Crails::AuthController for managing the
* session, and Crails::Odb::Controller to provide a database
* backend.
*/
typedef Crails::AuthController<
User,
Crails::Odb::Controller<Crails::Controller>
> ApplicationControllerBase;
class ApplicationController : public ApplicationControllerBase
{
protected:
ApplicationController(Crails::Context& context) :
ApplicationControllerBase(context)
{
vars["layout"] = std::string("layouts/application");
}
public:
/*
* Overload the initialize method and call
* user_session.get_current_user() to fetch the current
* user (if there is one). Then share a pointer to the
* current user with the views.
*/
void initialize() override
{
std::shared_ptr<User> user = user_session.get_current_user();
vars["current_user"] = user.get();
ApplicationControllerBase::initialize();
}
};
Since all the views can now see the current user, we'll update the layout to
display the current user name at the top of each page. First, we add the
current_user
variable we shared earlier:
app/views/layouts/application.html
#include "lib/assets.hpp"
#include "app/models/user.hpp"
User* @current_user;
const char* @yield = nullptr;
// END LINKING
...
And we'll add a navbar with a greeting to the user:
app/views/layouts/application.html
...
<% if (current_user) do %>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<span class="navbar-brand">
Hello, <%= current_user->get_name() %> !
</span>
</nav>
<% end %>
...
4.2 Disconnect the user
In the same navbar we just created, let's add buttons for the user to connect or disconnect:
<div style="float:right">
<% if (current_user) do %>
<%= link("/session",
"Disconnect",
{{"method","delete"},{"class","btn btn-primary"}}) %>
<% end else do %>
<%= link("/session/new",
"Connect",
{{"class","btn btn-primary"}}) %>
<% end %>
</div>
And here we are: you can now connect or disconnect of an account from any page in the application !
4.3 Require a connected user
There are some spaces in your application that you may not wish to be accessible to users that
aren't authenticated. The AuthController
class provides some helper methods to
quickly set up such restrictions.
We'll give it a try by making our UserController
accessible only to logged
users:
app/controllers/user.hpp
#pragma once
#include "app/controllers/application.hpp"
#include "app/models/user.hpp"
class UserController : public ApplicationController
{
public:
UserController(Crails::Context&);
// Add this method to make the magic happen:
bool require_authenticated_user() const override { return true; }
...
};
With this tiny change, users trying to access the User controller will now receive
a 401 Unauthorized response. The session check happens on controller
initialization, so you're also guaranteed to have access to a non-null current_user
in all the controllers actions,
But there's a problem ! Now that the controller is exclusively accessible to
connected users, newcomers cannot access the user creation form at /user/new
.
To fix that issue, we'll rewrite require_authenticated_user
with a condition
that excludes user creation from our new restrictions:
app/controllers/user.hpp
#pragma once
#include "app/controllers/application.hpp"
#include "app/models/user.hpp"
#include <algorithm> // required for std::find
class UserController : public ApplicationController
{
public:
UserController(Crails::Context&);
// Add this method to make the magic happen:
bool require_authenticated_user() const override
{
const std::vector<std::string> excluded_actions{"new_", "create"};
return std::find(
excluded_actions.begin(),
excluded_actions.end(),
params["controller-data"]["action"].as<std::string>()
) == excluded_actions.end();
}
...
};
With these new changes, the new_
and create
methods are no longer
concerned by current user restrictions and will be available to non-connected users.
4.4 Redirect non-connected users
We've restricted most of the user controller to connected users... but when non-connected users try to access private contents, their browser merely receives a 401 response with no content.
This behavior works great for web services... but for web applications, a more appropriate
behavior is to redirect users to the application's public space. To do so, we'll implement
the on_user_not_authenticated
method and override the default behavior for
unauthenticated users:
app/controllers/user.hpp
#pragma once
#include "app/controllers/application.hpp"
#include "app/models/user.hpp"
class UserController : public ApplicationController
{
public:
UserController(Crails::Context&);
bool require_authenticated_user() const override;
void on_user_not_authenticated() override
{
redirect_to("/session/new");
}
...
};
5. What's next ?
You are now able to authentify your application users and accurately manage private and public content !
A good follow-up for this tutorial would be to implement OAuth using libcrails-oauth
.