From 29543026ed9f3392cff84f77c768ad3fb14d8de7 Mon Sep 17 00:00:00 2001 From: "D. Scott Boggs" Date: Sat, 31 May 2025 10:07:44 -0400 Subject: [PATCH] Reduce controller code file size; add logging --- roc_fnb/website/database.py | 11 ---- roc_fnb/website/server.py | 81 ---------------------------- roc_fnb/website/server/__init__.py | 1 + roc_fnb/website/server/decorators.py | 38 +++++++++++++ roc_fnb/website/server/server.py | 46 ++++++++++++++++ roc_fnb/website/server/user.py | 31 +++++++++++ 6 files changed, 116 insertions(+), 92 deletions(-) delete mode 100644 roc_fnb/website/server.py create mode 100644 roc_fnb/website/server/__init__.py create mode 100644 roc_fnb/website/server/decorators.py create mode 100644 roc_fnb/website/server/server.py create mode 100644 roc_fnb/website/server/user.py diff --git a/roc_fnb/website/database.py b/roc_fnb/website/database.py index fca003e..f65a7c1 100644 --- a/roc_fnb/website/database.py +++ b/roc_fnb/website/database.py @@ -58,14 +58,3 @@ class Database(MongoClient): if user := self.db.users.find_one({'_id': id}): return User(**user) return None - - def get_user_from_token(self, token: str) -> Optional[User]: - """ - Verify a user and retreive a their full profile from the database. - - This is like User.verify_jwt except it also fetches fields from the - database which are not present in the client-visible token. - """ - if jwt_user := User.verify_jwt(token): - return self.get_user_by_id(jwt_user._id) - return None \ No newline at end of file diff --git a/roc_fnb/website/server.py b/roc_fnb/website/server.py deleted file mode 100644 index 714f96d..0000000 --- a/roc_fnb/website/server.py +++ /dev/null @@ -1,81 +0,0 @@ -from functools import wraps -import json -from pathlib import Path -from random import randbytes -from sys import stderr - -from flask import (Flask, redirect, url_for, request, send_file, make_response, - abort, render_template, session, g) - -from roc_fnb.util.env_file import env_file -from roc_fnb.website.database import Database -from roc_fnb.website.models.user import JwtUser - -db = Database.from_env() - -app = Flask( - import_name=__name__.split('.')[0], - static_url_path='/', - template_folder=Path(__file__).absolute().parent / 'templates', - static_folder=Path(__file__).absolute().parent / 'static', -) - -app.secret_key = env_file('FLASK_SECRET', default_file='./flask.secret', default_fn=lambda: randbytes(12)) - -@app.before_request -def decode_user(): - if user := session.get('user'): - g.user = JwtUser.from_json(data=json.loads(user)) - - -def require_user(admin = False, moderator = False): - """ - A decorator for any routes which require authentication. - - https://stackoverflow.com/a/51820573 - """ - def _require_user(handler): - @wraps(handler) - def __require_user(): - if getattr(g, 'user', None) is None \ - or (admin and not user.admin) \ - or (moderator and not user.moderator): - abort(401) - return handler() - return __require_user - return _require_user - -@app.route('/ig') -def ig_redir(): - return redirect('https://instagram.com/RocFNB') - - -@app.route('/donate') -def donate_redir(): - return redirect('https://venmo.com/RocFoodNotBombs') - - -@app.route('/') -def index(): - return redirect('/index.html') - - -@app.post('/login') -def submit_login(): - form = request.json - user = db.get_user_by_name(form['name']) - if not user.check_password(form['password']): - abort(401) # unauthorized - session['user'] = json.dumps(user.public_fields) - return redirect('/me') - -@app.get('/login') -def render_login_page(): - if getattr(g, 'user', None): - return redirect('/me') - return render_template('login.html') - -@app.get('/me') -@require_user() -def get_profile(): - return render_template('profile.html', user=g.user) diff --git a/roc_fnb/website/server/__init__.py b/roc_fnb/website/server/__init__.py new file mode 100644 index 0000000..862f9a2 --- /dev/null +++ b/roc_fnb/website/server/__init__.py @@ -0,0 +1 @@ +from roc_fnb.website.server.server import app \ No newline at end of file diff --git a/roc_fnb/website/server/decorators.py b/roc_fnb/website/server/decorators.py new file mode 100644 index 0000000..b099f14 --- /dev/null +++ b/roc_fnb/website/server/decorators.py @@ -0,0 +1,38 @@ +""" +Various decorators which may be applied to specific routes + +See https://stackoverflow.com/a/51820573 for reference. +""" +from flask import g, abort, request +from functools import wraps + + +def require_user(admin = False, moderator = False): + """A decorator for any routes which require authentication.""" + def _require_user(handler): + @wraps(handler) + def __require_user(): + if getattr(g, 'user', None) is None \ + or (admin and not g.user.admin) \ + or (moderator and not g.user.moderator): + abort(401) + return handler() + return __require_user + return _require_user + + +def logger_request_bindings(log): + """Applied to a route which logs something to have request data bound to the logger.""" + def _lrb(handler): + @wraps(handler) + def __lrb(): + log.bind( + path=request.path, + method=request.method, + user_agent=request.user_agent, + remote_ip=request.remote_addr, + user=getattr(g, 'user', '(anonymous)'), + ) + return handler(log) + return __lrb + return _lrb \ No newline at end of file diff --git a/roc_fnb/website/server/server.py b/roc_fnb/website/server/server.py new file mode 100644 index 0000000..a72c16e --- /dev/null +++ b/roc_fnb/website/server/server.py @@ -0,0 +1,46 @@ +from functools import wraps +import json +from pathlib import Path +from random import randbytes +from sys import stderr + +from flask import Flask, redirect + +from roc_fnb.util.env_file import env_file +from roc_fnb.util import log +from roc_fnb.website.database import Database +from roc_fnb.website.server.user import setup_user_routes + + +db = Database.from_env() + +app = Flask( + import_name=__name__.split('.')[0], + static_url_path='/', + template_folder=Path(__file__).absolute().parent / 'templates', + static_folder=Path(__file__).absolute().parent / 'static', +) + +app.secret_key = env_file('FLASK_SECRET', default_file='./flask.secret', default_fn=lambda: randbytes(12)) + +@app.before_request +def decode_user(): + if user := session.get('user'): + g.user = JwtUser.from_json(data=json.loads(user)) + + +@app.route('/ig') +def ig_redir(): + return redirect('https://instagram.com/RocFNB') + + +@app.route('/donate') +def donate_redir(): + return redirect('https://venmo.com/RocFoodNotBombs') + + +@app.route('/') +def index(): + return redirect('/index.html') + +setup_user_routes(app, db) \ No newline at end of file diff --git a/roc_fnb/website/server/user.py b/roc_fnb/website/server/user.py new file mode 100644 index 0000000..50ddc20 --- /dev/null +++ b/roc_fnb/website/server/user.py @@ -0,0 +1,31 @@ +import json + +from flask import request, redirect, render_template, g, abort + +from roc_fnb.util import log +from roc_fnb.website.server.decorators import require_user, logger_request_bindings + +def setup_user_routes(app, db): + @app.post('/login') + @logger_request_bindings(log) + def submit_login(log): + form = request.json + log.info('user attempting login', name=form.get('name')) + user = db.get_user_by_name(form['name']) + if not user.check_password(form['password']): + log.warn('incorrect password submitted', name=form['name']) + abort(401) # unauthorized + session['user'] = json.dumps(user.public_fields) + return redirect('/me') + + @app.get('/login') + def render_login_page(): + if getattr(g, 'user', None): + log.debug('user is already logged in', user=g.user) + return redirect('/me') + return render_template('login.html') + + @app.get('/me') + @require_user() + def get_profile(): + return render_template('profile.html', user=g.user) \ No newline at end of file