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..bb6c6628996e --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/app.yaml @@ -0,0 +1,18 @@ +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: + # Replace with your Firebase project ID. + FIREBASE_PROJECT_ID: '' 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..860ac96fccef --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/firebase_helper.py @@ -0,0 +1,121 @@ +# 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 google.appengine.api import urlfetch +from google.appengine.api import 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)) + +# [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. + + 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 +# [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) + + # 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 +# [EMD extract_public_key_from_certificate] + + +# [START verify_auth_token] +def verify_auth_token(request): + """Verifies the JWT auth token in the request. + + 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 + + # 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) + + # [START decrypt_token] + 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 + # [END decrypt_token] + + return claims +# [END verify_auth_token] 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/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..aaa74d4eb192 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -0,0 +1,122 @@ +# 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) + + +# [START note] +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) +# [END note] + + +# [START query_database] +def query_database(user_id): + """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() + + note_messages = [] + + for note in notes: + note_messages.append({ + 'friendly_id': note.friendly_id, + 'message': note.message, + 'created': note.created + }) + + 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.""" + + # Verify Firebase auth. + claims = firebase_helper.verify_auth_token(request) + if not claims: + return 'Unauthorized', 401 + + notes = query_database(claims['sub']) + + return jsonify(notes) +# [END list_notes] + + +# [START add_note] +@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." + } + """ + + # Verify Firebase auth. + claims = firebase_helper.verify_auth_token(request) + if not claims: + return 'Unauthorized', 401 + + # [START create_entity] + data = request.get_json() + + # 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']) + + # 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): + # 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..a11e988b0d92 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/main_test.py @@ -0,0 +1,94 @@ +# 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 json + +from google.appengine.ext import ndb +import jwt +import mock +import pytest + + +@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() + + +@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/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..c5f3580be4a3 --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/main.js @@ -0,0 +1,156 @@ +// 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. + 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); + + // [START onAuthStateChanged] + 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(); + + } + // [END onAuthStateChanged] + + }); + + } + + // [START configureFirebaseLoginWidget] + // 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); + } + // [END configureFirebaseLoginWidget] + + // [START fetchNotes] + // 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)); + }); + }); + } + // [END fetchNotes] + + // [START signOutBtn] + // 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); + }); + }); + // [END signOutBtn] + + // [START saveNoteBtn] + // 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(); + }); + + }); + // [END saveNoteBtn] + + 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..19b4f1d69bd1 --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/style.css @@ -0,0 +1,44 @@ +/* + 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; +} + +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; +} 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