diff --git a/pyproject.toml b/pyproject.toml index 55440e0..46f89b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,5 +27,9 @@ dynamic = ["version"] # setuptools to be a module without it. packages = ["roc_fnb"] +[project.scripts] +# Put scripts here +bootstrap-first-admin = "roc_fnb.scripts.bootstrap_first_admin:bootstrap_first_admin" + [tool.yapf] based_on_style = "facebook" diff --git a/roc_fnb/scripts/run-tests.sh b/roc_fnb/scripts/run-tests.sh deleted file mode 100755 index 0ae4cca..0000000 --- a/roc_fnb/scripts/run-tests.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -python -m pytest --capture no --ignore mounts \ No newline at end of file diff --git a/roc_fnb/website/database.py b/roc_fnb/website/database.py index f65a7c1..fca003e 100644 --- a/roc_fnb/website/database.py +++ b/roc_fnb/website/database.py @@ -58,3 +58,14 @@ 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 new file mode 100644 index 0000000..714f96d --- /dev/null +++ b/roc_fnb/website/server.py @@ -0,0 +1,81 @@ +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 deleted file mode 100644 index 862f9a2..0000000 --- a/roc_fnb/website/server/__init__.py +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index b099f14..0000000 --- a/roc_fnb/website/server/decorators.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -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 deleted file mode 100644 index a72c16e..0000000 --- a/roc_fnb/website/server/server.py +++ /dev/null @@ -1,46 +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 - -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 deleted file mode 100644 index 50ddc20..0000000 --- a/roc_fnb/website/server/user.py +++ /dev/null @@ -1,31 +0,0 @@ -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