Move auth logic to blueprint

This commit is contained in:
Peter Stockings
2025-07-23 21:46:13 +10:00
parent b0a172bee0
commit 19d855fb89
6 changed files with 94 additions and 88 deletions

91
app.py
View File

@@ -4,9 +4,9 @@ from flask import Flask, Response, jsonify, redirect, render_template, render_te
import jinja_partials import jinja_partials
from jinja2_fragments import render_block from jinja2_fragments import render_block
import requests import requests
from extensions import db, htmx, init_app from extensions import db, htmx, init_app, login_manager
from services import create_http_function_view_model, create_http_functions_view_model from services import create_http_function_view_model, create_http_functions_view_model
from flask_login import LoginManager, UserMixin, current_user, login_required, login_user, logout_user from flask_login import current_user, login_required
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -15,6 +15,7 @@ from routes.test import test
from routes.home import home from routes.home import home
from routes.http import http from routes.http import http
from routes.llm import llm from routes.llm import llm
from routes.auth import auth
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
import asyncio import asyncio
import aiohttp import aiohttp
@@ -28,9 +29,8 @@ app = Flask(__name__)
app.config.from_pyfile('config.py') app.config.from_pyfile('config.py')
app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f') app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f')
login_manager = LoginManager()
login_manager.init_app(app) login_manager.init_app(app)
login_manager.login_view = "login" login_manager.login_view = "auth.login"
jinja_partials.register_extensions(app) jinja_partials.register_extensions(app)
# Remove scheduler configuration and initialization # Remove scheduler configuration and initialization
@@ -41,21 +41,7 @@ app.register_blueprint(test, url_prefix='/test')
app.register_blueprint(home, url_prefix='/home') app.register_blueprint(home, url_prefix='/home')
app.register_blueprint(http, url_prefix='/http') app.register_blueprint(http, url_prefix='/http')
app.register_blueprint(llm, url_prefix='/llm') app.register_blueprint(llm, url_prefix='/llm')
app.register_blueprint(auth, url_prefix='/auth')
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
# Swith to inter app routing, which results in speed up from ~400ms to ~270ms # Swith to inter app routing, which results in speed up from ~400ms to ~270ms
# https://stackoverflow.com/questions/76886643/linking-two-not-exposed-dokku-apps # https://stackoverflow.com/questions/76886643/linking-two-not-exposed-dokku-apps
@@ -186,7 +172,7 @@ def execute_http_function(user_id, function):
# Check if the function is public, if not check if the user is authenticated and owns the function # Check if the function is public, if not check if the user is authenticated and owns the function
if not is_public: if not is_public:
if not current_user.is_authenticated: if not current_user.is_authenticated:
return login_manager.unauthorized() return redirect(url_for('auth.login', next=request.url))
if int(current_user.id) != user_id: if int(current_user.id) != user_id:
return jsonify({'error': 'Function belongs to another user'}), 404 return jsonify({'error': 'Function belongs to another user'}), 404
@@ -244,71 +230,6 @@ def execute_http_function(user_id, function):
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 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('home.index'))
@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")
if len(username) < 10 or len(password) < 10:
return render_template("signup.html", error="Both username and password must be at least 10 characters long")
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('home.index'))
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for('landing_page'))
@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__': if __name__ == '__main__':
# Bind to PORT if defined, otherwise default to 5000. # Bind to PORT if defined, otherwise default to 5000.

View File

@@ -2,9 +2,11 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
from db import DataBase from db import DataBase
from flask_htmx import HTMX from flask_htmx import HTMX
from flask import url_for from flask import url_for
from flask_login import LoginManager
db = DataBase() db = DataBase()
htmx = HTMX() htmx = HTMX()
login_manager = LoginManager()
environment = Environment( environment = Environment(
loader=FileSystemLoader("templates"), loader=FileSystemLoader("templates"),

83
routes/auth.py Normal file
View File

@@ -0,0 +1,83 @@
from flask import Blueprint, render_template, request, redirect, url_for
from flask_login import login_user, logout_user, login_required, UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from extensions import db, login_manager
auth = Blueprint('auth', __name__)
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
@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
@auth.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'])
login_user(user)
next = request.args.get('next')
return redirect(next or url_for('home.index'))
@auth.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")
if len(username) < 10 or len(password) < 10:
return render_template("signup.html", error="Both username and password must be at least 10 characters long")
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('home.index'))
@auth.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for('landing_page'))

View File

@@ -69,7 +69,7 @@
</div> </div>
<a class="inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground rounded-full border border-gray-200 w-8 h-8 dark:border-gray-800 cursor-pointer" <a class="inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground rounded-full border border-gray-200 w-8 h-8 dark:border-gray-800 cursor-pointer"
data-id="40" type="button" id="radix-:r1u:" aria-haspopup="menu" aria-expanded="false" data-id="40" type="button" id="radix-:r1u:" aria-haspopup="menu" aria-expanded="false"
data-state="closed" href="{{ url_for('logout') }}"> data-state="closed" href="{{ url_for('auth.logout') }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6"> stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"

View File

@@ -47,7 +47,7 @@
<hr class="my-4" data-id="15"> <hr class="my-4" data-id="15">
<div class="text-center" data-id="16"> <div class="text-center" data-id="16">
<p class="text-gray-500" data-id="17">Don't have an account? <a class="underline text-blue-500" <p class="text-gray-500" data-id="17">Don't have an account? <a class="underline text-blue-500"
data-id="18" href="{{ url_for('signup') }}" rel="ugc">Sign up</a> data-id="18" href="{{ url_for('auth.signup') }}" rel="ugc">Sign up</a>
</p> </p>
</div> </div>
</form> </form>

View File

@@ -44,7 +44,7 @@
<hr class="my-4" data-id="15"> <hr class="my-4" data-id="15">
<div class="text-center" data-id="16"> <div class="text-center" data-id="16">
<p class="text-gray-500" data-id="17">Already have an accont? <a class="underline text-blue-500" <p class="text-gray-500" data-id="17">Already have an accont? <a class="underline text-blue-500"
data-id="18" href="{{ url_for('login') }}" rel="ugc">Login</a> data-id="18" href="{{ url_for('auth.login') }}" rel="ugc">Login</a>
</p> </p>
</div> </div>
</form> </form>