close icon
Meteor

Developing Real-time Apps with Meteor

Let’s learn how to use Meteor to build a real-time web application and add authentication to it

Last Updated On: June 03, 2021

TL;DR: In this tutorial, I'll show you how easy it is to build a real-time web application with Meteor. Check out the repo to get the code.


Meteor is a full-stack JavaScript platform for developing modern web and mobile applications. Meteor provides a suite of technologies for building connected-client reactive applications, APIs, and a curated set of packages from the Node.js and general JavaScript community. It allows you develop in just one language, JavaScript, in all environments: server, web, and mobile.

Meteor is a project backed by the Meteor Development Group company. They are friends of the open source community. The MDG group also manages Apollo, the flexible production ready GraphQL client for React and Native apps. Meteor as a JavaScript platform has built a community around it over the years. Currently, there is a discussion forum, Stack Overflow channel, and Atmosphere - a repository of community packages. In addition, there is a community-curated list of meteor packages and resources on GitHub known as Awesome Meteor.

There are several websites and applications that run on Meteor. A few of them are Favro - a collaboration app, Reaction commerce - OSS platform for e-commerce sites, Oculus Health and Game Raven - An online gaming community.

Meteor Features

Meteor provides a lot out of the box. It ships with many features that make it worth considering when looking for a framework for your next project.

  • Authentication: Meteor ships with session management and authentication features out of the box.
  • Real-time Feature: Meteor is built from the ground up on the Distributed Data Protocol (DDP) to allow data transfer in both directions. In Meteor, you create publication endpoints that can push data from server to client.
  • Routing: Meteor provides a flow-router package that allows client-side routing.
  • Custom Templating Engines: Meteor ships with its own templating engine but allows you to use other view libraries.
  • Packaging for Mobile: Meteor allows you to easily package your web app into Android and iOS apps. With meteor, you can build for mobile.
  • Galaxy: The Meteor Development Group (MDG) provides a service to run Meteor apps. Galaxy is a distributed system that runs on Amazon AWS. It saves you a lot of trouble in configuring and deploying your app to production.

Meteor Key Requirements

In order to use Meteor, you need to have the following tools installed on your machine.

  • If you are operating on a windows machine, you need to have Chocolatey installed.
  • If you are operating on an OS X or Linux machine, you do not need to have any special tool installed. Your terminal should be able to make a curl request.
  • iOS development requires the latest Xcode.
  • MongoDB: Navigate to the mongodb website and install the MongoDB community server edition. If you are using a Mac, I'll recommend following this instruction. To avoid micromanaging from the terminal, I'll also recommend installing a MongoDB GUI, Robo 3T, formerly known as RoboMongo. You can then run mongod from the terminal to start up the MongoDB service on your machine.

Understanding Key Concepts in Meteor

Meteor uses the Publish-Subscribe pattern. Check out this excellent article on how publications and data loading works in Meteor. In a typical framework architecture, there exists a separation of concern of functionalities; presentation, business, and data access realm.

"Meteor uses the Publish and subscribe model."

Tweet

Tweet This

  • Data Layer: This is the data access layer. The data layer is typically stored in MongoDB.

  • View Layer: In a typical framework, the view simply presents data to the screen. In Meteor, there are template files. These files contains the view logic that accesses the Mongo Schemas. The view logic is typically placed in the client/imports/ui directory.

  • Business Logic Layer: In Meteor, the client and server directories exist. The business logic is typically placed in the client/imports/api directory. However, any sensitive code that you don’t want to be served to the client, such as code containing passwords or authentication mechanisms, should be kept in the server/ directory.

Build a Real-time Web App With Meteor

In this tutorial, we'll build a simple application called Slang Bucket. This app will allow users to add all sorts of slangs with their respective meanings. The Slang Bucket is a mini version of Urban Dictionary. Users will be able to add and delete slangs from the bucket.

Install Meteor and Scaffold Slang Bucket

Linux and Mac users can run the following command in the terminal to install Meteor:

curl https://install.meteor.com/ | sh

Windows users can install Meteor like so:

choco install meteor

The command above will install the latest version of Meteor. At the time of this writing, Meteor's latest version is 1.6.

Next, go ahead and create the Slang Bucket app like so:

meteor create slangbucket

The command above will create a new slangbucket directory with some boilerplate files.

Run the following command to get the app up and running in the browser:

cd slangbucket
meteor

Open the URL, http://localhost:3000, in the web browser to see the app running.

Slang Bucket - Default page Slang Bucket: Default page

Directory Structure

Open up the slangbucket code repository in an editor. These are the files that were created when you ran the command to scaffold the app.

  • client
    • main.js # a JavaScript entry point loaded on the client
    • main.html # an HTML file that defines view templates
    • main.css # a CSS file to define your app's styles
  • server
    • main.js # a JavaScript entry point loaded on the server
  • package.json # a control file for installing NPM packages
  • .meteor # internal Meteor files
  • .gitignore # a control file for git

Select Templating Engine

Meteor ships with a templating engine called Blaze. Blaze renders responses from HTML files and has a very familiar expression language. It uses double braces, {{ }}, and {{> }}.

  • {{> }} - Used to include Meteor templates in HTML files
  • {{ }} - Used to display data and logic from JavaScript files in the view(HTML) files.

Meteor is very configurable. You can use Angular and React with Meteor. If you want to use React as the view library, all you need to do is add react:

meteor npm install --save react react-dom

And configure it like so:

client/main.html

<head>
  <title>Todo List</title>
</head>

<body>
  <div id="render-target"></div>
</body>

client/main.jsx

import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';

import App from '../imports/ui/App.jsx';

Meteor.startup(() => {
  render(<App />, document.getElementById('render-target'));
});

Your UI elements can now be written with JSX.

If you want to use Angular, all you need to do is remove blaze:

meteor remove blaze-html-templates

And replace it with the UI package for Angular:

meteor add angular-templates

Furthermore, install angular and angular-meteor:

meteor npm install --save angular angular-meteor

Go ahead and configure your templates like so:

client/main.html

<head>
  <title>Todo List</title>
</head>

<body>
  <div class="container" ng-app="slang-bucket">
  </div>
</body>

client/main.js

import angular from 'angular';
import angularMeteor from 'angular-meteor';

angular.module('simple-todos', [
  angularMeteor
]);

In this tutorial, we'll use the default Meteor templating engine, Blaze.

Create Slang Bucket Views and Display Static Data

First, add bootstrap by running the command below:

meteor add twbs:bootstrap

Create a new directory, imports in the slangbucket project folder. Inside the imports directory, create a ui folder.

Go ahead and create the following files:

ui/body.html

<body>
  <div class="container">
    <header>
      <h1 class="text-center">Slang Bucket</h1>
    </header>
    <div class="col-sm-12">
      {{#each slangs}}
        {{> slang}}
      {{/each}}
    </div>
  </div>
</body>
<template name="slang">
    <div class="panel panel-primary">
      <div class="panel-heading">
        <h3 class="panel-title"><span class="btn">{{ slang }}</span></h3>
      </div>
      <div class="panel-body">
        <p> {{ definition }} </p>
      </div>
    </div>
</template>

ui/body.js

import { Template } from 'meteor/templating';

import './body.html';

Template.body.helpers({
  slangs: [
    { slang: "Yoruba Demon",
      definition: "Nigerian guy (yoruba) who goes after a young lady's heart with no intention of loving her. They are typically met at parties, and would mostly wear white agbada."
    },
    { slang: "Bye Felicia",
      definition: "The perfect dismissal phrase for waving goodbye to someone or something unimportant."
    },
    { slang: "GOAT",
      definition: "An acronym for praising someone doing well in a certain activity. 'Greatest of all time'."
    },
    { slang: "Low key",
      definition: "Keeping some activity or news under wraps."
    },
  ],
});

Head over to client/main.js. Remove everything there and replace with:

import '../imports/ui/body.js';

Right now, your web app should look like this:

Meteor - Static Data Meteor: Static Data

What's happening in the code above?

The client/main.js loads up the body.js file. In the body.js file, we have a couple of things going on. The body.html file is been imported. You can pass data into templates from JavaScript code using the Template.body.helpers. Here, we defined a slangs helper that returns an array of objects.

In body.html, we invoked the data returned from the slangs helper with the code below:

{{#each slangs}}
  {{> slang}}
{{/each}}

It loops through the array and inserts a slang template for each value. The slang template is shown below:

<template name="slang">
  <div class="panel panel-primary">
    <div class="panel-heading">
      <h3 class="panel-title"><span class="btn">{{ slang }}</span></h3>
    </div>
    <div class="panel-body">
      <p> {{ definition }} </p>
    </div>
  </div>
</template>

Data Storage

Currently, our data is stored in an array. Let's move our data to MongoDB. Meteor uses MongoDB by default. Meteor provides a way of storing and manipulating data from both the client and server side. However, there are ways to ensure that no one can inject data into the database from the browser's dev tools.

Create a new imports/api directory. We'll put our database logic in this directory.

Go ahead and create an imports/api/slangs.js file and add code to it like so:

import { Mongo } from 'meteor/mongo';

export const Slangs = new Mongo.Collection('slangs');

Import the module on the server to enable the creation of the collection and data-sending to the client.

server/main.js

import { Slangs } from '../imports/api/slangs.js';

import { Meteor } from 'meteor/meteor';

Meteor.startup(function () {
    // code to run on server at startup
});

Update body.js to fetch slangs from the Slangs collection rather than a static array.

imports/ui/body.js

import { Template } from 'meteor/templating';

import { Slangs } from '../api/slangs.js';

import './body.html';

Template.body.helpers({
  slangs() {
    return Slangs.find({}, { sort: { createdAt: -1 } });
  },
});

Note: Run the app. Nothing seems to appear again. Yes, nothing shows because our database is currently empty.

Let's add data to the database. We can decide to enter data from the mongo console via the terminal or we can write a script. The former is very tedious.

Go to server/main.js and update code to be like so:

import { Slangs } from '../imports/api/slangs.js';

import { Meteor } from 'meteor/meteor';

Meteor.startup(() => {
  Slangs.insert({slang: "Yoruba Demon", definition: "Nigerian guy (yoruba) who goes after a young lady's heart with no intention of loving her. They are typically met at parties, and would mostly wear white agbada."});
  Slangs.insert({slang: "Bye Felicia", definition: "The perfect dismissal phrase for waving goodbye to someone or something unimportant."});
  Slangs.insert({slang: "GOAT", definition: "An acronym for praising someone doing well in a certain activity. 'Greatest of all time'."});
  Slangs.insert({slang: "Low key", definition: "Keeping some activity or news under wraps."});
});

When the server starts up, it automatically inserts the data defined here into the database. Now, run your app again, you should see the data. Slick!

Note: Once it populates the database once, go ahead and remove the code to avoid duplicate insertion of data.

Add New Slangs

Let's add a form to our app to enable users to add new slangs. Within the body tag, update the code to be like so:

imports/ui/body.html

...
<div class="container">
    <header>
      <h1 class="text-center">Slang Bucket</h1>
    </header>
    <div class="col-sm-12">
      <form class="new-slang">
        <div class="form-group">
          <input type="text" name="slang" class="form-control" placeholder="Add new slangs" required="required" />
        </div>
        <div class="form-group">
          <textarea class="form-control" name="definition" placeholder="Add new slang definitions" required></textarea>
        </div>
         <div class="form-group">
          <input class="btn btn-small btn-info" type="submit" value="Add New Slang" />
        </div>
      </form>
      {{#each slangs}}
        {{> slang}}
      {{/each}}
    </div>
  </div>
...

Add the JavaScript code to listen to the submit event on the form:

imports/ui/body.js

...
Template.body.events({
  'submit .new-slang'(event) {
    // Prevent default browser form submit
    event.preventDefault();

    // Get value from form element
    const target = event.target;
    const slang = target.slang.value;
    const definition = target.definition.value;

    // Insert a task into the collection
    Slangs.insert({
      slang,
      definition,
      createdAt: new Date(), // current time
    });

    // Clear form
    target.slang.value = '';
    target.definition.value = '';
  },
});

In the code above, it listens to the submit event of the form, grabs the values and inserts them into the database. Run your app and try it out. Yes, it works!

Delete Slangs

Let's add functionality to delete existing slangs. We need to move the slang template to its own file. Create two files: imports/ui/slang.html and imports/ui/task.js.

imports/ui/slang.html

<template name="slang">
    <div class="panel panel-primary">
      <div class="panel-heading">
        <h3 class="panel-title"><span class="btn">{{ slang }}</span> <button class="delete btn btn-danger pull-right">&times;</button></h3>
      </div>
      <div class="panel-body">
        <p> {{ definition }} </p>
      </div>
    </div>
</template>

Note: Make sure you remove the slang template that was in the body.html file.

imports/ui/slang.js

import { Template } from 'meteor/templating';

import { Slangs } from '../api/slangs.js';

import './slang.html';

Template.slang.events({
  'click .delete'() {
    Slangs.remove(this._id);
  },
});

In the code above, we imported the slang template, slang.html, and we have a click event that invokes the remove method when a user clicks on a slang's delete button.

this refers to an individual slang object in the collection. _id is the unique field that MongoDB assigns to a document in a collection. With this _id, we can do almost anything: delete, update, and create.

One more thing. Import slang.js into the body.js file:

...
import './slang.js';
...

Run your app, click the delete button on any slang and watch it disappear instantly. It removes it from the UI and deletes it from the database.

Add Authentication

Meteor ships with an authentication system. Go ahead and install the auth packages via the terminal:

meteor add accounts-ui accounts-password

Add the authentication drop-down widget to the body.html file like so:

<body>
  <div class="container">
    <header>
      <h1 class="text-center">Slang Bucket</h1>
    </header>
    <div class="col-sm-12">
      {{> loginButtons}}
    ....

Create an imports/startup/accounts-config.js file and add the code below to it like so:

import { Accounts } from 'meteor/accounts-base';

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

Also import the imports/startup/accounts-config.js file in client/main.js:

import '../imports/startup/accounts-config.js';
import '../imports/ui/body.js';

Right now, we should be able to create an account. However, authentication is useless if we can't restrict access to functionality. Let's make sure only registered users can add new slangs. In addition, we can also reference the username of the user that added a slang.

A quick breakdown. We'll need to add new attributes to our Slang collection.

  • adderID: this will hold the _id of the user that added the slang.
  • username: this will hold the username of the user that added the slang.

Note: There are other efficient ways to handle the authentication schema of this app. However, for the sake of this tutorial, we'll keep things simple.

Open up imports/ui/body.js and modify it like so:

imports/ui/body.js

import { Meteor } from 'meteor/meteor';
...
...
// Insert a task into the collection
Slangs.insert({
  slang,
  definition,
  createdAt: new Date(), // current time
  adderID: Meteor.userId(),
  username: Meteor.user().username,
});
...

Open up imports/ui/body.html and modify it like so:

...
 {{> loginButtons}}
      {{#if currentUser}}
      <form class="new-slang">
        <div class="form-group">
          <input type="text" name="slang" class="form-control" placeholder="Add new slangs" required="required" />
        </div>
        <div class="form-group">
          <textarea class="form-control" name="definition" placeholder="Add new slang definitions" required></textarea>
        </div>
         <div class="form-group">
          <input class="btn btn-small btn-info" type="submit" value="Add New Slang" />
        </div>
      </form>
      {{/if}}
...

In the code above, we added the {{#if currentUser}} block helper. currentUser is a built-in helper that refers to the logged-in user. If the user is logged-in, show the add new slang form, or else hide the form.

Now, run your app.

Meteor - Nonlogged-in user User not logged in

No user is logged in, so no form to add new slangs. Now, create an account.

Meteor - Log In User about to log in

Meteor - Loggedin User User is logged in

Here, the user is logged in, so he or she is able to create a new slang.

One more thing, let's display the username of the logged-in user next to the slang.

Update imports/ui/slang.html to the code below:

<template name="slang">
  <div class="panel panel-primary">
    <div class="panel-heading">
      <h3 class="panel-title"><span class="btn">{{ slang }}</span><button class="delete btn btn-danger pull-right">&times;</button></h3>
      <span>@{{username}}</span>
    </div>
    <div class="panel-body">
      <p> {{ definition }} </p>
    </div>
  </div>
</template>

Meteor - Username of Slang Adder Username displayed next to Slang

Eliminate Client Update

Meteor is robust. They factored in the fact that people usually create quick demos so a user can update the database directly from the client side. However, in a real-world project, you want to be sure that the server validates everything that comes into the app and allows users to complete an action only if they are authorized!

The first step is to remove the insecure package. Meteor ships with this built-in package. This is the package that allows us to edit the database from the client.

meteor remove insecure

Next, we need to add some code to ensure validation happens right before the database methods are executed.

Open up imports/api/slangs.js. Add code to it like so:

import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';

export const Slangs = new Mongo.Collection('slangs');

Meteor.methods({
  'slangs.insert'(slang, definition) {
    check(slang, String);
    check(definition, String);

    // Make sure the user is logged in before inserting a task
    if (! Meteor.userId()) {
      throw new Meteor.Error('not-authorized');
    }

    Slangs.insert({
      slang,
      definition,
      createdAt: new Date(),
      adderID: Meteor.userId(),
      username: Meteor.user().username,
    });
  },
  'slangs.remove'(slangId) {
    check(slangId, String);

    Slangs.remove(slangId);
  },
});

Next, let's update the sections of the app that were executing some database operations.

imports/ui/body.js

...
...
Template.body.events({
  'submit .new-slang'(event) {
    // Prevent default browser form submit
    event.preventDefault();

    // Get value from form element
    const target = event.target;
    const slang = target.slang.value;
    const definition = target.definition.value;

    // Insert a slang into the collection
    Meteor.call('slangs.insert', slang, definition);

    // Clear form
    target.slang.value = '';
    target.definition.value = '';
  },
});

We replaced the slang insert section with Meteor.call('slangs.insert', slang, definition);.

imports/ui/slang.js

...

Template.slang.events({
  'click .delete'() {
    Meteor.call('slangs.remove', this._id);
  },
});

We replaced the slang remove code with Meteor.call('slangs.remove', this._id);.

Meteor.call sends a request to the server to run the method in a secure environment via an AJAX request.

Security Concerns - Filter Data

With an emphasis on security, we need to control which data Meteor sends to the client-side database. Go ahead and remove the autopublish package via the terminal:

meteor remove autopublish

Without the autopublish package, no data will be sent to the client and no access will be granted to the database. Therefore, the app will not show any data on the screen. To combat this scenario, we'll have to explicitly request the data from the server to the client. Meteor uses the Meteor.publish and Meteor.subscribe methods to accomplish this feat.

Open imports/api/slangs.js and add this code to it:

...
...

if (Meteor.isServer) {
  // This code only runs on the server
  Meteor.publish('slangs', function tasksPublication() {
    return Slangs.find();
  });
}
...

The code above adds a publication for all slangs. Next, let's subscribe to this publication.

Open imports/ui/body.js and add this code to it:

...
Template.body.onCreated(function bodyOnCreated() {
  Meteor.subscribe('slangs');
});
...

In the code above, it subscribes to the slangs publication once the body template is created. Now, our app is secure.

Run the app again. Everything should be in full working order!

Extra Functionality - Packages

There are lots of packages available for Meteor on AtmosphereJS. If there is a feature you want to implement, there is a high probability that it has been done by a developer before now and made available as a package. Explore!

Securing Meteor Applications with Auth0

Meteor is a hybrid framework that takes care of your client and server needs. In addition, it's very easy to create server-side APIs. Right now, let's go ahead and secure our Meteor API with JSON Web Tokens.

JSON Web Tokens, commonly known as JWTs, are tokens that are used to authenticate users on applications. This technology has gained popularity over the past few years because it enables backends to accept requests simply by validating the contents of these JWTs. That is, applications that use JWTs no longer have to hold cookies or other session data about their users. This characteristic facilitates scalability while keeping applications secure.

"Applications that use JWTs no longer have to hold cookies or other session data about their users."

Tweet

Tweet This

Whenever the user wants to access a protected route or resource (an endpoint), the user agent must send the JWT, usually in the Authorization header using the Bearer schema, along with the request.

When the API receives a request with a JWT, the first thing it does is to validate the token. This consists of a series of steps, and if any of these fail, the request must be rejected. The following list shows the validation steps needed:

  • Check that the JWT is well-formed
  • Check the signature
  • Validate the standard claims
  • Check the Client permissions (scopes)

We will make use of Auth0 to issue our JSON Web Tokens. With Auth0, we have to write just a few lines of code to get a solid identity management solution, including single sign-on, user management, support for social identity providers (like Facebook, GitHub, Twitter, etc.), enterprise (Active Directory, LDAP, SAML, etc.), and your own database of users.

For starters, if you haven't done so yet, this is a good time to sign up for a free Auth0 account. Having an Auth0 account, the first thing that we must do is to create a new API on the dashboard. An API is an entity that represents an external resource, capable of accepting and responding to protected resource requests made by clients. And we are dealing with an API here, SWAPI (Star Wars API).

Auth0 offers a generous free tier to get started with modern authentication.

Login to your Auth0 management dashboard and create a new API client.

Click on the APIs menu item and then the Create API button. You will need to give your API a name and an identifier. The name can be anything you choose, so make it as descriptive as you want.

The identifier will be used to identify your API, and this field cannot be changed once set. For our example, I'll name the API, Slang API, and for the identifier, I'll set it as https://slangsapi.com. We'll leave the signing algorithm as RS256 and click on the Create API button.

New API to be created Create a New API

Slangs API Creating the Slangs API

Head over to your terminal and install the following node modules:

meteor npm install express express-jwt jwks-rsa --save

Open your server/main.js file. Add this code at the top:

import express from 'express';
import jwt from 'express-jwt';
import { expressJwtSecret } from 'jwks-rsa';

const app = express();
WebApp.connectHandlers.use(app);

In the code above, we imported express, express-jwt, and jwks-rsa.

  • The express-jwt module is an express middleware that validates a JSON Web Token and sets the req.user with the attributes.
  • The jwks-rsa module is a library that helps retrieve RSA public keys from a JSON Web Key Set endpoint.

Then the code just below the imports statements starts up express server and hooks it into the port Meteor uses:

...
const app = express();
WebApp.connectHandlers.use(app);

Next, go ahead and add the following code:

server/main.js

...
const authCheck = jwt({
  secret: expressJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        // YOUR-AUTH0-DOMAIN name e.g https://prosper.auth0.com
        jwksUri: "{YOUR-AUTH0-DOMAIN}/.well-known/jwks.json"
    }),
    // This is the identifier we set when we created the API
    audience: '{YOUR-API-AUDIENCE-ATTRIBUTE}',
    issuer: '{YOUR-AUTH0-DOMAIN}',
    algorithms: ['RS256']
});

app.get('/api/slangs', (req, res) => {
  const slangs = Slangs.find().fetch();
  res.status(200).json({ message: slangs });
});

Note: Replace the YOUR-API-AUDIENCE-ATTRIBUTE and YOUR-AUTH0-DOMAIN placeholders with the API audience and Auth0 domain values from your Auth0 dashboard.

Run your app by going to the /api/slangs route. You should see the set of slangs displayed.

Now, go ahead and modify the route code by adding the authCheck variable as a middleware.

server/main.js

...
app.get('/api/slangs', authCheck, Meteor.bindEnvironment(function(req, res) {
  const slangs = Slangs.find().fetch();
  res.status(200).json({ message: slangs });
});

The authCheck variable does the check to validate the access tokens that are sent as Authorization headers. It validates the audience, issuer and algorithm used to sign the token.

Now, run your app with Postman again.

Invalid token Accessing the endpoint without an access token

Now, let's test it with a valid access token. Head over to the test tab of your newly created API on your Auth0 dashboard.

Grab the Access token from the Test tab

Get the Access token Grab the Access Token

Now use this access token in Postman by sending it as an Authorization header to make a GET request to api/slangs endpoint.

It validates the access token and successfully makes the request.

Conclusion

Well done! You have learned how to build a real-time web app with Meteor, and authenticate it using JWTs. It's a platform that enables you to cut down on development time. Meteor makes it incredibly easy to also flesh out an API like you can do with KeystoneJS and Loopback.

"Meteor is a platform that enables you to cut down on development time."

Tweet

Tweet This

In addition, Auth0 can help secure your API easily. Auth0 provides more than just username-password authentication. It provides features like multifactor auth, breached password detection, anomaly detection, enterprise federation, single sign on (SSO), and more. Sign up today so you can focus on building features unique to your app.

Please, let me know if you have any questions or observations in the comment section. 😊

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon