DEV Community

Anders Björkland
Anders Björkland

Posted on

Adding registration to SilverStripe and controlling priveleges

Today we are adding a fundamental feature to the basic Book Review platform we started with in our previous article. Last time we added book, author, and review models in this SilverStripe based application. We had the ability to add a review, albeit in a somewhat tedious way, but only one user could do so. Today we will see how to add a register page to our application so many more can take part, and control what content they can access.

And as previous, if you want to check out the code for this page, you can find it here: https://github.com/andersbjorkland/devto-book-reviewers/tree/registration

Registration page

There already exists the model for handling users in SilverStripe. This is represented by the Member-class and it comes out of the package with the CMS. We are going to build a registration page for creating new Members.

What we need is:

  • A RegisterController to serve the registration page with its form, and handle the form submission.
  • A Template to display the registration form.
  • A route to the registration page.

Sounds pretty simple, right? Well, let's say someone writing this had to iron out some errors because he thought it would be that simple. What we'll do here though is error-free (and we'll do it in a way that makes sense).

The Wonderous RegisterController

Wonderous might be a bit hyperbolic, but we'll start with a simple controller that we'll add some neat features to. The main purpose of the controller is to serve and handle the form submission. Let's take a peek at it.

./app/src/Controller/RegisterController.php
<?php

namespace App\Controller;

use App\Form\ValidatedAliasField;
use App\Form\ValidatedEmailField;
use App\Form\ValidatedPasswordField;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Security\Member; // Will be used later when we do register a new member.

class RegistrationController extends ContentController
{   
    private static $allowed_actions = [
        'registerForm'
    ];

    public function registerForm()
    {
        $fields = new FieldList(
            ValidatedAliasField::create( 'alias', 'Alias')->addExtraClass('text'),
            ValidatedEmailField::create('email', 'Email'),
            ValidatedPasswordField::create('password', 'Password'),
        );

        $actions = new FieldList(
            FormAction::create(
                'doRegister',   // methodName
                'Register'      // Label
            )
        );

        $required = new RequiredFields('alias', 'email', 'password');

        $form = new Form($this, 'RegisterForm', $fields, $actions, $required);

        return $form;
    }

    public function doRegister($data, Form $form)
    {
        // To be detailed later
    }
}

Enter fullscreen mode Exit fullscreen mode

As we can see, the controller is pretty simple. It has a method called registerForm that returns a form and will be accessed in a template with $registerForm. We will be creating a new Page-type further down that will be our point of entry to this controller and the template to display it.

The form that will be served is made up of a fieldlist with three fields. The first field is a ValidatedAliasField, the second is a ValidatedEmailField, and the third is a ValidatedPasswordField. These fields are custom fields that we will create with various validation rules. We have attached a register-button to the form that will submit the form to the doRegister method when clicked. Before we get to the doRegister method, we need to address the custom fields.

Custom Fields

With validation rules, we can control that an alias and an email address are unique. We will also require a password to be at least 8 characters long. Here's how we do it:

./app/src/Form/ValidatedAliasField.php
<?php 

namespace App\Form;

use SilverStripe\Forms\TextField;
use SilverStripe\Security\Member;

class ValidatedAliasField extends TextField
{
    public function validate($validator)
    {
        $alias = $this->Value();
        $member = Member::get()->filter(['FirstName' => $alias])->first();

        if ($member) {
            $validator->validationError(
                $this->name,
                'Alias is already in use',
                'validation'
            );
            return false;
        }
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

./app/src/Form/ValidatedEmailField.php
<?php 

namespace App\Form;

use SilverStripe\Forms\EmailField;
use SilverStripe\Security\Member;

class ValidatedEmailField extends EmailField
{
    public function validate($validator)
    {
        $email = $this->Value();
        $member = Member::get()->filter(['Email' => $email])->first();

        if ($member) {
            $validator->validationError(
                $this->name,
                'Email is already in use',
                'validation'
            );
            return false;
        }
        return true;
    }
}

Enter fullscreen mode Exit fullscreen mode

./app/src/Form/ValidatedPasswordField.php
<?php 

namespace App\Form;

use SilverStripe\Forms\PasswordField;

class ValidatedPasswordField extends PasswordField
{
    public function validate($validator)
    {
        $value = $this->Value();
        if (strlen($value) < 6) {
            $validator->validationError(
                $this->name,
                'Password must be at least 6 characters long',
                'validation'
            );
            return false;
        }
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

As may become appearant from these classes, we are just extending whatever field we want to validate. We need to override the validate() method and apply our own validation rules. For both alias and email we need to check if the value is already in use. If we find a user with it, then the validation fails. For the password we need to check if the length is at least 6 characters long. When we receive the form data in our controller we can use a validationResult() method to check if the form submission is valid. Read on below so seehow we do that when we create a new member.

Creating a new Member

So we have submitted a form and we need something that catches its content and store it in the database.

We update ./app/src/Controller/RegistrationController.php
<?php

namespace App\Controller;

use App\Form\ValidatedAliasField;
use App\Form\ValidatedEmailField;
use App\Form\ValidatedPasswordField;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Security\Member; // Yay, this comes in handy now!

class RegistrationController extends ContentController
{   
    private static $allowed_actions = [
        'registerForm'
    ];


    public function registerForm()
    {
        // ...
    }

    public function doRegister($data, Form $form)
    {
        // Make sure we have all the data we need
        $alias = $data['alias'] ?? null;
        $email = $data['email'] ?? null;
        $password = $data['password'] ?? null;

        /* 
         * Check if the fields clear their validation rules.
         * If there are errors, then the form will be updated with the errors
         * so the user may correct them.
         */
        $validationResults = $form->validationResult();

        if ($validationResults->isValid()) {
            $member = Member::create();
            $member->FirstName = $alias;
            $member->Email = $email;
            $member->Password = $password;
            $member->write();

            $form->sessionMessage('Registration successful', 'good');
        }

        return $this->redirectBack();
    }
}
Enter fullscreen mode Exit fullscreen mode

This method receives the form data and checks if it is valid. If it is, it creates a new Member and writes it to the database. If it isn't, it redirects the user back to the registration page, where the user will be informed of the errors.

Errors in the form are pointed out on each input field that is applicable

Something to note for the security-conscious developer is that it appears as we simply set a plain password to the Member. This is not a good practice, as it is not secure. It's a good thing then that the Member-class hashes the password before writing it to the database (with onBeforeWrite()). Read more about passwords and security on the official documentations on SilverStripe.

So far so good. We have a created a form and its fields. We have a controller that serves the form and handles its submission. But! We have no way of seeing it yet.

Creating a registration page

We need a page and route to serve the form. We also need a template that hooks into the $registerForm variable (or rather: the Controller-method that serves the form).

Here's the plan:

  • We create a RegisterPage that points to the RegistrationController.
  • We create a template that includes the registration form.
  • We create an instance of the RegisterPage, specifying its route and title.

Create RegisterPage as a subclass of Page

./app/src/Page/RegisterPage.php
<?php

namespace App\Page;

use App\Controller\RegistrationController;
use Page;

class RegistrationPage extends Page
{
    public function getControllerName()
    {
        return RegistrationController::class;
    }
}

Enter fullscreen mode Exit fullscreen mode

This is a new model of a Page-type (actually it's a SiteTree but that's beside the point). Whenever we create a new instance of this class, SilverStripe will try to look for a template that corresponds to its namespace. So let's create one.

Create a template layout

./themes/simple/templates/App/Page/Layout/RegistrationPage.ss
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
    <article>
        <h1>$Title</h1>
        <div class="content">$Content</div>
    </article>
    $registerForm
</div>
Enter fullscreen mode Exit fullscreen mode

A note on the theme: If you have created a copy of simple and are working on that one instead, remember to change the name of the theme to "whatever-you-have-called-it" in app\_config\theme.yml. Then you need to run composer vendor-expose to copy/expose any public assets to the public folder.

Instantiate a RegisterPage
We almost have a registration page now. We are going to use the admin-interface to create it, but before that we need to update the database to be ready for it. In the browser, visit: localhost:8000/dev/build

Now, visit localhost:8000/admin. When we are logged in, we will by default be shown the "Pages" tab. Let's click on Add new. For Step 1 we leave it at "Top level". In Step 2 we select Registration Page. This is the type we just coded. Next we click on Create. This takes us to a page where we can edit the page. Let's add the following:

Page name Registration
URL segment registration
Navigation label Registration
Content Register a new user

Then we can click Save. After the loading we can click Publish. We are now almost ready to accept new registered users to our site.

Adding priveleges to the new user

So our users can now register, but guess what? They can't access the admin-interface. We want them to be able to make their reviews, and possibly see other peoples reviews. So this is the plan:

  • We create a new Group called "Reviewers".
  • We add new members to that group in our controller.
  • We update our models (Author, Book, Review) to allow the new group to access them.

Creating a new Group

With the power of SilverStripe's admin interface, let's create this group. In the sidebar menu, click on Security. Then click on the Groups tab in the upper-right corner. Then click the button Add Group. Under the tab Members, let's enter for Group name Reviewers. Then switch to the tab Permissions. We will add the permission "Access to 'Reviews' section". Then click on Create.

Add member to the Reviewers-group

We now have a group with access priveleges to add reviews. Let's make sure that members gets this privelege when they register. We will do this by updating the doRegister method in our RegistrationController:

./app/src/Controller/RegistrationController.php
//...
    public function doRegister($data, Form $form)
    {
        $alias = $data['alias'] ?? null;
        $email = $data['email'] ?? null;
        $password = $data['password'] ?? null;

        $validationResults = $form->validationResult();

        if ($validationResults->isValid()) {
            $member = Member::create();
            $member->FirstName = $alias;
            $member->Email = $email;
            $member->Password = $password;
            $member->write();

            // HERE IS OUR UPDATE 👇
            $member->addToGroupByCode("reviewers");
            $member->write();

            $form->sessionMessage('Registration successful', 'good');
        }

        return $this->redirectBack();
    }
//...
Enter fullscreen mode Exit fullscreen mode

It may look clunky having to write the member twice. We do this so we have a database-ID for the member that can be associated to the group Reviewers.

Models and permissions

Now we need to update our models to allow users with the new group to access them. We will do this by adding the methods canView, canEdit, canCreate and canDelete to our models. In essence, we are ensuring that users that has access to view the Review-section of the CMS will have access to each model.

./app/src/Model/Author.php
<?php

namespace App\Model;

use App\Admin\ReviewAdmin;  // 👈 Remember to include this
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Permission; // 👈 and this

class Author extends DataObject
{
   // ...

    public function canView($member = null) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }

    public function canEdit($member = null) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }

    public function canDelete($member = null) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }

    public function canCreate($member = null, $context = []) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }
}
Enter fullscreen mode Exit fullscreen mode

./app/src/Model/Book.php
<?php

namespace App\Model;

use App\Admin\ReviewAdmin; // 👈 Remember to include this
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Permission; // 👈 and this

class Book extends DataObject
{
    // ...

    public function canView($member = null) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }

    public function canEdit($member = null) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }

    public function canDelete($member = null) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }

    public function canCreate($member = null, $context = []) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }   
}
Enter fullscreen mode Exit fullscreen mode

./app/src/Model/Review.php
<?php

namespace App\Model;

use App\Admin\ReviewAdmin; // 👈 Remember to include this
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission; // 👈 and this
use SilverStripe\Security\Security;

class Review extends DataObject
{
    // ...

    public function canView($member = null) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }

    public function canEdit($member = null) 
    {
        $reviewer = $this->Member()->ID;
        $currentUser = Security::getCurrentUser()->ID;
        if ($reviewer === $currentUser) {
            return true;
        } else {
            return false;
        }
    }

    public function canDelete($member = null) 
    {
        $reviewer = $this->Member()->ID;
        $currentUser = Security::getCurrentUser()->ID;
        if ($reviewer === $currentUser) {
            return true;
        } else {
            return false;
        }
    }

    public function canCreate($member = null, $context = []) 
    {
        return Permission::check('CMS_ACCESS_' . ReviewAdmin::class, 'any', $member);
    }

}
Enter fullscreen mode Exit fullscreen mode

We see that our Review model is somewhat different on its permissions than Author and Book. We let other reviewers see our reviews, but only the author can edit or delete them. Having updated our models we need to update the database. We visit `localhost:8000/dev/build'.

Register and review!

Wrapping it up, we have expanded our Book Review Platform to include a registration system. Users can now sign up and get started with that Dune-review!

Register as a new user and log in to /admin to start writing reviews.

What's next?

Writing reviews is still not very pleasant. When we next revisit this project we will see how we can make it better.

A few things to note before we leave: There's currently no check if a user is already logged in when registering. We could check for this in the RegisterController. Another thing is that we could add a login-link on the navigations page. We just assume that our users will think of visiting the /admin page.

Did you learn something, or have something to add? Feel free to leave a comment below.

Discussion (0)