Moin moin und hallo,

wie angekündigt geht es in diesem Teil der “Diskurs-App”-Reihe um Sicherheitsmechanismen und die Registrierungsmaske.


Ziel

Im letzten Artikel habe ich mich um die Umsetzung der Datenmodellierung in ein valdies Sails JS Datenmodell, für die MongoDB bemüht. Schlussendlich hatten ich eine REST-API, die zwar funktioniert aber für den Rest der Welt offen steht.

Diesen Umstand möchte ich nun beheben.

Sails JS Policies

Für genau diesen Zweck wurden Policies in Sails JS erfunden. Das Konzept als solches ist nicht neu und kann als “Aspekt” in der Verarbeitung jedes HTTP-Request verstanden werden.

Nachdem der Request verarbeitet wurde und bevor er noch an einen dazu gehörigen Controller, über den Router, weitergereicht wird, wird eine Policy angewandt.

Die Policy stellt führt Ihre Geschäftslogik durch und gibt dann das Signal zur Weiterverarbeitung.

Struktur einer Policy

Alle Policies werden in in dem “api/policies” angelegt.

Policies Verzeichnis

Schauen wir uns z.B. die Sicherheits-Mechanik für einen Moderator an “isModerator”:

module.exports = function (req, res, next) {

    if (req.session.user && req.session.user.moderator && req.session.user.moderator.length > 0 && req.session.user.moderator[0].id) {
        return next();
    }


    return res.forbidden('Sie sind kein Moderator und haben entsprechend keine Berechtigung für diese Aktion.');
};

Jede Policy erhält das Request-, Response-Object als auch den Callback, zum Aufruf der folgenden Logik, übergeben.

Zum dem Inhalt selbst gibt es wenig mehr zu sagen als das was dort schon steht.

Ich üebrprüfe und der Benutzer in seiner Sitzung schon angemeldet und ein Moderator ist.

Falls ja, dann wird die Anfrage weitergeleitet (“next()”).

Falls nicht, gitb es eine entsprechende Fehlermeldung “res.forbidden”.

Zu der Fehlermeldung gibt es noch zu sagen, dass dieser selbst feststellt ob der Request eine HTML oder JSON Antwort erwartet und entsprechend reagiert.


Integration einer Policy

Wie integriert man nun diese Policies in unserem Programmfluss?

Dafür gibt es unter “config/policies.js” eine Konfigurationsdatei, die ich derzeit wie folgt gestaltet habe:

module.exports.policies = {


    '*': true,

    ChannelController: {
        '*': ['isModerator']
    },

    ModeratorController: {
        '*': ['isModerator']
    },

    CommentatorController: {
        '*': ['isCommentator']
    },

    BlockController: {
        '*': ['isModerator']
    },

    CommentController: {
        '*': ['isModeratorOrCommentator', 'isNotBlocked']
    },

    AppController: {
        '*': ['isModerator']
    },

    UserController: {
        '*': ['isModeratorOrCommentator']
    }
};

Grundsätzlich erlaube ich also die Verarbeitung aller Request ‘:true* Nur in speziellen Controllern wird eine oder mehrere (* ‘‘: [‘isModeratorOrCommentator’, ‘isNotBlocked’]) Policies angewandt.

Die Wildcard “*” lässt sich natürlich auch durch entsprechende Funktionasnamen ersetzen und die Seicherheitsmechanik entsrechend noch feiner einstellen.

Frontend

Nachdem wir das Backend rudimentär geischert haben, können wir uns ein bisschen dem Frontend, sprich Registrierung von Moderatoren und Login, widmen.

Zunächst installieren wir mal mit Bower alle notwendigen Resourcen in das /assets Verzeichnis:

Assets

In dem Screenshot oben sind auch schon die “Auth-Apps” enthalten.

Registrierung

Für die Registrierung brauchen wir eigentlich nur eine DAO und bisschen Form-Validierungslogik:

angular.module('RegisterApp', ['ngResource', 'util'], function () {})

.controller('RegisterController', function ($scope, $resource, $log, MessageHelper) {

    var UserDao = $resource('/auth/:action', {}, {

        createModerator: {
            method: 'POST',
            params: {
                action: 'createModerator'
            }
        },

        createCommentator: {
            method: 'POST',
            params: {
                action: 'createCommentator'
            }
        }
    });


    function isUserDataValid() {
        return $scope.registerForm.$valid;
    }

    $scope.isPasswordMatch = function () {
        return $scope.user.password == $scope.passwordConfirm;
    }

    $scope.isFormValid = function () {
        return isUserDataValid() && $scope.isPasswordMatch();
    }

    $scope.registerModerator = function () {

        if ($scope.isFormValid()) {

            UserDao.createModerator(
                $scope.user,
                function success(response) {
                    toastr.success(response.message);

                    window.location.href = "/auth/login";
                },

                MessageHelper.handleResponseFail
            );
        } else {
            toastr.error('Ihre Daten sind ungültig. Bitte überprüfen Sie Ihre Eingaben und versuchen Sie es erneut.');
        }
    }


    function setup() {
        $scope.user = {};
    }

    setup();
});

Passend dazu wird eine Jade-View benötigt:
extends ../layout.pug


block body
    div.register-container.container(ng-app="RegisterApp", ng-controller="RegisterController")
        .panel.panel-primary
            .panel-heading 
                .panel-title  
                    b Registrierung

            .panel-body
                form(name="registerForm", novalidate)
                    fieldset
                        legend Benutzerdaten
                        .form-group(ng-class="{ 'has-error' : registerForm.name.$invalid && !registerForm.name.$pristine }")
                            label Name*
                            input.form-control(type="text", name="name", ng-model="user.name", required="true")
                            p(ng-show="registerForm.name.$invalid && !registerForm.name.$pristine", class="help-block") Bitte tragen Sie Ihren Namen ein.

                        .form-group(ng-class="{ 'has-error' : registerForm.email.$invalid && !registerForm.email.$pristine }")
                            label E-Mail*
                            input.form-control(type="email", name="email", ng-model="user.email", required="true")
                            p(ng-show="registerForm.email.$invalid && !registerForm.email.$pristine", class="help-block") Bitte geben Sie Ihre E-Mail Adresse ein.

                        .form-group(ng-class="{ 'has-error' : registerForm.password.$invalid && !registerForm.password.$pristine }")
                            label Passwort*
                            input.form-control(type="password", name="password", ng-model="user.password", required="true")
                            p(ng-show="registerForm.password.$invalid && !registerForm.password.$pristine", class="help-block") Bitte tragen Sie Ihr Passwort ein.

                        .form-group(ng-class="{ 'has-error' : registerForm.passwordConfirm.$invalid && !registerForm.passwordConfirm.$pristine || !isPasswordMatch()}")
                            label Pasworteingabe wiederholen*
                            input.form-control(type="password", name="passwordConfirm", ng-model="passwordConfirm", required="true")
                            p(ng-show="registerForm.passwordConfirm.$invalid && !registerForm.passwordConfirm.$pristine || !isPasswordMatch()", class="help-block") Ihr Passwort sowie die Wiederholen müssen übereinstimmen.

                    fieldset
                        legend Anschrift    
                        .form-group(ng-class="{ 'has-error' : registerForm.street.$invalid && !registerForm.street.$pristine }")
                            label Straße + Hausnummer*
                            input.form-control(type="text", name="street", ng-model="user.street", required="true")
                            p(ng-show="registerForm.street.$invalid && !registerForm.street.$pristine", class="help-block") Bitte geben Sie Ihre Straße und Hausnummer ein.

                        .form-group(ng-class="{ 'has-error' : registerForm.zip.$invalid && !registerForm.zip.$pristine }")
                            label Plz*
                            input.form-control(type="text", name="zip", ng-model="user.zip", required="true")
                            p(ng-show="registerForm.zip.$invalid && !registerForm.zip.$pristine", class="help-block") Bitte geben Sie Ihre Postleitzahl ein.

                        .form-group(ng-class="{ 'has-error' : registerForm.city.$invalid && !registerForm.city.$pristine }")
                            label Ort*
                            input.form-control(type="text", name="zip", ng-model="user.city", required="true")
                            p(ng-show="registerForm.city.$invalid && !registerForm.city.$pristine", class="help-block") Bitte geben Sie Ihren Wohnort ein.



            .panel-footer
                .row
                    .col-md-6.col-md-offset-3
                        button.btn.btn-success.btn-block(ng-click="registerModerator()", type="button", ng-disabled="!isFormValid()") 
                            i.glyphicon.glyphicon-check
                            |  Jetzt registrieren


block scripts

    script(src="/auth-apps/register-app.js")
    script(src="/util/messageHelper.js")        


Die Layout Datei sieht dazu wie folgt aus:

doctype html
html
  head
    title= title

    link(rel="apple-touch-icon", sizes="57x57", href="/apple-icon-57x57.png")
    link(rel="apple-touch-icon", sizes="60x60", href="/apple-icon-60x60.png")
    link(rel="apple-touch-icon", sizes="72x72", href="/apple-icon-72x72.png")
    link(rel="apple-touch-icon", sizes="76x76", href="/apple-icon-76x76.png")
    link(rel="apple-touch-icon", sizes="114x114", href="/apple-icon-114x114.png")
    link(rel="apple-touch-icon", sizes="120x120", href="/apple-icon-120x120.png")
    link(rel="apple-touch-icon", sizes="144x144", href="/apple-icon-144x144.png")
    link(rel="apple-touch-icon", sizes="152x152", href="/apple-icon-152x152.png")
    link(rel="apple-touch-icon", sizes="180x180", href="/apple-icon-180x180.png")

    link(rel="icon", type="image/png", sizes="192x192", href="/android-icon-192x192.png")
    link(rel="icon", type="image/png", sizes="32x32", href="/android-icon-32x32.png")
    link(rel="icon", type="image/png", sizes="96x96", href="/android-icon-96x96.png")
    link(rel="icon", type="image/png", sizes="16x16", href="/android-icon-16x16.png")

    link(rel="manifest", href="/manifest.json")

    meta(name="apple-mobile-web-app-capable" content="yes")
    meta(name="mobile-web-app-capable" content="yes")
    meta(name="msapplication-TileColor", content="#ffffff")
    meta(name="msapplication-TileImage", content="/ms-icon-144x144.png")
    meta(name="theme-color", content="#ffffff")
    meta(name="description", property="og:description", content="Verwalte deinen Autopass und informiere dich vor dem Autokauf über den tatsächlichen Zustand eines Autos.")
    meta(property="og:image", content="/images/banner.png")

    meta(name="viewport",content="width=device-width, initial-scale=1, maximum-scale=1")

    link(type='text/css', href='/bower_components/bootstrap/dist/css/paper.min.css', rel='stylesheet')
    link(type='text/css', href='/bower_components/font-awesome/css/font-awesome.min.css', rel='stylesheet')

    link(type='text/css', href='/bower_components/toastr/toastr.min.css', rel='stylesheet')

    link(type='text/css', href='/styles/importer.css', rel='stylesheet')
    link(type='text/css', href='/styles/animate.css', rel='stylesheet')

    meta(http-equiv="cache-control", content="public")

    block styles

  body
    block body


    script(src="/bower_components/jquery/dist/jquery.min.js")
    script(src="/bower_components/underscore/underscore-min.js")

    script(src="/bower_components/bootstrap/dist/js/bootstrap.js")

    script(src="/bower_components/angular/angular.min.js")
    script(src="/bower_components/angular-resource/angular-resource.min.js")

    script(src="/bower_components/toastr/toastr.min.js")

    block scripts

Wir man es schon erahnen kann geht die Registrierung-Anfrage an den AuthController/createModerator. Dieser Controller regelt für mich alle Angelegenheiten, die mit der Authentifizierung (Register, Login, Logout, usw.) zusammenhängen.

Die Registrierung selbst ist relativ “straight forward”, mit Hilfe von Promises implementiert.

createModerator: function (req, res) {
        var handleSuccess = function (user) {

           return res.json({
                message: req.__('msg.user.registration.success'),
                user: user
            });
        };

        var handleFail = function (err) {
            sails.log.error(err);
            res.badRequest({
                message: req.__('msg.user.registration.fail')
            });
        }

        var createUser = function () {
            return User.create(req.body);
        }

        var createModerator = function (user) {

            return new Promise(function (resolve, reject) {

                var moderatorVo = Moderator.createVoFromSource(req.body)
                moderatorVo.user = user

                return Moderator.create(moderatorVo)

                .then(function (mod) {
                    resolve(user)
                })
            })
        }

        UserService.isUserValid(req.body)
            .then(createUser)
            .then(createModerator)
            .then(handleSuccess)
            .catch(handleFail);
    }

Eine Besonderheit gibt es aber dann doch noch, nämlich der Aufruf req.__(‘’).

Dieser zieht sich die Übersetzungen aus der passenden i18N-Datei und gibt diese zurück.

Damit kann man je nach Spracheinstellung des Benutzers entsprechende Ausgaben tätigen.

In alle Details möchte ich jetzt nicht eingehen, weil diese keine größeren Überraschungen mehr parat halten.

Zum Beweis möchte ich hier mal die Login-Funktion zeigen:

doLogin: function (req, res) {

        var handleSuccess = function (user) {
            sails.log(user)
            req.session.user = user;
            return res.json({
                success: true,
                message: req.__('msg.user.login.success'),
                rootUrl: "/app"
            });
        }

        var handleFail = function (err) {
            sails.log.error(req.__(err.message));
            delete req.session.user;
            res.badRequest({
                message: req.__(err.message)
            });
        }

        UserService.isLoginValid(req.param('email'), req.param('password'))
            .then(handleSuccess)
            .catch(handleFail);
    }

### Fazit & Ausblick

Den Aufbau aus dem Registrierungs-Mechanismus kann man konsequent auf fast alle notwendingen Funktionen anwenden.

Das finale Ergebnis sieht dann ungefähr wie folgt aus:

Registrierung-Formular

Als nächstes Ziel habe ich mir das Dashboard des Moderators und CRUD für Kanäle gesetzt.

Dazu were ich eine klassiche Master-Detail-Ansicht implementieren und zeigen wie einfach das mit Angular JS umsetzbar ist.


#### In diesem Sinne, bis demnächst!