diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index 6f59353a..da6fcec8 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -1,5 +1,12 @@ FROM gitpod/workspace-full - -USER root -RUN sudo apt-get update -RUN sudo apt-get install -y libgbm-dev gconf-service libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxss1 libxtst6 libappindicator1 libnss3 libasound2 libatk1.0-0 libc6 ca-certificates fonts-liberation lsb-release xdg-utils wget + +USER gitpod + +# Copy the .python-version file into the Docker image to use it during the build +COPY .python-version /home/gitpod/.python-version + +# Install the specific Python version from .python-version and upgrade pip +RUN pyenv install $(cat /home/gitpod/.python-version) && \ + pyenv global $(cat /home/gitpod/.python-version) && \ + eval "$(pyenv init -)" && \ + pip install --upgrade pip \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml index 612085a0..482bfe1c 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,5 +1,5 @@ tasks: - - init: pip install -r requirements.txt & pyenv install + - init: pip install -r requirements.txt command: flask init & flask run ports: diff --git a/App/controllers/auth.py b/App/controllers/auth.py index b580b132..a4556d0b 100644 --- a/App/controllers/auth.py +++ b/App/controllers/auth.py @@ -1,43 +1,42 @@ -from flask_login import login_user, login_manager, logout_user, LoginManager -from flask_jwt_extended import create_access_token, jwt_required, JWTManager +from flask_jwt_extended import create_access_token, jwt_required, JWTManager, get_jwt_identity, verify_jwt_in_request from App.models import User -def jwt_authenticate(username, password): +def login(username, password): user = User.query.filter_by(username=username).first() if user and user.check_password(password): return create_access_token(identity=username) return None -def login(username, password): - user = User.query.filter_by(username=username).first() - if user and user.check_password(password): - return user - return None - -def setup_flask_login(app): - login_manager = LoginManager() - login_manager.init_app(app) - - @login_manager.user_loader - def load_user(user_id): - return User.query.get(user_id) - - return login_manager def setup_jwt(app): - jwt = JWTManager(app) - - @jwt.user_identity_loader - def user_identity_lookup(identity): - user = User.query.filter_by(username=identity).one_or_none() - if user: - return user.id - return None - - @jwt.user_lookup_loader - def user_lookup_callback(_jwt_header, jwt_data): - identity = jwt_data["sub"] - return User.query.get(identity) + jwt = JWTManager(app) + + # configure's flask jwt to resolve get_current_identity() to the corresponding user's ID + @jwt.user_identity_loader + def user_identity_lookup(identity): + user = User.query.filter_by(username=identity).one_or_none() + if user: + return user.id + return None - return jwt \ No newline at end of file + @jwt.user_lookup_loader + def user_lookup_callback(_jwt_header, jwt_data): + identity = jwt_data["sub"] + return User.query.get(identity) + return jwt + +# Context processor to make 'is_authenticated' available to all templates +def add_auth_context(app): + @app.context_processor + def inject_user(): + try: + verify_jwt_in_request() + user_id = get_jwt_identity() + current_user = User.query.get(user_id) + is_authenticated = True + except Exception as e: + print(e) + is_authenticated = False + current_user = None + return dict(is_authenticated=is_authenticated, current_user=current_user) \ No newline at end of file diff --git a/App/main.py b/App/main.py index 9cffaf93..d9ebf764 100644 --- a/App/main.py +++ b/App/main.py @@ -12,7 +12,7 @@ from App.controllers import ( setup_jwt, - setup_flask_login + add_auth_context ) from App.views import views @@ -36,12 +36,18 @@ def create_app(config_overrides={}): app.config['SEVER_NAME'] = '0.0.0.0' app.config['PREFERRED_URL_SCHEME'] = 'https' app.config['UPLOADED_PHOTOS_DEST'] = "App/uploads" + app.config['JWT_ACCESS_COOKIE_NAME'] = 'access_token' + app.config["JWT_TOKEN_LOCATION"] = ["cookies", "headers"] + app.config["JWT_COOKIE_SECURE"] = True + app.config["JWT_SECRET_KEY"] = "super-secret" + app.config["JWT_COOKIE_CSRF_PROTECT"] = False CORS(app) + add_auth_context(app) photos = UploadSet('photos', TEXT + DOCUMENTS + IMAGES) configure_uploads(app, photos) add_views(app) init_db(app) setup_jwt(app) - setup_flask_login(app) + app.app_context().push() return app \ No newline at end of file diff --git a/App/models/user.py b/App/models/user.py index 40caf3f4..8efe083a 100644 --- a/App/models/user.py +++ b/App/models/user.py @@ -1,8 +1,7 @@ from werkzeug.security import check_password_hash, generate_password_hash -from flask_login import UserMixin from App.database import db -class User(db.Model, UserMixin): +class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String, nullable=False, unique=True) password = db.Column(db.String(120), nullable=False) @@ -19,7 +18,7 @@ def get_json(self): def set_password(self, password): """Create hashed password.""" - self.password = generate_password_hash(password, method='sha256') + self.password = generate_password_hash(password) def check_password(self, password): """Check hashed password.""" diff --git a/App/templates/index.html b/App/templates/index.html index c4fb60e0..96ec1967 100644 --- a/App/templates/index.html +++ b/App/templates/index.html @@ -6,5 +6,8 @@ {% block content %}

Flask MVC

+ {% if is_authenticated %} +

Welcome {{current_user.username}}

+ {% endif %}

This is a boileplate flask application which follows the MVC pattern for structuring the project.

{% endblock %} \ No newline at end of file diff --git a/App/templates/layout.html b/App/templates/layout.html index 4f859c1f..0920c5fd 100644 --- a/App/templates/layout.html +++ b/App/templates/layout.html @@ -13,18 +13,42 @@ - + + + + {% with messages = get_flashed_messages() %} {% if messages %} diff --git a/App/templates/message.html b/App/templates/message.html new file mode 100644 index 00000000..e535fad3 --- /dev/null +++ b/App/templates/message.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} +{% block title %}{{title}}{% endblock %} +{% block page %}{{title}}{% endblock %} + +{{ super() }} + +{% block content %} +
+
+ {{message}} +
+
+{% endblock %} diff --git a/App/views/auth.py b/App/views/auth.py index 291a02bb..96d54ade 100644 --- a/App/views/auth.py +++ b/App/views/auth.py @@ -1,17 +1,17 @@ -from flask import Blueprint, render_template, jsonify, request, send_from_directory, flash, redirect, url_for -from flask_jwt_extended import jwt_required, current_user as jwt_current_user -from flask_login import login_required, login_user, current_user, logout_user +from flask import Blueprint, render_template, jsonify, request, flash, send_from_directory, flash, redirect, url_for +from flask_jwt_extended import jwt_required, current_user, unset_jwt_cookies, set_access_cookies from.index import index_views from App.controllers import ( - create_user, - jwt_authenticate, - login + login ) auth_views = Blueprint('auth_views', __name__, template_folder='../templates') + + + ''' Page/Action Routes ''' @@ -21,52 +21,52 @@ def get_user_page(): users = get_all_users() return render_template('users.html', users=users) - @auth_views.route('/identify', methods=['GET']) -@login_required +@jwt_required() def identify_page(): - return jsonify({'message': f"username: {current_user.username}, id : {current_user.id}"}) - + return render_template('message.html', title="Identify", message=f"You are logged in as {current_user.id} - {current_user.username}") + @auth_views.route('/login', methods=['POST']) def login_action(): data = request.form - user = login(data['username'], data['password']) - if user: - login_user(user) - return 'user logged in!' - return 'bad username or password given', 401 + token = login(data['username'], data['password']) + response = redirect(request.referrer) + if not token: + flash('Bad username or password given'), 401 + else: + flash('Login Successful') + set_access_cookies(response, token) + return response @auth_views.route('/logout', methods=['GET']) def logout_action(): - data = request.form - user = login(data['username'], data['password']) - return 'logged out!' + response = redirect(request.referrer) + flash("Logged Out!") + unset_jwt_cookies(response) + return response ''' API Routes ''' -@auth_views.route('/api/users', methods=['GET']) -def get_users_action(): - users = get_all_users_json() - return jsonify(users) - -@auth_views.route('/api/users', methods=['POST']) -def create_user_endpoint(): - data = request.json - create_user(data['username'], data['password']) - return jsonify({'message': f"user {data['username']} created"}) - @auth_views.route('/api/login', methods=['POST']) def user_login_api(): data = request.json - token = jwt_authenticate(data['username'], data['password']) + token = login(data['username'], data['password']) if not token: return jsonify(message='bad username or password given'), 401 - return jsonify(access_token=token) + response = jsonify(access_token=token) + set_access_cookies(response, token) + return response @auth_views.route('/api/identify', methods=['GET']) @jwt_required() -def identify_user_action(): - return jsonify({'message': f"username: {jwt_current_user.username}, id : {jwt_current_user.id}"}) \ No newline at end of file +def identify_user(): + return jsonify({'message': f"username: {current_user.username}, id : {current_user.id}"}) + +@auth_views.route('/api/logout', methods=['GET']) +def logout_api(): + response = jsonify(message="Logged Out!") + unset_jwt_cookies(response) + return response \ No newline at end of file diff --git a/App/views/user.py b/App/views/user.py index 6e9b229b..d4288acf 100644 --- a/App/views/user.py +++ b/App/views/user.py @@ -6,7 +6,6 @@ from App.controllers import ( create_user, - jwt_authenticate, get_all_users, get_all_users_json, jwt_required @@ -19,6 +18,13 @@ def get_user_page(): users = get_all_users() return render_template('users.html', users=users) +@user_views.route('/users', methods=['POST']) +def create_user_action(): + data = request.form + flash(f"User {data['username']} created!") + create_user(data['username'], data['password']) + return redirect(url_for('user_views.get_user_page')) + @user_views.route('/api/users', methods=['GET']) def get_users_action(): users = get_all_users_json() @@ -30,13 +36,6 @@ def create_user_endpoint(): create_user(data['username'], data['password']) return jsonify({'message': f"user {data['username']} created"}) -@user_views.route('/users', methods=['POST']) -def create_user_action(): - data = request.form - flash(f"User {data['username']} created!") - create_user(data['username'], data['password']) - return redirect(url_for('user_views.get_user_page')) - @user_views.route('/static/users', methods=['GET']) def static_user_page(): return send_from_directory('static', 'static-user.html') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 85c0612e..47450fa4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ greenlet==2.0.2 gunicorn==20.1.0 itsdangerous==2.1.2 python-dotenv==0.21.1 -Flask-Login==0.6.2 Flask-Migrate==3.1.0 Flask-Reuploaded==1.2.0 psycopg2-binary==2.9.3