From 6fa55e426df360ebf322d79bd5cb0a7703520968 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Tue, 29 Oct 2019 09:17:55 -0700 Subject: [PATCH 01/25] working prototype with postgres --- app/__init__.py | 10 ++++- app/config.py | 15 +++++--- app/models/models.py | 81 +++++++++++++++++++++++++++------------- app/routes.py | 1 + mynotes/reactbasics.txt | 47 +++++++++++++++++++++++ static/.env | 1 + static/public/index.html | 50 +++++++++++++------------ static/src/index.js | 2 + 8 files changed, 152 insertions(+), 55 deletions(-) create mode 100644 mynotes/reactbasics.txt diff --git a/app/__init__.py b/app/__init__.py index 2b8cbf8..add049a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -11,5 +11,13 @@ app = Flask(__name__, static_folder=STATIC_FOLDER, template_folder=TEMPLATE_FOLDER) app.config.from_object('app.config.ProductionConfig') +app.config['SQLALCHEMY_DATABASE_URI'] = f"postgresql://{os.environ['DBUSER']}:{os.environ['DBPASS']}@{os.environ['DBHOST']}/{os.environ['DBNAME']}" + db = SQLAlchemy(app) -bcrypt = Bcrypt(app) \ No newline at end of file +bcrypt = Bcrypt(app) + + +# $Env:DBHOST = "postgresdb1024.postgres.database.azure.com" +# $Env:DBUSER = "demo@postgresdb1024" +# $Env:DBNAME = "team_standup" +# $Env:DBPASS = "pass@123" \ No newline at end of file diff --git a/app/config.py b/app/config.py index 3ecb2dd..92cb0c6 100644 --- a/app/config.py +++ b/app/config.py @@ -7,12 +7,17 @@ class BaseConfig(object): class TestingConfig(BaseConfig): - SQLALCHEMY_DATABASE_URI = 'sqlite:///db.sqlite3' + # SQLALCHEMY_DATABASE_URI = 'sqlite:///db.sqlite3' + print("testingConfig") + SQLALCHEMY_DATABASE_URI = f"postgresql://{os.environ['DBUSER']}:{os.environ['DBPASS']}@{os.environ['DBHOST']}/{os.environ['DBNAME']}" DEBUG = True - SECRET_KEY = 'somekey' + SECRET_KEY = "somekey" # needed but don't know why class ProductionConfig(BaseConfig): - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', TestingConfig.SQLALCHEMY_DATABASE_URI) - SECRET_KEY = os.environ.get('SECRET_KEY', TestingConfig.SECRET_KEY) - \ No newline at end of file + print("productionConfig") + SQLALCHEMY_DATABASE_URI = os.environ.get( + "DATABASE_URL", TestingConfig.SQLALCHEMY_DATABASE_URI + ) + SECRET_KEY = os.environ.get("SECRET_KEY", TestingConfig.SECRET_KEY) + diff --git a/app/models/models.py b/app/models/models.py index 56dc409..2bf4ad0 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,36 +1,50 @@ from datetime import datetime, timedelta from sqlalchemy.exc import IntegrityError from app import db, bcrypt +from random import randint + + +_MIN = 1 +_MAX = 1000000000 class User(db.Model): - id = db.Column(db.Integer(), primary_key=True) + id = db.Column(db.BigInteger(), primary_key=True) email = db.Column(db.String(255), unique=True) password = db.Column(db.String(255)) first_name = db.Column(db.String(255)) last_name = db.Column(db.String(255)) - def __init__(self, first_name, last_name, email, password): + self.id = randint(_MIN, _MAX) self.first_name = first_name self.last_name = last_name self.email = email self.password = User.hashed_password(password) - + @staticmethod def create_user(payload): + print("models.py: create_user(payload)") + print(payload) user = User( email=payload["email"], password=payload["password"], first_name=payload["first_name"], last_name=payload["last_name"], ) - + # print(user.id) + # db.session.add(user) + # print("models.py: added user") + # db.session.commit() + # print("models.py: committed") try: db.session.add(user) + print("models.py: added user") db.session.commit() + print("models.py: committed user to db") return True except IntegrityError: + print("models.py: failed to commit user to db") return False @staticmethod @@ -53,16 +67,17 @@ def get_user_with_email_and_password(email, password): class Task(db.Model): class STATUS: - COMPLETED = 'COMPLETED' - IN_PROGRESS = 'IN_PROGRESS' + COMPLETED = "COMPLETED" + IN_PROGRESS = "IN_PROGRESS" - id = db.Column(db.Integer(), primary_key=True) + id = db.Column(db.BigInteger(), primary_key=True) date = db.Column(db.DateTime()) task = db.Column(db.String(255)) user_id = db.Column(db.String(255)) status = db.Column(db.String(255)) - + def __init__(self, task, user_id, status): + self.id = randint(_MIN, _MAX) self.date = datetime.utcnow().date() self.task = task self.user_id = user_id @@ -70,29 +85,33 @@ def __init__(self, task, user_id, status): @staticmethod def add_task(task, user_id, status): - task = Task( - task=task, - user_id=user_id, - status=status - ) - + print("models.py: add_task()") + task = Task(task=task, user_id=user_id, status=status) + print(task) db.session.add(task) + print("models.py: added task") try: db.session.commit() + print("models.py: committed addition of task_" + str(task.id)) return True, task.id except IntegrityError: + print("models.py: failed to add task--Integrity Error") return False, None - + @staticmethod def get_latest_tasks(): + print("models.py: get_latest_tasks()") user_to_task = {} result = db.engine.execute( """SELECT t.id, t.date, t.task, t.user_id, t.status, u.first_name, u.last_name from task t INNER JOIN "user" u - on t.user_id = u.email""") # join with users table - + on t.user_id = u.email""" + ) # join with users table + + print("result from db: ") + print(result) for t in result: if t.user_id in user_to_task: user_to_task.get(t.user_id).append(dict(t)) @@ -103,38 +122,48 @@ def get_latest_tasks(): @staticmethod def get_tasks_for_user(user_id): + print("models.py: get_tasks_for_user()") return Task.query.filter_by(user_id=user_id) @staticmethod def delete_task(task_id): task_to_delete = Task.query.filter_by(id=task_id).first() db.session.delete(task_to_delete) + print("models.py: deleted task") try: db.session.commit() + print("models.py: committed deletion") return True except IntegrityError: + print("models.py: failed to delete") return False - + @staticmethod def edit_task(task_id, task, status): task_to_edit = Task.query.filter_by(id=task_id).first() task_to_edit.task = task task_to_edit.status = status + print("models.py: edit_task ") + print("task: " + task + " task_id: " + str(task_id)) + print("task_to_edit: " + str(task_to_edit)) + try: db.session.commit() + print("models.py: commited edit") return True except IntegrityError: + print("models.py: failed to commit edit") return False @property def serialize(self): - """Return object data in easily serializeable format""" - return { - 'id' : self.id, - 'date' : self.date.strftime("%Y-%m-%d"), - 'task' : self.task, - 'user_id' : self.user_id, - 'status' : self.status, - } + """Return object data in easily serializeable format""" + return { + "id": self.id, + "date": self.date.strftime("%Y-%m-%d"), + "task": self.task, + "user_id": self.user_id, + "status": self.status, + } diff --git a/app/routes.py b/app/routes.py index 51e0db1..ab824df 100644 --- a/app/routes.py +++ b/app/routes.py @@ -32,6 +32,7 @@ def create_user(): success = User.create_user(incoming) if not success: + print("routes.py: fail to create user") return jsonify(message="User with that email already exists"), 409 new_user = User.query.filter_by(email=incoming["email"]).first() diff --git a/mynotes/reactbasics.txt b/mynotes/reactbasics.txt new file mode 100644 index 0000000..de666d3 --- /dev/null +++ b/mynotes/reactbasics.txt @@ -0,0 +1,47 @@ +//props + +class App extends Component{ + + state = { + todos: [ + { + id: 1, + title: 'to do 1' + }, + { + id: 2, + title: 'to do 2' + } + ] + } + + render(){ + console.log(this.state.todos) + return( +
+ //code goes into {} +
+ ) + } + +} + + +import PropTypes from 'prop-types'; +//now here in the todos class, can access the state passed through 'this.props' +class Todos extends Component{ + render(){ //only required life cycle method + console.log(this.props.todos) + return( + return this.props.todos.map((todo) =>( + //bc iteration, there's a list of , list needs each item to have unique key + )) + ) + } +} + +//PropTypes: array, object +Todos.propTypes = { //'Todos' should match the name of class + todos: PropTypes.array.isRequired +} + diff --git a/static/.env b/static/.env index b885521..21fb828 100644 --- a/static/.env +++ b/static/.env @@ -1 +1,2 @@ NODE_PATH=./src + diff --git a/static/public/index.html b/static/public/index.html index 39d89eb..fefd31e 100644 --- a/static/public/index.html +++ b/static/public/index.html @@ -1,21 +1,22 @@ - - - - - - - - - - - - - - VS Code Standup - - - -
- - - + + + + \ No newline at end of file diff --git a/static/src/index.js b/static/src/index.js index 8d42e0a..38faa64 100644 --- a/static/src/index.js +++ b/static/src/index.js @@ -1,5 +1,7 @@ import React from "react"; import ReactDOM from "react-dom"; + + import history from "./history/history"; import { Route, Router, Switch } from "react-router-dom"; import configureStore from "./store/configureStore"; From d8a8cb2689b3335999a0c9bdafb9c8a3cc1cc023 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Tue, 29 Oct 2019 11:00:18 -0700 Subject: [PATCH 02/25] this app uses cosmos instead of postgres --- README.md | 2 +- app/__init__.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 4fb26d1..cc0ea55 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Flask + React + Postgres Starter +# Flask + React + Cosmos This is a minimal sample Flask and React starter code that demonstrates how both frameworks can be used together in a single page web Application. diff --git a/app/__init__.py b/app/__init__.py index add049a..501cbf3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -17,7 +17,3 @@ bcrypt = Bcrypt(app) -# $Env:DBHOST = "postgresdb1024.postgres.database.azure.com" -# $Env:DBUSER = "demo@postgresdb1024" -# $Env:DBNAME = "team_standup" -# $Env:DBPASS = "pass@123" \ No newline at end of file From c35e265433813ef71adb8b0dd1038cbb4d8830bf Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Mon, 4 Nov 2019 17:11:44 -0800 Subject: [PATCH 03/25] working prototype with cosmos as db --- app/__init__.py | 18 +- app/config.py | 31 +++- app/models/models.py | 201 +++++++++++++---------- app/routes.py | 42 ++--- app/utils/auth.py | 26 +-- manage.py | 4 - static/src/actions/data.jsx | 5 +- static/src/utils/http_functions.js | 6 +- static/src/views/Dashboard/Dashboard.jsx | 28 ++-- 9 files changed, 206 insertions(+), 155 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 501cbf3..1517d27 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,19 +1,25 @@ from flask import Flask from flask_bcrypt import Bcrypt from flask_sqlalchemy import SQLAlchemy +from azure.cosmos import errors, CosmosClient import os APP_DIR = os.path.abspath(os.path.dirname(__file__)) -STATIC_FOLDER = os.path.join(APP_DIR, '../static/build/static') # Where your webpack build output folder is -TEMPLATE_FOLDER = os.path.join(APP_DIR, '../static/build') # Where your index.html file is located +STATIC_FOLDER = os.path.join( + APP_DIR, "../static/build/static" +) # Where your webpack build output folder is +TEMPLATE_FOLDER = os.path.join( + APP_DIR, "../static/build" +) # Where your index.html file is located app = Flask(__name__, static_folder=STATIC_FOLDER, template_folder=TEMPLATE_FOLDER) -app.config.from_object('app.config.ProductionConfig') +app.config.from_object("app.config.ProductionConfig") -app.config['SQLALCHEMY_DATABASE_URI'] = f"postgresql://{os.environ['DBUSER']}:{os.environ['DBPASS']}@{os.environ['DBHOST']}/{os.environ['DBNAME']}" -db = SQLAlchemy(app) -bcrypt = Bcrypt(app) +key = app.config["SECRET_KEY"] +uri = app.config["COSMOS_DB_URI"] +client = CosmosClient(uri, credential=key) +bcrypt = Bcrypt(app) diff --git a/app/config.py b/app/config.py index 92cb0c6..4bc711c 100644 --- a/app/config.py +++ b/app/config.py @@ -7,17 +7,32 @@ class BaseConfig(object): class TestingConfig(BaseConfig): - # SQLALCHEMY_DATABASE_URI = 'sqlite:///db.sqlite3' - print("testingConfig") - SQLALCHEMY_DATABASE_URI = f"postgresql://{os.environ['DBUSER']}:{os.environ['DBPASS']}@{os.environ['DBHOST']}/{os.environ['DBNAME']}" + COSMOS_DB_URI = "https://cosmosdb1029.documents.azure.com:443/" + + # not pushing keys to github + SECRET_KEY = "" #key to cosmos + AZURE_CLIENT_ID = "" + AZURE_TENANT_ID = "" + AZURE_CLIENT_SECRET = "" DEBUG = True - SECRET_KEY = "somekey" # needed but don't know why class ProductionConfig(BaseConfig): - print("productionConfig") - SQLALCHEMY_DATABASE_URI = os.environ.get( - "DATABASE_URL", TestingConfig.SQLALCHEMY_DATABASE_URI - ) + COSMOS_DB_URI = os.environ.get("COSMOS_DB_URI", TestingConfig.COSMOS_DB_URI) + + # this is the key to cosmos db, but also used to make token for authentication purpose + # probably shouldn't use the same key for different purposes and just let this key be the key to the db SECRET_KEY = os.environ.get("SECRET_KEY", TestingConfig.SECRET_KEY) + + # created an app service on Portal (b/c eventually want to use App Service to host app) + # creds returned by making that app a service principal + # also, in order to use Identity, need a Service Principal + AZURE_TENANT_ID = os.environ.get("AZURE_TENANT_ID", TestingConfig.AZURE_TENANT_ID) + AZURE_CLIENT_ID = os.environ.get("AZURE_CLIENT_ID", TestingConfig.AZURE_CLIENT_ID) + AZURE_CLIENT_SECRET = os.environ.get("AZURE_CLIENT_SECRET", TestingConfig.AZURE_CLIENT_SECRET) + + + + + diff --git a/app/models/models.py b/app/models/models.py index 2bf4ad0..f18b484 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,50 +1,58 @@ from datetime import datetime, timedelta from sqlalchemy.exc import IntegrityError -from app import db, bcrypt +from app import client, bcrypt from random import randint +import json, time +from azure.cosmos import CosmosClient, PartitionKey +# for task id _MIN = 1 _MAX = 1000000000 +user_container_name = "users" +task_container_name = "tasks" +db_name = "team_standup" -class User(db.Model): - id = db.Column(db.BigInteger(), primary_key=True) - email = db.Column(db.String(255), unique=True) - password = db.Column(db.String(255)) - first_name = db.Column(db.String(255)) - last_name = db.Column(db.String(255)) +# get container by walking down resource hierarchy +# client -> db -> container +db = client.get_database_client(db_name) +user_container = db.get_container_client(user_container_name) +task_container = db.get_container_client(task_container_name) + + +class User: def __init__(self, first_name, last_name, email, password): - self.id = randint(_MIN, _MAX) + # self.id = str(randint(_MIN, _MAX)) # id must be a string for cosmos + self.id = email self.first_name = first_name self.last_name = last_name self.email = email - self.password = User.hashed_password(password) + self.password = password @staticmethod - def create_user(payload): - print("models.py: create_user(payload)") - print(payload) - user = User( - email=payload["email"], - password=payload["password"], - first_name=payload["first_name"], - last_name=payload["last_name"], - ) - # print(user.id) - # db.session.add(user) - # print("models.py: added user") - # db.session.commit() - # print("models.py: committed") + def create_user(input): try: - db.session.add(user) - print("models.py: added user") - db.session.commit() - print("models.py: committed user to db") + user = User( + first_name=input["first_name"], + last_name=input["last_name"], + email=input["email"], + password=input["password"], + ) + + user_container.upsert_item( + { + "id": user.id, + "first_name": user.first_name, + "last_name": user.last_name, + "email": user.email, + "password": user.password, + } + ) + return True except IntegrityError: - print("models.py: failed to commit user to db") return False @staticmethod @@ -53,108 +61,135 @@ def hashed_password(password): @staticmethod def get_user_by_id(user_id): - user = User.query.filter_by(id=user_id).first() + query = "SELECT * FROM users u where u.id=" + user_id + users = user_container.query_items( + query=query, enable_cross_partition_query=True + ) + user = list(users)[0] + + return user + + @staticmethod + def get_user_by_email(user_email): + query = "SELECT * FROM users u where u.email='" + user_email + "'" + users = user_container.query_items( + query=query, enable_cross_partition_query=True + ) + user = list(users)[0] + return user @staticmethod def get_user_with_email_and_password(email, password): - user = User.query.filter_by(email=email).first() - if user and bcrypt.check_password_hash(user.password, password): - return user + + query = ( + "SELECT * FROM users u where u.email='" + + email + + "'" + + " and u.password='" + + password + + "'" + ) + users = user_container.query_items( + query=query, enable_cross_partition_query=True + ) + + if users: + user = list(users) + return user[0] else: return None -class Task(db.Model): +class Task: class STATUS: COMPLETED = "COMPLETED" IN_PROGRESS = "IN_PROGRESS" - id = db.Column(db.BigInteger(), primary_key=True) - date = db.Column(db.DateTime()) - task = db.Column(db.String(255)) - user_id = db.Column(db.String(255)) - status = db.Column(db.String(255)) - def __init__(self, task, user_id, status): - self.id = randint(_MIN, _MAX) - self.date = datetime.utcnow().date() + self.id = str(randint(_MIN, _MAX)) + self.date = str(datetime.utcnow().date()) self.task = task self.user_id = user_id self.status = status @staticmethod def add_task(task, user_id, status): - print("models.py: add_task()") - task = Task(task=task, user_id=user_id, status=status) - print(task) - db.session.add(task) - print("models.py: added task") try: - db.session.commit() - print("models.py: committed addition of task_" + str(task.id)) + task = Task(task=task, user_id=user_id, status=status) + task_container.upsert_item( + { + "id": task.id, + "date": task.date, + "task": task.task, + "user_id": task.user_id, #user_id is email here + "status": task.status, + } + ) + return True, task.id except IntegrityError: - print("models.py: failed to add task--Integrity Error") return False, None @staticmethod def get_latest_tasks(): - print("models.py: get_latest_tasks()") user_to_task = {} - result = db.engine.execute( - """SELECT t.id, t.date, t.task, t.user_id, t.status, u.first_name, u.last_name - from task t - INNER JOIN "user" u - on t.user_id = u.email""" - ) # join with users table - - print("result from db: ") - print(result) - for t in result: - if t.user_id in user_to_task: - user_to_task.get(t.user_id).append(dict(t)) - else: - user_to_task[t.user_id] = [dict(t)] + query = "SELECT * FROM users" + users = user_container.query_items( + query=query, enable_cross_partition_query=True + ) + + cnt = 0 + for u in users: + cnt+=1 + task_query = "SELECT * FROM tasks t where t.user_id='" + u["id"] + "'" + tasks = task_container.query_items( + query=task_query, enable_cross_partition_query=True + ) + for t in tasks: + t["first_name"] = u["first_name"] + t["last_name"] = u["last_name"] + if t["user_id"] in user_to_task: + user_to_task.get(t["user_id"]).append(t) + else: + user_to_task[t["user_id"]] = [t] return user_to_task @staticmethod def get_tasks_for_user(user_id): - print("models.py: get_tasks_for_user()") - return Task.query.filter_by(user_id=user_id) + query = "SELECT * FROM tasks t where t.user_id='" + user_id + "'" + tasks = task_container.query_items( + query=query, enable_cross_partition_query=True + ) + return tasks @staticmethod def delete_task(task_id): - task_to_delete = Task.query.filter_by(id=task_id).first() - db.session.delete(task_to_delete) - print("models.py: deleted task") - try: - db.session.commit() - print("models.py: committed deletion") + query = "SELECT * FROM tasks t where t.id='" + task_id + "'" + tasks = task_container.query_items( + query=query, enable_cross_partition_query=True + ) + task = list(tasks)[0] + task_container.delete_item(task, partition_key=task_id) + return True except IntegrityError: - print("models.py: failed to delete") return False @staticmethod def edit_task(task_id, task, status): - task_to_edit = Task.query.filter_by(id=task_id).first() - task_to_edit.task = task - task_to_edit.status = status + try: + task_to_edit = task_container.read_item(task_id, partition_key=task_id) + task_to_edit["task"] = task + task_to_edit["status"] = status - print("models.py: edit_task ") - print("task: " + task + " task_id: " + str(task_id)) - print("task_to_edit: " + str(task_to_edit)) + task_container.upsert_item(task_to_edit) - try: - db.session.commit() - print("models.py: commited edit") return True except IntegrityError: - print("models.py: failed to commit edit") return False @property diff --git a/app/routes.py b/app/routes.py index ab824df..302a5d2 100644 --- a/app/routes.py +++ b/app/routes.py @@ -8,45 +8,42 @@ import os -@app.route('/', methods=['GET']) +@app.route("/", methods=["GET"]) def index(): - return render_template('index.html') + return render_template("index.html") -@app.route('/', methods=['GET']) +@app.route("/", methods=["GET"]) def any_root_path(path): - return render_template('index.html') + return render_template("index.html") @app.route("/api/user", methods=["GET"]) @requires_auth def get_user(): - return jsonify(result=g.current_user, - tasks=Task.get_latest_tasks()) + return jsonify(result=g.current_user, tasks=Task.get_latest_tasks()) @app.route("/api/create_user", methods=["POST"]) def create_user(): incoming = request.get_json() - success = User.create_user(incoming) if not success: - print("routes.py: fail to create user") return jsonify(message="User with that email already exists"), 409 - new_user = User.query.filter_by(email=incoming["email"]).first() - - return jsonify( - id=new_user.id, - token=generate_token(new_user) - ) + new_user = User.get_user_by_email(incoming["email"]) + + return jsonify(id=new_user["id"], token=generate_token(new_user)) @app.route("/api/get_token", methods=["POST"]) def get_token(): incoming = request.get_json() - user = User.get_user_with_email_and_password(incoming["email"], incoming["password"]) + user = User.get_user_with_email_and_password( + incoming["email"], incoming["password"] + ) + if user: return jsonify(token=generate_token(user)) @@ -68,11 +65,8 @@ def is_token_valid(): @requires_auth def submit_task(): incoming = request.get_json() - success, id = Task.add_task( - incoming.get("task"), - incoming.get("user_id"), - incoming.get("status") + incoming.get("task"), incoming.get("user_id"), incoming.get("status") ) if not success: @@ -81,7 +75,6 @@ def submit_task(): return jsonify(success=True, id=id) - @app.route("/api/get_tasks_for_user", methods=["POST"]) @requires_auth def get_tasks_for_user(): @@ -91,12 +84,13 @@ def get_tasks_for_user(): tasks=[i.serialize for i in Task.get_tasks_for_user(incoming["user_id"]).all()] ) + @app.route("/api/delete_task", methods=["POST"]) @requires_auth def delete_task(): incoming = request.get_json() + success = Task.delete_task(incoming.get("task_id")) - success = Task.delete_task(incoming.get('task_id')) if not success: return jsonify(message="Error deleting task"), 409 @@ -107,12 +101,10 @@ def delete_task(): @requires_auth def edit_task(): incoming = request.get_json() - success = Task.edit_task( - incoming.get('task_id'), - incoming.get('task'), - incoming.get('status') + incoming.get("task_id"), incoming.get("task"), incoming.get("status") ) + if not success: return jsonify(message="Error editing task"), 409 diff --git a/app/utils/auth.py b/app/utils/auth.py index 9bd31ae..1ebbf58 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -6,21 +6,24 @@ TWO_WEEKS = 1209600 -SECRET_KEY = app.config['SECRET_KEY'] +SECRET_KEY = app.config["SECRET_KEY"] def generate_token(user, expiration=TWO_WEEKS): s = Serializer(SECRET_KEY, expires_in=expiration) - token = s.dumps({ - 'id': user.id, - 'email': user.email, - 'first_name': user.first_name, - 'last_name': user.last_name - }).decode('utf-8') + token = s.dumps( + { + "id": user["id"], + "email": user["email"], + "first_name": user["first_name"], + "last_name": user["last_name"], + } + ).decode("utf-8") return token def verify_token(token): s = Serializer(SECRET_KEY) + try: data = s.loads(token) except (BadSignature, SignatureExpired): @@ -31,14 +34,17 @@ def verify_token(token): def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): - token = request.headers.get('Authorization', None) + token = request.headers.get("Authorization", None) if token: - string_token = token.encode('ascii', 'ignore') + string_token = token.encode("ascii", "ignore") user = verify_token(string_token) if user: g.current_user = user return f(*args, **kwargs) - return jsonify(message="Authentication is required to access this resource"), 401 + return ( + jsonify(message="Authentication is required to access this resource"), + 401, + ) return decorated diff --git a/manage.py b/manage.py index 01e437c..5680cd4 100644 --- a/manage.py +++ b/manage.py @@ -2,12 +2,8 @@ from flask_migrate import Migrate, MigrateCommand from app.routes import app -from app import db -migrate = Migrate(app, db) manager = Manager(app) - -# migrations manager.add_command('db', MigrateCommand) diff --git a/static/src/actions/data.jsx b/static/src/actions/data.jsx index 2ad1b85..46bcb6c 100644 --- a/static/src/actions/data.jsx +++ b/static/src/actions/data.jsx @@ -14,7 +14,7 @@ export function receiveProtectedData(data, users) { users = Object.entries(users); if (user_has_tasks) { - users = users.sort(function(x, y) { + users = users.sort(function (x, y) { if (x[0] === data.email) { return -1 } else if (y[0] === data.email) { @@ -25,6 +25,7 @@ export function receiveProtectedData(data, users) { }); } + return { type: RECEIVE_PROTECTED_DATA, payload: { @@ -86,7 +87,7 @@ export function storeTask(token, email, task, updateTaskIdCallback) { //Pass a f .catch(error => { console.log(error); } - ); + ); }; } diff --git a/static/src/utils/http_functions.js b/static/src/utils/http_functions.js index 740a4bf..03be720 100644 --- a/static/src/utils/http_functions.js +++ b/static/src/utils/http_functions.js @@ -40,14 +40,14 @@ export function store_task(token, user_id, task, status) { user_id, status }, - tokenConfig(token)); + tokenConfig(token)); } export function delete_task(token, task_id) { return axios.post("/api/delete_task", { task_id }, - tokenConfig(token)); + tokenConfig(token)); } export function edit_task(token, task_id, task, status) { @@ -56,5 +56,5 @@ export function edit_task(token, task_id, task, status) { task, status }, - tokenConfig(token)); + tokenConfig(token)); } \ No newline at end of file diff --git a/static/src/views/Dashboard/Dashboard.jsx b/static/src/views/Dashboard/Dashboard.jsx index 3e54dd1..524df0e 100644 --- a/static/src/views/Dashboard/Dashboard.jsx +++ b/static/src/views/Dashboard/Dashboard.jsx @@ -23,20 +23,20 @@ class Dashboard extends React.Component { {!this.props.loaded ? (

Loading data...

) : ( - [ - (!this.props.data.user_has_tasks ? - : - ), - this.props.data.users.map(([key, value]) => ( - - )) - ] - )} + [ + (!this.props.data.user_has_tasks ? + : + ), + this.props.data.users.map(([key, value]) => ( + + )) + ] + )} ); } From 3796753e28308887d26802114c2cee283f99389c Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Wed, 6 Nov 2019 11:59:16 -0800 Subject: [PATCH 04/25] used key vault to store cosmos creds --- app/__init__.py | 2 +- app/config.py | 42 ++++++++++++++++++++++-------------------- app/models/models.py | 1 - 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 1517d27..562bfe7 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,6 +3,7 @@ from flask_sqlalchemy import SQLAlchemy from azure.cosmos import errors, CosmosClient + import os APP_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -16,7 +17,6 @@ app = Flask(__name__, static_folder=STATIC_FOLDER, template_folder=TEMPLATE_FOLDER) app.config.from_object("app.config.ProductionConfig") - key = app.config["SECRET_KEY"] uri = app.config["COSMOS_DB_URI"] diff --git a/app/config.py b/app/config.py index 4bc711c..5fcd159 100644 --- a/app/config.py +++ b/app/config.py @@ -1,4 +1,6 @@ import os +from azure.identity import DefaultAzureCredential +from azure.keyvault.secrets import SecretClient class BaseConfig(object): @@ -7,32 +9,32 @@ class BaseConfig(object): class TestingConfig(BaseConfig): - COSMOS_DB_URI = "https://cosmosdb1029.documents.azure.com:443/" + # flaskreact app service as service principal + AZURE_CLIENT_ID = os.environ["AZURE_CLIENT_ID"] + AZURE_TENANT_ID = os.environ["AZURE_TENANT_ID"] + AZURE_CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"] + KEY_VAULT_URL = os.environ["KEY_VAULT_URL"] + + credential = DefaultAzureCredential() + secret_client = SecretClient(vault_url=KEY_VAULT_URL, credential=credential) + + COSMOS_DB_URI = secret_client.get_secret("cosmosURI").value + SECRET_KEY = secret_client.get_secret("cosmosKey").value # key to cosmos - # not pushing keys to github - SECRET_KEY = "" #key to cosmos - AZURE_CLIENT_ID = "" - AZURE_TENANT_ID = "" - AZURE_CLIENT_SECRET = "" DEBUG = True class ProductionConfig(BaseConfig): - COSMOS_DB_URI = os.environ.get("COSMOS_DB_URI", TestingConfig.COSMOS_DB_URI) - # this is the key to cosmos db, but also used to make token for authentication purpose # probably shouldn't use the same key for different purposes and just let this key be the key to the db - SECRET_KEY = os.environ.get("SECRET_KEY", TestingConfig.SECRET_KEY) - - # created an app service on Portal (b/c eventually want to use App Service to host app) - # creds returned by making that app a service principal - # also, in order to use Identity, need a Service Principal - AZURE_TENANT_ID = os.environ.get("AZURE_TENANT_ID", TestingConfig.AZURE_TENANT_ID) - AZURE_CLIENT_ID = os.environ.get("AZURE_CLIENT_ID", TestingConfig.AZURE_CLIENT_ID) - AZURE_CLIENT_SECRET = os.environ.get("AZURE_CLIENT_SECRET", TestingConfig.AZURE_CLIENT_SECRET) - - - - + SECRET_KEY = TestingConfig.SECRET_KEY + COSMOS_DB_URI = TestingConfig.COSMOS_DB_URI + KEY_VAULT_URL = TestingConfig.KEY_VAULT_URL + # created an app service on Portal (b/c eventually want to use App Service to host app) + # creds returned by making that app a service principal + # also, in order to use Identity, need a Service Principal + AZURE_TENANT_ID = TestingConfig.AZURE_TENANT_ID + AZURE_CLIENT_ID = TestingConfig.AZURE_CLIENT_ID + AZURE_CLIENT_SECRET = TestingConfig.AZURE_CLIENT_SECRET diff --git a/app/models/models.py b/app/models/models.py index f18b484..22e769a 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -24,7 +24,6 @@ class User: def __init__(self, first_name, last_name, email, password): - # self.id = str(randint(_MIN, _MAX)) # id must be a string for cosmos self.id = email self.first_name = first_name self.last_name = last_name From 0863ec1796b340c3eac752053635a328e5446203 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Wed, 6 Nov 2019 16:20:34 -0800 Subject: [PATCH 05/25] use dotenv to read env vars --- app/config.py | 13 +++++++++---- requirements.txt | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/config.py b/app/config.py index 5fcd159..be7fc81 100644 --- a/app/config.py +++ b/app/config.py @@ -1,6 +1,8 @@ import os from azure.identity import DefaultAzureCredential from azure.keyvault.secrets import SecretClient +from app import APP_DIR +from dotenv import load_dotenv class BaseConfig(object): @@ -9,11 +11,14 @@ class BaseConfig(object): class TestingConfig(BaseConfig): + dotenv_path = os.path.join(APP_DIR, ".env") + load_dotenv(dotenv_path) + # flaskreact app service as service principal - AZURE_CLIENT_ID = os.environ["AZURE_CLIENT_ID"] - AZURE_TENANT_ID = os.environ["AZURE_TENANT_ID"] - AZURE_CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"] - KEY_VAULT_URL = os.environ["KEY_VAULT_URL"] + AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID") + AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID") + AZURE_CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET") + KEY_VAULT_URL = os.getenv("KEY_VAULT_URL") credential = DefaultAzureCredential() secret_client = SecretClient(vault_url=KEY_VAULT_URL, credential=credential) diff --git a/requirements.txt b/requirements.txt index a29d6d3..aa6074d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,15 @@ -flask_migrate==2.3.0 -flask_script==2.0.6 +azure-common==1.1.23 +azure-core==1.0.0 +azure-cosmos==4.0.0b5 +azure-identity==1.0.1 +azure-keyvault-secrets==4.0.0 +Flask==1.1.1 +Flask-Bcrypt==0.7.1 +Flask-Migrate==2.3.0 +Flask-Script==2.0.6 psycopg2==2.7.5 -flask_bcrypt==0.7.1 + + + + + From 1158785505f8377acea01e5ee1555cbe05bb0847 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Wed, 6 Nov 2019 16:22:52 -0800 Subject: [PATCH 06/25] add dotenv to requirement.txt --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index aa6074d..e929507 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,8 @@ Flask-Bcrypt==0.7.1 Flask-Migrate==2.3.0 Flask-Script==2.0.6 psycopg2==2.7.5 +python-dotenv==0.10.3 + From d5e183beb9d25bcd1f5c8e00b46b8cd9077aca49 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Wed, 6 Nov 2019 22:22:00 -0800 Subject: [PATCH 07/25] parameterized sql queries --- app/models/models.py | 54 ++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/app/models/models.py b/app/models/models.py index 22e769a..9364b2c 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -60,9 +60,12 @@ def hashed_password(password): @staticmethod def get_user_by_id(user_id): - query = "SELECT * FROM users u where u.id=" + user_id users = user_container.query_items( - query=query, enable_cross_partition_query=True + query = "SELECT * FROM users u WHERE u.id = @userId", + parameters = [ + dict(name='@userId', value=user_id) + ], + enable_cross_partition_query=True ) user = list(users)[0] @@ -70,9 +73,12 @@ def get_user_by_id(user_id): @staticmethod def get_user_by_email(user_email): - query = "SELECT * FROM users u where u.email='" + user_email + "'" users = user_container.query_items( - query=query, enable_cross_partition_query=True + query = "SELECT * FROM users u WHERE u.email = @userEmail", + parameters=[ + dict(name='@userEmail', value=user_email) + ], + enable_cross_partition_query=True ) user = list(users)[0] @@ -80,17 +86,13 @@ def get_user_by_email(user_email): @staticmethod def get_user_with_email_and_password(email, password): - - query = ( - "SELECT * FROM users u where u.email='" - + email - + "'" - + " and u.password='" - + password - + "'" - ) users = user_container.query_items( - query=query, enable_cross_partition_query=True + query="SELECT * FROM users u WHERE u.email = @userEmail AND u.password = @userPassword", + parameters=[ + dict(name='@userEmail', value=email), + dict(name='@userPassword', value=password) + ], + enable_cross_partition_query=True ) if users: @@ -134,17 +136,19 @@ def add_task(task, user_id, status): def get_latest_tasks(): user_to_task = {} - query = "SELECT * FROM users" users = user_container.query_items( - query=query, enable_cross_partition_query=True + query = "SELECT * FROM users", enable_cross_partition_query=True ) cnt = 0 for u in users: cnt+=1 - task_query = "SELECT * FROM tasks t where t.user_id='" + u["id"] + "'" tasks = task_container.query_items( - query=task_query, enable_cross_partition_query=True + query="SELECT * FROM tasks t WHERE t.user_id = @userId", + parameters= [ + dict(name='@userId', value=u["id"]) + ], + enable_cross_partition_query=True ) for t in tasks: t["first_name"] = u["first_name"] @@ -158,18 +162,24 @@ def get_latest_tasks(): @staticmethod def get_tasks_for_user(user_id): - query = "SELECT * FROM tasks t where t.user_id='" + user_id + "'" tasks = task_container.query_items( - query=query, enable_cross_partition_query=True + query="SELECT * FROM tasks t WHERE t.user_id = @userId", + parameters=[ + dict(name='@userId', value=user_id) + ], + enable_cross_partition_query=True ) return tasks @staticmethod def delete_task(task_id): try: - query = "SELECT * FROM tasks t where t.id='" + task_id + "'" tasks = task_container.query_items( - query=query, enable_cross_partition_query=True + query="SELECT * FROM tasks t WHERE t.id = @taskId", + parameters=[ + dict(name='@taskId', value=task_id) + ], + enable_cross_partition_query=True ) task = list(tasks)[0] task_container.delete_item(task, partition_key=task_id) From 8023c908e939afa8dcd832a7fe1430b467b286e0 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Thu, 7 Nov 2019 10:05:04 -0800 Subject: [PATCH 08/25] encapsulated all db things in utils/db.py --- app/__init__.py | 9 +-------- app/models/models.py | 14 ++++---------- app/utils/db.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 app/utils/db.py diff --git a/app/__init__.py b/app/__init__.py index 562bfe7..fe859ad 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,11 +1,8 @@ from flask import Flask from flask_bcrypt import Bcrypt -from flask_sqlalchemy import SQLAlchemy -from azure.cosmos import errors, CosmosClient - - import os + APP_DIR = os.path.abspath(os.path.dirname(__file__)) STATIC_FOLDER = os.path.join( APP_DIR, "../static/build/static" @@ -17,9 +14,5 @@ app = Flask(__name__, static_folder=STATIC_FOLDER, template_folder=TEMPLATE_FOLDER) app.config.from_object("app.config.ProductionConfig") -key = app.config["SECRET_KEY"] -uri = app.config["COSMOS_DB_URI"] - -client = CosmosClient(uri, credential=key) bcrypt = Bcrypt(app) diff --git a/app/models/models.py b/app/models/models.py index 9364b2c..5cd80a6 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,25 +1,19 @@ from datetime import datetime, timedelta from sqlalchemy.exc import IntegrityError -from app import client, bcrypt +from app import bcrypt from random import randint +from app.utils.db import cosmosDB import json, time -from azure.cosmos import CosmosClient, PartitionKey # for task id _MIN = 1 _MAX = 1000000000 -user_container_name = "users" -task_container_name = "tasks" -db_name = "team_standup" - - # get container by walking down resource hierarchy # client -> db -> container -db = client.get_database_client(db_name) -user_container = db.get_container_client(user_container_name) -task_container = db.get_container_client(task_container_name) +user_container = cosmosDB.get_container_client("users") +task_container = cosmosDB.get_container_client("tasks") class User: diff --git a/app/utils/db.py b/app/utils/db.py new file mode 100644 index 0000000..3513b8b --- /dev/null +++ b/app/utils/db.py @@ -0,0 +1,11 @@ +from app import app +from azure.cosmos import CosmosClient + + +key = app.config["SECRET_KEY"] +uri = app.config["COSMOS_DB_URI"] + +client = CosmosClient(uri, credential=key) + +db_name = "team_standup" +cosmosDB = client.get_database_client(db_name) From 75fc439af179165bdd9be8987bb2ae87ab007aff Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Thu, 7 Nov 2019 12:05:03 -0800 Subject: [PATCH 09/25] pass in user/task as dict to create/upsert item --- app/models/models.py | 88 +++++++++++++++----------------------------- app/utils/db.py | 2 +- 2 files changed, 31 insertions(+), 59 deletions(-) diff --git a/app/models/models.py b/app/models/models.py index 5cd80a6..8159616 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,8 +1,7 @@ from datetime import datetime, timedelta -from sqlalchemy.exc import IntegrityError from app import bcrypt from random import randint -from app.utils.db import cosmosDB +from app.utils.db import cosmosDB, exceptions import json, time @@ -16,7 +15,7 @@ task_container = cosmosDB.get_container_client("tasks") -class User: +class User(): def __init__(self, first_name, last_name, email, password): self.id = email self.first_name = first_name @@ -33,19 +32,9 @@ def create_user(input): email=input["email"], password=input["password"], ) - - user_container.upsert_item( - { - "id": user.id, - "first_name": user.first_name, - "last_name": user.last_name, - "email": user.email, - "password": user.password, - } - ) - + user_container.create_item(user.__dict__) return True - except IntegrityError: + except exceptions.CosmosHttpResponseError: return False @staticmethod @@ -55,11 +44,9 @@ def hashed_password(password): @staticmethod def get_user_by_id(user_id): users = user_container.query_items( - query = "SELECT * FROM users u WHERE u.id = @userId", - parameters = [ - dict(name='@userId', value=user_id) - ], - enable_cross_partition_query=True + query="SELECT * FROM users u WHERE u.id = @userId", + parameters=[dict(name="@userId", value=user_id)], + enable_cross_partition_query=True, ) user = list(users)[0] @@ -68,11 +55,9 @@ def get_user_by_id(user_id): @staticmethod def get_user_by_email(user_email): users = user_container.query_items( - query = "SELECT * FROM users u WHERE u.email = @userEmail", - parameters=[ - dict(name='@userEmail', value=user_email) - ], - enable_cross_partition_query=True + query="SELECT * FROM users u WHERE u.email = @userEmail", + parameters=[dict(name="@userEmail", value=user_email)], + enable_cross_partition_query=True, ) user = list(users)[0] @@ -81,12 +66,12 @@ def get_user_by_email(user_email): @staticmethod def get_user_with_email_and_password(email, password): users = user_container.query_items( - query="SELECT * FROM users u WHERE u.email = @userEmail AND u.password = @userPassword", + query="SELECT * FROM users u WHERE u.email = @userEmail AND u.password = @userPassword", parameters=[ - dict(name='@userEmail', value=email), - dict(name='@userPassword', value=password) - ], - enable_cross_partition_query=True + dict(name="@userEmail", value=email), + dict(name="@userPassword", value=password), + ], + enable_cross_partition_query=True, ) if users: @@ -112,18 +97,9 @@ def __init__(self, task, user_id, status): def add_task(task, user_id, status): try: task = Task(task=task, user_id=user_id, status=status) - task_container.upsert_item( - { - "id": task.id, - "date": task.date, - "task": task.task, - "user_id": task.user_id, #user_id is email here - "status": task.status, - } - ) - + task_container.upsert_item(task.__dict__) return True, task.id - except IntegrityError: + except exceptions.CosmosHttpResponseError: return False, None @staticmethod @@ -131,18 +107,16 @@ def get_latest_tasks(): user_to_task = {} users = user_container.query_items( - query = "SELECT * FROM users", enable_cross_partition_query=True + query="SELECT * FROM users", enable_cross_partition_query=True ) cnt = 0 for u in users: - cnt+=1 + cnt += 1 tasks = task_container.query_items( query="SELECT * FROM tasks t WHERE t.user_id = @userId", - parameters= [ - dict(name='@userId', value=u["id"]) - ], - enable_cross_partition_query=True + parameters=[dict(name="@userId", value=u["id"])], + enable_cross_partition_query=True, ) for t in tasks: t["first_name"] = u["first_name"] @@ -157,11 +131,9 @@ def get_latest_tasks(): @staticmethod def get_tasks_for_user(user_id): tasks = task_container.query_items( - query="SELECT * FROM tasks t WHERE t.user_id = @userId", - parameters=[ - dict(name='@userId', value=user_id) - ], - enable_cross_partition_query=True + query="SELECT * FROM tasks t WHERE t.user_id = @userId", + parameters=[dict(name="@userId", value=user_id)], + enable_cross_partition_query=True, ) return tasks @@ -170,16 +142,16 @@ def delete_task(task_id): try: tasks = task_container.query_items( query="SELECT * FROM tasks t WHERE t.id = @taskId", - parameters=[ - dict(name='@taskId', value=task_id) - ], - enable_cross_partition_query=True + parameters=[dict(name="@taskId", value=task_id)], + enable_cross_partition_query=True, ) task = list(tasks)[0] task_container.delete_item(task, partition_key=task_id) return True - except IntegrityError: + except exceptions.CosmosHttpResponseError: # item wasn't deleted successfully + return False + except exceptions.CosmosResourceNotFoundError: # item wasn't found return False @staticmethod @@ -192,7 +164,7 @@ def edit_task(task_id, task, status): task_container.upsert_item(task_to_edit) return True - except IntegrityError: + except exceptions.CosmosHttpResponseError: # item couldn't be upserted return False @property diff --git a/app/utils/db.py b/app/utils/db.py index 3513b8b..30e1e4c 100644 --- a/app/utils/db.py +++ b/app/utils/db.py @@ -1,5 +1,5 @@ from app import app -from azure.cosmos import CosmosClient +from azure.cosmos import CosmosClient, exceptions key = app.config["SECRET_KEY"] From 476098e103eb72519fbdcc022d84a3f3b51f090e Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Thu, 7 Nov 2019 17:00:30 -0800 Subject: [PATCH 10/25] updating readme --- README.md | 143 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index cc0ea55..2d681bf 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,121 @@ -# Flask + React + Cosmos +# Team Standup App -This is a minimal sample Flask and React starter code that demonstrates how both frameworks can be used together in a single page web Application. +This is a simple single page web app that integrates the Flask and React framework. It uses Track 2 versions of the [Azure SDK for Python](https://github.com/Azure/azure-sdk-for-python), specifically [azure-cosmos](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/cosmos/azure-cosmos) and [azure-keyvault-secrets](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/keyvault/azure-keyvault-secrets), in addition to [azure-identity](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/identity/azure-identity) for authentication purposes. -The code is based on https://github.com/dternyak/React-Redux-Flask and https://github.com/creativetimofficial/material-dashboard-react -## Tutorial +The code is based on https://github.com/dternyak/React-Redux-Flask and https://github.com/creativetimofficial/material-dashboard-react. -## 1. Setting Up The Project +## How to Run + +## 1. Setting up Azure Services +First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) account if you don't already have one. Sign into portal.azure.com. + +[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vaults. Then follow the instructions in the links below to create the resources (remember to store them in the resource group you created): + +1. [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/create-cosmosdb-resources-portal) + 1. When creating the database, name it 'team_standup'. For this app, you also need two containers named 'tasks' and 'users'. The [partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#choose-partitionkey) for both is '/id'. +2. [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-portal) + 1. This will store the credentials for the resources this app uses. For example, it'll store the key to the cosmos database. This way, you don't reveal any key in your code. + 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the cosmos key and URI respectively. To find these, click into the Cosmos DB account created, go to 'Keys' tab and get the Primary Key and the URI. + +## 2. Getting Access to Key Vault +To make a long story short, you need a service principal to have access to key vault. The service principal serves as an application ID, which is used during the authorization setup for access to other Azure resources via RBAC (role-based access control). We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal on Cloud Shell (click >_ on the top right hand corner in Portal to open). + +1. Create App Service Plan : + + ```az appservice plan create --name myServicePlanName --resource-group myResourceGroup --location westus``` + + There are locations other than westus. A json object will pop up when the command is done. + +2. Create a Web App instance : (Note that the app name must be unique.) + + ```az webapp create --name myUniqueAppName --plan myServicePlanName --resource-group myResourceGroup``` + +3. Make the web app a service principal : + + ```az ad sp create-for-rbac --name http://my-application --skip-assignment``` + + Use your web app's url. To find that, click on the app in Portal, go to the 'Overview' tab and look for 'URL' on the top right portion of the page. It looks something like https://myUniqueAppName.azurewebsites.net. + + After the above comand runs, something like this is returned: + ``` + { + "appId": "11b855c6-43a5-415b-bd34-042a4509c179", + "displayName": "myUniqueAppName.azurewebsites.net", + "name": "https://myUniqueAppName.azurewebsites.net", + "password": "5ad92c90-9e0b-4bcb-a3ad-a121e9076af1", + "tenant": "72f998be-86f2-51ae-01af-2d5cd110db40" + } + ``` + + Later you'll set environment variables, you'll need this info. The tenant will be saved as 'AZURE_TENANT_ID', appId as 'AZURE_CLIENT_ID', and password as 'AZURE_CLIENT_SECRET'. + + + +## 3. Setting Up The Project 1. Clone the reponsitory -```bash -git clone https://github.com/jeffreymew/Flask-React-Postgres.git -cd flask-react-postgres -``` + ```bash + git clone https://github.com/lilyjma/Flask-React-Postgres/tree/team_standup_app_cosmos + cd flask-react-postgres + ``` 2. Create and activate a virtual environment -In Bash -```bash -python3 -m venv venv -source venv/bin/activate -``` + In Bash + ```bash + python3 -m venv venv + source venv/bin/activate + ``` -In Powershell -```Powershell -py -3 -m venv env -env\scripts\activate -``` + In Powershell + ```Powershell + py -3 -m venv env + env\scripts\activate + ``` -In Powershell -```Powershell -py -3 -m venv env -env\scripts\activate -``` + In Powershell + ```Powershell + py -3 -m venv env + env\scripts\activate + ``` 2. Install requirements.txt -```bash -pip install -r requirements.txt -``` + ```bash + pip install -r requirements.txt + ``` 3. Import the project folder into VS Code -```bash -code . -``` + ```bash + code . + ``` -## 2. Running The Code Locally +4. Create a .env file in the root directory + 1. Put these environment variables and their corresponding value in (you saved these in Step 2) : AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, KEY_VAULT_URI. For example: + + ```AZURE_CLIENT_ID="11b855c6-43a5-415b-bd34-042a4509c179"``` + +## 4. Running The Code Locally 1. Build the react.js front-end. -```bash -npm install -npm run build -``` -2. Create the SQL database -```bash -cd .. -python manage.py create_db -``` -3. Start the Flask server -```bash -python manage.py runserver -``` + ``` + npm install + ``` + Go into the /static folder and do + + ``` + npm run build + ``` + + + +2. Start the Flask server + ``` + python manage.py runserver + ``` 4. Check ```localhost:5000``` in your browser to view the web application. -## 3. Deploying The Code To Azure +## 5. Deploying The Code To Azure 1. Go to the extensions tab on VS Code From 60604a003d1c0ba057d589122b26fc4862721c3c Mon Sep 17 00:00:00 2001 From: lilyjma Date: Thu, 7 Nov 2019 17:03:23 -0800 Subject: [PATCH 11/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d681bf..4005252 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The code is based on https://github.com/dternyak/React-Redux-Flask and https://g ## How to Run ## 1. Setting up Azure Services -First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) account if you don't already have one. Sign into portal.azure.com. +First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) account if you don't already have one. Sign into https://portal.azure.com. [Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vaults. Then follow the instructions in the links below to create the resources (remember to store them in the resource group you created): From d2a11d57651f8bba4ab278f35d939f1f3c63347d Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Thu, 7 Nov 2019 17:10:41 -0800 Subject: [PATCH 12/25] update readme --- README.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2d681bf..cf7c452 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ The code is based on https://github.com/dternyak/React-Redux-Flask and https://g ## How to Run ## 1. Setting up Azure Services -First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) account if you don't already have one. Sign into portal.azure.com. +First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) account if you don't already have one. Sign into https://portal.azure.com. -[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vaults. Then follow the instructions in the links below to create the resources (remember to store them in the resource group you created): +[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vault. Then follow the instructions in the links below to create each resource (remember to store them in the resource group you created): 1. [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/create-cosmosdb-resources-portal) 1. When creating the database, name it 'team_standup'. For this app, you also need two containers named 'tasks' and 'users'. The [partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#choose-partitionkey) for both is '/id'. @@ -33,7 +33,7 @@ To make a long story short, you need a service principal to have access to key v 3. Make the web app a service principal : - ```az ad sp create-for-rbac --name http://my-application --skip-assignment``` + ```az ad sp create-for-rbac --name http://my-applications-url --skip-assignment``` Use your web app's url. To find that, click on the app in Portal, go to the 'Overview' tab and look for 'URL' on the top right portion of the page. It looks something like https://myUniqueAppName.azurewebsites.net. @@ -48,7 +48,7 @@ To make a long story short, you need a service principal to have access to key v } ``` - Later you'll set environment variables, you'll need this info. The tenant will be saved as 'AZURE_TENANT_ID', appId as 'AZURE_CLIENT_ID', and password as 'AZURE_CLIENT_SECRET'. + Later when you set environment variables, you'll need this info. The tenant will be saved as 'AZURE_TENANT_ID', appId as 'AZURE_CLIENT_ID', and password as 'AZURE_CLIENT_SECRET'. @@ -74,12 +74,6 @@ To make a long story short, you need a service principal to have access to key v env\scripts\activate ``` - In Powershell - ```Powershell - py -3 -m venv env - env\scripts\activate - ``` - 2. Install requirements.txt ```bash pip install -r requirements.txt @@ -91,7 +85,7 @@ To make a long story short, you need a service principal to have access to key v ``` 4. Create a .env file in the root directory - 1. Put these environment variables and their corresponding value in (you saved these in Step 2) : AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, KEY_VAULT_URI. For example: + 1. Put these environment variables and their corresponding value in the file (you saved these in Step 2) : AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, KEY_VAULT_URI. For example: ```AZURE_CLIENT_ID="11b855c6-43a5-415b-bd34-042a4509c179"``` From a8b1407ef52a60ec5ee23aacf4c33c3efeac8315 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Fri, 8 Nov 2019 11:10:37 -0800 Subject: [PATCH 13/25] fixed path to get env vars correctly --- app/__init__.py | 5 +++++ app/config.py | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index fe859ad..c85de6a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,8 @@ from flask import Flask from flask_bcrypt import Bcrypt +from dotenv import load_dotenv import os +from pathlib import Path APP_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -11,6 +13,9 @@ APP_DIR, "../static/build" ) # Where your index.html file is located +dotenv_path = Path(".") / ".env" +load_dotenv(dotenv_path) + app = Flask(__name__, static_folder=STATIC_FOLDER, template_folder=TEMPLATE_FOLDER) app.config.from_object("app.config.ProductionConfig") diff --git a/app/config.py b/app/config.py index be7fc81..d79d826 100644 --- a/app/config.py +++ b/app/config.py @@ -1,9 +1,6 @@ import os from azure.identity import DefaultAzureCredential from azure.keyvault.secrets import SecretClient -from app import APP_DIR -from dotenv import load_dotenv - class BaseConfig(object): DEBUG = False @@ -11,9 +8,6 @@ class BaseConfig(object): class TestingConfig(BaseConfig): - dotenv_path = os.path.join(APP_DIR, ".env") - load_dotenv(dotenv_path) - # flaskreact app service as service principal AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID") AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID") From 6ccc15fd74291d6cbbc6c164fafc9438118b52e6 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Fri, 8 Nov 2019 11:21:47 -0800 Subject: [PATCH 14/25] update readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf7c452..4c789e9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ The code is based on https://github.com/dternyak/React-Redux-Flask and https://g ## 1. Setting up Azure Services First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) account if you don't already have one. Sign into https://portal.azure.com. -[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vault. Then follow the instructions in the links below to create each resource (remember to store them in the resource group you created): +[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vault. Then follow the instructions in the links below to create each resource: + +(Remember to store them in the resource group you created; this will make it easier to clean up the resources in the future.) 1. [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/create-cosmosdb-resources-portal) 1. When creating the database, name it 'team_standup'. For this app, you also need two containers named 'tasks' and 'users'. The [partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#choose-partitionkey) for both is '/id'. @@ -89,6 +91,8 @@ To make a long story short, you need a service principal to have access to key v ```AZURE_CLIENT_ID="11b855c6-43a5-415b-bd34-042a4509c179"``` + To get KEY_VAULT_URI from Portal, go to the key vault you created, then to the 'Overview' tap, finally look for 'DNS Name' on the top right part of the page. + ## 4. Running The Code Locally 1. Build the react.js front-end. From f27e7dc69e3f7654a55aeb4ffe96b61479243727 Mon Sep 17 00:00:00 2001 From: lilyjma Date: Fri, 8 Nov 2019 11:31:23 -0800 Subject: [PATCH 15/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c789e9..a3e5038 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) accou (Remember to store them in the resource group you created; this will make it easier to clean up the resources in the future.) -1. [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/create-cosmosdb-resources-portal) +1. [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/create-cosmosdb-resources-portal#create-an-azure-cosmos-db-account) 1. When creating the database, name it 'team_standup'. For this app, you also need two containers named 'tasks' and 'users'. The [partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#choose-partitionkey) for both is '/id'. 2. [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-portal) 1. This will store the credentials for the resources this app uses. For example, it'll store the key to the cosmos database. This way, you don't reveal any key in your code. From fb646a2fddcd087a425c0f4a7deb38a4dd155045 Mon Sep 17 00:00:00 2001 From: lilyjma Date: Fri, 8 Nov 2019 11:37:43 -0800 Subject: [PATCH 16/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3e5038..5eb3cda 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) accou 1. [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/create-cosmosdb-resources-portal#create-an-azure-cosmos-db-account) 1. When creating the database, name it 'team_standup'. For this app, you also need two containers named 'tasks' and 'users'. The [partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#choose-partitionkey) for both is '/id'. -2. [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-portal) +2. [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-portal#create-a-vault) 1. This will store the credentials for the resources this app uses. For example, it'll store the key to the cosmos database. This way, you don't reveal any key in your code. 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the cosmos key and URI respectively. To find these, click into the Cosmos DB account created, go to 'Keys' tab and get the Primary Key and the URI. From 9834e3401286bdf6b6af010b24f89e701469e2d2 Mon Sep 17 00:00:00 2001 From: lilyjma Date: Fri, 8 Nov 2019 11:42:35 -0800 Subject: [PATCH 17/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5eb3cda..01dafbd 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) accou 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the cosmos key and URI respectively. To find these, click into the Cosmos DB account created, go to 'Keys' tab and get the Primary Key and the URI. ## 2. Getting Access to Key Vault -To make a long story short, you need a service principal to have access to key vault. The service principal serves as an application ID, which is used during the authorization setup for access to other Azure resources via RBAC (role-based access control). We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal on Cloud Shell (click >_ on the top right hand corner in Portal to open). +To make a long story short, you need a service principal to have access to key vault. The service principal serves as an application ID that is used during the authorization setup for access to other Azure resources. We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal. We can do all of this on Cloud Shell (click >_ on the top right hand corner in Portal to open). 1. Create App Service Plan : From 166b2d60d3e12596de99330cf01b9cfd712d251e Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Fri, 8 Nov 2019 14:27:05 -0800 Subject: [PATCH 18/25] used uuid for task id --- README.md | 4 ++-- app/models/models.py | 34 +++++++++++++++------------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 01dafbd..5ace2fb 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) accou 1. [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/create-cosmosdb-resources-portal#create-an-azure-cosmos-db-account) 1. When creating the database, name it 'team_standup'. For this app, you also need two containers named 'tasks' and 'users'. The [partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#choose-partitionkey) for both is '/id'. 2. [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-portal#create-a-vault) - 1. This will store the credentials for the resources this app uses. For example, it'll store the key to the cosmos database. This way, you don't reveal any key in your code. - 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the cosmos key and URI respectively. To find these, click into the Cosmos DB account created, go to 'Keys' tab and get the Primary Key and the URI. + 1. This will store the credentials for the resources this app uses. For example, it'll store the key to the Cosmos DB. This way, you don't reveal any key in your code. + 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the Cosmos DB key and URI respectively. To find these, click into the database account created, go to 'Keys' tab and get the Primary Key and the URI. ## 2. Getting Access to Key Vault To make a long story short, you need a service principal to have access to key vault. The service principal serves as an application ID that is used during the authorization setup for access to other Azure resources. We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal. We can do all of this on Cloud Shell (click >_ on the top right hand corner in Portal to open). diff --git a/app/models/models.py b/app/models/models.py index 8159616..7a60eb6 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -2,12 +2,7 @@ from app import bcrypt from random import randint from app.utils.db import cosmosDB, exceptions -import json, time - - -# for task id -_MIN = 1 -_MAX = 1000000000 +import json, time, uuid # get container by walking down resource hierarchy # client -> db -> container @@ -15,7 +10,7 @@ task_container = cosmosDB.get_container_client("tasks") -class User(): +class User: def __init__(self, first_name, last_name, email, password): self.id = email self.first_name = first_name @@ -32,6 +27,7 @@ def create_user(input): email=input["email"], password=input["password"], ) + user_container.create_item(user.__dict__) return True except exceptions.CosmosHttpResponseError: @@ -44,8 +40,8 @@ def hashed_password(password): @staticmethod def get_user_by_id(user_id): users = user_container.query_items( - query="SELECT * FROM users u WHERE u.id = @userId", - parameters=[dict(name="@userId", value=user_id)], + query="SELECT * FROM users u WHERE u.id = @id", + parameters=[dict(name="@id", value=user_id)], enable_cross_partition_query=True, ) user = list(users)[0] @@ -55,8 +51,8 @@ def get_user_by_id(user_id): @staticmethod def get_user_by_email(user_email): users = user_container.query_items( - query="SELECT * FROM users u WHERE u.email = @userEmail", - parameters=[dict(name="@userEmail", value=user_email)], + query="SELECT * FROM users u WHERE u.email = @email", + parameters=[dict(name="@email", value=user_email)], enable_cross_partition_query=True, ) user = list(users)[0] @@ -66,10 +62,10 @@ def get_user_by_email(user_email): @staticmethod def get_user_with_email_and_password(email, password): users = user_container.query_items( - query="SELECT * FROM users u WHERE u.email = @userEmail AND u.password = @userPassword", + query="SELECT * FROM users u WHERE u.email = @email AND u.password = @password", parameters=[ - dict(name="@userEmail", value=email), - dict(name="@userPassword", value=password), + dict(name="@email", value=email), + dict(name="@password", value=password), ], enable_cross_partition_query=True, ) @@ -87,7 +83,7 @@ class STATUS: IN_PROGRESS = "IN_PROGRESS" def __init__(self, task, user_id, status): - self.id = str(randint(_MIN, _MAX)) + self.id = str(uuid.uuid4()) self.date = str(datetime.utcnow().date()) self.task = task self.user_id = user_id @@ -114,8 +110,8 @@ def get_latest_tasks(): for u in users: cnt += 1 tasks = task_container.query_items( - query="SELECT * FROM tasks t WHERE t.user_id = @userId", - parameters=[dict(name="@userId", value=u["id"])], + query="SELECT * FROM tasks t WHERE t.user_id = @id", + parameters=[dict(name="@id", value=u["id"])], enable_cross_partition_query=True, ) for t in tasks: @@ -141,8 +137,8 @@ def get_tasks_for_user(user_id): def delete_task(task_id): try: tasks = task_container.query_items( - query="SELECT * FROM tasks t WHERE t.id = @taskId", - parameters=[dict(name="@taskId", value=task_id)], + query="SELECT * FROM tasks t WHERE t.id = @id", + parameters=[dict(name="@id", value=task_id)], enable_cross_partition_query=True, ) task = list(tasks)[0] From 144a8cc20eb94f629c4a170a33fec425f24b573a Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Fri, 8 Nov 2019 14:55:13 -0800 Subject: [PATCH 19/25] update readme --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5ace2fb..304990b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The code is based on https://github.com/dternyak/React-Redux-Flask and https://g ## 1. Setting up Azure Services First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) account if you don't already have one. Sign into https://portal.azure.com. -[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vault. Then follow the instructions in the links below to create each resource: +[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vault. Then follow the instructions in the links below to create each resource in Azure Portal: (Remember to store them in the resource group you created; this will make it easier to clean up the resources in the future.) @@ -21,7 +21,10 @@ First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) accou 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the Cosmos DB key and URI respectively. To find these, click into the database account created, go to 'Keys' tab and get the Primary Key and the URI. ## 2. Getting Access to Key Vault -To make a long story short, you need a service principal to have access to key vault. The service principal serves as an application ID that is used during the authorization setup for access to other Azure resources. We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal. We can do all of this on Cloud Shell (click >_ on the top right hand corner in Portal to open). +To make a long story short, you need a service principal to have access to key vault. The service principal serves as an application ID that is used during the authorization setup for access to other Azure resources. We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal. We can do these using Azure CLI on Cloud Shell. + + +1. Click >_ on the top right hand corner of Azure Portal to open Cloud shell. 1. Create App Service Plan : @@ -33,11 +36,13 @@ To make a long story short, you need a service principal to have access to key v ```az webapp create --name myUniqueAppName --plan myServicePlanName --resource-group myResourceGroup``` + Click on the web app instance you've just created on Azure Portal. In the 'Overview' tab, find 'URL' on the top right portion of the page. This is your app's URL, and it looks something like this : https://myUniqueAppName.azurewebsites.net. Save it to use for the next step. + 3. Make the web app a service principal : ```az ad sp create-for-rbac --name http://my-applications-url --skip-assignment``` - Use your web app's url. To find that, click on the app in Portal, go to the 'Overview' tab and look for 'URL' on the top right portion of the page. It looks something like https://myUniqueAppName.azurewebsites.net. + After ```--name``` , user your app's url. After the above comand runs, something like this is returned: ``` @@ -76,17 +81,17 @@ To make a long story short, you need a service principal to have access to key v env\scripts\activate ``` -2. Install requirements.txt +3. Install requirements.txt ```bash pip install -r requirements.txt ``` -3. Import the project folder into VS Code +4. Import the project folder into VS Code ```bash code . ``` -4. Create a .env file in the root directory +5. Create a .env file in the root directory 1. Put these environment variables and their corresponding value in the file (you saved these in Step 2) : AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, KEY_VAULT_URI. For example: ```AZURE_CLIENT_ID="11b855c6-43a5-415b-bd34-042a4509c179"``` From 52918545b743a494cb0aa3ad704ec1280eac3977 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Fri, 8 Nov 2019 15:06:46 -0800 Subject: [PATCH 20/25] added .env.tmp --- .env.tmp | 5 +++++ README.md | 4 ++-- app/__init__.py | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .env.tmp diff --git a/.env.tmp b/.env.tmp new file mode 100644 index 0000000..8bb19fc --- /dev/null +++ b/.env.tmp @@ -0,0 +1,5 @@ +# set your env vars here and rename this file to .env +AZURE_CLIENT_ID="" +AZURE_CLIENT_SECRET="" +AZURE_TENANT_ID="" +KEY_VAULT_URL="" \ No newline at end of file diff --git a/README.md b/README.md index 304990b..4077c8d 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ To make a long story short, you need a service principal to have access to key v } ``` - Later when you set environment variables, you'll need this info. The tenant will be saved as 'AZURE_TENANT_ID', appId as 'AZURE_CLIENT_ID', and password as 'AZURE_CLIENT_SECRET'. + Later when you set environment variables, you'll need this info. The *tenant* will be saved as 'AZURE_TENANT_ID', *appId* as 'AZURE_CLIENT_ID', and *password* as 'AZURE_CLIENT_SECRET'. @@ -86,7 +86,7 @@ To make a long story short, you need a service principal to have access to key v pip install -r requirements.txt ``` -4. Import the project folder into VS Code +4. Open the project folder in VS Code ```bash code . ``` diff --git a/app/__init__.py b/app/__init__.py index c85de6a..52c6a98 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -13,8 +13,7 @@ APP_DIR, "../static/build" ) # Where your index.html file is located -dotenv_path = Path(".") / ".env" -load_dotenv(dotenv_path) +load_dotenv() app = Flask(__name__, static_folder=STATIC_FOLDER, template_folder=TEMPLATE_FOLDER) app.config.from_object("app.config.ProductionConfig") From fbf02c6a0f015dc796180903fc9ef521cf47675f Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Fri, 8 Nov 2019 15:15:13 -0800 Subject: [PATCH 21/25] update readme --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4077c8d..e44acc3 100644 --- a/README.md +++ b/README.md @@ -91,12 +91,11 @@ To make a long story short, you need a service principal to have access to key v code . ``` -5. Create a .env file in the root directory - 1. Put these environment variables and their corresponding value in the file (you saved these in Step 2) : AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, KEY_VAULT_URI. For example: +5. Change the .env.tmp file in the root directory to .env - ```AZURE_CLIENT_ID="11b855c6-43a5-415b-bd34-042a4509c179"``` - - To get KEY_VAULT_URI from Portal, go to the key vault you created, then to the 'Overview' tap, finally look for 'DNS Name' on the top right part of the page. + Put in the correct value for each environment variable (you got the values in Step 2) : AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, KEY_VAULT_URI. + + To get KEY_VAULT_URI from Azure Portal, go to the key vault you created, then to the 'Overview' tap, finally look for 'DNS Name' on the top right part of the page. ## 4. Running The Code Locally @@ -116,7 +115,7 @@ To make a long story short, you need a service principal to have access to key v ``` python manage.py runserver ``` -4. Check ```localhost:5000``` in your browser to view the web application. +3. Check ```localhost:5000``` in your browser to view the web application. ## 5. Deploying The Code To Azure From 7d1f785718d9bda259897635c87a90774cf593f1 Mon Sep 17 00:00:00 2001 From: Lily Ma Date: Fri, 8 Nov 2019 17:22:08 -0800 Subject: [PATCH 22/25] update readme: authorize service principal to perform operations to keyvault --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e44acc3..93d40a0 100644 --- a/README.md +++ b/README.md @@ -20,25 +20,27 @@ First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) accou 1. This will store the credentials for the resources this app uses. For example, it'll store the key to the Cosmos DB. This way, you don't reveal any key in your code. 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the Cosmos DB key and URI respectively. To find these, click into the database account created, go to 'Keys' tab and get the Primary Key and the URI. +You should be able to click into your resource group on the Azure Portal home page and see these two resources. + ## 2. Getting Access to Key Vault To make a long story short, you need a service principal to have access to key vault. The service principal serves as an application ID that is used during the authorization setup for access to other Azure resources. We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal. We can do these using Azure CLI on Cloud Shell. 1. Click >_ on the top right hand corner of Azure Portal to open Cloud shell. -1. Create App Service Plan : +2. Create App Service Plan : ```az appservice plan create --name myServicePlanName --resource-group myResourceGroup --location westus``` There are locations other than westus. A json object will pop up when the command is done. -2. Create a Web App instance : (Note that the app name must be unique.) +3. Create a Web App instance : (Note that the app name must be unique.) ```az webapp create --name myUniqueAppName --plan myServicePlanName --resource-group myResourceGroup``` - Click on the web app instance you've just created on Azure Portal. In the 'Overview' tab, find 'URL' on the top right portion of the page. This is your app's URL, and it looks something like this : https://myUniqueAppName.azurewebsites.net. Save it to use for the next step. + Find the web app instance you've just created on Azure Portal's. In the 'Overview' tab, find 'URL' on the top right portion of the page. This is your app's URL, and it looks something like this : https://myUniqueAppName.azurewebsites.net. Save it to use for the next step. -3. Make the web app a service principal : +4. Make the web app a service principal : ```az ad sp create-for-rbac --name http://my-applications-url --skip-assignment``` @@ -57,6 +59,15 @@ To make a long story short, you need a service principal to have access to key v Later when you set environment variables, you'll need this info. The *tenant* will be saved as 'AZURE_TENANT_ID', *appId* as 'AZURE_CLIENT_ID', and *password* as 'AZURE_CLIENT_SECRET'. +5. Authorize the service principal to perform key operations in your key vault: + + ``` + export AZURE_CLIENT_ID="your-azure-client-id" + ``` + + ``` + az keyvault set-policy --name my-key-vault --spn $AZURE_CLIENT_ID --secret-permissions get set list delete backup recover restore purge + ``` ## 3. Setting Up The Project @@ -99,7 +110,7 @@ To make a long story short, you need a service principal to have access to key v ## 4. Running The Code Locally -1. Build the react.js front-end. +1. Build the react.js front-end ``` npm install ``` @@ -111,7 +122,7 @@ To make a long story short, you need a service principal to have access to key v -2. Start the Flask server +2. Change back to root directory and start the Flask server ``` python manage.py runserver ``` From bd86c40269c2d561e74831e40b80edd2c7e1e19b Mon Sep 17 00:00:00 2001 From: lilyjma Date: Mon, 11 Nov 2019 10:35:14 -0800 Subject: [PATCH 23/25] Update README.md --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 93d40a0..a8667f6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Team Standup App -This is a simple single page web app that integrates the Flask and React framework. It uses Track 2 versions of the [Azure SDK for Python](https://github.com/Azure/azure-sdk-for-python), specifically [azure-cosmos](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/cosmos/azure-cosmos) and [azure-keyvault-secrets](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/keyvault/azure-keyvault-secrets), in addition to [azure-identity](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/identity/azure-identity) for authentication purposes. +This is a simple single page web app that integrates the Flask and React framework. It uses Track 2 version of the [Azure SDK for Python](https://github.com/Azure/azure-sdk-for-python), specifically [azure-cosmos](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/cosmos/azure-cosmos) and [azure-keyvault-secrets](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/keyvault/azure-keyvault-secrets), in addition to [azure-identity](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/identity/azure-identity) for authentication purposes. The code is based on https://github.com/dternyak/React-Redux-Flask and https://github.com/creativetimofficial/material-dashboard-react. @@ -10,20 +10,20 @@ The code is based on https://github.com/dternyak/React-Redux-Flask and https://g ## 1. Setting up Azure Services First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) account if you don't already have one. Sign into https://portal.azure.com. -[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key vault. Then follow the instructions in the links below to create each resource in Azure Portal: +[Create a resource group](https://github.com/lilyjma/azurethings/blob/master/createResourceGroup.md) to store the resources that you'll be using here--Azure Cosmos DB and Key Vault. Then follow instructions in the links below to create each resource in Azure Portal: (Remember to store them in the resource group you created; this will make it easier to clean up the resources in the future.) 1. [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/create-cosmosdb-resources-portal#create-an-azure-cosmos-db-account) - 1. When creating the database, name it 'team_standup'. For this app, you also need two containers named 'tasks' and 'users'. The [partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#choose-partitionkey) for both is '/id'. + 1. When creating the database, give it an id of 'team_standup'. You also need two containers--give them id 'tasks' and 'users'. The [partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/partitioning-overview#choose-partitionkey) for both is '/id'. 2. [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-portal#create-a-vault) - 1. This will store the credentials for the resources this app uses. For example, it'll store the key to the Cosmos DB. This way, you don't reveal any key in your code. - 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the Cosmos DB key and URI respectively. To find these, click into the database account created, go to 'Keys' tab and get the Primary Key and the URI. + 1. This will store credentials for the resources used by this app. For example, it'll store the key to the Cosmos DB. This way, you don't reveal any key in your code and have a centralized place for all keys the app uses. + 2. You'll add two secrets called 'cosmosKey' and 'cosmosURI' to Key Vault to hold the Cosmos DB key and URI respectively. To find these, click into the database account created, go to 'Keys' tab and get the *Primary Key* and *URI*. You should be able to click into your resource group on the Azure Portal home page and see these two resources. ## 2. Getting Access to Key Vault -To make a long story short, you need a service principal to have access to key vault. The service principal serves as an application ID that is used during the authorization setup for access to other Azure resources. We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal. We can do these using Azure CLI on Cloud Shell. +To make a long story short, you need a service principal to have access to Key Vault. The service principal serves as an application ID used during the authorization setup for access to other Azure resources. We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal and give it permission to perform operations to Key Vault. We can do these using Azure CLI on Cloud Shell. 1. Click >_ on the top right hand corner of Azure Portal to open Cloud shell. @@ -32,37 +32,37 @@ To make a long story short, you need a service principal to have access to key v ```az appservice plan create --name myServicePlanName --resource-group myResourceGroup --location westus``` - There are locations other than westus. A json object will pop up when the command is done. + There are locations other than *westus*. A json object will pop up when the command is done. 3. Create a Web App instance : (Note that the app name must be unique.) ```az webapp create --name myUniqueAppName --plan myServicePlanName --resource-group myResourceGroup``` - Find the web app instance you've just created on Azure Portal's. In the 'Overview' tab, find 'URL' on the top right portion of the page. This is your app's URL, and it looks something like this : https://myUniqueAppName.azurewebsites.net. Save it to use for the next step. + Find the web app instance you've just created on Azure Portal. In the 'Overview' tab, find *URL* on the top right portion of the page. This is your app's URL, and it looks something like this : https://myUniqueAppName.azurewebsites.net. Save it to use for the next step. 4. Make the web app a service principal : - ```az ad sp create-for-rbac --name http://my-applications-url --skip-assignment``` + ```az ad sp create-for-rbac --name https://myUniqueAppName.azurewebsites.net --skip-assignment``` - After ```--name``` , user your app's url. + After ```--name``` , use your app's url. - After the above comand runs, something like this is returned: + When the command finishes running, something like this is returned: ``` { - "appId": "11b855c6-43a5-415b-bd34-042a4509c179", + "appId": "my-app-id", "displayName": "myUniqueAppName.azurewebsites.net", "name": "https://myUniqueAppName.azurewebsites.net", - "password": "5ad92c90-9e0b-4bcb-a3ad-a121e9076af1", - "tenant": "72f998be-86f2-51ae-01af-2d5cd110db40" + "password": "my-password", + "tenant": "my-tenant" } ``` - Later when you set environment variables, you'll need this info. The *tenant* will be saved as 'AZURE_TENANT_ID', *appId* as 'AZURE_CLIENT_ID', and *password* as 'AZURE_CLIENT_SECRET'. + Save this info in your favorite editor. In the next step, you'll set *appId* as an environment variable called 'AZURE_CLIENT_ID', and in a later step, you'll also set *tenant* as 'AZURE_TENANT_ID'and *password* as 'AZURE_CLIENT_SECRET'. -5. Authorize the service principal to perform key operations in your key vault: +5. Authorize the service principal to perform operations in your key vault: ``` - export AZURE_CLIENT_ID="your-azure-client-id" + export AZURE_CLIENT_ID="my-azure-client-id" ``` ``` @@ -106,7 +106,7 @@ To make a long story short, you need a service principal to have access to key v Put in the correct value for each environment variable (you got the values in Step 2) : AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, KEY_VAULT_URI. - To get KEY_VAULT_URI from Azure Portal, go to the key vault you created, then to the 'Overview' tap, finally look for 'DNS Name' on the top right part of the page. + To get KEY_VAULT_URI from Azure Portal, go to the key vault you created, then to the 'Overview' tap and look for *DNS Name* on the top right portion of the page. ## 4. Running The Code Locally From efe01c5535b9bd7157f6a7d20ee41bf881bf33c7 Mon Sep 17 00:00:00 2001 From: lilyjma Date: Mon, 11 Nov 2019 10:37:47 -0800 Subject: [PATCH 24/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8667f6..3d2df95 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ To make a long story short, you need a service principal to have access to Key V } ``` - Save this info in your favorite editor. In the next step, you'll set *appId* as an environment variable called 'AZURE_CLIENT_ID', and in a later step, you'll also set *tenant* as 'AZURE_TENANT_ID'and *password* as 'AZURE_CLIENT_SECRET'. + Save this info in your favorite editor. In the next step, you'll set *appId* as an environment variable called AZURE_CLIENT_ID, and in a later step, you'll also set *tenant* as AZURE_TENANT_ID and *password* as AZURE_CLIENT_SECRET. 5. Authorize the service principal to perform operations in your key vault: From b422d27587b3c660589bc2fc8b6bde82f5451710 Mon Sep 17 00:00:00 2001 From: lilyjma Date: Tue, 12 Nov 2019 09:35:46 -0800 Subject: [PATCH 25/25] Service principal change Changed what needs to be done to create a service principal. Previously misunderstood the --name parameter in the command--it just has to be a name that starts with 'http://' and doesn't have to be the URI to a real, existing site. --- README.md | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 3d2df95..d8d8397 100644 --- a/README.md +++ b/README.md @@ -23,46 +23,34 @@ First, sign up for a free [Azure](https://azure.microsoft.com/en-us/free/) accou You should be able to click into your resource group on the Azure Portal home page and see these two resources. ## 2. Getting Access to Key Vault -To make a long story short, you need a service principal to have access to Key Vault. The service principal serves as an application ID used during the authorization setup for access to other Azure resources. We'll use a Web App instance as our service principal. To do that, we create an App Service Plan, then a Web App instance, then make that our service principal and give it permission to perform operations to Key Vault. We can do these using Azure CLI on Cloud Shell. +One way to access Key Vault is through a service principal. The service principal serves as an application ID used during the authorization setup for access to Azure resources. After making our service principal, we give it permission to perform operations to Key Vault. We can do these using Azure CLI on Cloud Shell. 1. Click >_ on the top right hand corner of Azure Portal to open Cloud shell. -2. Create App Service Plan : - - ```az appservice plan create --name myServicePlanName --resource-group myResourceGroup --location westus``` - - There are locations other than *westus*. A json object will pop up when the command is done. - -3. Create a Web App instance : (Note that the app name must be unique.) - - ```az webapp create --name myUniqueAppName --plan myServicePlanName --resource-group myResourceGroup``` - - Find the web app instance you've just created on Azure Portal. In the 'Overview' tab, find *URL* on the top right portion of the page. This is your app's URL, and it looks something like this : https://myUniqueAppName.azurewebsites.net. Save it to use for the next step. - -4. Make the web app a service principal : +2. Create a service principal : - ```az ad sp create-for-rbac --name https://myUniqueAppName.azurewebsites.net --skip-assignment``` + ```az ad sp create-for-rbac --name http://my-application --skip-assignment``` - After ```--name``` , use your app's url. + Replace 'http://my-application' with a name of your choice. Just make sure that it starts with 'http://', otherwise, you'll get a warning saying that the name you gave is changed to a valid URI, the required format used for service principal names. When the command finishes running, something like this is returned: ``` { - "appId": "my-app-id", - "displayName": "myUniqueAppName.azurewebsites.net", - "name": "https://myUniqueAppName.azurewebsites.net", - "password": "my-password", - "tenant": "my-tenant" + "appId": "generated app id", + "displayName": "my-application", + "name": "http://my-application", + "password": "random password", + "tenant": "tenant id" } ``` - Save this info in your favorite editor. In the next step, you'll set *appId* as an environment variable called AZURE_CLIENT_ID, and in a later step, you'll also set *tenant* as AZURE_TENANT_ID and *password* as AZURE_CLIENT_SECRET. + Save this info somewhere. In the next step, you'll set *appId* as an environment variable called AZURE_CLIENT_ID, and in a later step, you'll also set *tenant* as AZURE_TENANT_ID and *password* as AZURE_CLIENT_SECRET. -5. Authorize the service principal to perform operations in your key vault: +3. Authorize the service principal to perform operations in your Key Vault: ``` - export AZURE_CLIENT_ID="my-azure-client-id" + export AZURE_CLIENT_ID="generated app id" ``` ``` @@ -119,8 +107,6 @@ To make a long story short, you need a service principal to have access to Key V ``` npm run build ``` - - 2. Change back to root directory and start the Flask server ```