From d207082ab3dc2eb133a0b38210948d835541fc0e Mon Sep 17 00:00:00 2001 From: Jeremy Cook Date: Thu, 5 Dec 2024 09:30:51 +1300 Subject: [PATCH] initial commit --- .github/workflows/release.yml | 40 +++++ .gitignore | 6 + microservices/frontend/.env | 2 + microservices/frontend/.flaskenv | 1 + microservices/frontend/.gitignore | 5 + microservices/frontend/Dockerfile | 8 + microservices/frontend/README.md | 8 + .../frontend/application/__init__.py | 30 ++++ .../frontend/application/frontend/__init__.py | 6 + .../application/frontend/api/OrderClient.py | 50 +++++++ .../application/frontend/api/ProductClient.py | 17 +++ .../application/frontend/api/UserClient.py | 55 +++++++ .../application/frontend/api/__init__.py | 0 .../frontend/application/frontend/forms.py | 32 ++++ .../frontend/application/frontend/views.py | 141 ++++++++++++++++++ .../frontend/application/static/css/style.css | 68 +++++++++ .../application/static/images/product1.jpg | Bin 0 -> 10682 bytes .../application/static/images/product2.jpg | Bin 0 -> 10682 bytes .../application/static/images/sample.jpg | Bin 0 -> 10682 bytes .../application/templates/_messages.html | 11 ++ .../application/templates/admin/index.html | 19 +++ .../frontend/application/templates/base.html | 24 +++ .../application/templates/base_col_1.html | 11 ++ .../application/templates/base_col_2.html | 19 +++ .../application/templates/home/index.html | 37 +++++ .../application/templates/login/index.html | 18 +++ .../templates/macros/_macros_basket.html | 10 ++ .../templates/macros/_macros_form.html | 53 +++++++ .../application/templates/nav_header.html | 24 +++ .../application/templates/order/thankyou.html | 7 + .../application/templates/product/index.html | 30 ++++ .../application/templates/register/index.html | 20 +++ microservices/frontend/config.py | 24 +++ microservices/frontend/docker-compose.yml | 103 +++++++++++++ microservices/frontend/docker-compose.yml.bak | 18 +++ microservices/frontend/requirements.txt | 38 +++++ microservices/frontend/run.py | 7 + microservices/order-service/.env | 2 + microservices/order-service/.flaskenv | 1 + microservices/order-service/.gitignore | 5 + microservices/order-service/Dockerfile | 8 + microservices/order-service/README.md | 8 + .../order-service/application/__init__.py | 20 +++ .../order-service/application/models.py | 46 ++++++ .../application/order_api/__init__.py | 6 + .../application/order_api/api/UserClient.py | 15 ++ .../application/order_api/api/__init__.py | 0 .../application/order_api/routes.py | 96 ++++++++++++ microservices/order-service/config.py | 27 ++++ .../order-service/docker-compose.yml | 37 +++++ microservices/order-service/requirements.txt | 38 +++++ microservices/order-service/run.py | 10 ++ microservices/product-service/.env | 2 + microservices/product-service/.flaskenv | 1 + microservices/product-service/.gitignore | 5 + microservices/product-service/Dockerfile | 8 + microservices/product-service/README.md | 8 + .../product-service/application/__init__.py | 21 +++ .../product-service/application/models.py | 22 +++ .../application/product_api/__init__.py | 6 + .../application/product_api/routes.py | 45 ++++++ microservices/product-service/config.py | 23 +++ .../product-service/docker-compose.yml | 38 +++++ .../product-service/requirements.txt | 38 +++++ microservices/product-service/run.py | 10 ++ microservices/user-service/.env | 2 + microservices/user-service/.flaskenv | 1 + microservices/user-service/.gitignore | 5 + microservices/user-service/Dockerfile | 8 + microservices/user-service/README.md | 18 +++ .../user-service/application/__init__.py | 24 +++ .../user-service/application/models.py | 40 +++++ .../application/user_api/__init__.py | 6 + .../application/user_api/routes.py | 100 +++++++++++++ microservices/user-service/config.py | 27 ++++ microservices/user-service/docker-compose.yml | 38 +++++ microservices/user-service/requirements.txt | 39 +++++ microservices/user-service/run.py | 33 ++++ 78 files changed, 1829 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 microservices/frontend/.env create mode 100644 microservices/frontend/.flaskenv create mode 100644 microservices/frontend/.gitignore create mode 100644 microservices/frontend/Dockerfile create mode 100644 microservices/frontend/README.md create mode 100644 microservices/frontend/application/__init__.py create mode 100644 microservices/frontend/application/frontend/__init__.py create mode 100644 microservices/frontend/application/frontend/api/OrderClient.py create mode 100644 microservices/frontend/application/frontend/api/ProductClient.py create mode 100644 microservices/frontend/application/frontend/api/UserClient.py create mode 100644 microservices/frontend/application/frontend/api/__init__.py create mode 100644 microservices/frontend/application/frontend/forms.py create mode 100644 microservices/frontend/application/frontend/views.py create mode 100644 microservices/frontend/application/static/css/style.css create mode 100644 microservices/frontend/application/static/images/product1.jpg create mode 100644 microservices/frontend/application/static/images/product2.jpg create mode 100644 microservices/frontend/application/static/images/sample.jpg create mode 100644 microservices/frontend/application/templates/_messages.html create mode 100644 microservices/frontend/application/templates/admin/index.html create mode 100644 microservices/frontend/application/templates/base.html create mode 100644 microservices/frontend/application/templates/base_col_1.html create mode 100644 microservices/frontend/application/templates/base_col_2.html create mode 100644 microservices/frontend/application/templates/home/index.html create mode 100644 microservices/frontend/application/templates/login/index.html create mode 100644 microservices/frontend/application/templates/macros/_macros_basket.html create mode 100644 microservices/frontend/application/templates/macros/_macros_form.html create mode 100644 microservices/frontend/application/templates/nav_header.html create mode 100644 microservices/frontend/application/templates/order/thankyou.html create mode 100644 microservices/frontend/application/templates/product/index.html create mode 100644 microservices/frontend/application/templates/register/index.html create mode 100644 microservices/frontend/config.py create mode 100644 microservices/frontend/docker-compose.yml create mode 100644 microservices/frontend/docker-compose.yml.bak create mode 100644 microservices/frontend/requirements.txt create mode 100644 microservices/frontend/run.py create mode 100644 microservices/order-service/.env create mode 100644 microservices/order-service/.flaskenv create mode 100644 microservices/order-service/.gitignore create mode 100644 microservices/order-service/Dockerfile create mode 100644 microservices/order-service/README.md create mode 100644 microservices/order-service/application/__init__.py create mode 100644 microservices/order-service/application/models.py create mode 100644 microservices/order-service/application/order_api/__init__.py create mode 100644 microservices/order-service/application/order_api/api/UserClient.py create mode 100644 microservices/order-service/application/order_api/api/__init__.py create mode 100644 microservices/order-service/application/order_api/routes.py create mode 100644 microservices/order-service/config.py create mode 100644 microservices/order-service/docker-compose.yml create mode 100644 microservices/order-service/requirements.txt create mode 100644 microservices/order-service/run.py create mode 100644 microservices/product-service/.env create mode 100644 microservices/product-service/.flaskenv create mode 100644 microservices/product-service/.gitignore create mode 100644 microservices/product-service/Dockerfile create mode 100644 microservices/product-service/README.md create mode 100644 microservices/product-service/application/__init__.py create mode 100644 microservices/product-service/application/models.py create mode 100644 microservices/product-service/application/product_api/__init__.py create mode 100644 microservices/product-service/application/product_api/routes.py create mode 100644 microservices/product-service/config.py create mode 100644 microservices/product-service/docker-compose.yml create mode 100644 microservices/product-service/requirements.txt create mode 100644 microservices/product-service/run.py create mode 100644 microservices/user-service/.env create mode 100644 microservices/user-service/.flaskenv create mode 100644 microservices/user-service/.gitignore create mode 100644 microservices/user-service/Dockerfile create mode 100644 microservices/user-service/README.md create mode 100644 microservices/user-service/application/__init__.py create mode 100644 microservices/user-service/application/models.py create mode 100644 microservices/user-service/application/user_api/__init__.py create mode 100644 microservices/user-service/application/user_api/routes.py create mode 100644 microservices/user-service/config.py create mode 100644 microservices/user-service/docker-compose.yml create mode 100644 microservices/user-service/requirements.txt create mode 100644 microservices/user-service/run.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..95ba0d3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Build + +on: + push: + tags: + - '*.*.*' + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set env + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Package + run: | + echo packaging... + tar -czf release-${{ env.RELEASE_VERSION }}.tar.gz microservices + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: release-${{ env.RELEASE_VERSION }} + path: release-${{ env.RELEASE_VERSION }}.tar.gz + + - name: Make Release + uses: softprops/action-gh-release@v0.1.5 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + release-${{ env.RELEASE_VERSION }}.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fefec8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +.git/ +__pycache__/ +*.py[cod] +*$py.class +migrations/ \ No newline at end of file diff --git a/microservices/frontend/.env b/microservices/frontend/.env new file mode 100644 index 0000000..a2785b1 --- /dev/null +++ b/microservices/frontend/.env @@ -0,0 +1,2 @@ +#.env +CONFIGURATION_SETUP="config.ProductionConfig" \ No newline at end of file diff --git a/microservices/frontend/.flaskenv b/microservices/frontend/.flaskenv new file mode 100644 index 0000000..7105c57 --- /dev/null +++ b/microservices/frontend/.flaskenv @@ -0,0 +1 @@ +FLASK_APP=run.py \ No newline at end of file diff --git a/microservices/frontend/.gitignore b/microservices/frontend/.gitignore new file mode 100644 index 0000000..630da0c --- /dev/null +++ b/microservices/frontend/.gitignore @@ -0,0 +1,5 @@ +.idea/ +.git/ +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/microservices/frontend/Dockerfile b/microservices/frontend/Dockerfile new file mode 100644 index 0000000..40effcf --- /dev/null +++ b/microservices/frontend/Dockerfile @@ -0,0 +1,8 @@ +# Dockerfile +FROM python:3.7 +COPY requirements.txt /frontendapp/requirements.txt +WORKDIR /frontendapp +RUN pip install -r requirements.txt +COPY . /frontendapp +ENTRYPOINT ["python"] +CMD ["run.py"] \ No newline at end of file diff --git a/microservices/frontend/README.md b/microservices/frontend/README.md new file mode 100644 index 0000000..e21cd21 --- /dev/null +++ b/microservices/frontend/README.md @@ -0,0 +1,8 @@ +## Running application in docker containers: +#### Using Docker CLI +``` +docker network ls +docker network create --driver bridge micro_network (skip if already created) +docker build -t frontend-srv . +docker run -p 5000:5000 --detach --name frontend-service --net=micro_network frontend-srv +``` \ No newline at end of file diff --git a/microservices/frontend/application/__init__.py b/microservices/frontend/application/__init__.py new file mode 100644 index 0000000..a19c412 --- /dev/null +++ b/microservices/frontend/application/__init__.py @@ -0,0 +1,30 @@ +# application/__init__.py +import config +import os +from flask import Flask +from flask_bootstrap import Bootstrap +from flask_login import LoginManager + +login_manager = LoginManager() +bootstrap = Bootstrap() +UPLOAD_FOLDER = 'application/static/images' + + +def create_app(): + app = Flask(__name__, static_folder='static') + app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + + environment_configuration = os.environ['CONFIGURATION_SETUP'] + app.config.from_object(environment_configuration) + + login_manager.init_app(app) + login_manager.login_message = "You must be login to access this page." + login_manager.login_view = "frontend.login" + + bootstrap.init_app(app) + + with app.app_context(): + from .frontend import frontend_blueprint + app.register_blueprint(frontend_blueprint) + + return app diff --git a/microservices/frontend/application/frontend/__init__.py b/microservices/frontend/application/frontend/__init__.py new file mode 100644 index 0000000..b7027f0 --- /dev/null +++ b/microservices/frontend/application/frontend/__init__.py @@ -0,0 +1,6 @@ +# application/frontend/__init__.py +from flask import Blueprint + +frontend_blueprint = Blueprint('frontend', __name__) + +from . import views diff --git a/microservices/frontend/application/frontend/api/OrderClient.py b/microservices/frontend/application/frontend/api/OrderClient.py new file mode 100644 index 0000000..6ee4a93 --- /dev/null +++ b/microservices/frontend/application/frontend/api/OrderClient.py @@ -0,0 +1,50 @@ +# application/frontend/api/OrderClient.py +from flask import session +import requests + + +class OrderClient: + @staticmethod + def get_order(): + headers = { + 'Authorization': 'Basic ' + session['user_api_key'] + } + url = 'http://corder-service:5003/api/order' + response = requests.request(method="GET", url=url, headers=headers) + order = response.json() + return order + + @staticmethod + def post_add_to_cart(product_id, qty=1): + payload = { + 'product_id': product_id, + 'qty': qty + } + url = 'http://corder-service:5003/api/order/add-item' + + headers = { + 'Authorization': 'Basic ' + session['user_api_key'] + } + response = requests.request("POST", url=url, data=payload, headers=headers) + if response: + order = response.json() + return order + + @staticmethod + def post_checkout(): + url = 'http://corder-service:5003/api/order/checkout' + + headers = { + 'Authorization': 'Basic ' + session['user_api_key'] + } + response = requests.request("POST", url=url, headers=headers) + order = response.json() + return order + + @staticmethod + def get_order_from_session(): + default_order = { + 'items': {}, + 'total': 0, + } + return session.get('order', default_order) diff --git a/microservices/frontend/application/frontend/api/ProductClient.py b/microservices/frontend/application/frontend/api/ProductClient.py new file mode 100644 index 0000000..d82d680 --- /dev/null +++ b/microservices/frontend/application/frontend/api/ProductClient.py @@ -0,0 +1,17 @@ +# application/frontend/api/ProductClient.py +import requests + + +class ProductClient: + + @staticmethod + def get_products(): + r = requests.get('http://cproduct-service:5002/api/products') + products = r.json() + return products + + @staticmethod + def get_product(slug): + response = requests.request(method="GET", url='http://cproduct-service:5002/api/product/' + slug) + product = response.json() + return product diff --git a/microservices/frontend/application/frontend/api/UserClient.py b/microservices/frontend/application/frontend/api/UserClient.py new file mode 100644 index 0000000..3aec5ce --- /dev/null +++ b/microservices/frontend/application/frontend/api/UserClient.py @@ -0,0 +1,55 @@ +# application/frontend/api/UserClient.py +import requests +from flask import session, request + + +class UserClient: + @staticmethod + def post_login(form): + api_key = False + payload = { + 'username': form.username.data, + 'password': form.password.data + } + url = 'http://cuser-service:5001/api/user/login' + response = requests.request("POST", url=url, data=payload) + if response: + d = response.json() + print("This is response from user api: " + str(d)) + if d['api_key'] is not None: + api_key = d['api_key'] + return api_key + + @staticmethod + def get_user(): + + headers = { + 'Authorization': 'Basic ' + session['user_api_key'] + } + url = 'http://cuser-service:5001/api/user' + response = requests.request(method="GET", url=url, headers=headers) + user = response.json() + return user + + @staticmethod + def post_user_create(form): + user = False + payload = { + 'email': form.email.data, + 'password': form.password.data, + 'first_name': form.first_name.data, + 'last_name': form.last_name.data, + 'username': form.username.data + } + url = 'http://cuser-service:5001/api/user/create' + response = requests.request("POST", url=url, data=payload) + if response: + user = response.json() + return user + + @staticmethod + def does_exist(username): + url = 'http://cuser-service:5001/api/user/' + username + '/exists' + response = requests.request("GET", url=url) + return response.status_code == 200 + diff --git a/microservices/frontend/application/frontend/api/__init__.py b/microservices/frontend/application/frontend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/microservices/frontend/application/frontend/forms.py b/microservices/frontend/application/frontend/forms.py new file mode 100644 index 0000000..29f623c --- /dev/null +++ b/microservices/frontend/application/frontend/forms.py @@ -0,0 +1,32 @@ +# application/frontend/forms.py +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, SubmitField, HiddenField, IntegerField + +from wtforms.validators import DataRequired, Email + + +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') + + +class RegistrationForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First name', validators=[DataRequired()]) + last_name = StringField('Last name', validators=[DataRequired()]) + email = StringField('Email address', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Register') + + +class OrderItemForm(FlaskForm): + product_id = HiddenField(validators=[DataRequired()]) + quantity = IntegerField(validators=[DataRequired()]) + order_id = HiddenField() + submit = SubmitField('Update') + + +class ItemForm(FlaskForm): + product_id = HiddenField(validators=[DataRequired()]) + quantity = HiddenField(validators=[DataRequired()], default=1) diff --git a/microservices/frontend/application/frontend/views.py b/microservices/frontend/application/frontend/views.py new file mode 100644 index 0000000..e84bd5b --- /dev/null +++ b/microservices/frontend/application/frontend/views.py @@ -0,0 +1,141 @@ +# application/frontend/views.py +import requests +from . import forms +from . import frontend_blueprint +from .. import login_manager +from .api.UserClient import UserClient +from .api.ProductClient import ProductClient +from .api.OrderClient import OrderClient +from flask import render_template, session, redirect, url_for, flash, request + +from flask_login import current_user + + +@login_manager.user_loader +def load_user(user_id): + return None + + +@frontend_blueprint.route('/', methods=['GET']) +def home(): + if current_user.is_authenticated: + session['order'] = OrderClient.get_order_from_session() + + try: + products = ProductClient.get_products() + except requests.exceptions.ConnectionError: + products = { + 'results': [] + } + + return render_template('home/index.html', products=products) + + +@frontend_blueprint.route('/register', methods=['GET', 'POST']) +def register(): + form = forms.RegistrationForm(request.form) + if request.method == "POST": + if form.validate_on_submit(): + username = form.username.data + + # Search for existing user + user = UserClient.does_exist(username) + if user: + # Existing user found + flash('Please try another username', 'error') + return render_template('register/index.html', form=form) + else: + # Attempt to create new user + user = UserClient.post_user_create(form) + if user: + flash('Thanks for registering, please login', 'success') + return redirect(url_for('frontend.login')) + + else: + flash('Errors found', 'error') + + return render_template('register/index.html', form=form) + + +@frontend_blueprint.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('frontend.home')) + form = forms.LoginForm() + if request.method == "POST": + if form.validate_on_submit(): + api_key = UserClient.post_login(form) + if api_key: + session['user_api_key'] = api_key + user = UserClient.get_user() + session['user'] = user['result'] + + order = OrderClient.get_order() + if order.get('result', False): + session['order'] = order['result'] + + flash('Welcome back, ' + user['result']['username'], 'success') + return redirect(url_for('frontend.home')) + else: + flash('Cannot login', 'error') + else: + flash('Errors found', 'error') + return render_template('login/index.html', form=form) + + +@frontend_blueprint.route('/logout', methods=['GET']) +def logout(): + session.clear() + return redirect(url_for('frontend.home')) + + +@frontend_blueprint.route('/product/', methods=['GET', 'POST']) +def product(slug): + response = ProductClient.get_product(slug) + item = response['result'] + + form = forms.ItemForm(product_id=item['id']) + + if request.method == "POST": + if 'user' not in session: + flash('Please login', 'error') + return redirect(url_for('frontend.login')) + order = OrderClient.post_add_to_cart(product_id=item['id'], qty=1) + session['order'] = order['result'] + flash('Order has been updated', 'success') + return render_template('product/index.html', product=item, form=form) + + +@frontend_blueprint.route('/checkout', methods=['GET']) +def summary(): + if 'user' not in session: + flash('Please login', 'error') + return redirect(url_for('frontend.login')) + + if 'order' not in session: + flash('No order found', 'error') + return redirect(url_for('frontend.home')) + order = OrderClient.get_order() + + if len(order['result']['items']) == 0: + flash('No order found', 'error') + return redirect(url_for('frontend.home')) + + OrderClient.post_checkout() + + return redirect(url_for('frontend.thank_you')) + +@frontend_blueprint.route('/order/thank-you', methods=['GET']) +def thank_you(): + if 'user' not in session: + flash('Please login', 'error') + return redirect(url_for('frontend.login')) + + if 'order' not in session: + flash('No order found', 'error') + return redirect(url_for('frontend.home')) + + session.pop('order', None) + flash('Thank you for your order', 'success') + + return render_template('order/thankyou.html') \ No newline at end of file diff --git a/microservices/frontend/application/static/css/style.css b/microservices/frontend/application/static/css/style.css new file mode 100644 index 0000000..f40c0e8 --- /dev/null +++ b/microservices/frontend/application/static/css/style.css @@ -0,0 +1,68 @@ +body, html { + width: 100%; + height: 100%; +} + +body, h1, h2, h3 { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 700; +} + +body{ + background-color:#DDD; +} + +a, .navbar-default .navbar-brand, .navbar-default .navbar-nav > li > a { + color: #aec251; +} + +a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav > li > a:hover { + color: #687430; +} + +footer { + padding: 50px 0; + background-color: #f8f8f8; +} + +p.copyright { + margin: 15px 0 0; +} + +.alert-info { + width: 50%; + margin: auto; + color: #687430; + background-color: #e6ecca; + border-color: #aec251; +} + +.btn-default { + border-color: #aec251; + color: #aec251; +} + +.btn-default:hover { + background-color: #aec251; +} + +.center { + margin: auto; + width: 50%; + padding: 10px; +} + +.content-section { + padding: 50px 0; + border-top: 1px solid #e7e7e7; +} + + +.img-block{ + text-align:center; +} +.img-block img{ + + max-width:200px; + max-height:200px; +} diff --git a/microservices/frontend/application/static/images/product1.jpg b/microservices/frontend/application/static/images/product1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f8a4e8d1939a72774e182262785ff27e9e060a27 GIT binary patch literal 10682 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 error_message }} +{% endfor %} + +{% for success_message in get_flashed_messages(category_filter=["success"]) %} +
{{ success_message }}
+{% endfor %} + +{% for info_message in get_flashed_messages(category_filter=["info"]) %} +
{{ info_message }}
+{% endfor %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/admin/index.html b/microservices/frontend/application/templates/admin/index.html new file mode 100644 index 0000000..9b1cc16 --- /dev/null +++ b/microservices/frontend/application/templates/admin/index.html @@ -0,0 +1,19 @@ +{% extends "base_col_1.html" %} +{% from "macros/_macros_form.html" import render_field %} +{% block title %}Product page{% endblock %} + +{% block pageContent %} + {{ message }} +

Add Product

+ +
+
+ + + + + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/base.html b/microservices/frontend/application/templates/base.html new file mode 100644 index 0000000..66a5ab9 --- /dev/null +++ b/microservices/frontend/application/templates/base.html @@ -0,0 +1,24 @@ + + + + + {% block title %} {% endblock %} + + + {% block styles %} + + + + + + {% endblock %} + + + + +{% block content %} +{% endblock %} + + \ No newline at end of file diff --git a/microservices/frontend/application/templates/base_col_1.html b/microservices/frontend/application/templates/base_col_1.html new file mode 100644 index 0000000..aa7497c --- /dev/null +++ b/microservices/frontend/application/templates/base_col_1.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +{% include 'nav_header.html' %} +
+ + {% include '_messages.html' %} + {% block pageContent %} {% endblock %} + +
+{% endblock %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/base_col_2.html b/microservices/frontend/application/templates/base_col_2.html new file mode 100644 index 0000000..7f0f7e7 --- /dev/null +++ b/microservices/frontend/application/templates/base_col_2.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + + + +{% block content %} +{% include 'nav_header.html' %} +
+ + {% include '_messages.html' %} +
+
+ +
+
+ {% block pageContent %} {% endblock %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/home/index.html b/microservices/frontend/application/templates/home/index.html new file mode 100644 index 0000000..454fdb9 --- /dev/null +++ b/microservices/frontend/application/templates/home/index.html @@ -0,0 +1,37 @@ +{% extends "base_col_1.html" %} +{% block title %}Home Page{% endblock %} + +{% block pageContent %} + +{% if products | length > 0 %} + {% for product in products.results %} + +{% set url = "/product/" + product.slug %} +{% set imageSRC = "images/" + product.image %} + +
+
+
+

{{ product.name }}

+
+
+
+ +
+
+ +
+
+{% endfor %} + + {% else %} + No products found. + {% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/login/index.html b/microservices/frontend/application/templates/login/index.html new file mode 100644 index 0000000..89e741a --- /dev/null +++ b/microservices/frontend/application/templates/login/index.html @@ -0,0 +1,18 @@ +{% extends "base_col_1.html" %} +{% from "macros/_macros_form.html" import render_field %} +{% block title %}Login{% endblock %} + +{% block pageContent %} +

Login

+ {{ message }} + +
+ {{ form.hidden_tag() }} + {{ render_field(form.username) }} + {{ render_field(form.password) }} + {{ form.submit(class_="btn btn-success pull-right") }} + + +
+ +{% endblock %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/macros/_macros_basket.html b/microservices/frontend/application/templates/macros/_macros_basket.html new file mode 100644 index 0000000..5811950 --- /dev/null +++ b/microservices/frontend/application/templates/macros/_macros_basket.html @@ -0,0 +1,10 @@ +{{ session['order'] | pprint }} +{% macro count_items()%} +{% set counter = namespace(a=0) %} + {% if session['order'] %} + {% for item in session['order']['items'] %} + {% set counter.a = counter.a + item['quantity'] %} + {% endfor %} + {% endif %} +{{ counter.a }} +{% endmacro %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/macros/_macros_form.html b/microservices/frontend/application/templates/macros/_macros_form.html new file mode 100644 index 0000000..a6a0304 --- /dev/null +++ b/microservices/frontend/application/templates/macros/_macros_form.html @@ -0,0 +1,53 @@ +{% macro render_field(field) %} +
+ {{ field.label(class_="form-label") }}: + {% if field.errors %} + + {% for error in field.errors %} + {{ error }} + {% endfor %} + + {% endif %} + + + {{ field(class_="form-control") }} +
+{% endmacro %} + + + +{% macro render_field_without_label(field) %} +
{{ field(**kwargs)|safe }} + {% if field.errors %} + + {% for error in field.errors %} + {{ error }} + {% endfor %} + + {% endif %} +
+{% endmacro %} + + +{% macro render_boolean_field(field) %} +
{{ field(**kwargs)|safe }} {{ field.label }} + {% if field.errors %} + + {% for error in field.errors %} + {{ error }} + {% endfor %} + + {% endif %} +
+{% endmacro %} + + +{% macro render_errors(field) %} + {% if field.errors %} + + {% for error in field.errors %} + {{ error }} + {% endfor %} + + {% endif %} +{% endmacro %} diff --git a/microservices/frontend/application/templates/nav_header.html b/microservices/frontend/application/templates/nav_header.html new file mode 100644 index 0000000..9960422 --- /dev/null +++ b/microservices/frontend/application/templates/nav_header.html @@ -0,0 +1,24 @@ +{% from "macros/_macros_basket.html" import count_items %} +
+ +
\ No newline at end of file diff --git a/microservices/frontend/application/templates/order/thankyou.html b/microservices/frontend/application/templates/order/thankyou.html new file mode 100644 index 0000000..f128f34 --- /dev/null +++ b/microservices/frontend/application/templates/order/thankyou.html @@ -0,0 +1,7 @@ +{% extends "base_col_1.html" %} +{% block title %}Thank you{% endblock %} + +{% block pageContent %} +

Thank you

+

your order is being processed

+{% endblock %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/product/index.html b/microservices/frontend/application/templates/product/index.html new file mode 100644 index 0000000..653e502 --- /dev/null +++ b/microservices/frontend/application/templates/product/index.html @@ -0,0 +1,30 @@ +{% extends "base_col_1.html" %} +{% from "macros/_macros_form.html" import render_field %} +{% block title %}Product page{% endblock %} + +{% block pageContent %} + + {% set url = "/product/" + product.slug %} + {% set imageSRC = "images/" + product.image %} +
+
+
+

{{ product.name }}

+
+
+
+ +
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/microservices/frontend/application/templates/register/index.html b/microservices/frontend/application/templates/register/index.html new file mode 100644 index 0000000..fd1ce99 --- /dev/null +++ b/microservices/frontend/application/templates/register/index.html @@ -0,0 +1,20 @@ +{% extends "base_col_1.html" %} +{% from "macros/_macros_form.html" import render_field %} +{% block title %}Register{% endblock %} + +{% block pageContent %} +

Register

+ {{ message }} + +
+ {{ form.hidden_tag() }} + {{ render_field(form.username) }} + {{ render_field(form.first_name) }} + {{ render_field(form.last_name) }} + {{ render_field(form.email) }} + {{ render_field(form.password) }} + {{ form.submit(class_="btn btn-success pull-right") }} + +
+ +{% endblock %} \ No newline at end of file diff --git a/microservices/frontend/config.py b/microservices/frontend/config.py new file mode 100644 index 0000000..b8b3393 --- /dev/null +++ b/microservices/frontend/config.py @@ -0,0 +1,24 @@ +# config.py +import os +from dotenv import load_dotenv + +dotenv_path = os.path.join(os.path.dirname(__file__), '.env') + +if os.path.exists(dotenv_path): + load_dotenv(dotenv_path) + + +class Config: + SECRET_KEY = 'y2BH8xD9pyZhDT5qkyZZRgjcJCMHdQ' + WTF_CSRF_SECRET_KEY = 'VyOyqv5Fm3Hs3qB1AmNeeuvPpdRqTJbTs5wKvWCS' + + +class DevelopmentConfig(Config): + ENV = "development" + DEBUG = True + + +class ProductionConfig(Config): + ENV = "production" + DEBUG = False + diff --git a/microservices/frontend/docker-compose.yml b/microservices/frontend/docker-compose.yml new file mode 100644 index 0000000..fea8d78 --- /dev/null +++ b/microservices/frontend/docker-compose.yml @@ -0,0 +1,103 @@ +# docker-compose.deploy.yml +version: '3.8' + +volumes: + userdb_vol: + productdb_vol: + orderdb_vol: + +networks: + micro_network: + name: micro_network + +services: + user-api: + container_name: cuser-service + build: + context: ../user-service + ports: + - "5001:5001" + depends_on: + - user-db + networks: + - micro_network + restart: always + + user-db: + container_name: cuser_dbase + image: mysql:8 + ports: + - "32000:3306" + environment: + MYSQL_ROOT_PASSWORD: pfm_dc_2020 + MYSQL_DATABASE: user + MYSQL_USER: cloudacademy + MYSQL_PASSWORD: pfm_2020 + networks: + - micro_network + volumes: + - userdb_vol:/var/lib/mysql + + product-api: + container_name: cproduct-service + build: + context: ../product-service + ports: + - "5002:5002" + depends_on: + - product-db + networks: + - micro_network + restart: always + + product-db: + container_name: cproduct_dbase + image: mysql:8 + ports: + - "32001:3306" + environment: + MYSQL_ROOT_PASSWORD: pfm_dc_2020 + MYSQL_DATABASE: product + MYSQL_USER: cloudacademy + MYSQL_PASSWORD: pfm_2020 + networks: + - micro_network + volumes: + - productdb_vol:/var/lib/mysql + + order-api: + container_name: corder-service + build: + context: ../order-service + ports: + - "5003:5003" + depends_on: + - order-db + networks: + - micro_network + restart: always + + order-db: + container_name: corder_dbase + image: mysql:8 + ports: + - "32002:3306" + environment: + MYSQL_ROOT_PASSWORD: pfm_dc_2020 + MYSQL_DATABASE: order + MYSQL_USER: cloudacademy + MYSQL_PASSWORD: pfm_2020 + networks: + - micro_network + volumes: + - orderdb_vol:/var/lib/mysql + + frontend-app: + container_name: cfrontend-app + build: + context: . + ports: + - "5555:5000" + networks: + - micro_network + restart: always \ No newline at end of file diff --git a/microservices/frontend/docker-compose.yml.bak b/microservices/frontend/docker-compose.yml.bak new file mode 100644 index 0000000..33eb4a0 --- /dev/null +++ b/microservices/frontend/docker-compose.yml.bak @@ -0,0 +1,18 @@ +# docker-compose.yml +version: '3.8' + +networks: + micro_network: + external: + name: micro_network + +services: + frontend-app: + container_name: cfrontend-app + build: + context: . + ports: + - "5000:5000" + networks: + - micro_network + restart: always \ No newline at end of file diff --git a/microservices/frontend/requirements.txt b/microservices/frontend/requirements.txt new file mode 100644 index 0000000..d371b69 --- /dev/null +++ b/microservices/frontend/requirements.txt @@ -0,0 +1,38 @@ +alembic==1.4.2 +autoenv==1.0.0 +blinker==1.4 +certifi==2019.11.28 +cffi==1.14.0 +chardet==3.0.4 +click==7.1.1 +cryptography==2.8 +dnspython==1.16.0 +dominate==2.5.1 +Flask==1.1.1 +Flask-Bootstrap==3.3.7.1 +Flask-DotEnv==0.1.2 +Flask-Login==0.5.0 +Flask-Migrate==2.5.3 +Flask-SQLAlchemy==2.4.1 +Flask-Uploads==0.2.1 +Flask-WTF==0.14.3 +idna==2.9 +itsdangerous==1.1.0 +Jinja2==2.11.1 +Mako==1.1.2 +MarkupSafe==1.1.1 +marshmallow==3.5.1 +passlib==1.7.2 +protobuf==3.6.1 +pycparser==2.20 +PyMySQL==0.9.3 +python-dateutil==2.8.1 +python-dotenv==0.12.0 +python-editor==1.0.4 +requests==2.23.0 +six==1.14.0 +SQLAlchemy==1.3.15 +urllib3==1.25.8 +visitor==0.1.3 +Werkzeug==1.0.1 +WTForms==2.2.1 diff --git a/microservices/frontend/run.py b/microservices/frontend/run.py new file mode 100644 index 0000000..7d93da5 --- /dev/null +++ b/microservices/frontend/run.py @@ -0,0 +1,7 @@ +# run.py +from application import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/microservices/order-service/.env b/microservices/order-service/.env new file mode 100644 index 0000000..4db88e2 --- /dev/null +++ b/microservices/order-service/.env @@ -0,0 +1,2 @@ +# .env +CONFIGURATION_SETUP="config.ProductionConfig" \ No newline at end of file diff --git a/microservices/order-service/.flaskenv b/microservices/order-service/.flaskenv new file mode 100644 index 0000000..7105c57 --- /dev/null +++ b/microservices/order-service/.flaskenv @@ -0,0 +1 @@ +FLASK_APP=run.py \ No newline at end of file diff --git a/microservices/order-service/.gitignore b/microservices/order-service/.gitignore new file mode 100644 index 0000000..630da0c --- /dev/null +++ b/microservices/order-service/.gitignore @@ -0,0 +1,5 @@ +.idea/ +.git/ +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/microservices/order-service/Dockerfile b/microservices/order-service/Dockerfile new file mode 100644 index 0000000..efd3129 --- /dev/null +++ b/microservices/order-service/Dockerfile @@ -0,0 +1,8 @@ +# Dockerfile +FROM python:3.7 +COPY requirements.txt /orderapp/requirements.txt +WORKDIR /orderapp +RUN pip install -r requirements.txt +COPY . /orderapp +ENTRYPOINT ["python"] +CMD ["run.py"] \ No newline at end of file diff --git a/microservices/order-service/README.md b/microservices/order-service/README.md new file mode 100644 index 0000000..9fcad04 --- /dev/null +++ b/microservices/order-service/README.md @@ -0,0 +1,8 @@ +## Running application in docker containers: +#### Using Docker CLI +``` +docker network ls +docker network create --driver bridge micro_network (skip if already created) +docker build -t order-srv . +docker run -p 5003:5003 --detach --name order-service --net=micro_network order-srv +``` \ No newline at end of file diff --git a/microservices/order-service/application/__init__.py b/microservices/order-service/application/__init__.py new file mode 100644 index 0000000..8fe3958 --- /dev/null +++ b/microservices/order-service/application/__init__.py @@ -0,0 +1,20 @@ +# application/__init__.py +import config +import os +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +def create_app(): + app = Flask(__name__) + environment_configuration = os.environ['CONFIGURATION_SETUP'] + app.config.from_object(environment_configuration) + + db.init_app(app) + + with app.app_context(): + from .order_api import order_api_blueprint + app.register_blueprint(order_api_blueprint) + return app diff --git a/microservices/order-service/application/models.py b/microservices/order-service/application/models.py new file mode 100644 index 0000000..95dde86 --- /dev/null +++ b/microservices/order-service/application/models.py @@ -0,0 +1,46 @@ +# application/models.py +from . import db +from datetime import datetime + +class Order(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer) + items = db.relationship('OrderItem', backref='orderItem') + is_open = db.Column(db.Boolean, default=True) + date_added = db.Column(db.DateTime, default=datetime.utcnow) + date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) + + def create(self, user_id): + self.user_id = user_id + self.is_open = True + return self + + def to_json(self): + items = [] + for i in self.items: + items.append(i.to_json()) + + return { + 'items': items, + 'is_open': self.is_open, + 'user_id': self.user_id + } + + +class OrderItem(db.Model): + id = db.Column(db.Integer, primary_key=True) + order_id = db.Column(db.Integer, db.ForeignKey('order.id')) + product_id = db.Column(db.Integer) + quantity = db.Column(db.Integer, default=1) + date_added = db.Column(db.DateTime, default=datetime.utcnow) + date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) + + def __init__(self, product_id, quantity): + self.product_id = product_id + self.quantity = quantity + + def to_json(self): + return { + 'product': self.product_id, + 'quantity': self.quantity + } \ No newline at end of file diff --git a/microservices/order-service/application/order_api/__init__.py b/microservices/order-service/application/order_api/__init__.py new file mode 100644 index 0000000..4a70f91 --- /dev/null +++ b/microservices/order-service/application/order_api/__init__.py @@ -0,0 +1,6 @@ +# application/order_api/__init__.py +from flask import Blueprint + +order_api_blueprint = Blueprint('order_api', __name__) + +from . import routes \ No newline at end of file diff --git a/microservices/order-service/application/order_api/api/UserClient.py b/microservices/order-service/application/order_api/api/UserClient.py new file mode 100644 index 0000000..0a4e445 --- /dev/null +++ b/microservices/order-service/application/order_api/api/UserClient.py @@ -0,0 +1,15 @@ +# application/order_api/api/UserClient.py +import requests + + +class UserClient: + @staticmethod + def get_user(api_key): + headers = { + 'Authorization': api_key + } + response = requests.request(method="GET", url='http://cuser-service:5001/api/user', headers=headers) + if response.status_code == 401: + return False + user = response.json() + return user diff --git a/microservices/order-service/application/order_api/api/__init__.py b/microservices/order-service/application/order_api/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/microservices/order-service/application/order_api/routes.py b/microservices/order-service/application/order_api/routes.py new file mode 100644 index 0000000..f570032 --- /dev/null +++ b/microservices/order-service/application/order_api/routes.py @@ -0,0 +1,96 @@ +# application/order_api/routes.py +from flask import jsonify, request, make_response +from . import order_api_blueprint +from .. import db +from ..models import Order, OrderItem +from .api.UserClient import UserClient + + +@order_api_blueprint.route('/api/orders', methods=['GET']) +def orders(): + items = [] + for row in Order.query.all(): + items.append(row.to_json()) + + response = jsonify(items) + return response + + +@order_api_blueprint.route('/api/order/add-item', methods=['POST']) +def order_add_item(): + api_key = request.headers.get('Authorization') + response = UserClient.get_user(api_key) + + if not response: + return make_response(jsonify({'message': 'Not logged in'}), 401) + + user = response['result'] + p_id = int(request.form['product_id']) + qty = int(request.form['qty']) + u_id = int(user['id']) + + known_order = Order.query.filter_by(user_id=u_id, is_open=1).first() + + if known_order is None: + known_order = Order() + known_order.is_open = True + known_order.user_id = u_id + + order_item = OrderItem(p_id, qty) + known_order.items.append(order_item) + else: + found = False + + for item in known_order.items: + if item.product_id == p_id: + found = True + item.quantity += qty + + if found is False: + order_item = OrderItem(p_id, qty) + known_order.items.append(order_item) + + db.session.add(known_order) + db.session.commit() + response = jsonify({'result': known_order.to_json()}) + return response + + +@order_api_blueprint.route('/api/order', methods=['GET']) +def order(): + api_key = request.headers.get('Authorization') + + response = UserClient.get_user(api_key) + + if not response: + return make_response(jsonify({'message': 'Not logged in'}), 401) + + user = response['result'] + open_order = Order.query.filter_by(user_id=user['id'], is_open=1).first() + + if open_order is None: + response = jsonify({'message': 'No order found'}) + else: + response = jsonify({'result': open_order.to_json()}) + return response + + +@order_api_blueprint.route('/api/order/checkout', methods=['POST']) +def checkout(): + api_key = request.headers.get('Authorization') + + response = UserClient.get_user(api_key) + + if not response: + return make_response(jsonify({'message': 'Not logged in'}), 401) + + user = response['result'] + + order_model = Order.query.filter_by(user_id=user['id'], is_open=1).first() + order_model.is_open = 0 + + db.session.add(order_model) + db.session.commit() + + response = jsonify({'result': order_model.to_json()}) + return response diff --git a/microservices/order-service/config.py b/microservices/order-service/config.py new file mode 100644 index 0000000..1bb7285 --- /dev/null +++ b/microservices/order-service/config.py @@ -0,0 +1,27 @@ +# config.py +import os +from dotenv import load_dotenv + +dotenv_path = os.path.join(os.path.dirname(__file__), '.env') +if os.path.exists(dotenv_path): + load_dotenv(dotenv_path) + + +class Config: + SQLALCHEMY_TRACK_MODIFICATIONS = False + + +class DevelopmentConfig(Config): + ENV = "development" + DEBUG = True + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@host.docker.internal:3306/order_dev' + SQLALCHEMY_ECHO = True + + +class ProductionConfig(Config): + ENV = "production" + DEBUG = False + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@order-db:3306/order' + SQLALCHEMY_ECHO = False + + diff --git a/microservices/order-service/docker-compose.yml b/microservices/order-service/docker-compose.yml new file mode 100644 index 0000000..c7d34ea --- /dev/null +++ b/microservices/order-service/docker-compose.yml @@ -0,0 +1,37 @@ +# docker-compose.yml +version: '3.8' +volumes: + orderdb_vol: + +networks: + micro_network: + external: + name: micro_network + +services: + order-api: + container_name: corder-service + build: + context: . + ports: + - "5003:5003" + depends_on: + - order-db + networks: + - micro_network + restart: always + + order-db: + container_name: corder_dbase + image: mysql:8 + ports: + - "32002:3306" + environment: + MYSQL_ROOT_PASSWORD: pfm_dc_2020 + MYSQL_DATABASE: order + MYSQL_USER: cloudacademy + MYSQL_PASSWORD: pfm_2020 + networks: + - micro_network + volumes: + - orderdb_vol:/var/lib/mysql \ No newline at end of file diff --git a/microservices/order-service/requirements.txt b/microservices/order-service/requirements.txt new file mode 100644 index 0000000..d371b69 --- /dev/null +++ b/microservices/order-service/requirements.txt @@ -0,0 +1,38 @@ +alembic==1.4.2 +autoenv==1.0.0 +blinker==1.4 +certifi==2019.11.28 +cffi==1.14.0 +chardet==3.0.4 +click==7.1.1 +cryptography==2.8 +dnspython==1.16.0 +dominate==2.5.1 +Flask==1.1.1 +Flask-Bootstrap==3.3.7.1 +Flask-DotEnv==0.1.2 +Flask-Login==0.5.0 +Flask-Migrate==2.5.3 +Flask-SQLAlchemy==2.4.1 +Flask-Uploads==0.2.1 +Flask-WTF==0.14.3 +idna==2.9 +itsdangerous==1.1.0 +Jinja2==2.11.1 +Mako==1.1.2 +MarkupSafe==1.1.1 +marshmallow==3.5.1 +passlib==1.7.2 +protobuf==3.6.1 +pycparser==2.20 +PyMySQL==0.9.3 +python-dateutil==2.8.1 +python-dotenv==0.12.0 +python-editor==1.0.4 +requests==2.23.0 +six==1.14.0 +SQLAlchemy==1.3.15 +urllib3==1.25.8 +visitor==0.1.3 +Werkzeug==1.0.1 +WTForms==2.2.1 diff --git a/microservices/order-service/run.py b/microservices/order-service/run.py new file mode 100644 index 0000000..1502b73 --- /dev/null +++ b/microservices/order-service/run.py @@ -0,0 +1,10 @@ +# run.py +from application import create_app, db +from flask_migrate import Migrate +from application import models + +app = create_app() +migrate = Migrate(app, db) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5003) diff --git a/microservices/product-service/.env b/microservices/product-service/.env new file mode 100644 index 0000000..a2785b1 --- /dev/null +++ b/microservices/product-service/.env @@ -0,0 +1,2 @@ +#.env +CONFIGURATION_SETUP="config.ProductionConfig" \ No newline at end of file diff --git a/microservices/product-service/.flaskenv b/microservices/product-service/.flaskenv new file mode 100644 index 0000000..7105c57 --- /dev/null +++ b/microservices/product-service/.flaskenv @@ -0,0 +1 @@ +FLASK_APP=run.py \ No newline at end of file diff --git a/microservices/product-service/.gitignore b/microservices/product-service/.gitignore new file mode 100644 index 0000000..630da0c --- /dev/null +++ b/microservices/product-service/.gitignore @@ -0,0 +1,5 @@ +.idea/ +.git/ +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/microservices/product-service/Dockerfile b/microservices/product-service/Dockerfile new file mode 100644 index 0000000..ef1e784 --- /dev/null +++ b/microservices/product-service/Dockerfile @@ -0,0 +1,8 @@ +# Dockerfile +FROM python:3.7 +COPY requirements.txt /productapp/requirements.txt +WORKDIR /productapp +RUN pip install -r requirements.txt +COPY . /productapp +ENTRYPOINT ["python"] +CMD ["run.py"] \ No newline at end of file diff --git a/microservices/product-service/README.md b/microservices/product-service/README.md new file mode 100644 index 0000000..da65b10 --- /dev/null +++ b/microservices/product-service/README.md @@ -0,0 +1,8 @@ +## Running application in docker containers: +#### Using Docker CLI +``` +docker network ls +docker network create --driver bridge micro_network (skip if already created) +docker build -t product-srv . +docker run -p 5002:5002 --detach --name product-service --net=micro_network product-srv +``` \ No newline at end of file diff --git a/microservices/product-service/application/__init__.py b/microservices/product-service/application/__init__.py new file mode 100644 index 0000000..babd59a --- /dev/null +++ b/microservices/product-service/application/__init__.py @@ -0,0 +1,21 @@ +# application/__init__.py +import config +import os +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +def create_app(): + app = Flask(__name__) + + environment_configuration = os.environ['CONFIGURATION_SETUP'] + app.config.from_object(environment_configuration) + + db.init_app(app) + + with app.app_context(): + from .product_api import product_api_blueprint + app.register_blueprint(product_api_blueprint) + return app diff --git a/microservices/product-service/application/models.py b/microservices/product-service/application/models.py new file mode 100644 index 0000000..a6814b0 --- /dev/null +++ b/microservices/product-service/application/models.py @@ -0,0 +1,22 @@ +# application/models.py +from . import db +from datetime import datetime + + +class Product(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), unique=True, nullable=False) + slug = db.Column(db.String(255), unique=True, nullable=False) + price = db.Column(db.Integer, nullable=False) + image = db.Column(db.String(255), unique=False, nullable=True) + date_added = db.Column(db.DateTime, default=datetime.utcnow) + date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) + + def to_json(self): + return { + 'id': self.id, + 'name': self.name, + 'slug': self.slug, + 'price': self.price, + 'image': self.image + } diff --git a/microservices/product-service/application/product_api/__init__.py b/microservices/product-service/application/product_api/__init__.py new file mode 100644 index 0000000..838df4a --- /dev/null +++ b/microservices/product-service/application/product_api/__init__.py @@ -0,0 +1,6 @@ +# application/product_api/__init__.py +from flask import Blueprint + +product_api_blueprint = Blueprint('product_api', __name__) + +from . import routes diff --git a/microservices/product-service/application/product_api/routes.py b/microservices/product-service/application/product_api/routes.py new file mode 100644 index 0000000..42cc8cd --- /dev/null +++ b/microservices/product-service/application/product_api/routes.py @@ -0,0 +1,45 @@ +# application/product_api/routes.py +from . import product_api_blueprint +from .. import db +from ..models import Product +from flask import jsonify, request + + +@product_api_blueprint.route('/api/products', methods=['GET']) +def products(): + items = [] + for row in Product.query.all(): + items.append(row.to_json()) + + response = jsonify({'results': items}) + return response + + +@product_api_blueprint.route('/api/product/create', methods=['POST']) +def post_create(): + name = request.form['name'] + slug = request.form['slug'] + image = request.form['image'] + price = request.form['price'] + + item = Product() + item.name = name + item.slug = slug + item.image = image + item.price = price + + db.session.add(item) + db.session.commit() + + response = jsonify({'message': 'Product added', 'product': item.to_json()}) + return response + + +@product_api_blueprint.route('/api/product/', methods=['GET']) +def product(slug): + item = Product.query.filter_by(slug=slug).first() + if item is not None: + response = jsonify({'result': item.to_json()}) + else: + response = jsonify({'message': 'Cannot find product'}), 404 + return response diff --git a/microservices/product-service/config.py b/microservices/product-service/config.py new file mode 100644 index 0000000..a9144ed --- /dev/null +++ b/microservices/product-service/config.py @@ -0,0 +1,23 @@ +# config.py +import os +from dotenv import load_dotenv + +dotenv_path = os.path.join(os.path.dirname(__file__), '.env') +if os.path.exists(dotenv_path): + load_dotenv(dotenv_path) + + +class Config: + SQLALCHEMY_TRACK_MODIFICATIONS = False + + +class DevelopmentConfig(Config): + ENV = "development" + DEBUG = True + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@localhost:3306/product_dev' + + +class ProductionConfig(Config): + ENV = "production" + DEBUG = False + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@product-db:3306/product' diff --git a/microservices/product-service/docker-compose.yml b/microservices/product-service/docker-compose.yml new file mode 100644 index 0000000..d8870b2 --- /dev/null +++ b/microservices/product-service/docker-compose.yml @@ -0,0 +1,38 @@ +# docker-compose.yml +version: '3.8' + +volumes: + productdb_vol: + +networks: + micro_network: + external: + name: micro_network + +services: + product-api: + container_name: cproduct-service + build: + context: . + ports: + - "5002:5002" + depends_on: + - product-db + networks: + - micro_network + restart: always + + product-db: + container_name: cproduct_dbase + image: mysql:8 + ports: + - "32001:3306" + environment: + MYSQL_ROOT_PASSWORD: pfm_dc_2020 + MYSQL_DATABASE: product + MYSQL_USER: cloudacademy + MYSQL_PASSWORD: pfm_2020 + networks: + - micro_network + volumes: + - productdb_vol:/var/lib/mysql \ No newline at end of file diff --git a/microservices/product-service/requirements.txt b/microservices/product-service/requirements.txt new file mode 100644 index 0000000..d371b69 --- /dev/null +++ b/microservices/product-service/requirements.txt @@ -0,0 +1,38 @@ +alembic==1.4.2 +autoenv==1.0.0 +blinker==1.4 +certifi==2019.11.28 +cffi==1.14.0 +chardet==3.0.4 +click==7.1.1 +cryptography==2.8 +dnspython==1.16.0 +dominate==2.5.1 +Flask==1.1.1 +Flask-Bootstrap==3.3.7.1 +Flask-DotEnv==0.1.2 +Flask-Login==0.5.0 +Flask-Migrate==2.5.3 +Flask-SQLAlchemy==2.4.1 +Flask-Uploads==0.2.1 +Flask-WTF==0.14.3 +idna==2.9 +itsdangerous==1.1.0 +Jinja2==2.11.1 +Mako==1.1.2 +MarkupSafe==1.1.1 +marshmallow==3.5.1 +passlib==1.7.2 +protobuf==3.6.1 +pycparser==2.20 +PyMySQL==0.9.3 +python-dateutil==2.8.1 +python-dotenv==0.12.0 +python-editor==1.0.4 +requests==2.23.0 +six==1.14.0 +SQLAlchemy==1.3.15 +urllib3==1.25.8 +visitor==0.1.3 +Werkzeug==1.0.1 +WTForms==2.2.1 diff --git a/microservices/product-service/run.py b/microservices/product-service/run.py new file mode 100644 index 0000000..66eaeeb --- /dev/null +++ b/microservices/product-service/run.py @@ -0,0 +1,10 @@ +# run.py +from application import create_app, db +from application import models +from flask_migrate import Migrate + +app = create_app() +migrate = Migrate(app, db) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5002) diff --git a/microservices/user-service/.env b/microservices/user-service/.env new file mode 100644 index 0000000..4db88e2 --- /dev/null +++ b/microservices/user-service/.env @@ -0,0 +1,2 @@ +# .env +CONFIGURATION_SETUP="config.ProductionConfig" \ No newline at end of file diff --git a/microservices/user-service/.flaskenv b/microservices/user-service/.flaskenv new file mode 100644 index 0000000..7105c57 --- /dev/null +++ b/microservices/user-service/.flaskenv @@ -0,0 +1 @@ +FLASK_APP=run.py \ No newline at end of file diff --git a/microservices/user-service/.gitignore b/microservices/user-service/.gitignore new file mode 100644 index 0000000..630da0c --- /dev/null +++ b/microservices/user-service/.gitignore @@ -0,0 +1,5 @@ +.idea/ +.git/ +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/microservices/user-service/Dockerfile b/microservices/user-service/Dockerfile new file mode 100644 index 0000000..2cf6a45 --- /dev/null +++ b/microservices/user-service/Dockerfile @@ -0,0 +1,8 @@ +# Dockerfile +FROM python:3.7 +COPY requirements.txt /userapp/requirements.txt +WORKDIR /userapp +RUN pip install -r requirements.txt +COPY . /userapp +ENTRYPOINT ["python"] +CMD ["run.py"] \ No newline at end of file diff --git a/microservices/user-service/README.md b/microservices/user-service/README.md new file mode 100644 index 0000000..8fa6e34 --- /dev/null +++ b/microservices/user-service/README.md @@ -0,0 +1,18 @@ +## Running application in docker containers: +#### Using Docker CLI + +``` +docker network create --driver bridge micro_network (skip if already created) +docker build -t user-srv . +docker run -p 5001:5001 --detach --name user-service --net=micro_network user-srv +``` + +## Using 'flask shell' to access flask application +``` +$ flask shell +from application.models import User +from application import db +admin = User(username="foo", email="foo@admin.com",first_name="foo", last_name="bar", password="admin2020",is_admin=True) +db.session.add(admin) +db.session.commit() +``` \ No newline at end of file diff --git a/microservices/user-service/application/__init__.py b/microservices/user-service/application/__init__.py new file mode 100644 index 0000000..10eb592 --- /dev/null +++ b/microservices/user-service/application/__init__.py @@ -0,0 +1,24 @@ +# application/__init__.py +import config +import os +from flask import Flask +from flask_login import LoginManager +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() +login_manager = LoginManager() + + +def create_app(): + app = Flask(__name__) + environment_configuration = os.environ['CONFIGURATION_SETUP'] + app.config.from_object(environment_configuration) + + db.init_app(app) + login_manager.init_app(app) + + with app.app_context(): + # Register blueprints + from .user_api import user_api_blueprint + app.register_blueprint(user_api_blueprint) + return app diff --git a/microservices/user-service/application/models.py b/microservices/user-service/application/models.py new file mode 100644 index 0000000..5092fd8 --- /dev/null +++ b/microservices/user-service/application/models.py @@ -0,0 +1,40 @@ +# application/models.py +from . import db +from datetime import datetime +from flask_login import UserMixin +from passlib.hash import sha256_crypt + + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(255), unique=True, nullable=False) + email = db.Column(db.String(255), unique=True, nullable=False) + first_name = db.Column(db.String(255), unique=False, nullable=True) + last_name = db.Column(db.String(255), unique=False, nullable=True) + password = db.Column(db.String(255), unique=False, nullable=False) + is_admin = db.Column(db.Boolean, default=False) + authenticated = db.Column(db.Boolean, default=False) + api_key = db.Column(db.String(255), unique=True, nullable=True) + date_added = db.Column(db.DateTime, default=datetime.utcnow) + date_updated = db.Column(db.DateTime, onupdate=datetime.utcnow) + + def encode_api_key(self): + self.api_key = sha256_crypt.hash(self.username + str(datetime.utcnow)) + + def encode_password(self): + self.password = sha256_crypt.hash(self.password) + + def __repr__(self): + return '' % (self.username) + + def to_json(self): + return { + 'first_name': self.first_name, + 'last_name': self.last_name, + 'username': self.username, + 'email': self.email, + 'id': self.id, + 'api_key': self.api_key, + 'is_active': True, + 'is_admin': self.is_admin + } \ No newline at end of file diff --git a/microservices/user-service/application/user_api/__init__.py b/microservices/user-service/application/user_api/__init__.py new file mode 100644 index 0000000..c49e5aa --- /dev/null +++ b/microservices/user-service/application/user_api/__init__.py @@ -0,0 +1,6 @@ +# application/user_api/__init__.py +from flask import Blueprint + +user_api_blueprint = Blueprint('user_api', __name__) + +from . import routes diff --git a/microservices/user-service/application/user_api/routes.py b/microservices/user-service/application/user_api/routes.py new file mode 100644 index 0000000..eb11105 --- /dev/null +++ b/microservices/user-service/application/user_api/routes.py @@ -0,0 +1,100 @@ +# application/user_api/routes.py +from . import user_api_blueprint +from .. import db, login_manager +from ..models import User +from flask import make_response, request, jsonify +from flask_login import current_user, login_user, logout_user, login_required + +from passlib.hash import sha256_crypt + + +@login_manager.user_loader +def load_user(user_id): + return User.query.filter_by(id=user_id).first() + + +@login_manager.request_loader +def load_user_from_request(request): + api_key = request.headers.get('Authorization') + if api_key: + api_key = api_key.replace('Basic ', '', 1) + user = User.query.filter_by(api_key=api_key).first() + if user: + return user + return None + + +@user_api_blueprint.route('/api/users', methods=['GET']) +def get_users(): + data = [] + for row in User.query.all(): + data.append(row.to_json()) + + response = jsonify(data) + return response + +@user_api_blueprint.route('/api/user/create', methods=['POST']) +def post_register(): + first_name = request.form['first_name'] + last_name = request.form['last_name'] + email = request.form['email'] + username = request.form['username'] + + password = sha256_crypt.hash((str(request.form['password']))) + + user = User() + user.email = email + user.first_name = first_name + user.last_name = last_name + user.password = password + user.username = username + user.authenticated = True + + db.session.add(user) + db.session.commit() + + response = jsonify({'message': 'User added', 'result': user.to_json()}) + + return response + + +@user_api_blueprint.route('/api/user/login', methods=['POST']) +def post_login(): + username = request.form['username'] + user = User.query.filter_by(username=username).first() + if user: + if sha256_crypt.verify(str(request.form['password']), user.password): + user.encode_api_key() + db.session.commit() + login_user(user) + + return make_response(jsonify({'message': 'Logged in', 'api_key': user.api_key})) + + return make_response(jsonify({'message': 'Not logged in'}), 401) + + +@user_api_blueprint.route('/api/user/logout', methods=['POST']) +def post_logout(): + if current_user.is_authenticated: + logout_user() + return make_response(jsonify({'message': 'You are logged out'})) + return make_response(jsonify({'message': 'You are not logged in'})) + + +@user_api_blueprint.route('/api/user//exists', methods=['GET']) +def get_username(username): + item = User.query.filter_by(username=username).first() + if item is not None: + response = jsonify({'result': True}) + else: + response = jsonify({'message': 'Cannot find username'}), 404 + return response + + +@login_required +@user_api_blueprint.route('/api/user', methods=['GET']) +def get_user(): + if current_user.is_authenticated: + return make_response(jsonify({'result': current_user.to_json()})) + + return make_response(jsonify({'message': 'Not logged in'})), 401 \ No newline at end of file diff --git a/microservices/user-service/config.py b/microservices/user-service/config.py new file mode 100644 index 0000000..b2419d9 --- /dev/null +++ b/microservices/user-service/config.py @@ -0,0 +1,27 @@ +# config.py +import os +from dotenv import load_dotenv + +dotenv_path = os.path.join(os.path.dirname(__file__), '.env') +if os.path.exists(dotenv_path): + load_dotenv(dotenv_path) + + +class Config: + SECRET_KEY = "mrfrIMEngCl0pAKqIIBS_g" + SQLALCHEMY_TRACK_MODIFICATIONS = False + + +class DevelopmentConfig(Config): + ENV = "development" + DEBUG = True + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@host.docker.internal:3306/user_dev' + SQLALCHEMY_ECHO = True + + +class ProductionConfig(Config): + ENV = "production" + DEBUG = False + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://cloudacademy:pfm_2020@user-db:3306/user' + SQLALCHEMY_ECHO = False + diff --git a/microservices/user-service/docker-compose.yml b/microservices/user-service/docker-compose.yml new file mode 100644 index 0000000..09a1845 --- /dev/null +++ b/microservices/user-service/docker-compose.yml @@ -0,0 +1,38 @@ +# docker-compose.yml +version: '3.8' + +volumes: + userdb_vol: + +networks: + micro_network: + external: + name: micro_network + +services: + user-api: + container_name: cuser-service + build: + context: . + ports: + - "5001:5001" + depends_on: + - user-db + networks: + - micro_network + restart: always + + user-db: + container_name: cuser_dbase + image: mysql:8 + ports: + - "32000:3306" + environment: + MYSQL_ROOT_PASSWORD: pfm_dc_2020 + MYSQL_DATABASE: user + MYSQL_USER: cloudacademy + MYSQL_PASSWORD: pfm_2020 + networks: + - micro_network + volumes: + - userdb_vol:/var/lib/mysql \ No newline at end of file diff --git a/microservices/user-service/requirements.txt b/microservices/user-service/requirements.txt new file mode 100644 index 0000000..04b9791 --- /dev/null +++ b/microservices/user-service/requirements.txt @@ -0,0 +1,39 @@ +alembic==1.4.2 +autoenv==1.0.0 +blinker==1.4 +certifi==2019.11.28 +cffi==1.14.0 +chardet==3.0.4 +click==7.1.1 +cryptography==2.8 +dnspython==1.16.0 +dominate==2.5.1 +Flask==1.1.1 +Flask-Bootstrap==3.3.7.1 +Flask-DotEnv==0.1.2 +Flask-Login==0.5.0 +Flask-Migrate==2.5.3 +Flask-SQLAlchemy==2.4.1 +Flask-Uploads==0.2.1 +Flask-WTF==0.14.3 +gunicorn==20.0.4 +idna==2.9 +itsdangerous==1.1.0 +Jinja2==2.11.1 +Mako==1.1.2 +MarkupSafe==1.1.1 +marshmallow==3.5.1 +passlib==1.7.2 +protobuf==3.6.1 +pycparser==2.20 +PyMySQL==0.9.3 +python-dateutil==2.8.1 +python-dotenv==0.12.0 +python-editor==1.0.4 +requests==2.23.0 +six==1.14.0 +SQLAlchemy==1.3.15 +urllib3==1.25.8 +visitor==0.1.3 +Werkzeug==1.0.1 +WTForms==2.2.1 diff --git a/microservices/user-service/run.py b/microservices/user-service/run.py new file mode 100644 index 0000000..664718d --- /dev/null +++ b/microservices/user-service/run.py @@ -0,0 +1,33 @@ +# run.py +from application import create_app, db +from application import models +from flask_migrate import Migrate + +app = create_app() +migrate = Migrate(app, db) + +from flask import g +from flask.sessions import SecureCookieSessionInterface +from flask_login import user_loaded_from_header + + +class CustomSessionInterface(SecureCookieSessionInterface): + """Prevent creating session from API requests.""" + + def save_session(self, *args, **kwargs): + if g.get('login_via_header'): + return + return super(CustomSessionInterface, self).save_session(*args, + **kwargs) + + +app.session_interface = CustomSessionInterface() + + +@user_loaded_from_header.connect +def user_loaded_from_header(self, user=None): + g.login_via_header = True + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5001)