From 64055767201e781d1d22045b4ce9876c7c8d3f8d Mon Sep 17 00:00:00 2001 From: tristangoogle Date: Wed, 31 Aug 2016 15:57:02 -0700 Subject: [PATCH 1/8] Add firenotes firebase auth example --- .../standard/firebase/firenotes/README.md | 60 ++++++++ .../firebase/firenotes/backend/.gitignore | 1 + .../firebase/firenotes/backend/app.yaml | 17 +++ .../firenotes/backend/appengine_config.py | 18 +++ .../firenotes/backend/firebase_helper.py | 105 ++++++++++++++ .../firebase/firenotes/backend/index.yaml | 22 +++ .../firebase/firenotes/backend/main.py | 104 ++++++++++++++ .../firebase/firenotes/backend/main_test.py | 27 ++++ .../firenotes/backend/requirements.txt | 3 + .../firebase/firenotes/frontend/app.yaml | 15 ++ .../firebase/firenotes/frontend/index.html | 44 ++++++ .../firebase/firenotes/frontend/main.js | 133 ++++++++++++++++++ .../firebase/firenotes/frontend/style.css | 29 ++++ 13 files changed, 578 insertions(+) create mode 100644 appengine/standard/firebase/firenotes/README.md create mode 100644 appengine/standard/firebase/firenotes/backend/.gitignore create mode 100644 appengine/standard/firebase/firenotes/backend/app.yaml create mode 100644 appengine/standard/firebase/firenotes/backend/appengine_config.py create mode 100644 appengine/standard/firebase/firenotes/backend/firebase_helper.py create mode 100644 appengine/standard/firebase/firenotes/backend/index.yaml create mode 100644 appengine/standard/firebase/firenotes/backend/main.py create mode 100644 appengine/standard/firebase/firenotes/backend/main_test.py create mode 100644 appengine/standard/firebase/firenotes/backend/requirements.txt create mode 100644 appengine/standard/firebase/firenotes/frontend/app.yaml create mode 100644 appengine/standard/firebase/firenotes/frontend/index.html create mode 100644 appengine/standard/firebase/firenotes/frontend/main.js create mode 100644 appengine/standard/firebase/firenotes/frontend/style.css diff --git a/appengine/standard/firebase/firenotes/README.md b/appengine/standard/firebase/firenotes/README.md new file mode 100644 index 000000000000..b9ac5d250650 --- /dev/null +++ b/appengine/standard/firebase/firenotes/README.md @@ -0,0 +1,60 @@ +# Firenotes: Firebase Authentication on Google App Engine + +A simple note-taking application that stores users' notes in their own personal +notebooks separated by a unique user ID generated by Firebase. Uses Firebase +Authentication, Google App Engine, and Google Cloud Datastore. + +You'll need to have [Python 2.7](https://www.python.org/), the +[App Engine SDK](https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Python), +and the [Google Cloud SDK](https://cloud.google.com/sdk/?hl=en) +installed and initialized to an App Engine project before running the code in +this sample. + +## Setup + +1. Clone this repo: + + git clone https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/appengine/standard/firebase/auth/firenotes + +1. Within a virtualenv, install the dependencies to the backend service: + + pip install -r requirements.txt -t lib + pip install pycrypto + + Although the pycrypto library is built in to the App Engine standard + environment, it will not be bundled until deployment since it is + platform-dependent. Thus, the app.yaml file includes the bundled version of + pycrypto at runtime, but you still need to install it manually to run the + application on the App Engine local development server. + +1. [Add Firebase to your app.](https://firebase.google.com/docs/web/setup#add_firebase_to_your_app) +1. Add your Firebase Project ID to the backend’s `app.yaml` file as an +environment variable. +1. Select which providers you want to enable. Delete the providers from +`main.js` that you do no want to offer. Enable the providers you chose to keep +in the Firebase console under **Auth** > **SIGN-IN METHOD** > +**Sign-in providers**. +1. In the Firebase console, under **OAuth redirect domains**, click +**ADD DOMAIN** and enter the domain of your app on App Engine: +[PROJECT_ID].appspot.com. Do not include "http://" before the domain name. + +## Run Locally +1. Add the backend host URL to `main.js`: http://localhost:8081. +1. Navigate to the root directory of the application and start the development +server with the following command: + + dev_appserver.py frontend/app.yaml backend/app.yaml + +1. Visit [http://locahost:8080/](http://locahost:8080/) in a web browser. + +## Deploy +1. Change the backend host URL in `main.js` to +https://backend-dot-[PROJECT_ID].appspot.com. +1. Deploy the application using the Cloud SDK command-line interface: + + gcloud app deploy backend/index.yaml frontend/app.yaml backend/app.yaml + + The Cloud Datastore indexes can take a while to update, so the application + might not be fully functional immediately after deployment. + +1. View the application live at https://[PROJECT_ID].appspot.com. diff --git a/appengine/standard/firebase/firenotes/backend/.gitignore b/appengine/standard/firebase/firenotes/backend/.gitignore new file mode 100644 index 000000000000..a65b41774ad5 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/.gitignore @@ -0,0 +1 @@ +lib diff --git a/appengine/standard/firebase/firenotes/backend/app.yaml b/appengine/standard/firebase/firenotes/backend/app.yaml new file mode 100644 index 000000000000..5883165e160f --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/app.yaml @@ -0,0 +1,17 @@ +runtime: python27 +api_version: 1 +threadsafe: true +service: backend + +handlers: +- url: /.* + script: main.app + +libraries: +- name: ssl + version: 2.7.11 +- name: pycrypto + version: 2.6 + +env_variables: + FIREBASE_PROJECT_ID: '' \ No newline at end of file diff --git a/appengine/standard/firebase/firenotes/backend/appengine_config.py b/appengine/standard/firebase/firenotes/backend/appengine_config.py new file mode 100644 index 000000000000..c903d9a0ac5e --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2016 Google Inc. +# +# 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. + +from google.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/firebase/firenotes/backend/firebase_helper.py b/appengine/standard/firebase/firenotes/backend/firebase_helper.py new file mode 100644 index 000000000000..12294646993a --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/firebase_helper.py @@ -0,0 +1,105 @@ +# Copyright 2016 Google Inc. +# +# 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. +import json +import logging +import os +import ssl + +from Crypto.Util import asn1 +from flask import request +from google.appengine.api import urlfetch, urlfetch_errors +import jwt +from jwt.contrib.algorithms.pycrypto import RSAAlgorithm +import jwt.exceptions + + +# For App Engine, pyjwt needs to use PyCrypto instead of Cryptography. +jwt.register_algorithm('RS256', RSAAlgorithm(RSAAlgorithm.SHA256)) + +FIREBASE_CERTIFICATES_URL = ( + 'https://www.googleapis.com/robot/v1/metadata/x509/' + 'securetoken@system.gserviceaccount.com') + + +def get_firebase_certificates(): + """Fetches the firebase certificates from firebase. + + Note: in a production application, you should cache this for at least + an hour. + """ + try: + result = urlfetch.Fetch(FIREBASE_CERTIFICATES_URL, + validate_certificate=True) + data = result.content + except urlfetch_errors.Error: + logging.error('Error while fetching Firebase certificates') + raise + + certificates = json.loads(data) + + return certificates + + +def extract_public_key_from_certificate(x509_certificate): + """Extracts the PEM public key from an x509 certificate.""" + der_certificate_string = ssl.PEM_cert_to_DER_cert(x509_certificate) + + # Extract subjectPublicKeyInfo field from X.509 certificate (see RFC3280) + der_certificate = asn1.DerSequence() + der_certificate.decode(der_certificate_string) + tbs_certification = asn1.DerSequence() # To Be Signed certificate + tbs_certification.decode(der_certificate[0]) + + subject_public_key_info = tbs_certification[6] + + return subject_public_key_info + + +def verify_auth_token(): + """Verifies the JWT auth token in the request. + If none is found or if the token is invalid, returns None. Otherwise, + it returns a dictionary containing the JWT claims.""" + if 'Authorization' not in request.headers: + return None + + # Auth header is in format 'Bearer {jwt}'. + request_jwt = request.headers['Authorization'].split(' ').pop() + + # Determine which certificate was used to sign the JWT. + header = jwt.get_unverified_header(request_jwt) + kid = header['kid'] + + certificates = get_firebase_certificates() + + try: + certificate = certificates[kid] + except KeyError: + logging.warning('JWT signed with unkown kid {}'.format(header['kid'])) + return None + + # Get the public key from the certificate. This is used to verify the + # jwt signature. + public_key = extract_public_key_from_certificate(certificate) + + try: + claims = jwt.decode( + request_jwt, + public_key, + algorithms=['RS256'], + audience=os.environ['FIREBASE_PROJECT_ID']) + except jwt.exceptions.InvalidTokenError as e: + logging.warning('JWT verification failed: {}'.format(e)) + return None + + return claims diff --git a/appengine/standard/firebase/firenotes/backend/index.yaml b/appengine/standard/firebase/firenotes/backend/index.yaml new file mode 100644 index 000000000000..b0ebb4ffea8a --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/index.yaml @@ -0,0 +1,22 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +- kind: Note + ancestor: yes + properties: + - name: created + +- kind: Note + ancestor: yes + properties: + - name: created + direction: desc diff --git a/appengine/standard/firebase/firenotes/backend/main.py b/appengine/standard/firebase/firenotes/backend/main.py new file mode 100644 index 000000000000..333f7d8cdd66 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -0,0 +1,104 @@ +# Copyright 2016 Google Inc. +# +# 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. + +# [START app] +import logging + +from flask import Flask, jsonify, request +import flask_cors +from google.appengine.ext import ndb + +import firebase_helper + + +app = Flask(__name__) +flask_cors.CORS(app) + + +class Note(ndb.Model): + """NDB model class for a user's note. + Key is user id from decrypted token.""" + friendly_id = ndb.StringProperty() + message = ndb.TextProperty() + created = ndb.DateTimeProperty(auto_now_add=True) + + +def query_database(user_id): + """Fetches all notes associated with user_id and orders them + by date created, with most recent note processed first.""" + ancestor_key = ndb.Key(Note, user_id) + query = Note.query(ancestor=ancestor_key).order(-Note.created) + notes = query.fetch() + + note_messages = [] + + for note in notes: + note_messages.append({'friendly_id': note.friendly_id, + 'message': note.message, + 'created': note.created}) + + return note_messages + + +@app.route('/notes', methods=['GET']) +def list_notes(): + """Queries database for user's notes to display.""" + claims = firebase_helper.verify_auth_token() + if not claims: + return 'Unauthorized', 401 + + notes = query_database(claims['sub']) + + return jsonify(notes) + + +@app.route('/notes', methods=['POST', 'PUT']) +def add_note(): + """ + Adds a note to the user's notebook. The request should be in this format: + + { + "message": "note message." + } + """ + + claims = firebase_helper.verify_auth_token() + if not claims: + return 'Unauthorized', 401 + + data = request.get_json() + + # Populates note properties according to the model, + # with the user ID as the key. + note = Note(parent=ndb.Key(Note, claims['sub']), + message=data['message']) + + # Some providers do not provide one of these so either can be used. + if 'name' in claims: + note.friendly_id = claims['name'] + else: + note.friendly_id = claims['email'] + + # Stores note in database. + note.put() + + return 'OK', 200 + + +@app.errorhandler(500) +def server_error(e): + # Log the error and stacktrace. + logging.exception('An error occurred during a request.') + return 'An internal error occurred.', 500 +# [END app] diff --git a/appengine/standard/firebase/firenotes/backend/main_test.py b/appengine/standard/firebase/firenotes/backend/main_test.py new file mode 100644 index 000000000000..ab6db4fe5df9 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/main_test.py @@ -0,0 +1,27 @@ +# Copyright 2016 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. + +import pytest + + +@pytest.fixture +def app(): + import main + main.app.testing = True + return main.app.test_client() + + +def test_index(app): + r = app.get('/') + assert r.status_code == 200 diff --git a/appengine/standard/firebase/firenotes/backend/requirements.txt b/appengine/standard/firebase/firenotes/backend/requirements.txt new file mode 100644 index 000000000000..97001bede02c --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.11.1 +pyjwt==1.4.1 +flask-cors==3.0.0 diff --git a/appengine/standard/firebase/firenotes/frontend/app.yaml b/appengine/standard/firebase/firenotes/frontend/app.yaml new file mode 100644 index 000000000000..e205ff25c092 --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/app.yaml @@ -0,0 +1,15 @@ +runtime: python27 +api_version: 1 +service: default +threadsafe: true + +handlers: + +# root +- url: / + static_files: index.html + upload: index.html + +- url: /(.+) + static_files: \1 + upload: (.+) diff --git a/appengine/standard/firebase/firenotes/frontend/index.html b/appengine/standard/firebase/firenotes/frontend/index.html new file mode 100644 index 000000000000..675d9d635502 --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + Firenotes + + +
+

Firenotes

+

Sign in to access your notebook

+
+
+ +
+

Welcome, !

+

Enter a note and save it to your personal notebook

+
+
+
+ +
+
+ + +
+
+
+ +
+
+ + diff --git a/appengine/standard/firebase/firenotes/frontend/main.js b/appengine/standard/firebase/firenotes/frontend/main.js new file mode 100644 index 000000000000..80307a7c0d6d --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/main.js @@ -0,0 +1,133 @@ +$(function(){ + /* This is the host for the backend. + TODO: When running Firenotes locally, set to http://localhost:8081. Before + deploying the application to a live production environment, change to + https://backend-dot-.appspot.com as specified in the + backend's app.yaml file. */ + var backendHostUrl = ''; + + // Initialize Firebase + // TODO: Replace with your project's customized code snippet + var config = { + apiKey: "", + authDomain: ".firebaseapp.com", + databaseURL: "https://.firebaseio.com", + storageBucket: ".appspot.com", + }; + + // This is passed into the backend to authenticate the user. + var userIdToken = null; + + // Firebase log-in + function configureFirebaseLogin() { + + firebase.initializeApp(config); + + firebase.auth().onAuthStateChanged(function(user) { + if (user) { + $('#logged-out').hide(); + var name = user.displayName; + + /* If the provider gives a display name, use the name for the + personal welcome message. Otherwise, use the user's email. */ + var welcomeName = name ? name : user.email; + + user.getToken().then(function(idToken) { + userIdToken = idToken; + + /* Now that the user is authenicated, fetch the notes. */ + fetchNotes(); + + $('#user').text(welcomeName); + $('#logged-in').show(); + + }); + + } else { + $('#logged-in').hide(); + $('#logged-out').show(); + + } + + }); + + } + + // Firebase log-in widget + function configureFirebaseLoginWidget() { + var uiConfig = { + 'signInSuccessUrl': '/', + 'signInOptions': [ + // Leave the lines as is for the providers you want to offer your users. + firebase.auth.GoogleAuthProvider.PROVIDER_ID, + firebase.auth.FacebookAuthProvider.PROVIDER_ID, + firebase.auth.TwitterAuthProvider.PROVIDER_ID, + firebase.auth.GithubAuthProvider.PROVIDER_ID, + firebase.auth.EmailAuthProvider.PROVIDER_ID + ], + // Terms of service url + 'tosUrl': '', + }; + + var ui = new firebaseui.auth.AuthUI(firebase.auth()); + ui.start('#firebaseui-auth-container', uiConfig); + } + + // Fetch notes from the backend. + function fetchNotes() { + $.ajax(backendHostUrl + '/notes', { + /* Set header for the XMLHttpRequest to get data from the web server + associated with userIdToken */ + headers: { + 'Authorization': 'Bearer ' + userIdToken + } + }).then(function(data){ + $('#notes-container').empty(); + // Iterate over user data to display user's notes from database. + data.forEach(function(note){ + $('#notes-container').append($('

').text(note.message)); + }); + }); + } + + // Sign out a user + var signOutBtn =$('#sign-out'); + signOutBtn.click(function(event) { + event.preventDefault(); + + firebase.auth().signOut().then(function() { + console.log("Sign out successful"); + }, function(error) { + console.log(error); + }); + }); + + // Save a note to the backend + var saveNoteBtn = $('#add-note'); + saveNoteBtn.click(function(event) { + event.preventDefault(); + + var noteField = $('#note-content'); + var note = noteField.val(); + noteField.val(""); + + /* Send note data to backend, storing in database with existing data + associated with userIdToken */ + $.ajax(backendHostUrl + '/notes', { + headers: { + 'Authorization': 'Bearer ' + userIdToken + }, + method: 'POST', + data: JSON.stringify({'message': note}), + contentType : 'application/json' + }).then(function(){ + // Refresh notebook display. + fetchNotes(); + }); + + }); + + configureFirebaseLogin(); + configureFirebaseLoginWidget(); + +}); diff --git a/appengine/standard/firebase/firenotes/frontend/style.css b/appengine/standard/firebase/firenotes/frontend/style.css new file mode 100644 index 000000000000..ad62161aa21a --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/style.css @@ -0,0 +1,29 @@ +body { + font-family: "helvetica", sans-serif; + text-align: center; +} + +form { + padding: 5px 0 10px; + margin-bottom: 30px; +} +h3,legend { + font-weight: 400; + padding: 18px 0 15px; + margin: 0 0 0; +} + +div.form-group { + margin-bottom: 10px; +} + +input, textarea { + width: 250px; + font-size: 14px; + padding: 6px; +} + +textarea { + vertical-align: top; + height: 75px; +} From 832f758ccde0aeed2b462086a9771652dadd2746 Mon Sep 17 00:00:00 2001 From: Tristan Vanech Date: Fri, 2 Sep 2016 13:31:22 -0700 Subject: [PATCH 2/8] Specified comment on Datastore entity creation --- appengine/standard/firebase/firenotes/backend/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appengine/standard/firebase/firenotes/backend/main.py b/appengine/standard/firebase/firenotes/backend/main.py index 333f7d8cdd66..1168ada9d650 100644 --- a/appengine/standard/firebase/firenotes/backend/main.py +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -80,7 +80,7 @@ def add_note(): data = request.get_json() # Populates note properties according to the model, - # with the user ID as the key. + # with the user ID as the key name. note = Note(parent=ndb.Key(Note, claims['sub']), message=data['message']) From c1161a0a9acdeeb5508a2d8d2d94a965e5a708b8 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 6 Sep 2016 10:27:48 -0700 Subject: [PATCH 3/8] Fix small style issues. Change-Id: Ife77c092a659a8864f744e7dc287dc245dfee527 --- .../firebase/firenotes/backend/app.yaml | 3 +- .../firenotes/backend/firebase_helper.py | 26 ++++++++++------- .../firebase/firenotes/backend/main.py | 29 +++++++++++++------ .../firebase/firenotes/frontend/main.js | 23 +++++++++++---- .../firebase/firenotes/frontend/style.css | 15 ++++++++++ 5 files changed, 71 insertions(+), 25 deletions(-) diff --git a/appengine/standard/firebase/firenotes/backend/app.yaml b/appengine/standard/firebase/firenotes/backend/app.yaml index 5883165e160f..bb6c6628996e 100644 --- a/appengine/standard/firebase/firenotes/backend/app.yaml +++ b/appengine/standard/firebase/firenotes/backend/app.yaml @@ -14,4 +14,5 @@ libraries: version: 2.6 env_variables: - FIREBASE_PROJECT_ID: '' \ No newline at end of file + # Replace with your Firebase project ID. + FIREBASE_PROJECT_ID: '' diff --git a/appengine/standard/firebase/firenotes/backend/firebase_helper.py b/appengine/standard/firebase/firenotes/backend/firebase_helper.py index 12294646993a..8ecbd4a9b565 100644 --- a/appengine/standard/firebase/firenotes/backend/firebase_helper.py +++ b/appengine/standard/firebase/firenotes/backend/firebase_helper.py @@ -11,14 +11,15 @@ # 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. + import json import logging import os import ssl from Crypto.Util import asn1 -from flask import request -from google.appengine.api import urlfetch, urlfetch_errors +from google.appengine.api import urlfetch +from google.appengine.api import urlfetch_errors import jwt from jwt.contrib.algorithms.pycrypto import RSAAlgorithm import jwt.exceptions @@ -27,23 +28,26 @@ # For App Engine, pyjwt needs to use PyCrypto instead of Cryptography. jwt.register_algorithm('RS256', RSAAlgorithm(RSAAlgorithm.SHA256)) +# This URL contains a list of active certificates used to sign Firebase +# auth tokens. FIREBASE_CERTIFICATES_URL = ( 'https://www.googleapis.com/robot/v1/metadata/x509/' 'securetoken@system.gserviceaccount.com') def get_firebase_certificates(): - """Fetches the firebase certificates from firebase. + """Fetches the current Firebase certificates. Note: in a production application, you should cache this for at least an hour. """ try: - result = urlfetch.Fetch(FIREBASE_CERTIFICATES_URL, - validate_certificate=True) + result = urlfetch.Fetch( + FIREBASE_CERTIFICATES_URL, + validate_certificate=True) data = result.content except urlfetch_errors.Error: - logging.error('Error while fetching Firebase certificates') + logging.error('Error while fetching Firebase certificates.') raise certificates = json.loads(data) @@ -66,10 +70,12 @@ def extract_public_key_from_certificate(x509_certificate): return subject_public_key_info -def verify_auth_token(): +def verify_auth_token(request): """Verifies the JWT auth token in the request. - If none is found or if the token is invalid, returns None. Otherwise, - it returns a dictionary containing the JWT claims.""" + + If no token is found or if the token is invalid, returns None. + Otherwise, it returns a dictionary containing the JWT claims. + """ if 'Authorization' not in request.headers: return None @@ -89,7 +95,7 @@ def verify_auth_token(): return None # Get the public key from the certificate. This is used to verify the - # jwt signature. + # JWT signature. public_key = extract_public_key_from_certificate(certificate) try: diff --git a/appengine/standard/firebase/firenotes/backend/main.py b/appengine/standard/firebase/firenotes/backend/main.py index 1168ada9d650..184e735596d7 100644 --- a/appengine/standard/firebase/firenotes/backend/main.py +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -28,15 +28,20 @@ class Note(ndb.Model): """NDB model class for a user's note. - Key is user id from decrypted token.""" + + Key is user id from decrypted token. + """ friendly_id = ndb.StringProperty() message = ndb.TextProperty() created = ndb.DateTimeProperty(auto_now_add=True) def query_database(user_id): - """Fetches all notes associated with user_id and orders them - by date created, with most recent note processed first.""" + """Fetches all notes associated with user_id. + + Notes are ordered them by date created, with most recent note added + first. + """ ancestor_key = ndb.Key(Note, user_id) query = Note.query(ancestor=ancestor_key).order(-Note.created) notes = query.fetch() @@ -44,16 +49,20 @@ def query_database(user_id): note_messages = [] for note in notes: - note_messages.append({'friendly_id': note.friendly_id, - 'message': note.message, - 'created': note.created}) + note_messages.append({ + 'friendly_id': note.friendly_id, + 'message': note.message, + 'created': note.created + }) return note_messages @app.route('/notes', methods=['GET']) def list_notes(): - """Queries database for user's notes to display.""" + """Returns a list of notes added by the current Firebase user.""" + + # Verify Firebase auth. claims = firebase_helper.verify_auth_token() if not claims: return 'Unauthorized', 401 @@ -73,6 +82,7 @@ def add_note(): } """ + # Verify Firebase auth. claims = firebase_helper.verify_auth_token() if not claims: return 'Unauthorized', 401 @@ -81,8 +91,9 @@ def add_note(): # Populates note properties according to the model, # with the user ID as the key name. - note = Note(parent=ndb.Key(Note, claims['sub']), - message=data['message']) + note = Note( + parent=ndb.Key(Note, claims['sub']), + message=data['message']) # Some providers do not provide one of these so either can be used. if 'name' in claims: diff --git a/appengine/standard/firebase/firenotes/frontend/main.js b/appengine/standard/firebase/firenotes/frontend/main.js index 80307a7c0d6d..34a7685f9fca 100644 --- a/appengine/standard/firebase/firenotes/frontend/main.js +++ b/appengine/standard/firebase/firenotes/frontend/main.js @@ -1,9 +1,22 @@ +// Copyright 2016, Google, Inc. +// 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. + $(function(){ - /* This is the host for the backend. - TODO: When running Firenotes locally, set to http://localhost:8081. Before - deploying the application to a live production environment, change to - https://backend-dot-.appspot.com as specified in the - backend's app.yaml file. */ + // This is the host for the backend. + // TODO: When running Firenotes locally, set to http://localhost:8081. Before + // deploying the application to a live production environment, change to + // https://backend-dot-.appspot.com as specified in the + // backend's app.yaml file. var backendHostUrl = ''; // Initialize Firebase diff --git a/appengine/standard/firebase/firenotes/frontend/style.css b/appengine/standard/firebase/firenotes/frontend/style.css index ad62161aa21a..19b4f1d69bd1 100644 --- a/appengine/standard/firebase/firenotes/frontend/style.css +++ b/appengine/standard/firebase/firenotes/frontend/style.css @@ -1,3 +1,18 @@ +/* + Copyright 2016, Google, Inc. + 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. +*/ + body { font-family: "helvetica", sans-serif; text-align: center; From dad6df61162b0925567b26293d8e2a2bbe74ed11 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 6 Sep 2016 10:29:03 -0700 Subject: [PATCH 4/8] Fixing missing request argument Change-Id: I42dedc2a5124b61d4c013552e7ea53ff582ff804 --- appengine/standard/firebase/firenotes/backend/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appengine/standard/firebase/firenotes/backend/main.py b/appengine/standard/firebase/firenotes/backend/main.py index 184e735596d7..ea644e6c2d8a 100644 --- a/appengine/standard/firebase/firenotes/backend/main.py +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -63,7 +63,7 @@ def list_notes(): """Returns a list of notes added by the current Firebase user.""" # Verify Firebase auth. - claims = firebase_helper.verify_auth_token() + claims = firebase_helper.verify_auth_token(request) if not claims: return 'Unauthorized', 401 @@ -83,7 +83,7 @@ def add_note(): """ # Verify Firebase auth. - claims = firebase_helper.verify_auth_token() + claims = firebase_helper.verify_auth_token(request) if not claims: return 'Unauthorized', 401 From 98b5d252d2063d2a1e4d52d9b9db3d7688042ea4 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 6 Sep 2016 10:56:17 -0700 Subject: [PATCH 5/8] Add basic tests for the backend Change-Id: If0463ed74270d5e436d867a02c8ac31199be0432 --- .../firebase/firenotes/backend/main.py | 5 +- .../firebase/firenotes/backend/main_test.py | 71 ++++++++++++++++++- requirements-dev.txt | 1 + 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/appengine/standard/firebase/firenotes/backend/main.py b/appengine/standard/firebase/firenotes/backend/main.py index ea644e6c2d8a..c54acbb15704 100644 --- a/appengine/standard/firebase/firenotes/backend/main.py +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -96,10 +96,7 @@ def add_note(): message=data['message']) # Some providers do not provide one of these so either can be used. - if 'name' in claims: - note.friendly_id = claims['name'] - else: - note.friendly_id = claims['email'] + note.friendly_id = claims.get('name', claims.get('email', 'Unknown')) # Stores note in database. note.put() diff --git a/appengine/standard/firebase/firenotes/backend/main_test.py b/appengine/standard/firebase/firenotes/backend/main_test.py index ab6db4fe5df9..9c7ef8b54ee4 100644 --- a/appengine/standard/firebase/firenotes/backend/main_test.py +++ b/appengine/standard/firebase/firenotes/backend/main_test.py @@ -12,16 +12,83 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + +from google.appengine.ext import ndb +import jwt import pytest +import mock @pytest.fixture def app(): + # Remove any existing pyjwt handlers, as firebase_helper will register + # its own. + try: + jwt.unregister_algorithm('RS256') + except KeyError: + pass + import main main.app.testing = True return main.app.test_client() -def test_index(app): - r = app.get('/') +@pytest.fixture +def mock_token(): + with mock.patch('main.firebase_helper.verify_auth_token') as mock_verify: + yield mock_verify + + +@pytest.fixture +def test_data(): + from main import Note + ancestor_key = ndb.Key(Note, '123') + notes = [ + Note(parent=ancestor_key, message='1'), + Note(parent=ancestor_key, message='2') + ] + ndb.put_multi(notes) + yield + + +def test_list_notes_with_mock_token(testbed, app, mock_token, test_data): + mock_token.return_value = {'sub': '123'} + + r = app.get('/notes') assert r.status_code == 200 + + data = json.loads(r.data) + assert len(data) == 2 + assert data[0]['message'] == '2' + + +def test_list_notes_with_bad_mock_token(testbed, app, mock_token): + mock_token.return_value = None + + r = app.get('/notes') + assert r.status_code == 401 + + +def test_add_note_with_mock_token(testbed, app, mock_token): + mock_token.return_value = {'sub': '123'} + + r = app.post( + '/notes', + data=json.dumps({'message': 'Hello, world!'}), + content_type='application/json') + + assert r.status_code == 200 + + from main import Note + + results = Note.query().fetch() + assert len(results) == 1 + assert results[0].message == 'Hello, world!' + + +def test_add_note_with_bad_mock_token(testbed, app, mock_token): + mock_token.return_value = None + + r = app.post('/notes') + assert r.status_code == 401 diff --git a/requirements-dev.txt b/requirements-dev.txt index cb4b029038d0..5d0773bce47f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -29,6 +29,7 @@ pyasn1-modules==0.0.8 pyasn1==0.1.9 PyAudio==0.2.9 PyCrypto==2.6.1 +pyjwt==1.4.1 pymemcache==1.3.6 PyMySQL==0.7.7 pytest-cov==2.3.1 From b004ac04c2cf6f35f31e13b1de5283027fa44644 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 6 Sep 2016 14:09:20 -0700 Subject: [PATCH 6/8] Add test for firebase_helper Change-Id: Ie94f6bb447e4c040db27b86374c5e82b2a158615 --- .../firenotes/backend/firebase_helper_test.py | 176 ++++++++++++++++++ .../firebase/firenotes/backend/main_test.py | 2 +- 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 appengine/standard/firebase/firenotes/backend/firebase_helper_test.py diff --git a/appengine/standard/firebase/firenotes/backend/firebase_helper_test.py b/appengine/standard/firebase/firenotes/backend/firebase_helper_test.py new file mode 100644 index 000000000000..72a5f12d4d98 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/firebase_helper_test.py @@ -0,0 +1,176 @@ +# Copyright 2016 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. + +import datetime +import os +import time + +# Remove any existing pyjwt handlers, as firebase_helper will register +# its own. +try: + import jwt + jwt.unregister_algorithm('RS256') +except KeyError: + pass + +import mock +import pytest + +import firebase_helper + + +def test_get_firebase_certificates(testbed): + certs = firebase_helper.get_firebase_certificates() + assert certs + assert len(certs.keys()) + + +@pytest.fixture +def test_certificate(): + from cryptography import utils + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives import serialization + from cryptography.x509.oid import NameOID + + one_day = datetime.timedelta(1, 0, 0) + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend()) + public_key = private_key.public_key() + builder = x509.CertificateBuilder() + + builder = builder.subject_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u'example.com'), + ])) + builder = builder.issuer_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u'example.com'), + ])) + builder = builder.not_valid_before(datetime.datetime.today() - one_day) + builder = builder.not_valid_after(datetime.datetime.today() + one_day) + builder = builder.serial_number( + utils.int_from_bytes(os.urandom(20), "big") >> 1) + builder = builder.public_key(public_key) + + builder = builder.add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True) + + certificate = builder.sign( + private_key=private_key, algorithm=hashes.SHA256(), + backend=default_backend()) + + certificate_pem = certificate.public_bytes(serialization.Encoding.PEM) + public_key_bytes = certificate.public_key().public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo) + private_key_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption()) + + yield certificate, certificate_pem, public_key_bytes, private_key_bytes + + +def test_extract_public_key_from_certificate(test_certificate): + _, certificate_pem, public_key_bytes, _ = test_certificate + public_key = firebase_helper.extract_public_key_from_certificate( + certificate_pem) + assert public_key == public_key_bytes + + +def make_jwt(private_key_bytes, claims=None, headers=None): + jwt_claims = { + 'iss': 'http://example.com', + 'aud': 'test_audience', + 'user_id': '123', + 'sub': '123', + 'iat': int(time.time()), + 'exp': int(time.time()) + 60, + 'email': 'user@example.com' + } + + jwt_claims.update(claims if claims else {}) + if not headers: + headers = {} + + return jwt.encode( + jwt_claims, private_key_bytes, algorithm='RS256', + headers=headers) + + +def test_verify_auth_token(test_certificate, monkeypatch): + _, certificate_pem, _, private_key_bytes = test_certificate + + # The Firebase project ID is used as the JWT audience. + monkeypatch.setenv('FIREBASE_PROJECT_ID', 'test_audience') + + # Generate a jwt to include in the request. + jwt = make_jwt(private_key_bytes, headers={'kid': '1'}) + + # Make a mock request + request = mock.Mock() + request.headers = {'Authorization': 'Bearer {}'.format(jwt)} + + get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates') + with get_cert_patch as get_cert_mock: + # Make get_firebase_certificates return our test certificate. + get_cert_mock.return_value = {'1': certificate_pem} + claims = firebase_helper.verify_auth_token(request) + + assert claims['user_id'] == '123' + + +def test_verify_auth_token_no_auth_header(): + request = mock.Mock() + request.headers = {} + assert firebase_helper.verify_auth_token(request) is None + + +def test_verify_auth_token_invalid_key_id(test_certificate): + _, _, _, private_key_bytes = test_certificate + jwt = make_jwt(private_key_bytes, headers={'kid': 'invalid'}) + request = mock.Mock() + request.headers = {'Authorization': 'Bearer {}'.format(jwt)} + + get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates') + with get_cert_patch as get_cert_mock: + # Make get_firebase_certificates return no certificates + get_cert_mock.return_value = {} + assert firebase_helper.verify_auth_token(request) is None + + +def test_verify_auth_token_expired(test_certificate, monkeypatch): + _, certificate_pem, _, private_key_bytes = test_certificate + + # The Firebase project ID is used as the JWT audience. + monkeypatch.setenv('FIREBASE_PROJECT_ID', 'test_audience') + + # Generate a jwt to include in the request. + jwt = make_jwt( + private_key_bytes, + claims={'exp': int(time.time()) - 60}, + headers={'kid': '1'}) + + # Make a mock request + request = mock.Mock() + request.headers = {'Authorization': 'Bearer {}'.format(jwt)} + + get_cert_patch = mock.patch('firebase_helper.get_firebase_certificates') + with get_cert_patch as get_cert_mock: + # Make get_firebase_certificates return our test certificate. + get_cert_mock.return_value = {'1': certificate_pem} + assert firebase_helper.verify_auth_token(request) is None diff --git a/appengine/standard/firebase/firenotes/backend/main_test.py b/appengine/standard/firebase/firenotes/backend/main_test.py index 9c7ef8b54ee4..a11e988b0d92 100644 --- a/appengine/standard/firebase/firenotes/backend/main_test.py +++ b/appengine/standard/firebase/firenotes/backend/main_test.py @@ -16,8 +16,8 @@ from google.appengine.ext import ndb import jwt -import pytest import mock +import pytest @pytest.fixture From 86a74d4cfa3ba9dc1ed99cc8128ebfcb22188459 Mon Sep 17 00:00:00 2001 From: Tristan Vanech Date: Tue, 6 Sep 2016 16:52:46 -0700 Subject: [PATCH 7/8] Added region tags --- .../firebase/firenotes/backend/firebase_helper.py | 11 ++++++++++- .../standard/firebase/firenotes/backend/main.py | 15 ++++++++++----- .../standard/firebase/firenotes/frontend/main.js | 10 ++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/appengine/standard/firebase/firenotes/backend/firebase_helper.py b/appengine/standard/firebase/firenotes/backend/firebase_helper.py index 8ecbd4a9b565..875e9cf79922 100644 --- a/appengine/standard/firebase/firenotes/backend/firebase_helper.py +++ b/appengine/standard/firebase/firenotes/backend/firebase_helper.py @@ -28,13 +28,14 @@ # For App Engine, pyjwt needs to use PyCrypto instead of Cryptography. jwt.register_algorithm('RS256', RSAAlgorithm(RSAAlgorithm.SHA256)) +# [START fetch_certificates] # This URL contains a list of active certificates used to sign Firebase # auth tokens. FIREBASE_CERTIFICATES_URL = ( 'https://www.googleapis.com/robot/v1/metadata/x509/' 'securetoken@system.gserviceaccount.com') - +# [START get_firebase_certificates] def get_firebase_certificates(): """Fetches the current Firebase certificates. @@ -53,8 +54,11 @@ def get_firebase_certificates(): certificates = json.loads(data) return certificates +# [END get_firebase_certificates] +# [END fetch_certificates] +# [START extract_public_key_from_certificate] def extract_public_key_from_certificate(x509_certificate): """Extracts the PEM public key from an x509 certificate.""" der_certificate_string = ssl.PEM_cert_to_DER_cert(x509_certificate) @@ -68,8 +72,10 @@ def extract_public_key_from_certificate(x509_certificate): subject_public_key_info = tbs_certification[6] return subject_public_key_info +# [EMD extract_public_key_from_certificate] +# [START verify_auth_token] def verify_auth_token(request): """Verifies the JWT auth token in the request. @@ -98,6 +104,7 @@ def verify_auth_token(request): # JWT signature. public_key = extract_public_key_from_certificate(certificate) + # [START decrypt_token] try: claims = jwt.decode( request_jwt, @@ -107,5 +114,7 @@ def verify_auth_token(request): except jwt.exceptions.InvalidTokenError as e: logging.warning('JWT verification failed: {}'.format(e)) return None + # [END decrypt_token] return claims +# [END verify_auth_token] diff --git a/appengine/standard/firebase/firenotes/backend/main.py b/appengine/standard/firebase/firenotes/backend/main.py index c54acbb15704..f4df5ca93ad3 100644 --- a/appengine/standard/firebase/firenotes/backend/main.py +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -25,7 +25,7 @@ app = Flask(__name__) flask_cors.CORS(app) - +# [START note] class Note(ndb.Model): """NDB model class for a user's note. @@ -34,8 +34,9 @@ class Note(ndb.Model): friendly_id = ndb.StringProperty() message = ndb.TextProperty() created = ndb.DateTimeProperty(auto_now_add=True) +# [END note] - +# [START query_database] def query_database(user_id): """Fetches all notes associated with user_id. @@ -56,8 +57,9 @@ def query_database(user_id): }) return note_messages +# [END query_database] - +# [START list_notes] @app.route('/notes', methods=['GET']) def list_notes(): """Returns a list of notes added by the current Firebase user.""" @@ -70,8 +72,9 @@ def list_notes(): notes = query_database(claims['sub']) return jsonify(notes) +# [END list_notes] - +# [START add_note] @app.route('/notes', methods=['POST', 'PUT']) def add_note(): """ @@ -87,6 +90,7 @@ def add_note(): if not claims: return 'Unauthorized', 401 + # [START create_entity] data = request.get_json() # Populates note properties according to the model, @@ -97,12 +101,13 @@ def add_note(): # Some providers do not provide one of these so either can be used. note.friendly_id = claims.get('name', claims.get('email', 'Unknown')) + # [END create_entity] # Stores note in database. note.put() return 'OK', 200 - +# [END add_note] @app.errorhandler(500) def server_error(e): diff --git a/appengine/standard/firebase/firenotes/frontend/main.js b/appengine/standard/firebase/firenotes/frontend/main.js index 34a7685f9fca..c5f3580be4a3 100644 --- a/appengine/standard/firebase/firenotes/frontend/main.js +++ b/appengine/standard/firebase/firenotes/frontend/main.js @@ -36,6 +36,7 @@ $(function(){ firebase.initializeApp(config); + // [START onAuthStateChanged] firebase.auth().onAuthStateChanged(function(user) { if (user) { $('#logged-out').hide(); @@ -61,11 +62,13 @@ $(function(){ $('#logged-out').show(); } + // [END onAuthStateChanged] }); } + // [START configureFirebaseLoginWidget] // Firebase log-in widget function configureFirebaseLoginWidget() { var uiConfig = { @@ -85,7 +88,9 @@ $(function(){ var ui = new firebaseui.auth.AuthUI(firebase.auth()); ui.start('#firebaseui-auth-container', uiConfig); } + // [END configureFirebaseLoginWidget] + // [START fetchNotes] // Fetch notes from the backend. function fetchNotes() { $.ajax(backendHostUrl + '/notes', { @@ -102,7 +107,9 @@ $(function(){ }); }); } + // [END fetchNotes] + // [START signOutBtn] // Sign out a user var signOutBtn =$('#sign-out'); signOutBtn.click(function(event) { @@ -114,7 +121,9 @@ $(function(){ console.log(error); }); }); + // [END signOutBtn] + // [START saveNoteBtn] // Save a note to the backend var saveNoteBtn = $('#add-note'); saveNoteBtn.click(function(event) { @@ -139,6 +148,7 @@ $(function(){ }); }); + // [END saveNoteBtn] configureFirebaseLogin(); configureFirebaseLoginWidget(); From 1d12d1546ce8233956056de5e17fc2cb1d4f6bc5 Mon Sep 17 00:00:00 2001 From: Tristan Vanech Date: Wed, 7 Sep 2016 10:27:54 -0700 Subject: [PATCH 8/8] Added region tags --- .../standard/firebase/firenotes/backend/firebase_helper.py | 1 + appengine/standard/firebase/firenotes/backend/main.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/appengine/standard/firebase/firenotes/backend/firebase_helper.py b/appengine/standard/firebase/firenotes/backend/firebase_helper.py index 875e9cf79922..860ac96fccef 100644 --- a/appengine/standard/firebase/firenotes/backend/firebase_helper.py +++ b/appengine/standard/firebase/firenotes/backend/firebase_helper.py @@ -35,6 +35,7 @@ 'https://www.googleapis.com/robot/v1/metadata/x509/' 'securetoken@system.gserviceaccount.com') + # [START get_firebase_certificates] def get_firebase_certificates(): """Fetches the current Firebase certificates. diff --git a/appengine/standard/firebase/firenotes/backend/main.py b/appengine/standard/firebase/firenotes/backend/main.py index f4df5ca93ad3..aaa74d4eb192 100644 --- a/appengine/standard/firebase/firenotes/backend/main.py +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -25,6 +25,7 @@ app = Flask(__name__) flask_cors.CORS(app) + # [START note] class Note(ndb.Model): """NDB model class for a user's note. @@ -36,6 +37,7 @@ class Note(ndb.Model): created = ndb.DateTimeProperty(auto_now_add=True) # [END note] + # [START query_database] def query_database(user_id): """Fetches all notes associated with user_id. @@ -59,6 +61,7 @@ def query_database(user_id): return note_messages # [END query_database] + # [START list_notes] @app.route('/notes', methods=['GET']) def list_notes(): @@ -74,6 +77,7 @@ def list_notes(): return jsonify(notes) # [END list_notes] + # [START add_note] @app.route('/notes', methods=['POST', 'PUT']) def add_note(): @@ -109,6 +113,7 @@ def add_note(): return 'OK', 200 # [END add_note] + @app.errorhandler(500) def server_error(e): # Log the error and stacktrace.