Browse Source

Add authenticated-json-api sample. (#3)

master
Dana Silver 8 years ago
committed by GitHub
parent
commit
5715f8c53c
  1. 40
      authenticated-json-api/README.md
  2. 13
      authenticated-json-api/database.rules.json
  3. 11
      authenticated-json-api/firebase.json
  4. 126
      authenticated-json-api/functions/index.js
  5. 11
      authenticated-json-api/functions/package.json
  6. 108
      authenticated-json-api/public/index.html
  7. 68
      authenticated-json-api/public/main.css
  8. 150
      authenticated-json-api/public/main.js

40
authenticated-json-api/README.md

@ -0,0 +1,40 @@
# Authenticated JSON API
This sample shows how to authenticate access to a JSON API to only allow
access to data for a specific Firebase user.
Only users who pass a valid Firebase ID token as a Bearer token in the
`Authorization` header of the HTTP request are authorized to use the API.
This sample comes with a web-based API explorer UI whose code is in the [public](public) directory.
It lets you sign in to Firebase with your Google account, and create messages whose sentiments are
detected by the [Cloud Natural Language API](https://cloud.google.com/natural-language/).
## Setting up the sample
1. Create a Firebase Project using the [Firebase Console](https://console.firebase.google.com).
1. Enable the **Google** Provider in the **Auth** section.
1. Enable Billing on your project (to connect to the Natural Language API) by switching to the Blaze or Flame plan.
1. Clone or download this repo and open the `authenticated-json-api` directory.
1. You must have the Firebase CLI installed. If you don't have it install it with `npm install -g firebase-tools` and then configure it with `firebase login`.
1. Configure the CLI locally by using `firebase use --add` and select your project in the list.
1. Install dependencies locally by running: `cd functions; npm install; cd -`
1. Enable the Google Cloud Natural Language API: https://console.cloud.google.com/apis/api/language.googleapis.com/overview?project=_
## Deploy and test
This sample comes with a web-based UI for testing the function. To test it out:
1. Deploy your project using `firebase deploy`
1. Open the app using `firebase open hosting:site`, this will open a browser.
1. Sign in to the web app in the browser using Google Sign-In
1. Create messages and explore them using the List and Detail sections.
1. Sign out. You should no longer be able to access the API.
## Contributing
We'd love that you contribute to the project. Before doing so please read our [Contributor guide](../CONTRIBUTING.md).
## License
© Google, 2017. Licensed under an [Apache-2](../LICENSE) license.

13
authenticated-json-api/database.rules.json

@ -0,0 +1,13 @@
{
"rules": {
"users": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid",
"messages": {
".indexOn": ["category"]
}
}
}
}
}

11
authenticated-json-api/firebase.json

@ -0,0 +1,11 @@
{
"hosting": {
"public": "public",
"rewrites": [
{ "source": "/api/**", "function": "api" }
]
},
"database": {
"rules": "database.rules.json"
}
}

126
authenticated-json-api/functions/index.js

@ -0,0 +1,126 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const Language = require('@google-cloud/language');
const express = require('express');
const app = express();
const language = new Language({projectId: process.env.GCLOUD_PROJECT});
admin.initializeApp(functions.config().firebase);
// Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// when decoded successfully, the ID Token content will be added as `req.user`.
const authenticate = (req, res, next) => {
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) {
res.status(403).send('Unauthorized');
return;
}
const idToken = req.headers.authorization.split('Bearer ')[1];
admin.auth().verifyIdToken(idToken).then(decodedIdToken => {
req.user = decodedIdToken;
next();
}).catch(error => {
res.status(403).send('Unauthorized');
});
};
app.use(authenticate);
// POST /api/messages
// Create a new message, get its sentiment using Google Cloud NLP,
// and categorize the sentiment before saving.
app.post('/api/messages', (req, res) => {
const message = req.body.message;
language.detectSentiment(message).then(results => {
const category = categorizeScore(results[0].score);
const data = {message: message, sentiment: results, category: category};
return admin.database().ref(`/users/${req.user.uid}/messages`).push(data);
}).then(snapshot => {
return snapshot.ref.once('value');
}).then(snapshot => {
const val = snapshot.val();
res.status(201).json({message: val.message, category: val.category});
}).catch(error => {
console.log('Error detecting sentiment or saving message', error.message);
res.sendStatus(500);
});
});
// GET /api/messages?category={category}
// Get all messages, optionally specifying a category to filter on
app.get('/api/messages', (req, res) => {
const category = req.query.category;
let query = admin.database().ref(`/users/${req.user.uid}/messages`);
if (category && ['positive', 'negative', 'neutral'].indexOf(category) > -1) {
// Update the query with the valid category
query = query.orderByChild('category').equalTo(category);
} else if (category) {
return res.status(404).json({errorCode: 404, errorMessage: `category '${category}' not found`});
}
query.once('value').then(snapshot => {
var messages = [];
snapshot.forEach(childSnapshot => {
messages.push({key: childSnapshot.key, message: childSnapshot.val().message});
});
return res.status(200).json(messages);
}).catch(error => {
console.log('Error getting messages', error.message);
res.sendStatus(500);
});
});
// GET /api/message/{messageId}
// Get details about a message
app.get('/api/message/:messageId', (req, res) => {
const messageId = req.params.messageId;
admin.database().ref(`/users/${req.user.uid}/messages/${messageId}`).once('value').then(snapshot => {
if (snapshot.val() !== null) {
// Cache details in the browser for 5 minutes
res.set('Cache-Control', 'private, max-age=300');
res.status(200).json(snapshot.val());
} else {
res.status(404).json({errorCode: 404, errorMessage: `message '${messageId}' not found`});
}
}).catch(error => {
console.log('Error getting message details', messageId, error.message);
res.sendStatus(500);
});
});
// Expose the API as a function
exports.api = functions.https.onRequest(app);
// Helper function to categorize a sentiment score as positive, negative, or neutral
const categorizeScore = score => {
if (score > 0.25) {
return 'positive';
} else if (score < -0.25) {
return 'negative';
} else {
return 'neutral';
}
}

11
authenticated-json-api/functions/package.json

@ -0,0 +1,11 @@
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"dependencies": {
"@google-cloud/language": "^0.10.3",
"express": "^4.15.2",
"firebase-admin": "~4.1.2",
"firebase-functions": "^0.5"
},
"private": true
}

108
authenticated-json-api/public/index.html

@ -0,0 +1,108 @@
<!DOCTYPE html>
<!--
Copyright 2017 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Authenticated JSON API</title>
<!-- Material Design Lite -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.light_blue-pink.min.css">
<link rel="stylesheet" href="main.css">
</head>
<body>
<header class="mdl-layout__header mdl-color-text--white mdl-color--light-blue-700">
<div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid">
<div class="mdl-layout__header-row mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-cell--8-col-desktop">
<h3>Authenticated JSON API Demo</h3>
</div>
</div>
</header>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--2-offset--desktop mdl-cell--4-col mdl-shadow--2dp">
<div id="demo-sign-in-card" class="mdl-card">
<div class="mdl-card__title mdl-color--light-blue-100">
<h2 class="mdl-card__title-text">Sign in with Google</h2>
</div>
<div class="mdl-card__supporting-text mdl-color-text--grey-600">
This web application demonstrates how to expose an authenticated JSON API via Firebase Hosting and
Cloud Functions for Firebase.
</div>
<div class="mdl-card__actions mdl-card--border">
<button id="demo-sign-in-button" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect">
<i class="material-icons">account_circle</i>
Sign in with Google
</button>
<button id="demo-sign-out-button" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect">
Sign out
</button>
</div>
</div>
</div>
</div>
<div class="mdl-grid">
<div class="mdl-cell">
<h4>Explore the API <small>Requires Sign In</small></h4>
</div>
</div>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--8-col mdl-shadow--2dp demo-create-message">
<h5>New Message</h5>
<div class="mdl-textfield mdl-js-textfield">
<textarea class="mdl-textfield__input" type="text" rows="1" id="demo-message" ></textarea>
<label class="mdl-textfield__label" for="demo-message">Type a message to analyze...</label>
</div>
<a id="demo-create-message" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect">
Save and Analyze
</a>
<span id="demo-create-message-result"></span>
</div>
</div>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--4-col mdl-shadow--2dp">
<div class="demo-message-list">
<h5>List Messages</h5>
<button id="message-list-button-all" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect message-list-button">All</button>
<button class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect message-list-button">Positive</button>
<button class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect message-list-button">Negative</button>
<button class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect message-list-button">Neutral</button>
<ul id="demo-message-list" class="mdl-list"></ul>
</div>
</div>
<div class="mdl-cell mdl-cell--4-col mdl-shadow--2dp">
<div class="demo-message-details">
<h5>Message Details</h5>
<pre id="demo-message-details"></pre>
</div>
</div>
</div>
<script src="https://code.getmdl.io/1.1.3/material.min.js"></script>
<script src="/__/firebase/3.9.0/firebase-app.js"></script>
<script src="/__/firebase/3.9.0/firebase-auth.js"></script>
<script src="/__/firebase/3.9.0/firebase-database.js"></script>
<script src="/__/firebase/init.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
<script src="main.js"></script>
</body>
</html>

68
authenticated-json-api/public/main.css

@ -0,0 +1,68 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.mdl-card {
width: 100%;
}
#demo-sign-in-card > .mdl-card__title {
height: 125px;
}
#demo-create-message-result {
margin-left: 10px;
}
.mdl-textfield, .mdl-textfield > textarea {
width: 100%;
resize: none;
}
h5 {
margin: 20px 5px;
}
button:not(:first-child) {
margin-left: 5px;
}
li:hover {
background-color: rgba(158,158,158,.2);
cursor: pointer;
}
li.selected {
background-color: rgba(158,158,158,.4);
}
.demo-create-message {
padding: 10px;
}
.demo-message-list,
.demo-message-details {
padding-left: 10px;
}
.demo-message-list > ul {
height: 335px;
overflow: auto;
}
.demo-message-details > pre {
height: 400px;
overflow: auto;
}

150
authenticated-json-api/public/main.js

@ -0,0 +1,150 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
function Demo() {
$(function() {
this.$signInButton = $('#demo-sign-in-button');
this.$signOutButton = $('#demo-sign-out-button');
this.$messageTextarea = $('#demo-message');
this.$createMessageButton = $('#demo-create-message');
this.$createMessageResult = $('#demo-create-message-result');
this.$messageListButtons = $('.message-list-button');
this.$messageList = $('#demo-message-list');
this.$messageDetails = $('#demo-message-details');
this.$signInButton.on('click', this.signIn.bind(this));
this.$signOutButton.on('click', this.signOut.bind(this));
this.$createMessageButton.on('click', this.createMessage.bind(this));
this.$messageListButtons.on('click', this.listMessages.bind(this));
firebase.auth().onAuthStateChanged(this.onAuthStateChanged.bind(this));
}.bind(this));
}
Demo.prototype.signIn = function() {
firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider());
};
Demo.prototype.signOut = function() {
firebase.auth().signOut();
};
Demo.prototype.onAuthStateChanged = function(user) {
if (user) {
// If we have a user, simulate a click to get all their messages.
// Material Design Lite will create a <span> child that we'll expect to be clicked
$('#message-list-button-all > span').click();
this.$messageTextarea.removeAttr('disabled');
this.$createMessageButton.removeAttr('disabled');
} else {
this.$messageTextarea.attr('disabled', true);
this.$createMessageButton.attr('disabled', true);
this.$createMessageResult.html('');
this.$messageList.html('');
this.$messageDetails.html('');
}
};
Demo.prototype.createMessage = function() {
var message = this.$messageTextarea.val();
if (message === '') return;
// Make an authenticated POST request to create a new message
this.authenticatedRequest('POST', '/api/messages', {message: message}).then(function(response) {
this.$messageTextarea.val('');
this.$messageTextarea.parent().removeClass('is-dirty');
this.$createMessageResult.html('Created <b>' + response.category + '</b> message: ' + response.message);
}.bind(this)).catch(function(error) {
console.log('Error creating message:', message);
throw error;
});
};
Demo.prototype.listMessages = function(event) {
this.$messageListButtons.removeClass('mdl-button--accent');
$(event.target).parent().addClass('mdl-button--accent');
this.$messageList.html('');
this.$messageDetails.html('');
// Make an authenticated GET request for a list of messages
// Optionally specifying a category (positive, negative, neutral)
var label = $(event.target).parent().text().toLowerCase();
var category = label === 'all' ? '' : label;
var url = category ? '/api/messages?category=' + category : '/api/messages';
this.authenticatedRequest('GET', url).then(function(response) {
var elements = response.map(function(message) {
return $('<li>')
.text(message.message)
.addClass('mdl-list__item')
.data('key', message.key)
.on('click', this.messageDetails.bind(this));
}.bind(this));
// Append items to the list and simulate a click to fetch the first message's details
this.$messageList.append(elements);
if (elements.length > 0) {
elements[0].click();
}
}.bind(this)).catch(function(error) {
console.log('Error listing messages.');
throw error;
});
};
Demo.prototype.messageDetails = function(event) {
$('li').removeClass('selected');
$(event.target).addClass('selected');
var key = $(event.target).data('key');
this.authenticatedRequest('GET', '/api/message/' + key).then(function(response) {
this.$messageDetails.text(JSON.stringify(response, null, 2));
}.bind(this)).catch(function(error) {
console.log('Error getting message details.');
throw error;
});
};
Demo.prototype.authenticatedRequest = function(method, url, body) {
if (!firebase.auth().currentUser) {
throw new Error('Not authenticated. Make sure you\'re signed in!');
}
// Get the Firebase auth token to authenticate the request
return firebase.auth().currentUser.getToken().then(function(token) {
var request = {
method: method,
url: url,
dataType: 'json',
beforeSend: function(xhr) { xhr.setRequestHeader('Authorization', 'Bearer ' + token); }
};
if (method === 'POST') {
request.contentType = 'application/json';
request.data = JSON.stringify(body);
}
console.log('Making authenticated request:', method, url);
return $.ajax(request).catch(function() {
throw new Error('Request error: ' + method + ' ' + url);
});
});
};
window.demo = new Demo();
Loading…
Cancel
Save