Dana Silver
8 years ago
committed by
GitHub
8 changed files with 527 additions and 0 deletions
@ -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. |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"rules": { |
|||
"users": { |
|||
"$uid": { |
|||
".read": "auth.uid === $uid", |
|||
".write": "auth.uid === $uid", |
|||
"messages": { |
|||
".indexOn": ["category"] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
{ |
|||
"hosting": { |
|||
"public": "public", |
|||
"rewrites": [ |
|||
{ "source": "/api/**", "function": "api" } |
|||
] |
|||
}, |
|||
"database": { |
|||
"rules": "database.rules.json" |
|||
} |
|||
} |
@ -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'; |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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> |
@ -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; |
|||
} |
@ -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…
Reference in new issue