├── frontend ├── .flaskenv ├── application │ ├── frontend │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── ProductClient.py │ │ │ ├── OrderClient.py │ │ │ └── UserClient.py │ │ ├── __init__.py │ │ ├── forms.py │ │ └── views.py │ ├── static │ │ ├── images │ │ │ ├── sample.jpg │ │ │ ├── product1.jpg │ │ │ └── product2.jpg │ │ └── css │ │ │ └── style.css │ ├── templates │ │ ├── order │ │ │ └── thankyou.html │ │ ├── base_col_1.html │ │ ├── macros │ │ │ ├── _macros_basket.html │ │ │ └── _macros_form.html │ │ ├── base_col_2.html │ │ ├── login │ │ │ └── index.html │ │ ├── _messages.html │ │ ├── admin │ │ │ └── index.html │ │ ├── register │ │ │ └── index.html │ │ ├── base.html │ │ ├── nav_header.html │ │ ├── home │ │ │ └── index.html │ │ └── product │ │ │ └── index.html │ └── __init__.py ├── .env ├── .gitignore ├── run.py ├── Dockerfile ├── README.md ├── docker-compose.yml ├── config.py ├── requirements.txt └── docker-compose.deploy.yml ├── user-service ├── .flaskenv ├── .env ├── .gitignore ├── application │ ├── user_api │ │ ├── __init__.py │ │ └── routes.py │ ├── __init__.py │ └── models.py ├── Dockerfile ├── README.md ├── config.py ├── docker-compose.yml ├── requirements.txt └── run.py ├── order-service ├── .flaskenv ├── application │ ├── order_api │ │ ├── api │ │ │ ├── __init__.py │ │ │ └── UserClient.py │ │ ├── __init__.py │ │ └── routes.py │ ├── __init__.py │ └── models.py ├── .env ├── .gitignore ├── Dockerfile ├── run.py ├── README.md ├── config.py ├── docker-compose.yml └── requirements.txt ├── product-service ├── .flaskenv ├── .env ├── .gitignore ├── application │ ├── product_api │ │ ├── __init__.py │ │ └── routes.py │ ├── __init__.py │ └── models.py ├── Dockerfile ├── run.py ├── README.md ├── config.py ├── requirements.txt └── docker-compose.yml ├── .gitignore └── README.md /frontend/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py -------------------------------------------------------------------------------- /user-service/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py -------------------------------------------------------------------------------- /order-service/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py -------------------------------------------------------------------------------- /product-service/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=run.py -------------------------------------------------------------------------------- /frontend/application/frontend/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /order-service/application/order_api/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | #.env 2 | CONFIGURATION_SETUP="config.ProductionConfig" -------------------------------------------------------------------------------- /user-service/.env: -------------------------------------------------------------------------------- 1 | # .env 2 | CONFIGURATION_SETUP="config.ProductionConfig" -------------------------------------------------------------------------------- /order-service/.env: -------------------------------------------------------------------------------- 1 | # .env 2 | CONFIGURATION_SETUP="config.ProductionConfig" -------------------------------------------------------------------------------- /product-service/.env: -------------------------------------------------------------------------------- 1 | #.env 2 | CONFIGURATION_SETUP="config.DevelopmentConfig" -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | migrations/ -------------------------------------------------------------------------------- /order-service/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class -------------------------------------------------------------------------------- /product-service/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class -------------------------------------------------------------------------------- /user-service/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class -------------------------------------------------------------------------------- /frontend/application/static/images/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqibbutt/python-flask-microservices/HEAD/frontend/application/static/images/sample.jpg -------------------------------------------------------------------------------- /frontend/application/static/images/product1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqibbutt/python-flask-microservices/HEAD/frontend/application/static/images/product1.jpg -------------------------------------------------------------------------------- /frontend/application/static/images/product2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqibbutt/python-flask-microservices/HEAD/frontend/application/static/images/product2.jpg -------------------------------------------------------------------------------- /frontend/run.py: -------------------------------------------------------------------------------- 1 | # run.py 2 | from application import create_app 3 | 4 | app = create_app() 5 | 6 | if __name__ == '__main__': 7 | app.run(host='0.0.0.0', port=5000) -------------------------------------------------------------------------------- /frontend/application/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | # application/frontend/__init__.py 2 | from flask import Blueprint 3 | 4 | frontend_blueprint = Blueprint('frontend', __name__) 5 | 6 | from . import views 7 | -------------------------------------------------------------------------------- /order-service/application/order_api/__init__.py: -------------------------------------------------------------------------------- 1 | # application/order_api/__init__.py 2 | from flask import Blueprint 3 | 4 | order_api_blueprint = Blueprint('order_api', __name__) 5 | 6 | from . import routes -------------------------------------------------------------------------------- /user-service/application/user_api/__init__.py: -------------------------------------------------------------------------------- 1 | # application/user_api/__init__.py 2 | from flask import Blueprint 3 | 4 | user_api_blueprint = Blueprint('user_api', __name__) 5 | 6 | from . import routes 7 | -------------------------------------------------------------------------------- /product-service/application/product_api/__init__.py: -------------------------------------------------------------------------------- 1 | # application/product_api/__init__.py 2 | from flask import Blueprint 3 | 4 | product_api_blueprint = Blueprint('product_api', __name__) 5 | 6 | from . import routes 7 | -------------------------------------------------------------------------------- /user-service/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | COPY requirements.txt /userapp/requirements.txt 4 | WORKDIR /userapp 5 | RUN pip install -r requirements.txt 6 | COPY . /userapp 7 | ENTRYPOINT ["python"] 8 | CMD ["run.py"] -------------------------------------------------------------------------------- /order-service/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | COPY requirements.txt /orderapp/requirements.txt 4 | WORKDIR /orderapp 5 | RUN pip install -r requirements.txt 6 | COPY . /orderapp 7 | ENTRYPOINT ["python"] 8 | CMD ["run.py"] -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | COPY requirements.txt /frontendapp/requirements.txt 4 | WORKDIR /frontendapp 5 | RUN pip install -r requirements.txt 6 | COPY . /frontendapp 7 | ENTRYPOINT ["python"] 8 | CMD ["run.py"] -------------------------------------------------------------------------------- /product-service/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | COPY requirements.txt /productapp/requirements.txt 4 | WORKDIR /productapp 5 | RUN pip install -r requirements.txt 6 | COPY . /productapp 7 | ENTRYPOINT ["python"] 8 | CMD ["run.py"] -------------------------------------------------------------------------------- /frontend/application/templates/order/thankyou.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% block title %}Thank you{% endblock %} 3 | 4 | {% block pageContent %} 5 |

Thank you

6 |

your order is being processed

7 | {% endblock %} -------------------------------------------------------------------------------- /order-service/run.py: -------------------------------------------------------------------------------- 1 | # run.py 2 | from application import create_app, db 3 | from flask_migrate import Migrate 4 | from application import models 5 | 6 | app = create_app() 7 | migrate = Migrate(app, db) 8 | 9 | if __name__ == '__main__': 10 | app.run(host='0.0.0.0', port=5003) 11 | -------------------------------------------------------------------------------- /product-service/run.py: -------------------------------------------------------------------------------- 1 | # run.py 2 | from application import create_app, db 3 | from application import models 4 | from flask_migrate import Migrate 5 | 6 | app = create_app() 7 | migrate = Migrate(app, db) 8 | 9 | if __name__ == '__main__': 10 | app.run(host='0.0.0.0', port=5002) 11 | -------------------------------------------------------------------------------- /order-service/README.md: -------------------------------------------------------------------------------- 1 | ## Running application in docker containers: 2 | #### Using Docker CLI 3 | ``` 4 | docker network ls 5 | docker network create --driver bridge micro_network (skip if already created) 6 | docker build -t order-srv . 7 | docker run -p 5003:5003 --detach --name order-service --net=micro_network order-srv 8 | ``` -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Running application in docker containers: 2 | #### Using Docker CLI 3 | ``` 4 | docker network ls 5 | docker network create --driver bridge micro_network (skip if already created) 6 | docker build -t frontend-srv . 7 | docker run -p 5000:5000 --detach --name frontend-service --net=micro_network frontend-srv 8 | ``` -------------------------------------------------------------------------------- /frontend/application/templates/base_col_1.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include 'nav_header.html' %} 5 |
6 | 7 | {% include '_messages.html' %} 8 | {% block pageContent %} {% endblock %} 9 | 10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /product-service/README.md: -------------------------------------------------------------------------------- 1 | ## Running application in docker containers: 2 | #### Using Docker CLI 3 | ``` 4 | docker network ls 5 | docker network create --driver bridge micro_network (skip if already created) 6 | docker build -t product-srv . 7 | docker run -p 5002:5002 --detach --name product-service --net=micro_network product-srv 8 | ``` -------------------------------------------------------------------------------- /frontend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3.8' 3 | 4 | networks: 5 | micro_network: 6 | external: 7 | name: micro_network 8 | 9 | services: 10 | frontend-app: 11 | container_name: cfrontend-app 12 | build: 13 | context: . 14 | ports: 15 | - "5000:5000" 16 | networks: 17 | - micro_network 18 | restart: always -------------------------------------------------------------------------------- /frontend/application/templates/macros/_macros_basket.html: -------------------------------------------------------------------------------- 1 | {{ session['order'] | pprint }} 2 | {% macro count_items()%} 3 | {% set counter = namespace(a=0) %} 4 | {% if session['order'] %} 5 | {% for item in session['order']['items'] %} 6 | {% set counter.a = counter.a + item['quantity'] %} 7 | {% endfor %} 8 | {% endif %} 9 | {{ counter.a }} 10 | {% endmacro %} -------------------------------------------------------------------------------- /frontend/application/templates/base_col_2.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block content %} 6 | {% include 'nav_header.html' %} 7 |
8 | 9 | {% include '_messages.html' %} 10 |
11 |
12 | 13 |
14 |
15 | {% block pageContent %} {% endblock %} 16 |
17 |
18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /order-service/application/order_api/api/UserClient.py: -------------------------------------------------------------------------------- 1 | # application/order_api/api/UserClient.py 2 | import requests 3 | 4 | 5 | class UserClient: 6 | @staticmethod 7 | def get_user(api_key): 8 | headers = { 9 | 'Authorization': api_key 10 | } 11 | response = requests.request(method="GET", url='http://cuser-service:5001/api/user', headers=headers) 12 | if response.status_code == 401: 13 | return False 14 | user = response.json() 15 | return user 16 | -------------------------------------------------------------------------------- /frontend/application/templates/login/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% from "macros/_macros_form.html" import render_field %} 3 | {% block title %}Login{% endblock %} 4 | 5 | {% block pageContent %} 6 |

Login

7 | {{ message }} 8 | 9 |
10 | {{ form.hidden_tag() }} 11 | {{ render_field(form.username) }} 12 | {{ render_field(form.password) }} 13 | {{ form.submit(class_="btn btn-success pull-right") }} 14 | 15 | 16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/frontend/api/ProductClient.py: -------------------------------------------------------------------------------- 1 | # application/frontend/api/ProductClient.py 2 | import requests 3 | 4 | 5 | class ProductClient: 6 | 7 | @staticmethod 8 | def get_products(): 9 | r = requests.get('http://cproduct-service:5002/api/products') 10 | products = r.json() 11 | return products 12 | 13 | @staticmethod 14 | def get_product(slug): 15 | response = requests.request(method="GET", url='http://cproduct-service:5002/api/product/' + slug) 16 | product = response.json() 17 | return product 18 | -------------------------------------------------------------------------------- /frontend/application/templates/_messages.html: -------------------------------------------------------------------------------- 1 | {% for error_message in get_flashed_messages(category_filter=["error"]) %} 2 |
{{ error_message }}
3 | {% endfor %} 4 | 5 | {% for success_message in get_flashed_messages(category_filter=["success"]) %} 6 |
{{ success_message }}
7 | {% endfor %} 8 | 9 | {% for info_message in get_flashed_messages(category_filter=["info"]) %} 10 |
{{ info_message }}
11 | {% endfor %} -------------------------------------------------------------------------------- /frontend/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | 7 | if os.path.exists(dotenv_path): 8 | load_dotenv(dotenv_path) 9 | 10 | 11 | class Config: 12 | SECRET_KEY = 'y2BH8xD9pyZhDT5qkyZZRgjcJCMHdQ' 13 | WTF_CSRF_SECRET_KEY = 'VyOyqv5Fm3Hs3qB1AmNeeuvPpdRqTJbTs5wKvWCS' 14 | 15 | 16 | class DevelopmentConfig(Config): 17 | ENV = "development" 18 | DEBUG = True 19 | 20 | 21 | class ProductionConfig(Config): 22 | ENV = "production" 23 | DEBUG = False 24 | 25 | -------------------------------------------------------------------------------- /order-service/application/__init__.py: -------------------------------------------------------------------------------- 1 | # application/__init__.py 2 | import config 3 | import os 4 | from flask import Flask 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | db = SQLAlchemy() 8 | 9 | 10 | def create_app(): 11 | app = Flask(__name__) 12 | environment_configuration = os.environ['CONFIGURATION_SETUP'] 13 | app.config.from_object(environment_configuration) 14 | 15 | db.init_app(app) 16 | 17 | with app.app_context(): 18 | from .order_api import order_api_blueprint 19 | app.register_blueprint(order_api_blueprint) 20 | return app 21 | -------------------------------------------------------------------------------- /product-service/application/__init__.py: -------------------------------------------------------------------------------- 1 | # application/__init__.py 2 | import config 3 | import os 4 | from flask import Flask 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | db = SQLAlchemy() 8 | 9 | 10 | def create_app(): 11 | app = Flask(__name__) 12 | 13 | environment_configuration = os.environ['CONFIGURATION_SETUP'] 14 | app.config.from_object(environment_configuration) 15 | 16 | db.init_app(app) 17 | 18 | with app.app_context(): 19 | from .product_api import product_api_blueprint 20 | app.register_blueprint(product_api_blueprint) 21 | return app 22 | -------------------------------------------------------------------------------- /user-service/README.md: -------------------------------------------------------------------------------- 1 | ## Running application in docker containers: 2 | #### Using Docker CLI 3 | 4 | ``` 5 | docker network create --driver bridge micro_network (skip if already created) 6 | docker build -t user-srv . 7 | docker run -p 5001:5001 --detach --name user-service --net=micro_network user-srv 8 | ``` 9 | 10 | ## Using 'flask shell' to access flask application 11 | ``` 12 | $ flask shell 13 | from application.models import User 14 | from application import db 15 | admin = User(username="foo", email="foo@admin.com",first_name="foo", last_name="bar", password="admin2020",is_admin=True) 16 | db.session.add(admin) 17 | db.session.commit() 18 | ``` -------------------------------------------------------------------------------- /frontend/application/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% from "macros/_macros_form.html" import render_field %} 3 | {% block title %}Product page{% endblock %} 4 | 5 | {% block pageContent %} 6 | {{ message }} 7 |

Add Product

8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/register/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% from "macros/_macros_form.html" import render_field %} 3 | {% block title %}Register{% endblock %} 4 | 5 | {% block pageContent %} 6 |

Register

7 | {{ message }} 8 | 9 |
10 | {{ form.hidden_tag() }} 11 | {{ render_field(form.username) }} 12 | {{ render_field(form.first_name) }} 13 | {{ render_field(form.last_name) }} 14 | {{ render_field(form.email) }} 15 | {{ render_field(form.password) }} 16 | {{ form.submit(class_="btn btn-success pull-right") }} 17 | 18 |
19 | 20 | {% endblock %} -------------------------------------------------------------------------------- /product-service/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | if os.path.exists(dotenv_path): 7 | load_dotenv(dotenv_path) 8 | 9 | 10 | class Config: 11 | SQLALCHEMY_TRACK_MODIFICATIONS = False 12 | 13 | 14 | class DevelopmentConfig(Config): 15 | ENV = "development" 16 | DEBUG = True 17 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@localhost:3306/product_dev' 18 | 19 | 20 | class ProductionConfig(Config): 21 | ENV = "production" 22 | DEBUG = False 23 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@product-db:3306/product' 24 | -------------------------------------------------------------------------------- /user-service/application/__init__.py: -------------------------------------------------------------------------------- 1 | # application/__init__.py 2 | import config 3 | import os 4 | from flask import Flask 5 | from flask_login import LoginManager 6 | from flask_sqlalchemy import SQLAlchemy 7 | 8 | db = SQLAlchemy() 9 | login_manager = LoginManager() 10 | 11 | 12 | def create_app(): 13 | app = Flask(__name__) 14 | environment_configuration = os.environ['CONFIGURATION_SETUP'] 15 | app.config.from_object(environment_configuration) 16 | 17 | db.init_app(app) 18 | login_manager.init_app(app) 19 | 20 | with app.app_context(): 21 | # Register blueprints 22 | from .user_api import user_api_blueprint 23 | app.register_blueprint(user_api_blueprint) 24 | return app 25 | -------------------------------------------------------------------------------- /order-service/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | if os.path.exists(dotenv_path): 7 | load_dotenv(dotenv_path) 8 | 9 | 10 | class Config: 11 | SQLALCHEMY_TRACK_MODIFICATIONS = False 12 | 13 | 14 | class DevelopmentConfig(Config): 15 | ENV = "development" 16 | DEBUG = True 17 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@host.docker.internal:3306/order_dev' 18 | SQLALCHEMY_ECHO = True 19 | 20 | 21 | class ProductionConfig(Config): 22 | ENV = "production" 23 | DEBUG = False 24 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@order-db:3306/order' 25 | SQLALCHEMY_ECHO = False 26 | 27 | 28 | -------------------------------------------------------------------------------- /user-service/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | if os.path.exists(dotenv_path): 7 | load_dotenv(dotenv_path) 8 | 9 | 10 | class Config: 11 | SECRET_KEY = "mrfrIMEngCl0pAKqIIBS_g" 12 | SQLALCHEMY_TRACK_MODIFICATIONS = False 13 | 14 | 15 | class DevelopmentConfig(Config): 16 | ENV = "development" 17 | DEBUG = True 18 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@host.docker.internal:3306/user_dev' 19 | SQLALCHEMY_ECHO = True 20 | 21 | 22 | class ProductionConfig(Config): 23 | ENV = "production" 24 | DEBUG = False 25 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@user-db:3306/user' 26 | SQLALCHEMY_ECHO = False 27 | 28 | -------------------------------------------------------------------------------- /frontend/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.2 2 | autoenv==1.0.0 3 | blinker==1.4 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==2.8 9 | dnspython==1.16.0 10 | dominate==2.5.1 11 | Flask==1.1.1 12 | Flask-Bootstrap==3.3.7.1 13 | Flask-DotEnv==0.1.2 14 | Flask-Login==0.5.0 15 | Flask-Migrate==2.5.3 16 | Flask-SQLAlchemy==2.4.1 17 | Flask-Uploads==0.2.1 18 | Flask-WTF==0.14.3 19 | idna==2.9 20 | itsdangerous==1.1.0 21 | Jinja2==2.11.1 22 | Mako==1.1.2 23 | MarkupSafe==1.1.1 24 | marshmallow==3.5.1 25 | passlib==1.7.2 26 | protobuf==3.6.1 27 | pycparser==2.20 28 | PyMySQL==0.9.3 29 | python-dateutil==2.8.1 30 | python-dotenv==0.12.0 31 | python-editor==1.0.4 32 | requests==2.23.0 33 | six==1.14.0 34 | SQLAlchemy==1.3.15 35 | urllib3==1.25.8 36 | visitor==0.1.3 37 | Werkzeug==1.0.1 38 | WTForms==2.2.1 39 | -------------------------------------------------------------------------------- /order-service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3.8' 3 | volumes: 4 | orderdb_vol: 5 | 6 | networks: 7 | micro_network: 8 | external: 9 | name: micro_network 10 | 11 | services: 12 | order-api: 13 | container_name: corder-service 14 | build: 15 | context: . 16 | ports: 17 | - "5003:5003" 18 | depends_on: 19 | - order-db 20 | networks: 21 | - micro_network 22 | restart: always 23 | 24 | order-db: 25 | container_name: corder_dbase 26 | image: mysql:8 27 | ports: 28 | - "32002:3306" 29 | environment: 30 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 31 | MYSQL_DATABASE: order 32 | MYSQL_USER: cloudacademy 33 | MYSQL_PASSWORD: pfm_2020 34 | networks: 35 | - micro_network 36 | volumes: 37 | - orderdb_vol:/var/lib/mysql -------------------------------------------------------------------------------- /user-service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3.8' 3 | 4 | volumes: 5 | userdb_vol: 6 | 7 | networks: 8 | micro_network: 9 | external: 10 | name: micro_network 11 | 12 | services: 13 | user-api: 14 | container_name: cuser-service 15 | build: 16 | context: . 17 | ports: 18 | - "5001:5001" 19 | depends_on: 20 | - user-db 21 | networks: 22 | - micro_network 23 | restart: always 24 | 25 | user-db: 26 | container_name: cuser_dbase 27 | image: mysql:8 28 | ports: 29 | - "32000:3306" 30 | environment: 31 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 32 | MYSQL_DATABASE: user 33 | MYSQL_USER: cloudacademy 34 | MYSQL_PASSWORD: pfm_2020 35 | networks: 36 | - micro_network 37 | volumes: 38 | - userdb_vol:/var/lib/mysql -------------------------------------------------------------------------------- /order-service/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.2 2 | autoenv==1.0.0 3 | blinker==1.4 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==2.8 9 | dnspython==1.16.0 10 | dominate==2.5.1 11 | Flask==1.1.1 12 | Flask-Bootstrap==3.3.7.1 13 | Flask-DotEnv==0.1.2 14 | Flask-Login==0.5.0 15 | Flask-Migrate==2.5.3 16 | Flask-SQLAlchemy==2.4.1 17 | Flask-Uploads==0.2.1 18 | Flask-WTF==0.14.3 19 | idna==2.9 20 | itsdangerous==1.1.0 21 | Jinja2==2.11.1 22 | Mako==1.1.2 23 | MarkupSafe==1.1.1 24 | marshmallow==3.5.1 25 | passlib==1.7.2 26 | protobuf==3.6.1 27 | pycparser==2.20 28 | PyMySQL==0.9.3 29 | python-dateutil==2.8.1 30 | python-dotenv==0.12.0 31 | python-editor==1.0.4 32 | requests==2.23.0 33 | six==1.14.0 34 | SQLAlchemy==1.3.15 35 | urllib3==1.25.8 36 | visitor==0.1.3 37 | Werkzeug==1.0.1 38 | WTForms==2.2.1 39 | -------------------------------------------------------------------------------- /product-service/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.2 2 | autoenv==1.0.0 3 | blinker==1.4 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==2.8 9 | dnspython==1.16.0 10 | dominate==2.5.1 11 | Flask==1.1.1 12 | Flask-Bootstrap==3.3.7.1 13 | Flask-DotEnv==0.1.2 14 | Flask-Login==0.5.0 15 | Flask-Migrate==2.5.3 16 | Flask-SQLAlchemy==2.4.1 17 | Flask-Uploads==0.2.1 18 | Flask-WTF==0.14.3 19 | idna==2.9 20 | itsdangerous==1.1.0 21 | Jinja2==2.11.1 22 | Mako==1.1.2 23 | MarkupSafe==1.1.1 24 | marshmallow==3.5.1 25 | passlib==1.7.2 26 | protobuf==3.6.1 27 | pycparser==2.20 28 | PyMySQL==0.9.3 29 | python-dateutil==2.8.1 30 | python-dotenv==0.12.0 31 | python-editor==1.0.4 32 | requests==2.23.0 33 | six==1.14.0 34 | SQLAlchemy==1.3.15 35 | urllib3==1.25.8 36 | visitor==0.1.3 37 | Werkzeug==1.0.1 38 | WTForms==2.2.1 39 | -------------------------------------------------------------------------------- /product-service/application/models.py: -------------------------------------------------------------------------------- 1 | # application/models.py 2 | from . import db 3 | from datetime import datetime 4 | 5 | 6 | class Product(db.Model): 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(255), unique=True, nullable=False) 9 | slug = db.Column(db.String(255), unique=True, nullable=False) 10 | price = db.Column(db.Integer, nullable=False) 11 | image = db.Column(db.String(255), unique=False, nullable=True) 12 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 13 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 14 | 15 | def to_json(self): 16 | return { 17 | 'id': self.id, 18 | 'name': self.name, 19 | 'slug': self.slug, 20 | 'price': self.price, 21 | 'image': self.image 22 | } 23 | -------------------------------------------------------------------------------- /product-service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3.8' 3 | 4 | volumes: 5 | productdb_vol: 6 | 7 | networks: 8 | micro_network: 9 | external: 10 | name: micro_network 11 | 12 | services: 13 | product-api: 14 | container_name: cproduct-service 15 | build: 16 | context: . 17 | ports: 18 | - "5002:5002" 19 | depends_on: 20 | - product-db 21 | networks: 22 | - micro_network 23 | restart: always 24 | 25 | product-db: 26 | container_name: cproduct_dbase 27 | image: mysql:8 28 | ports: 29 | - "32001:3306" 30 | environment: 31 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 32 | MYSQL_DATABASE: product 33 | MYSQL_USER: cloudacademy 34 | MYSQL_PASSWORD: pfm_2020 35 | networks: 36 | - micro_network 37 | volumes: 38 | - productdb_vol:/var/lib/mysql -------------------------------------------------------------------------------- /user-service/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.2 2 | autoenv==1.0.0 3 | blinker==1.4 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | click==7.1.1 8 | cryptography==2.8 9 | dnspython==1.16.0 10 | dominate==2.5.1 11 | Flask==1.1.1 12 | Flask-Bootstrap==3.3.7.1 13 | Flask-DotEnv==0.1.2 14 | Flask-Login==0.5.0 15 | Flask-Migrate==2.5.3 16 | Flask-SQLAlchemy==2.4.1 17 | Flask-Uploads==0.2.1 18 | Flask-WTF==0.14.3 19 | gunicorn==20.0.4 20 | idna==2.9 21 | itsdangerous==1.1.0 22 | Jinja2==2.11.1 23 | Mako==1.1.2 24 | MarkupSafe==1.1.1 25 | marshmallow==3.5.1 26 | passlib==1.7.2 27 | protobuf==3.6.1 28 | pycparser==2.20 29 | PyMySQL==0.9.3 30 | python-dateutil==2.8.1 31 | python-dotenv==0.12.0 32 | python-editor==1.0.4 33 | requests==2.23.0 34 | six==1.14.0 35 | SQLAlchemy==1.3.15 36 | urllib3==1.25.8 37 | visitor==0.1.3 38 | Werkzeug==1.0.1 39 | WTForms==2.2.1 40 | -------------------------------------------------------------------------------- /frontend/application/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %} {% endblock %} 6 | 7 | 8 | {% block styles %} 9 | 10 | 11 | 12 | 15 | 16 | {% endblock %} 17 | 18 | 19 | 20 | 21 | {% block content %} 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-flask-microservices 2 | ### Training material for https://cloudacademy.com/course/mastering-microservices-with-python-flask-docker-1118/course-introduction/ 3 | 4 | ## Project Detail 5 | ###### 'python-flask-microservices' has 4 projects within. 6 | 7 | - frontend 8 | - order-service 9 | - product-service 10 | - user-service 11 | 12 | ###### Preferably configure a separate python virtual environment and install python packages. 13 | ``` 14 | pip install -r requirements.txt 15 | ``` 16 | 17 | ## Deployment in Production 18 | ###### For deployment in production, use docker-compose.deploy.yml in 'frontend' project folder. 19 | 20 | ## Python extensions reference 21 | ##### Flask-SQLAlchemy: https://flask-sqlalchemy.palletsprojects.com/en/2.x/ 22 | ##### Flask-Login: https://flask-login.readthedocs.io/en/latest/ 23 | ##### Flask-Migrate: https://github.com/miguelgrinberg/flask-migrate/ 24 | ##### Requests: https://requests.readthedocs.io/en/master/ 25 | -------------------------------------------------------------------------------- /frontend/application/__init__.py: -------------------------------------------------------------------------------- 1 | # application/__init__.py 2 | import config 3 | import os 4 | from flask import Flask 5 | from flask_bootstrap import Bootstrap 6 | from flask_login import LoginManager 7 | 8 | login_manager = LoginManager() 9 | bootstrap = Bootstrap() 10 | UPLOAD_FOLDER = 'application/static/images' 11 | 12 | 13 | def create_app(): 14 | app = Flask(__name__, static_folder='static') 15 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 16 | 17 | environment_configuration = os.environ['CONFIGURATION_SETUP'] 18 | app.config.from_object(environment_configuration) 19 | 20 | login_manager.init_app(app) 21 | login_manager.login_message = "You must be login to access this page." 22 | login_manager.login_view = "frontend.login" 23 | 24 | bootstrap.init_app(app) 25 | 26 | with app.app_context(): 27 | from .frontend import frontend_blueprint 28 | app.register_blueprint(frontend_blueprint) 29 | 30 | return app 31 | -------------------------------------------------------------------------------- /user-service/run.py: -------------------------------------------------------------------------------- 1 | # run.py 2 | from application import create_app, db 3 | from application import models 4 | from flask_migrate import Migrate 5 | 6 | app = create_app() 7 | migrate = Migrate(app, db) 8 | 9 | from flask import g 10 | from flask.sessions import SecureCookieSessionInterface 11 | from flask_login import user_loaded_from_header 12 | 13 | 14 | class CustomSessionInterface(SecureCookieSessionInterface): 15 | """Prevent creating session from API requests.""" 16 | 17 | def save_session(self, *args, **kwargs): 18 | if g.get('login_via_header'): 19 | return 20 | return super(CustomSessionInterface, self).save_session(*args, 21 | **kwargs) 22 | 23 | 24 | app.session_interface = CustomSessionInterface() 25 | 26 | 27 | @user_loaded_from_header.connect 28 | def user_loaded_from_header(self, user=None): 29 | g.login_via_header = True 30 | 31 | 32 | if __name__ == '__main__': 33 | app.run(host='0.0.0.0', port=5001) 34 | -------------------------------------------------------------------------------- /frontend/application/templates/nav_header.html: -------------------------------------------------------------------------------- 1 | {% from "macros/_macros_basket.html" import count_items %} 2 |
3 | 24 |
-------------------------------------------------------------------------------- /frontend/application/templates/home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% block title %}Home Page{% endblock %} 3 | 4 | {% block pageContent %} 5 | 6 | {% if products | length > 0 %} 7 | {% for product in products.results %} 8 | 9 | {% set url = "/product/" + product.slug %} 10 | {% set imageSRC = "images/" + product.image %} 11 | 12 |
13 |
14 |
15 |

{{ product.name }}

16 |
17 |
18 |
19 | 20 |
21 |
22 | 28 |
29 |
30 | {% endfor %} 31 | 32 | {% else %} 33 | No products found. 34 | {% endif %} 35 | 36 | 37 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/templates/product/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_col_1.html" %} 2 | {% from "macros/_macros_form.html" import render_field %} 3 | {% block title %}Product page{% endblock %} 4 | 5 | {% block pageContent %} 6 | 7 | {% set url = "/product/" + product.slug %} 8 | {% set imageSRC = "images/" + product.image %} 9 |
10 |
11 |
12 |

{{ product.name }}

13 |
14 |
15 |
16 | 17 |
18 |
19 | 28 |
29 |
30 | {% endblock %} -------------------------------------------------------------------------------- /frontend/application/static/css/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | body, h1, h2, h3 { 7 | font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-weight: 700; 9 | } 10 | 11 | body{ 12 | background-color:#DDD; 13 | } 14 | 15 | a, .navbar-default .navbar-brand, .navbar-default .navbar-nav > li > a { 16 | color: #aec251; 17 | } 18 | 19 | a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav > li > a:hover { 20 | color: #687430; 21 | } 22 | 23 | footer { 24 | padding: 50px 0; 25 | background-color: #f8f8f8; 26 | } 27 | 28 | p.copyright { 29 | margin: 15px 0 0; 30 | } 31 | 32 | .alert-info { 33 | width: 50%; 34 | margin: auto; 35 | color: #687430; 36 | background-color: #e6ecca; 37 | border-color: #aec251; 38 | } 39 | 40 | .btn-default { 41 | border-color: #aec251; 42 | color: #aec251; 43 | } 44 | 45 | .btn-default:hover { 46 | background-color: #aec251; 47 | } 48 | 49 | .center { 50 | margin: auto; 51 | width: 50%; 52 | padding: 10px; 53 | } 54 | 55 | .content-section { 56 | padding: 50px 0; 57 | border-top: 1px solid #e7e7e7; 58 | } 59 | 60 | 61 | .img-block{ 62 | text-align:center; 63 | } 64 | .img-block img{ 65 | 66 | max-width:200px; 67 | max-height:200px; 68 | } 69 | -------------------------------------------------------------------------------- /frontend/application/templates/macros/_macros_form.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |
3 | {{ field.label(class_="form-label") }}: 4 | {% if field.errors %} 5 | 6 | {% for error in field.errors %} 7 | {{ error }} 8 | {% endfor %} 9 | 10 | {% endif %} 11 | 12 | 13 | {{ field(class_="form-control") }} 14 |
15 | {% endmacro %} 16 | 17 | 18 | 19 | {% macro render_field_without_label(field) %} 20 |
{{ field(**kwargs)|safe }} 21 | {% if field.errors %} 22 | 23 | {% for error in field.errors %} 24 | {{ error }} 25 | {% endfor %} 26 | 27 | {% endif %} 28 |
29 | {% endmacro %} 30 | 31 | 32 | {% macro render_boolean_field(field) %} 33 |
{{ field(**kwargs)|safe }} {{ field.label }} 34 | {% if field.errors %} 35 | 36 | {% for error in field.errors %} 37 | {{ error }} 38 | {% endfor %} 39 | 40 | {% endif %} 41 |
42 | {% endmacro %} 43 | 44 | 45 | {% macro render_errors(field) %} 46 | {% if field.errors %} 47 | 48 | {% for error in field.errors %} 49 | {{ error }} 50 | {% endfor %} 51 | 52 | {% endif %} 53 | {% endmacro %} 54 | -------------------------------------------------------------------------------- /frontend/application/frontend/forms.py: -------------------------------------------------------------------------------- 1 | # application/frontend/forms.py 2 | from flask_wtf import FlaskForm 3 | from wtforms import StringField, PasswordField, SubmitField, HiddenField, IntegerField 4 | 5 | from wtforms.validators import DataRequired, Email 6 | 7 | 8 | class LoginForm(FlaskForm): 9 | username = StringField('Username', validators=[DataRequired()]) 10 | password = PasswordField('Password', validators=[DataRequired()]) 11 | submit = SubmitField('Login') 12 | 13 | 14 | class RegistrationForm(FlaskForm): 15 | username = StringField('Username', validators=[DataRequired()]) 16 | first_name = StringField('First name', validators=[DataRequired()]) 17 | last_name = StringField('Last name', validators=[DataRequired()]) 18 | email = StringField('Email address', validators=[DataRequired(), Email()]) 19 | password = PasswordField('Password', validators=[DataRequired()]) 20 | submit = SubmitField('Register') 21 | 22 | 23 | class OrderItemForm(FlaskForm): 24 | product_id = HiddenField(validators=[DataRequired()]) 25 | quantity = IntegerField(validators=[DataRequired()]) 26 | order_id = HiddenField() 27 | submit = SubmitField('Update') 28 | 29 | 30 | class ItemForm(FlaskForm): 31 | product_id = HiddenField(validators=[DataRequired()]) 32 | quantity = HiddenField(validators=[DataRequired()], default=1) 33 | -------------------------------------------------------------------------------- /product-service/application/product_api/routes.py: -------------------------------------------------------------------------------- 1 | # application/product_api/routes.py 2 | from . import product_api_blueprint 3 | from .. import db 4 | from ..models import Product 5 | from flask import jsonify, request 6 | 7 | 8 | @product_api_blueprint.route('/api/products', methods=['GET']) 9 | def products(): 10 | items = [] 11 | for row in Product.query.all(): 12 | items.append(row.to_json()) 13 | 14 | response = jsonify({'results': items}) 15 | return response 16 | 17 | 18 | @product_api_blueprint.route('/api/product/create', methods=['POST']) 19 | def post_create(): 20 | name = request.form['name'] 21 | slug = request.form['slug'] 22 | image = request.form['image'] 23 | price = request.form['price'] 24 | 25 | item = Product() 26 | item.name = name 27 | item.slug = slug 28 | item.image = image 29 | item.price = price 30 | 31 | db.session.add(item) 32 | db.session.commit() 33 | 34 | response = jsonify({'message': 'Product added', 'product': item.to_json()}) 35 | return response 36 | 37 | 38 | @product_api_blueprint.route('/api/product/', methods=['GET']) 39 | def product(slug): 40 | item = Product.query.filter_by(slug=slug).first() 41 | if item is not None: 42 | response = jsonify({'result': item.to_json()}) 43 | else: 44 | response = jsonify({'message': 'Cannot find product'}), 404 45 | return response 46 | -------------------------------------------------------------------------------- /order-service/application/models.py: -------------------------------------------------------------------------------- 1 | # application/models.py 2 | from . import db 3 | from datetime import datetime 4 | 5 | class Order(db.Model): 6 | id = db.Column(db.Integer, primary_key=True) 7 | user_id = db.Column(db.Integer) 8 | items = db.relationship('OrderItem', backref='orderItem') 9 | is_open = db.Column(db.Boolean, default=True) 10 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 11 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 12 | 13 | def create(self, user_id): 14 | self.user_id = user_id 15 | self.is_open = True 16 | return self 17 | 18 | def to_json(self): 19 | items = [] 20 | for i in self.items: 21 | items.append(i.to_json()) 22 | 23 | return { 24 | 'items': items, 25 | 'is_open': self.is_open, 26 | 'user_id': self.user_id 27 | } 28 | 29 | 30 | class OrderItem(db.Model): 31 | id = db.Column(db.Integer, primary_key=True) 32 | order_id = db.Column(db.Integer, db.ForeignKey('order.id')) 33 | product_id = db.Column(db.Integer) 34 | quantity = db.Column(db.Integer, default=1) 35 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 36 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 37 | 38 | def __init__(self, product_id, quantity): 39 | self.product_id = product_id 40 | self.quantity = quantity 41 | 42 | def to_json(self): 43 | return { 44 | 'product': self.product_id, 45 | 'quantity': self.quantity 46 | } -------------------------------------------------------------------------------- /frontend/application/frontend/api/OrderClient.py: -------------------------------------------------------------------------------- 1 | # application/frontend/api/OrderClient.py 2 | from flask import session 3 | import requests 4 | 5 | 6 | class OrderClient: 7 | @staticmethod 8 | def get_order(): 9 | headers = { 10 | 'Authorization': 'Basic ' + session['user_api_key'] 11 | } 12 | url = 'http://corder-service:5003/api/order' 13 | response = requests.request(method="GET", url=url, headers=headers) 14 | order = response.json() 15 | return order 16 | 17 | @staticmethod 18 | def post_add_to_cart(product_id, qty=1): 19 | payload = { 20 | 'product_id': product_id, 21 | 'qty': qty 22 | } 23 | url = 'http://corder-service:5003/api/order/add-item' 24 | 25 | headers = { 26 | 'Authorization': 'Basic ' + session['user_api_key'] 27 | } 28 | response = requests.request("POST", url=url, data=payload, headers=headers) 29 | if response: 30 | order = response.json() 31 | return order 32 | 33 | @staticmethod 34 | def post_checkout(): 35 | url = 'http://corder-service:5003/api/order/checkout' 36 | 37 | headers = { 38 | 'Authorization': 'Basic ' + session['user_api_key'] 39 | } 40 | response = requests.request("POST", url=url, headers=headers) 41 | order = response.json() 42 | return order 43 | 44 | @staticmethod 45 | def get_order_from_session(): 46 | default_order = { 47 | 'items': {}, 48 | 'total': 0, 49 | } 50 | return session.get('order', default_order) 51 | -------------------------------------------------------------------------------- /user-service/application/models.py: -------------------------------------------------------------------------------- 1 | # application/models.py 2 | from . import db 3 | from datetime import datetime 4 | from flask_login import UserMixin 5 | from passlib.hash import sha256_crypt 6 | 7 | 8 | class User(UserMixin, db.Model): 9 | id = db.Column(db.Integer, primary_key=True) 10 | username = db.Column(db.String(255), unique=True, nullable=False) 11 | email = db.Column(db.String(255), unique=True, nullable=False) 12 | first_name = db.Column(db.String(255), unique=False, nullable=True) 13 | last_name = db.Column(db.String(255), unique=False, nullable=True) 14 | password = db.Column(db.String(255), unique=False, nullable=False) 15 | is_admin = db.Column(db.Boolean, default=False) 16 | authenticated = db.Column(db.Boolean, default=False) 17 | api_key = db.Column(db.String(255), unique=True, nullable=True) 18 | date_added = db.Column(db.DateTime, default=datetime.utcnow) 19 | date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) 20 | 21 | def encode_api_key(self): 22 | self.api_key = sha256_crypt.hash(self.username + str(datetime.utcnow)) 23 | 24 | def encode_password(self): 25 | self.password = sha256_crypt.hash(self.password) 26 | 27 | def __repr__(self): 28 | return '' % (self.username) 29 | 30 | def to_json(self): 31 | return { 32 | 'first_name': self.first_name, 33 | 'last_name': self.last_name, 34 | 'username': self.username, 35 | 'email': self.email, 36 | 'id': self.id, 37 | 'api_key': self.api_key, 38 | 'is_active': True, 39 | 'is_admin': self.is_admin 40 | } -------------------------------------------------------------------------------- /frontend/application/frontend/api/UserClient.py: -------------------------------------------------------------------------------- 1 | # application/frontend/api/UserClient.py 2 | import requests 3 | from flask import session, request 4 | 5 | 6 | class UserClient: 7 | @staticmethod 8 | def post_login(form): 9 | api_key = False 10 | payload = { 11 | 'username': form.username.data, 12 | 'password': form.password.data 13 | } 14 | url = 'http://cuser-service:5001/api/user/login' 15 | response = requests.request("POST", url=url, data=payload) 16 | if response: 17 | d = response.json() 18 | print("This is response from user api: " + str(d)) 19 | if d['api_key'] is not None: 20 | api_key = d['api_key'] 21 | return api_key 22 | 23 | @staticmethod 24 | def get_user(): 25 | 26 | headers = { 27 | 'Authorization': 'Basic ' + session['user_api_key'] 28 | } 29 | url = 'http://cuser-service:5001/api/user' 30 | response = requests.request(method="GET", url=url, headers=headers) 31 | user = response.json() 32 | return user 33 | 34 | @staticmethod 35 | def post_user_create(form): 36 | user = False 37 | payload = { 38 | 'email': form.email.data, 39 | 'password': form.password.data, 40 | 'first_name': form.first_name.data, 41 | 'last_name': form.last_name.data, 42 | 'username': form.username.data 43 | } 44 | url = 'http://cuser-service:5001/api/user/create' 45 | response = requests.request("POST", url=url, data=payload) 46 | if response: 47 | user = response.json() 48 | return user 49 | 50 | @staticmethod 51 | def does_exist(username): 52 | url = 'http://cuser-service:5001/api/user/' + username + '/exists' 53 | response = requests.request("GET", url=url) 54 | return response.status_code == 200 55 | 56 | -------------------------------------------------------------------------------- /frontend/docker-compose.deploy.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.deploy.yml 2 | version: '3.8' 3 | 4 | volumes: 5 | userdb_vol: 6 | productdb_vol: 7 | orderdb_vol: 8 | 9 | networks: 10 | micro_network: 11 | external: 12 | name: micro_network 13 | 14 | services: 15 | user-api: 16 | container_name: cuser-service 17 | build: 18 | context: ../user-service 19 | ports: 20 | - "5001:5001" 21 | depends_on: 22 | - user-db 23 | networks: 24 | - micro_network 25 | restart: always 26 | 27 | user-db: 28 | container_name: cuser_dbase 29 | image: mysql:8 30 | ports: 31 | - "32000:3306" 32 | environment: 33 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 34 | MYSQL_DATABASE: user 35 | MYSQL_USER: cloudacademy 36 | MYSQL_PASSWORD: pfm_2020 37 | networks: 38 | - micro_network 39 | volumes: 40 | - userdb_vol:/var/lib/mysql 41 | 42 | product-api: 43 | container_name: cproduct-service 44 | build: 45 | context: ../product-service 46 | ports: 47 | - "5002:5002" 48 | depends_on: 49 | - product-db 50 | networks: 51 | - micro_network 52 | restart: always 53 | 54 | product-db: 55 | container_name: cproduct_dbase 56 | image: mysql:8 57 | ports: 58 | - "32001:3306" 59 | environment: 60 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 61 | MYSQL_DATABASE: product 62 | MYSQL_USER: cloudacademy 63 | MYSQL_PASSWORD: pfm_2020 64 | networks: 65 | - micro_network 66 | volumes: 67 | - productdb_vol:/var/lib/mysql 68 | 69 | order-api: 70 | container_name: corder-service 71 | build: 72 | context: ../order-service 73 | ports: 74 | - "5003:5003" 75 | depends_on: 76 | - order-db 77 | networks: 78 | - micro_network 79 | restart: always 80 | 81 | order-db: 82 | container_name: corder_dbase 83 | image: mysql:8 84 | ports: 85 | - "32002:3306" 86 | environment: 87 | MYSQL_ROOT_PASSWORD: pfm_dc_2020 88 | MYSQL_DATABASE: order 89 | MYSQL_USER: cloudacademy 90 | MYSQL_PASSWORD: pfm_2020 91 | networks: 92 | - micro_network 93 | volumes: 94 | - orderdb_vol:/var/lib/mysql 95 | 96 | frontend-app: 97 | container_name: cfrontend-app 98 | build: 99 | context: . 100 | ports: 101 | - "5000:5000" 102 | networks: 103 | - micro_network 104 | restart: always -------------------------------------------------------------------------------- /order-service/application/order_api/routes.py: -------------------------------------------------------------------------------- 1 | # application/order_api/routes.py 2 | from flask import jsonify, request, make_response 3 | from . import order_api_blueprint 4 | from .. import db 5 | from ..models import Order, OrderItem 6 | from .api.UserClient import UserClient 7 | 8 | 9 | @order_api_blueprint.route('/api/orders', methods=['GET']) 10 | def orders(): 11 | items = [] 12 | for row in Order.query.all(): 13 | items.append(row.to_json()) 14 | 15 | response = jsonify(items) 16 | return response 17 | 18 | 19 | @order_api_blueprint.route('/api/order/add-item', methods=['POST']) 20 | def order_add_item(): 21 | api_key = request.headers.get('Authorization') 22 | response = UserClient.get_user(api_key) 23 | 24 | if not response: 25 | return make_response(jsonify({'message': 'Not logged in'}), 401) 26 | 27 | user = response['result'] 28 | p_id = int(request.form['product_id']) 29 | qty = int(request.form['qty']) 30 | u_id = int(user['id']) 31 | 32 | known_order = Order.query.filter_by(user_id=u_id, is_open=1).first() 33 | 34 | if known_order is None: 35 | known_order = Order() 36 | known_order.is_open = True 37 | known_order.user_id = u_id 38 | 39 | order_item = OrderItem(p_id, qty) 40 | known_order.items.append(order_item) 41 | else: 42 | found = False 43 | 44 | for item in known_order.items: 45 | if item.product_id == p_id: 46 | found = True 47 | item.quantity += qty 48 | 49 | if found is False: 50 | order_item = OrderItem(p_id, qty) 51 | known_order.items.append(order_item) 52 | 53 | db.session.add(known_order) 54 | db.session.commit() 55 | response = jsonify({'result': known_order.to_json()}) 56 | return response 57 | 58 | 59 | @order_api_blueprint.route('/api/order', methods=['GET']) 60 | def order(): 61 | api_key = request.headers.get('Authorization') 62 | 63 | response = UserClient.get_user(api_key) 64 | 65 | if not response: 66 | return make_response(jsonify({'message': 'Not logged in'}), 401) 67 | 68 | user = response['result'] 69 | open_order = Order.query.filter_by(user_id=user['id'], is_open=1).first() 70 | 71 | if open_order is None: 72 | response = jsonify({'message': 'No order found'}) 73 | else: 74 | response = jsonify({'result': open_order.to_json()}) 75 | return response 76 | 77 | 78 | @order_api_blueprint.route('/api/order/checkout', methods=['POST']) 79 | def checkout(): 80 | api_key = request.headers.get('Authorization') 81 | 82 | response = UserClient.get_user(api_key) 83 | 84 | if not response: 85 | return make_response(jsonify({'message': 'Not logged in'}), 401) 86 | 87 | user = response['result'] 88 | 89 | order_model = Order.query.filter_by(user_id=user['id'], is_open=1).first() 90 | order_model.is_open = 0 91 | 92 | db.session.add(order_model) 93 | db.session.commit() 94 | 95 | response = jsonify({'result': order_model.to_json()}) 96 | return response 97 | -------------------------------------------------------------------------------- /user-service/application/user_api/routes.py: -------------------------------------------------------------------------------- 1 | # application/user_api/routes.py 2 | from . import user_api_blueprint 3 | from .. import db, login_manager 4 | from ..models import User 5 | from flask import make_response, request, jsonify 6 | from flask_login import current_user, login_user, logout_user, login_required 7 | 8 | from passlib.hash import sha256_crypt 9 | 10 | 11 | @login_manager.user_loader 12 | def load_user(user_id): 13 | return User.query.filter_by(id=user_id).first() 14 | 15 | 16 | @login_manager.request_loader 17 | def load_user_from_request(request): 18 | api_key = request.headers.get('Authorization') 19 | if api_key: 20 | api_key = api_key.replace('Basic ', '', 1) 21 | user = User.query.filter_by(api_key=api_key).first() 22 | if user: 23 | return user 24 | return None 25 | 26 | 27 | @user_api_blueprint.route('/api/users', methods=['GET']) 28 | def get_users(): 29 | data = [] 30 | for row in User.query.all(): 31 | data.append(row.to_json()) 32 | 33 | response = jsonify(data) 34 | return response 35 | 36 | @user_api_blueprint.route('/api/user/create', methods=['POST']) 37 | def post_register(): 38 | first_name = request.form['first_name'] 39 | last_name = request.form['last_name'] 40 | email = request.form['email'] 41 | username = request.form['username'] 42 | 43 | password = sha256_crypt.hash((str(request.form['password']))) 44 | 45 | user = User() 46 | user.email = email 47 | user.first_name = first_name 48 | user.last_name = last_name 49 | user.password = password 50 | user.username = username 51 | user.authenticated = True 52 | 53 | db.session.add(user) 54 | db.session.commit() 55 | 56 | response = jsonify({'message': 'User added', 'result': user.to_json()}) 57 | 58 | return response 59 | 60 | 61 | @user_api_blueprint.route('/api/user/login', methods=['POST']) 62 | def post_login(): 63 | username = request.form['username'] 64 | user = User.query.filter_by(username=username).first() 65 | if user: 66 | if sha256_crypt.verify(str(request.form['password']), user.password): 67 | user.encode_api_key() 68 | db.session.commit() 69 | login_user(user) 70 | 71 | return make_response(jsonify({'message': 'Logged in', 'api_key': user.api_key})) 72 | 73 | return make_response(jsonify({'message': 'Not logged in'}), 401) 74 | 75 | 76 | @user_api_blueprint.route('/api/user/logout', methods=['POST']) 77 | def post_logout(): 78 | if current_user.is_authenticated: 79 | logout_user() 80 | return make_response(jsonify({'message': 'You are logged out'})) 81 | return make_response(jsonify({'message': 'You are not logged in'})) 82 | 83 | 84 | @user_api_blueprint.route('/api/user//exists', methods=['GET']) 85 | def get_username(username): 86 | item = User.query.filter_by(username=username).first() 87 | if item is not None: 88 | response = jsonify({'result': True}) 89 | else: 90 | response = jsonify({'message': 'Cannot find username'}), 404 91 | return response 92 | 93 | 94 | @login_required 95 | @user_api_blueprint.route('/api/user', methods=['GET']) 96 | def get_user(): 97 | if current_user.is_authenticated: 98 | return make_response(jsonify({'result': current_user.to_json()})) 99 | 100 | return make_response(jsonify({'message': 'Not logged in'})), 401 -------------------------------------------------------------------------------- /frontend/application/frontend/views.py: -------------------------------------------------------------------------------- 1 | # application/frontend/views.py 2 | import requests 3 | from . import forms 4 | from . import frontend_blueprint 5 | from .. import login_manager 6 | from .api.UserClient import UserClient 7 | from .api.ProductClient import ProductClient 8 | from .api.OrderClient import OrderClient 9 | from flask import render_template, session, redirect, url_for, flash, request 10 | 11 | from flask_login import current_user 12 | 13 | 14 | @login_manager.user_loader 15 | def load_user(user_id): 16 | return None 17 | 18 | 19 | @frontend_blueprint.route('/', methods=['GET']) 20 | def home(): 21 | if current_user.is_authenticated: 22 | session['order'] = OrderClient.get_order_from_session() 23 | 24 | try: 25 | products = ProductClient.get_products() 26 | except requests.exceptions.ConnectionError: 27 | products = { 28 | 'results': [] 29 | } 30 | 31 | return render_template('home/index.html', products=products) 32 | 33 | 34 | @frontend_blueprint.route('/register', methods=['GET', 'POST']) 35 | def register(): 36 | form = forms.RegistrationForm(request.form) 37 | if request.method == "POST": 38 | if form.validate_on_submit(): 39 | username = form.username.data 40 | 41 | # Search for existing user 42 | user = UserClient.does_exist(username) 43 | if user: 44 | # Existing user found 45 | flash('Please try another username', 'error') 46 | return render_template('register/index.html', form=form) 47 | else: 48 | # Attempt to create new user 49 | user = UserClient.post_user_create(form) 50 | if user: 51 | flash('Thanks for registering, please login', 'success') 52 | return redirect(url_for('frontend.login')) 53 | 54 | else: 55 | flash('Errors found', 'error') 56 | 57 | return render_template('register/index.html', form=form) 58 | 59 | 60 | @frontend_blueprint.route('/login', methods=['GET', 'POST']) 61 | def login(): 62 | if current_user.is_authenticated: 63 | return redirect(url_for('frontend.home')) 64 | form = forms.LoginForm() 65 | if request.method == "POST": 66 | if form.validate_on_submit(): 67 | api_key = UserClient.post_login(form) 68 | if api_key: 69 | session['user_api_key'] = api_key 70 | user = UserClient.get_user() 71 | session['user'] = user['result'] 72 | 73 | order = OrderClient.get_order() 74 | if order.get('result', False): 75 | session['order'] = order['result'] 76 | 77 | flash('Welcome back, ' + user['result']['username'], 'success') 78 | return redirect(url_for('frontend.home')) 79 | else: 80 | flash('Cannot login', 'error') 81 | else: 82 | flash('Errors found', 'error') 83 | return render_template('login/index.html', form=form) 84 | 85 | 86 | @frontend_blueprint.route('/logout', methods=['GET']) 87 | def logout(): 88 | session.clear() 89 | return redirect(url_for('frontend.home')) 90 | 91 | 92 | @frontend_blueprint.route('/product/', methods=['GET', 'POST']) 93 | def product(slug): 94 | response = ProductClient.get_product(slug) 95 | item = response['result'] 96 | 97 | form = forms.ItemForm(product_id=item['id']) 98 | 99 | if request.method == "POST": 100 | if 'user' not in session: 101 | flash('Please login', 'error') 102 | return redirect(url_for('frontend.login')) 103 | order = OrderClient.post_add_to_cart(product_id=item['id'], qty=1) 104 | session['order'] = order['result'] 105 | flash('Order has been updated', 'success') 106 | return render_template('product/index.html', product=item, form=form) 107 | 108 | 109 | @frontend_blueprint.route('/checkout', methods=['GET']) 110 | def summary(): 111 | if 'user' not in session: 112 | flash('Please login', 'error') 113 | return redirect(url_for('frontend.login')) 114 | 115 | if 'order' not in session: 116 | flash('No order found', 'error') 117 | return redirect(url_for('frontend.home')) 118 | order = OrderClient.get_order() 119 | 120 | if len(order['result']['items']) == 0: 121 | flash('No order found', 'error') 122 | return redirect(url_for('frontend.home')) 123 | 124 | OrderClient.post_checkout() 125 | 126 | return redirect(url_for('frontend.thank_you')) 127 | 128 | @frontend_blueprint.route('/order/thank-you', methods=['GET']) 129 | def thank_you(): 130 | if 'user' not in session: 131 | flash('Please login', 'error') 132 | return redirect(url_for('frontend.login')) 133 | 134 | if 'order' not in session: 135 | flash('No order found', 'error') 136 | return redirect(url_for('frontend.home')) 137 | 138 | session.pop('order', None) 139 | flash('Thank you for your order', 'success') 140 | 141 | return render_template('order/thankyou.html') --------------------------------------------------------------------------------