Meteor filter collecties

In de vorige tutorial hebben we collecties beveiligd voor ongewenste insert/update of remove statements door middel van

Meteor.allow()

. In deze tutorial ga ik de chatapplicatie uit vorige tutorial uitbreiden door zowel private als public berichten te voorzien waarbij private berichten enkel zichtbaar zijn voor de gebruiker zelf. Dit wilt dus zeggen dat de Collection

find()

functie bepaalde objecten niet mag tonen afhankelijk van welke gebruiker ze ophaalt.

Heads up!

Deze tutorial gaat verder waar we bij de vorige tutorial geëindigd waren.

Project opzetten

Dit project gaat verder op de code die we in de tutorial rond Meteor security gezien hebben, het is daarom aangeraden om die code te downloaden en hieruit te vertrekken. Eenmaal klaar moeten we enkel nog ervoor zorgen dat de module autopublish uitstaat. Hiervoor voeren we volgend commando uit in het project:

meteor remove autopublish

De autopublish module zorgt ervoor dat automatisch de volledige collectie gepublished wordt vanuit de server naar de client. Als we dit weghalen kunnen we zelf kiezen wat gepublished wordt en wat niet.

Verbetering vorig project

Tijdens de vorige tutorial heb ik de

Usernames

als een aparte tabel aangemaakt, maar bij verder uit te zoeken hoe alles werkt kwam ik erachter dat je ook kan opteren om te registreren met een username (of een username en e-mail). Om dit aan te zetten gebruik je de volgende code:

Accounts.ui.config({
    passwordSignupFields: 'USERNAME_ONLY'
});

Hiermee kunnen we dan ook alle code die te maken had met usernames gewoon verwijderen. De code hierdoor wordt:

/** Model */
Messages = new Meteor.Collection("messages");
Messages.allow({
    'insert': function(userId, message) {
        return userId && message.ownerId && message.ownerId == userId;
    },

    'update': function(userId, messages, fields, modifier) {
        return false;
    },

    'remove': function(userId, message) {
        return userId && message.ownerId && message.ownerId == userId;
    }
});

/** Client */
if (Meteor.isClient) {
    Accounts.ui.config({
        passwordSignupFields: 'USERNAME_ONLY'
    });

    Template.messages.messages = function() {
        return Messages.find({}, {
            sort: {
                timestamp: 1
            }
        });
    };

    Template.message.isOwner = function(ownerId) {
        return (ownerId == Meteor.userId());
    };
    Template.message.events = {
        'click .close': function(evt) {
            Messages.remove({
                _id: evt.target.getAttribute('data-id')
            });
            evt.preventDefault();
        }
    };

    Template.new_message.isLoggedIn = function() {
        return Meteor.userId();
    };
    Template.new_message.events = {
        'click button.btn-primary, submit' : function(evt) {
            var messageNode = document.getElementById('message');
            Messages.insert({
                message: messageNode.value,
                timestamp: new Date(),
                ownerId: Meteor.userId(),
                name: Meteor.user().username
            });
            messageNode.value = "";
            evt.preventDefault();
        }
    };
}

if (Meteor.isServer) {

}

Ook uit de templates mogen we nu vanalles verwijderen totdat we onderstaande code over houden.

<head>
    <title>Chat applicatie</title>
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet" />
    <script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
    <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
</head>

<body>
    <div class="container">
        <div class="header row">
            <div class="span7">
                <h3>Chat applicatie</h3>
            </div>
            <div class="span5">
                <div class="pull-right">
                    {{loginButtons align="right"}}
                </div>
            </div>
        </div>
        {{> messages}}
        {{> new_message}}
    </div>
</body>

<template name="messages">
    {{#each messages}}
        {{> message}}
    {{/each}}
</template>

<template name="message">
    <div class="row">
        <p>
            <strong class="span2">{{name}}:</strong> <span class="span9">{{message}}</span>
            {{#if isOwner ownerId}}
                <a class="close pull-right" href="#" data-id="{{_id}}">&times;</a>
            {{/if}}
        </p>
    </div>
</template>

<template name="new_message">
    <hr />
    <form>
        <div class="controls controls-row">
            {{#if isLoggedIn}}
                <input type="text" class="span9" id="message" placeholder="Bericht" />
                <button class="btn btn-primary span3"><i class="icon-envelope icon-white"></i> Chat!</button>
            {{else}}
                <input type="text" class="span9 disabled" id="message" disabled placeholder="Bericht" />
                <button class="btn btn-primary span3 disabled" disabled><i class="icon-envelope icon-white"></i> Chat!</button>
            {{/if}}
            <input type="submit" class="hide" />
        </div>
    </form>
</template>

Filteren van collecties

Om collecties te kunnen filteren moet je eerst begrijpen wat de collecties juist zijn en wat er gebeurt als jij de MongoDB API vanuit de client aanspreekt. Meteor maakt overal achter de schermen gebruik van het publish en subscribe pattern waarbij de server data pusht naar de client. Via de autopublish module wordt automatisch elke collectie overgepompt naar de client zodat de data hiervan beschikbaar is.

We kunnen zelf echter ook in de server de publish-actie gaan customizen. Dit doen we door bij de server-code het volgende te voorzien:

Meteor.publish("messages", function() {
    ...
});

Als deze code uitsluitend op de server uitgevoerd moet worden, dan moet je deze in de if onderaan zetten.

if (Meteor.isServer) {
    ...
}

Voor de berichten gaan we nu een extra veld voorzien met de naam

private

. Indien deze waarde op

true

komt te staan dan mag dat bericht enkel getoond worden voor de persoon die het bericht geschreven heeft.

Als eerste stap gaan we de template new_message aanpassen zodat we hier een checkbox hebben die bepaalt of het bericht privé is of niet. De code voor deze template wordt:

<template name="new_message">
    <hr />
    <form>
        <div class="controls controls-row">
            {{#if isLoggedIn}}
                <input type="text" class="span9" id="message" placeholder="Bericht" />
                <button class="btn btn-primary span3"><i class="icon-envelope icon-white"></i> Chat!</button>
            {{else}}
                <input type="text" class="span9 disabled" id="message" disabled placeholder="Bericht" />
                <button class="btn btn-primary span3 disabled" disabled><i class="icon-envelope icon-white"></i> Chat!</button>
            {{/if}}
        </div>
        <div class="controls controls-row">
            <input type="submit" class="hide" />
            {{#if isLoggedIn}}
                <label class="checkbox">
                    <input type="checkbox" id="private" />
                    Privé
                </label>
            {{else}}
                <label class="checkbox">
                    <input type="checkbox" id="private" disabled />
                    Privé
                </label>
            {{/if}}
        </div>
    </form>
</template>

De volgende stap is dat we de custom publish/subscribe in gang zetten. Hiervoor plaatsen we de volgende code onderaan in het script (in de

if (meteor.isServer)

).

Meteor.publish("messages", function() {
    return Messages.find({
        $or: [
            { private: false },
            { ownerId: this.userId }
        ]
    }, {
        sort: {
            timestamp: 1
        }
    });
});

Hiermee zorgen we ervoor dat het bericht publiek moet zijn of door de persoon in kwestie geplaatst moet zijn.

Ook de event handler voor het plaatsen van een bericht moet gewijzigd worden zodat er rekening gehouden wordt met de checkbox die aan- of uitgevinkt kan zijn. Dit doen we dmv volgende code:

Template.new_message.events = {
    'click button.btn-primary, submit' : function(evt) {
        var messageNode = document.getElementById('message');
        var privateNode = document.getElementById('private');
        Messages.insert({
            message: messageNode.value,
            timestamp: new Date(),
            ownerId: Meteor.userId(),
            name: Meteor.user().username,
            private: privateNode.checked
        });
        messageNode.value = "";
        evt.preventDefault();
    }
};

Bovenaan in de client moeten we er uiteraard ook voor zorgen dat we subscriben op de gefilterde berichten. Dit doen we door middel van de volgende lijn code:

Meteor.subscribe("messages");

Uiteindelijk ziet de code er zo uit:

/** Model */
Messages = new Meteor.Collection("messages");
Messages.allow({
    'insert': function(userId, message) {
        return userId && message.ownerId && message.ownerId == userId;
    },

    'update': function(userId, messages, fields, modifier) {
        return false;
    },

    'remove': function(userId, message) {
        return userId && message.ownerId && message.ownerId == userId;
    }
});

/** Client */
if (Meteor.isClient) {
    Meteor.subscribe("messages");
    Accounts.ui.config({
        passwordSignupFields: 'USERNAME_ONLY'
    });

    Template.messages.messages = function() {
        return Messages.find({}, {
            sort: {
                timestamp: 1
            }
        });
    };

    Template.message.isOwner = function(ownerId) {
        return (ownerId == Meteor.userId());
    };
    Template.message.events = {
        'click .close': function(evt) {
            Messages.remove({
                _id: evt.target.getAttribute('data-id')
            });
            evt.preventDefault();
        }
    };

    Template.new_message.isLoggedIn = function() {
        return Meteor.userId();
    };
    Template.new_message.events = {
        'click button.btn-primary, submit' : function(evt) {
            var messageNode = document.getElementById('message');
            var privateNode = document.getElementById('private');
            Messages.insert({
                message: messageNode.value,
                timestamp: new Date(),
                ownerId: Meteor.userId(),
                name: Meteor.user().username,
                private: privateNode.checked
            });
            messageNode.value = "";
            evt.preventDefault();
        }
    };
}

if (Meteor.isServer) {
    Meteor.publish("messages", function() {
        return Messages.find({
            $or: [
                { private: false },
                { ownerId: this.userId }
            ]
        }, {
            sort: {
                timestamp: 1
            }
        });
    });
}

Testen

Met deze nieuwe aanpassingen kunnen we nu aan de slag gaan en controleren ofdat alles werkt zoals het moet werken. Als je je nu registreert zal je niet meer om je e-mail gevraagd worden maar om je username. Deze username gebruiken we dan ook voor de berichten die geplaatst worden.

accounts-ui-config

Je kan nog steeds berichten plaatsen, maar er word nu niet meer gevraagd naar een gebruikersnaam omdat we deze opgehaald hebben via de accounts-ui module.

post-private

Als je je uitlogt, zullen de privé berichten niet meer verschijnen. Nu kan je dus op basis van zelf geschreven filters de toegang tot een collectie regelen. In de vorige tutorial hebben we autorisatie voorzien voor het invoegen/updaten/verwijderen van objecten en in deze tutorial hebben we er ook voor gezorgd dat het ophalen van objecten gewijzigd kan worden.

private-hidden

Het mooie is dat dankzij de transparante client API het wel lijkt alsof je data in de client verwerkt, het feitelijk toch via de server gaat langs een REST API. Het is dus niet zo dat dit alles te omzeilen valt omdat de authenticatie/autorisatie wel degelijk op de server gebeurt.

Download project

Download – meteor-chat-app-filter.tar.gz (2 kB)

g00glen00b

Consultant at Cronos and Tech lead at Aquafin. Usually you can find me trying out new libraries and technologies. Loves both Java and JavaScript.