From 30e16277dfcbf1528fc5fd324438cd3167f814fa Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Wed, 20 Dec 2023 22:26:11 +1100 Subject: [PATCH] Add in authentication via Flask-Login, still need to modify http functions so they are linked to a user --- app.py | 87 +++++++++++++++++++++++++++++++++++++++++++ db.py | 17 ++++++++- requirements.txt | 3 +- templates/login.html | 57 ++++++++++++++++++++++++++++ templates/signup.html | 48 ++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 templates/login.html create mode 100644 templates/signup.html diff --git a/app.py b/app.py index def2cb9..0d031c6 100644 --- a/app.py +++ b/app.py @@ -7,13 +7,35 @@ from flask_htmx import HTMX import requests from db import DataBase from services import create_http_function_view_model, create_http_functions_view_model +from flask_login import LoginManager, UserMixin, login_required, login_user +from werkzeug.security import check_password_hash, generate_password_hash +login_manager = LoginManager() app = Flask(__name__) app.config.from_pyfile('config.py') +app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f') +login_manager.init_app(app) +login_manager.login_view = "login" jinja_partials.register_extensions(app) htmx = HTMX(app) db = DataBase(app) +class User(UserMixin): + def __init__(self, id, username, password_hash, created_at): + self.id = id + self.username = username + self.password_hash = password_hash + self.created_at = created_at + + @staticmethod + def get(user_id): + user_data = db.get_user(int(user_id)) + + if user_data: + return User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at']) + return None + + API_URL = 'https://isolator.peterstockings.com/execute' DEFAULT_FUNCTION_NAME = 'foo' @@ -65,22 +87,26 @@ def client(function): return render_template("client.html", **http_function) @ app.route("/dashboard", methods=["GET"]) +@login_required def dashboard(): http_functions = db.get_http_functions() http_functions = create_http_functions_view_model(http_functions) return render_template("dashboard.html", http_functions=http_functions) @ app.route("/dashboard/http_functions", methods=["GET"]) +@login_required def dashboard_http_functions(): http_functions = db.get_http_functions() http_functions = create_http_functions_view_model(http_functions) return render_template("dashboard/http_functions.html", http_functions=http_functions) @ app.route("/dashboard/http_functions/add_form", methods=["GET"]) +@login_required def get_http_function_add_form(): return render_template("dashboard/http_functions/new.html", name=DEFAULT_FUNCTION_NAME, script=DEFAULT_SCRIPT, environment_info=DEFAULT_ENVIRONMENT) @ app.route("/dashboard/http_functions/create", methods=["POST"]) +@login_required def create_http_function(): try: name = request.json.get('name') @@ -97,6 +123,7 @@ def create_http_function(): return { "status": "error", "message": str(e) } @ app.route("/dashboard/http_functions/edit_form", methods=["GET"]) +@login_required def get_http_function_edit_form(): name = request.args.get('name') http_function = db.get_http_function(name) @@ -107,6 +134,7 @@ def get_http_function_edit_form(): return render_template("dashboard/http_functions/edit.html", name=name, script=script, environment_info=environment_info) @ app.route("/dashboard/http_functions/edit", methods=["POST"]) +@login_required def edit_http_function(): try: name = request.json.get('name') @@ -120,6 +148,7 @@ def edit_http_function(): return { "status": "error", "message": str(e) } @ app.route("/dashboard/http_functions/delete", methods=["DELETE"]) +@login_required def delete_http_function(): try: name = request.args.get('name') @@ -132,6 +161,7 @@ def delete_http_function(): return jsonify({"status": 'error', "message": str(e)}), 500 @ app.route("/dashboard/http_functions/logs", methods=["GET"]) +@login_required def get_http_function_logs(): name = request.args.get('name') http_function = db.get_http_function(name) @@ -144,6 +174,7 @@ def get_http_function_logs(): @ app.route("/dashboard/timer_functions", methods=["GET"]) +@login_required def dashboard_timer_functions(): return render_template("dashboard/timer_functions.html") @@ -227,6 +258,62 @@ def execute_http_function(function): except Exception as e: return jsonify({'error': str(e)}), 500 +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'GET': + return render_template("login.html") + + username = request.form.get('username') + password = request.form.get('password') + if not username or not password: + return render_template("login.html", error="Both username and password must be entered") + + user_data = db.get_user_by_username(username) + if not user_data: + return render_template("login.html", error="User does not exist") + + if not check_password_hash(user_data['password_hash'], password): + return render_template("login.html", error="Invalid username or password") + + user = User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at']) + + # user should be an instance of your `User` class + login_user(user) + #flask.flash('Logged in successfully.') + + next = request.args.get('next') + return redirect(next or url_for('dashboard')) + +@app.route('/signup', methods=['GET', 'POST']) +def signup(): + if request.method == 'GET': + return render_template("signup.html") + + username = request.form.get('username') + password = request.form.get('password') + if not username or not password: + return render_template("signup.html", error="Both username and password must be entered") + + user = db.get_user_by_username(username) + if user: + return render_template("signup.html", error="User already exists") + + hashed_password = generate_password_hash(password) + user_data = db.create_new_user(username, hashed_password) + + user = User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at']) + login_user(user) + + return redirect(url_for('dashboard')) + + +@login_manager.user_loader +def load_user(user_id): + user_data = db.get_user(int(user_id)) + if user_data: + return User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at']) + return None + if __name__ == '__main__': # Bind to PORT if defined, otherwise default to 5000. diff --git a/db.py b/db.py index be1579a..8fe5ccb 100644 --- a/db.py +++ b/db.py @@ -83,4 +83,19 @@ class DataBase(): FROM http_function_invocations WHERE http_function_id=%s ORDER BY invocation_time DESC""", [http_function_id]) - return http_function_invocations \ No newline at end of file + return http_function_invocations + + def get_user(self, user_id): + user = self.execute( + 'SELECT id, username, password_hash, created_at FROM users WHERE id=%s', [int(user_id)], one=True) + return user + + def get_user_by_username(self, username): + user = self.execute( + 'SELECT id, username, password_hash, created_at FROM users WHERE username=%s', [username], one=True) + return user + + def create_new_user(self, username, password_hash): + new_user = self.execute( + 'INSERT INTO users (username, password_hash) VALUES (%s, %s) RETURNING id, username, password_hash, created_at', [username, password_hash], commit=True) + return new_user \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 00fcc87..c758f53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ flask-htmx==0.2.0 python-dateutil==2.8.2 jinja2-fragments==0.3.0 Werkzeug==2.2.2 -requests==2.26.0 \ No newline at end of file +requests==2.26.0 +Flask-Login==0.6.3 \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..4787719 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,57 @@ +{% extends 'base.html' %} + +{% block content %} + +
+
+
+

+ Login +

+ {% if error %} +

+ {{ error }} +

+ {% endif %} +
+
+
+ + +
+
+ + +
+
+ + Forgot password? + + +
+
+
+

Don't have an account? Sign up +

+
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..8a8a60c --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,48 @@ +{% extends 'base.html' %} + +{% block content %} + +
+
+
+

+ Signup +

+ {% if error %} +

+ {{ error }} +

+ {% endif %} +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +{% endblock %} \ No newline at end of file