This post originally appeared on strongloop.com. StrongLoop provides tools and services for companies developing APIs in Node. Learn more...
I have a confession: I hate native mobile development. Most of the reason why I started working with NodeJS was my struggles with managing mobile app development for a small startup in 2012. Back then, native mobile development struggled with painfully slow feedback - to see the results of your work in action, you need to start a server, re-compile, wait for the app to load, and click around in a simulator.
Code sharing was also a lost cause. In my experience, it's hard to get code right once. When your server is in Ruby, your iOS app is in Objective-C, your Android app is in Java, and your web client is in JavaScript, you end up maintaining 4 separate implementations of the same code in 4 different languages. Even worse, each implementation is bound to have its own bugs, quirks, and performance issues.
Now, with StrongLoop LoopBack and the Ionic framework, you can quickly build out a server and a corresponding mobile app using only JavaScript. LoopBack enables you to quickly build up a REST API from your command line. Ionic enables you to build "hybrid" mobile apps, that is, mobile apps that work by launching a browser and executing JavaScript.
In this series of articles, you'll see how you can build a server using LoopBack. Then you'll use LoopBack's AngularJS SDK to create AngularJS components which you will use to create an Ionic framework hybrid mobile app. There will be 4 articles in this series:
- Building a LoopBack REST API
- Building AngularJS Directives with the LoopBack AngularJS SDK
- Using AngularJS Directives in Ionic Framework Mobile Apps
- Testing Your Mobile App in Travis
You'll be building a simple stopwatch application. Users of the app will be able to log in with Facebook, track times, and save their times to the server. Let's get started!
Getting Started with LoopBack
StrongLoop LoopBack is a tool for rapidly generating NodeJS REST API's. LoopBack also has a powerful AngularJS SDK that enables you to generate AngularJS clients for your REST API's. Since the Ionic framework is based on AngularJS, the LoopBack AngularJS SDK makes building a mobile app even easier. You'll learn more about the AngularJS SDK in the next article. In this article, you'll use LoopBack to create a REST API with Facebook login. You can find the complete stopwatch server on GitHub
The first step is to install the strongloop
npm module using
npm install strongloop -g
. The strongloop
module gives you the slc
executable. Run slc loopback
to start a new application - you should see
the below output in your terminal. Call your application
'stopwatch-server-example'.
$ ./node_modules/strongloop/bin/slc loopback
_-----_
| | .--------------------------.
|--(o)--| | Let's create a LoopBack |
`---------´ | application! |
( _´U`_ ) '--------------------------'
/___A___\
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What's the name of your application? stopwatch-server-example
LoopBack will create a directory called stopwatch-server-example
. The
stopwatch-server-example directory's code will look like this once LoopBack is done.
Setting Up Facebook Login
TLDR; See a diff for this step on GitHub
Adding Facebook login to your LoopBack server is easy. LoopBack is great for reducing the amount of wiring code you need to write; most of the code you'll write in this section will be either on the command line or in JSON configurations rather than actual JavaScript. There's nothing stopping you from writing a plain-old Express REST API on top of LoopBack's scaffolding. However, effective LoopBack developers rely on the command line interface to do most of the work.
First, you're going to need to add 2 npm modules to the
'stopwatch-server-example' directory's package.json
file:
npm install loopback-component-passport --save
npm install passport-facebook --save
Once you have these 2 modules, you're going to write a little code to wire them
together in your server. First, open up your server/model-config.json
file.
This is where LoopBack stores high-level metadata about your data models. In
order to integrate Facebook login with LoopBack, you're going to need to pull
in the loopback-component-passport
module's
UserIdentity
and UserCredential
models. You won't have to interact with these models directly, but LoopBack
needs to be aware of them.
To add these models, first you should add
"./node_modules/loopback-component-passport/lib/models"
to the sources
list. This tells LoopBack where to find these models. You also need to add
'UserCredential' and 'UserIdentity' to the top-level object:
"UserCredential": {
"dataSource": "db",
"public": true
},
"UserIdentity": {
"dataSource": "db",
"public": true
}
Once that's done, you're going to have to configure your server to allow
Facebook login. You should
add a boot script to LoopBack's server/boot
directory that configures LoopBack's PassportConfigurator
.
Boot scripts
are a powerful mechanism for configuring your LoopBack server. A boot script
enables you to add operations to your server startup process without adding
extra bloat to your server/server.js
file.
// Make sure to also put this in `server/server.js`
var PassportConfigurator =
require('loopback-component-passport').PassportConfigurator;
// Include this in your 'facebook-oauth.js' boot script in `server/boot`.
var passportConfigurator = new PassportConfigurator(app);
passportConfigurator.init();
passportConfigurator.setupModels({
userModel: app.models.User,
userIdentityModel: app.models.UserIdentity,
userCredentialModel: app.models.UserCredential
});
passportConfigurator.configureProvider('facebook-login',
require('../providers.json')['facebook-login']);
Notice the providers.json
file that require()
loads above. That file
contains JSON configuration parameters for Facebook login, including your
client id and secret. In the interest of avoiding Facebook's inane labyrinth
of OAuth configurations, a functioning providers.json
is included in the
above commit and shown below.
{
"facebook-login": {
"provider": "facebook",
"module": "passport-facebook",
"clientID": "919651901409502",
"clientSecret": "5ca481b467aa7f80c24702f093e64417",
"callbackURL": "http://localhost:3000/auth/facebook/callback",
"authPath": "/auth/facebook",
"callbackPath": "/auth/facebook/callback",
"successRedirect": "/api/Users/me",
"scope": ["public_profile"]
}
}
That's all you need to set up Facebook login! Now, if you start the server with
node .
and go to http://localhost:3000/auth/facebook
, LoopBack will redirect
users back to the /api/Users/me
route. Notice, however, that this route is
currently not implemented. While you can log in, there's no way to get data
about the currently logged in user. That's what you'll implement in the next
section.
Getting the Currently Logged In User
TLDR; See a diff for this step on GitHub
The ability to send the user to Facebook to log in is just the first step - logging in isn't very useful if you can't track the currently logged in user. To actually track the currently logged in user, you're going to have to add a session store and a cookie parser.
npm install express-session --save
npm install cookie-parser --save
Once you have installed the session middleware and cookie parser middleware, you'll need to plug them into your REST API. Specifically, you'll need to plug in 3 middleware modules:
- A middleware that parses cookies.
- A middleware that uses cookies to track the currently logged in user via sessions.
- A middleware that converts the
/me
part of the/api/Users/me
into a reference to the currently logged in user.
To add these middleware, add the below code to your server/middleware.json
file.
"session:before": {
"cookie-parser": { "params": "test secret" }
},
"session": {
"express-session": {
"params": {
"secret": "other test secret",
"saveUninitialized": true,
"resave": true
}
}
}
You will also need to add the below code to your server/boot/authentication.js
boot script.
server.middleware('auth', loopback.token({
model: server.models.accessToken,
currentUserLiteral: 'me'
}));
The first two middleware modules are standard boilerplate that you may have
seen if you've used Express before. In
fact, what you're doing with the middleware.json
file is wiring up the the
cookie-parser
and express-session
npm packages, respectively, to your
LoopBack server.
Once you set up the first two middleware modules and log in using Facebook, you
should be able to visit http://localhost:3000/api/Users/1
and see what
information LoopBack has stored about you. For instance, below is the output
when I log in.
{"username":"facebook-login.383798881820166","email":"383798881820166@loopback.facebook.com","id":1}
However, getting this data depends on you knowing what your user ID is. You'll
notice that your Facebook login is configured to redirect the user to http://localhost:3000/api/Users/me
when they've successfully logged in. The
third middleware, the 'auth' middleware, transforms /me
into the ID for the
currently logged in user. This makes it easier to set up an Ionic framework
app on top of your REST API. Since you can leverage browser cookies in your
hybrid mobile app, your app code doesn't need to track any sort of
authentication token. LoopBack enables you to specify the token in your
Authorization
header, but, for the purposes of this tutorial, you'll use
cookies in order to minimize the amount of code you need to write.
Setting Up a Times Model
TLDR; See a diff for this step on GitHub
Once you have authentication set up, now you need to create a new model that will store the user's recorded times. The times will contain the recorded time in milliseconds and an optional tag. For instance, if your user is using the app to track their 200 meter and 400 meter sprint times, they could tag once set of times with "200m" and the other with "400m" to see their progress in each sprint over time.
To create a new model, use the slc loopback:model
command. Let's call this
model 'Time'. You can use any of the databases LoopBack supports, but, in the
interest of keeping this tutorial lightweight, you're just going to use the
in-memory database.
$ slc loopback:model
? Enter the model name: Time
? Select the data-source to attach Time to: db (memory)
? Select model's base class: PersistedModel
? Expose Time via the REST API? Yes
? Custom plural form (used to build REST URL): Times
Let's add some Time properties now.
Enter an empty property name when done.
? Property name: time
invoke loopback:property
? Property type: number
? Required? Yes
Let's add another Time property.
Enter an empty property name when done.
? Property name: tag
invoke loopback:property
? Property type: string
? Required? No
Let's add another Time property.
Enter an empty property name when done.
? Property name:
Once you have run this command and filled out the prompts as shown above,
you should now have a common/models/time.json
file that looks like what
you see below.
{
"name": "Time",
"plural": "Times",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"time": {
"type": "number",
"required": true
},
"tag": {
"type": "string"
}
},
"validations": [],
"relations": {},
"acls": [],
"methods": []
}
You now have a 'Time' model with 2 properties: a required time
number, and
an optional tag
string. However, this model doesn't take into account one
important detail: the relationship between users and times. Specifically,
you have a one-to-many relationship between users and times. LoopBack has a
neat
command line interface for creating relations between models.
But since 'User' is a built-in model that lives in your node_modules
directory,
it isn't tracked by source control. You can declare the relation in your
server/server.js
file.
var User = app.models.User;
User.hasMany(app.models.Time, { as: 'times', foreignKey: 'userId' });
Now 'Times' has a 'userId' field that tracks which user this time belongs to.
So now, when you log in, you should be able to go to
http://localhost:3000/api/Users/me/times
and get an HTTP 401 unauthorized.
Believe it or not, this error is a good thing: this means that you have
created the relation correctly. Now you just need to set up permissions.
First, you should create permissions for the 'Time' model. Users should only
be able to access times that belong to them, and authenticated users should
be able to create new times. LoopBack has the slc loopback:acl
command
for creating permissions. You can use this command 3 times to create 3
access control rules for the 'Time' model.
$ slc loopback:acl
? Select the model to apply the ACL entry to: Time
? Select the ACL scope: All methods and properties
? Select the access type: All (match all types)
? Select the role: All users
? Select the permission to apply: Explicitly deny access
$ slc loopback:acl
? Select the model to apply the ACL entry to: Time
? Select the ACL scope: All methods and properties
? Select the access type: All (match all types)
? Select the role: The user owning the object
? Select the permission to apply: Explicitly grant access
$ slc loopback:acl
? Select the model to apply the ACL entry to: Time
? Select the ACL scope: A single method
? Enter the method name: create
? Select the role: Any authenticated user
? Select the permission to apply: Explicitly grant access
The above process defines 3 access control rules for the 'Time' model.
- By default, access to all operations on the 'Time' model is denied.
- A user can do whatever they want to a 'Time' instance that they own.
- An authenticated user can create a new 'Time' instance.
Take a look at common/models/time.json
, you should see the following:
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "create"
}
]
Even after you do this, you're still going to get an HTTP 401 when you go to
http://localhost:3000/api/Users/me/times
. In order to make this work, you're
going to have to add an ACL to the User model as well. The
__get__times
function is the function the User model uses to get related times.
Once again, since the 'User' model isn't tracked by git, the easiest place to
make this change is in
a boot script called server/boot/user-permissions.js
.
module.exports = function(app) {
var User = app.models.User;
var ACL = app.models.ACL;
User.hasMany(app.models.Time, { as: 'times', foreignKey: 'userId' });
ACL.create({
accessType: ACL.ALL,
permission: ACL.ALLOW,
principalType: ACL.ROLE,
principalId: '$owner',
model: 'User',
property: '__get__times'
});
};
Now you should be able to access http://localhost:3000/api/Users/me/times
after logging in. There's just one tiny permission issue left. Remember that
you enabled any authenticated user to create a new time. The problem is that
you didn't put any restriction on the userId
field, so any user can create
times for any other user! Naturally, you want to disallow this. However,
this is a 3-liner using LoopBack remote hooks. The easiest
place to make this change is in
the common/models/Time.js
file, which is where 'Time' model configuration
belongs.
app.models.Time.beforeRemote('create', function(ctx, modelInstance, next) {
ctx.args.data.userId = ctx.req.accessToken.userId;
next();
});
Moving On
Congratulations, you just built a full REST API with permissions and OAuth integration with 3 simple steps. Once you get familiar with LoopBack, you can generate REST APIs with minimal work. However, this is just the tip of the iceberg with LoopBack. In the next article, you'll use the LoopBack AngularJS SDK to build AngularJS directives that will form the basis of your mobile app.