Adding registration to SilverStripe and controlling priveleges
Tags: php, silverstripe
Published: 2021-11-25 13:54:06

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.

{% details ./app/src/Controller/RegisterController.php %}
```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
    }
}

```
{% enddetails %}

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:

{% details ./app/src/Form/ValidatedAliasField.php %}
```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;
    }
}
```
{% enddetails %}

    
{% details ./app/src/Form/ValidatedEmailField.php %}
```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;
    }
}

```
{% enddetails %}

{% details ./app/src/Form/ValidatedPasswordField.php %}
```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;
    }
}
```
{% enddetails %}

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. 

{% details We update ./app/src/Controller/RegistrationController.php %}

```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();
    }
}
```
{% enddetails %}

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2a94ds5oq9wfq5h9ycj9.png)

> 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](https://docs.silverstripe.org/en/4/developer_guides/security/secure_coding/#passwords).

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**
{% details ./app/src/Page/RegisterPage.php %}
```php
<?php

namespace App\Page;

use App\Controller\RegistrationController;
use Page;

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

```
{% enddetails %}
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**
{% details ./themes/simple/templates/App/Page/Layout/RegistrationPage.ss %}
```twig
<% include SideBar %>
<div class="content-container unit size3of4 lastUnit">
	<article>
		<h1>$Title</h1>
		<div class="content">$Content</div>
	</article>
    $registerForm
</div>
```
{% enddetails %}

> 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`:  
    
{% details ./app/src/Controller/RegistrationController.php %} 

```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();
    }
//...
```
{% enddetails %}

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


{% details ./app/src/Model/Author.php %}
```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);
    }
}
```
{% enddetails %}

{% details ./app/src/Model/Book.php %}
```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);
    }   
}
```
{% enddetails %}

{% details ./app/src/Model/Review.php %}
```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);
    }

}
```
{% enddetails %}

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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ue4cgv2qqftfsqm639l6.gif)


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