diff --git a/Vagrantfile b/Vagrantfile index c3fd6bf..2041ef5 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,11 +1,24 @@ -FLASK_PORT=5000 -POSTGRESQL_PORT=5432 +db_file_exists = "test -f /vagrant/edison/db.sql" +restore_db = "sudo -u postgres psql edison < /vagrant/edison/db.sql" +db_restored_msg = "echo \"Database restored.\"" +db_not_exists_msg = "echo \"db.sql not exists.\"" +try_restore_db = "bash -c '#{db_file_exists} && #{restore_db} && #{db_restored_msg} || #{db_not_exists_msg}'" +save_db_data = "sudo -u postgres pg_dump edison > /vagrant/edison/db.sql" Vagrant.configure("2") do |config| + config.trigger.before :destroy do |trigger| + trigger.info = "Saving database data inside synced folder..." + trigger.run_remote = {inline: "#{save_db_data}"} + end + + config.trigger.after :up do |trigger| + trigger.info = "Trying to restore database from /vagrant/edison/db.sql..." + trigger.run_remote = {inline: "#{try_restore_db}"} + end + config.vm.box = "ubuntu/bionic64" config.vm.provision :shell, path: "setup.sh" - config.vm.network :forwarded_port, guest: FLASK_PORT, host: FLASK_PORT - config.vm.network :forwarded_port, guest: POSTGRESQL_PORT, host: POSTGRESQL_PORT + config.vm.network "private_network", type: "dhcp" config.vm.provider "virtualbox" do |v| v.gui = false v.name = "Edison_test" @@ -14,3 +27,11 @@ Vagrant.configure("2") do |config| end end + +# Fixes a dhcp configuration conflict of the private network. +# Issue: https://github.com/hashicorp/vagrant/issues/8878 +class VagrantPlugins::ProviderVirtualBox::Action::Network + def dhcp_server_matches_config?(dhcp_server, config) + true + end +end diff --git a/edison/__init__.py b/edison/__init__.py new file mode 100644 index 0000000..d4ff709 --- /dev/null +++ b/edison/__init__.py @@ -0,0 +1,12 @@ +import os + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from edison.config import get_config_object + + +# Put app here so the entire app could import it. +app = Flask(__name__) +app.config.from_object(get_config_object(app.config["ENV"])) +basedir = os.path.abspath(os.path.dirname(__file__)) +db = SQLAlchemy(app) diff --git a/edison/app.py b/edison/app.py new file mode 100644 index 0000000..34f3711 --- /dev/null +++ b/edison/app.py @@ -0,0 +1,34 @@ +import edison +import edison.models as models + +from flask import render_template +from flask_restful import Api +from flask_jwt_extended import JWTManager + + +app = edison.app +db = edison.db +api = Api(app) + +# Creates all tables defined in the database models and the only ones that are not created yet. +# If there's any change in the database models you should perform a migration to apply this change in the database itself. +# More about database migrations can be found in /edison/migrations/README. +db.create_all() + +# Creation of Json-Web-Token manager. +# In order to reach secured endpoints client should add an authorization header with the value Bearer . +jwt = JWTManager(app) + +# This decorator is a callback and it is called every time user is trying to access secured endpoints. +# The function under the decorator should return true or false depending on if the passed token is blacklisted. +@jwt.token_in_blacklist_loader +def check_if_token_in_blacklist(decrypted_token): + jti = decrypted_token['jti'] + return models.Token.query.filter_by(jti=jti).first() is not None + +@app.route("/") +def index(): + return render_template('index.html') + +if __name__ == "__main__": + app.run(host='0.0.0.0') diff --git a/edison/config.py b/edison/config.py new file mode 100644 index 0000000..ed04143 --- /dev/null +++ b/edison/config.py @@ -0,0 +1,40 @@ +import secrets +import importlib, inspect +import sys, inspect + +config_dict = {} + +# If config_dict is empty this function builds it dynamically +# and returns the appropriate config object path. +def get_config_object(env_keyword: str): + if(len(config_dict) == 0): + # Iterating through all config.py members + for name, obj in inspect.getmembers(sys.modules[__name__]): + # We're interested only with the derived classes of the Config class + if inspect.isclass(obj) and name != "Config": + config_dict[obj.ENV_KEYWORD] = ".".join([obj.__module__, name]) + + return config_dict[env_keyword] + +class Config: + ENV_KEYWORD = "" + DEBUG = False + # Enables response message for unauthenticated requests + PROPAGATE_EXCEPTIONS = True + # This tells the JWTManager to use jwt.token_in_blacklist_loader callback + JWT_BLACKLIST_ENABLED = True + # JWTManager uses this secret key for creating tokens + JWT_SECRET_KEY = secrets.token_hex(24) + # We're going to check if both access_token and refresh_token are black listed + JWT_BLACKLIST_TOKEN_CHECKS = ['access', 'refresh'] + # Turns off the Flask-SQLAlchemy event system + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:edison@127.0.0.1/edison' + +# PostgreSQL connection string should be updated once an actual production environment is established. +class ProductionConfig(Config): + ENV_KEYWORD = "production" + +class DevelopmentConfig(Config): + ENV_KEYWORD = "development" + DEBUG = True diff --git a/edison/models/__init__.py b/edison/models/__init__.py new file mode 100644 index 0000000..9b6631e --- /dev/null +++ b/edison/models/__init__.py @@ -0,0 +1,2 @@ +from .token import Token +from .user import User diff --git a/edison/models/token.py b/edison/models/token.py new file mode 100644 index 0000000..4797546 --- /dev/null +++ b/edison/models/token.py @@ -0,0 +1,12 @@ +from edison import db + + +class Token(db.Model): + __tablename__ = 'token_blacklist' + + id = db.Column(db.Integer, primary_key=True) + jti = db.Column(db.String(150), nullable=False, unique=True) + creation_timestamp = db.Column(db.TIMESTAMP(timezone=False), nullable=False) + + def __repr__(self): + return f"" diff --git a/edison/models/user.py b/edison/models/user.py new file mode 100644 index 0000000..22cb23c --- /dev/null +++ b/edison/models/user.py @@ -0,0 +1,25 @@ +from edison import db + + +class User(db.Model): + __tablename__ = 'users' + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(50), nullable=False, unique=True) + password = db.Column(db.String(150), nullable=False) + first_name = db.Column(db.String(50), nullable=False) + last_name = db.Column(db.String(50), nullable=False) + email = db.Column(db.String(150), nullable=False) + + def to_json(self): + return { + "username": self.username, + "first_name": self.first_name, + "last_name": self.last_name, + "email": self.email + } + + def __repr__(self): + return f"" diff --git a/static/hello-world-image.png b/edison/static/hello-world-image.png similarity index 100% rename from static/hello-world-image.png rename to edison/static/hello-world-image.png diff --git a/templates/index.html b/edison/templates/index.html similarity index 100% rename from templates/index.html rename to edison/templates/index.html diff --git a/flask_init.py b/flask_init.py deleted file mode 100644 index 14ad216..0000000 --- a/flask_init.py +++ /dev/null @@ -1,10 +0,0 @@ -from flask import Flask, render_template - -app = Flask(__name__) - -@app.route("/") -def index(): - return render_template('index.html') - -if __name__ == "__main__": - app.run(host='0.0.0.0') diff --git a/requirements.txt b/requirements.txt index 32e8968..a0082cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,6 @@ Flask==1.1.1 +flask-sqlalchemy==2.4.1 +psycopg2-binary==2.8.5 +flask-restful==0.3.8 +flask-jwt-extended==3.24.1 +passlib==1.7.2 diff --git a/setup.sh b/setup.sh index e344782..147887d 100644 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,7 @@ #!/bin/bash +FLASK_PORT=5000 + echo "updating apt before installation" sudo apt-get update @@ -15,6 +17,12 @@ sudo apt-get install -y postgresql postgresql-contrib echo "install requirements" pip3 install -r /vagrant/requirements.txt -echo "running flask_init.py" -export FLASK_APP=/vagrant/flask_init.py -python3 -m flask run --host=0.0.0.0 >> /vagrant/log.log 2>&1 & +echo "configuring database" +sudo -u postgres createdb edison +sudo -u postgres psql -c "ALTER ROLE postgres WITH PASSWORD 'edison';" + +export FLASK_ENV=development + +echo "running app.py" +export FLASK_APP=/vagrant/edison/app.py +flask run -h 0.0.0.0 -p $FLASK_PORT >> /vagrant/edison/app.log 2>&1 & diff --git a/test/init_test.py b/tests/init_test.py similarity index 100% rename from test/init_test.py rename to tests/init_test.py