From 2e5f1a2c2fe541252151af39f61511d2ccc4e3a8 Mon Sep 17 00:00:00 2001 From: Burathar Date: Tue, 21 Jul 2020 13:21:39 +0200 Subject: [PATCH] move qr helpers to utils.py, implement user hash login --- app/auth/routes.py | 44 ++++++++++++++++++++-- app/main/routes.py | 49 ++----------------------- app/models/user.py | 14 +++++-- app/templates/auth/user.html | 7 ---- app/templates/auth/user_hash_login.html | 14 +++++++ app/templates/game_player.html | 4 +- app/utils.py | 21 +++++++++++ 7 files changed, 92 insertions(+), 61 deletions(-) delete mode 100644 app/templates/auth/user.html create mode 100644 app/templates/auth/user_hash_login.html create mode 100644 app/utils.py diff --git a/app/auth/routes.py b/app/auth/routes.py index d533746..6b86e1b 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -1,9 +1,10 @@ from datetime import datetime -from flask import render_template, flash, redirect, url_for, abort +from flask import render_template, flash, redirect, request, url_for, abort, Markup, escape from flask_login import login_user, logout_user, current_user, login_required from app import db from app.auth import bp +from app.utils import generate_qr_code, serve_pil_image from app.models import User from app.auth.forms import LoginForm, RegistrationForm @@ -17,11 +18,14 @@ def login(): if user is None or not user.check_password(form.password.data): flash('Invalid username or password') return redirect(url_for('auth.login')) - login_user(user, remember=form.remember_me.data) - user.last_login = datetime.utcnow() - return redirect(url_for('main.index')) + return user_login(user, form.remember_me.data) return render_template('auth/login.html', title='Sign In', form=form) +def user_login(user, remember): + login_user(user, remember) + user.last_login = datetime.utcnow() + return redirect(url_for('main.index')) + @bp.route('/logout') @login_required def logout(): @@ -58,3 +62,35 @@ def generate_auth_hash(username): user.set_auth_hash() db.session.commit() return 'nothing' + +@bp.route('/user/') +def user_hash_login(auth_hash): + user = User.query.filter_by(auth_hash=auth_hash).first_or_404() + if current_user.is_authenticated: + if current_user.owns_game_played_by(user): + safe_username = escape(user.name) + flash(Markup(f"The login link for {safe_username} works! " + "However, logged in users like you can't access it")) + return redirect(url_for('main.game_player', game_name=current_user.owned_game_played_by(user).name, username=user.name)) + flash(f'You are aleady logged in as {current_user.name}!') + return redirect(url_for('main.index')) + if user.password_hash: + flash('Please login with your username and password!') + abort(404) + if user.last_login: + return user_login(user, True) + if request.args.get('login', default='false', type=str).lower() == 'true': + return user_login(user, True) + return render_template('auth/user_hash_login.html', title=f'User: {user.name}', user=user) + +@bp.route('/user//qrcode.png') +@login_required +def user_qrcode(auth_hash): + user = User.query.filter_by(auth_hash=auth_hash).first_or_404() + if not current_user.owns_game_played_by(user): + abort(403) + if user.last_login: + flash('After a player has logged in, it is no longer possible to request their QR code.') + abort(403) + img = generate_qr_code(url_for('auth.user_hash_login', auth_hash=auth_hash, _external=True)) + return serve_pil_image(img) diff --git a/app/main/routes.py b/app/main/routes.py index 1f3b3ad..59970c1 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -3,8 +3,7 @@ import json from os import listdir from pathlib import Path from datetime import datetime -from io import BytesIO -import qrcode + from flask import render_template, flash, redirect, url_for, request, abort, send_file, current_app, send_from_directory from flask_login import current_user, login_required from werkzeug.utils import secure_filename @@ -13,12 +12,10 @@ from sqlalchemy import and_ from app import db from app.main import bp +from app.utils import generate_qr_code, serve_pil_image from app.models import User, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder, PlayerCaughtPlayer from app.main.forms import CreateGameForm, ObjectiveForm, PlayerAddForm, UserCreateForm, PlayerUpdateForm, CatchBunnyForm - -import os - @bp.before_app_request def before_request(): if current_user.is_authenticated: @@ -36,7 +33,6 @@ def index(): @bp.route('/create_game', methods=['GET', 'POST']) @login_required def create_game(): - print(current_user.is_authenticated) form = CreateGameForm() if form.validate_on_submit(): game = Game(name=form.game_name.data, start_time=form.start_time.data, end_time=form.end_time.data) @@ -211,34 +207,13 @@ def game_player(game_name, username): form.role.choices = [(role.value, role.name) for role in Role] form.role.default = player.role.value form.process() - + if form.validate_on_submit(): player.role = Role[form.role.data] db.session.commit() return redirect(url_for('main.game_dashboard', game_name=game.name)) return render_template('game_player.html', title=f'{user.name} in {game_name}', player=player, form=form, json=json, location_encoder=LocationEncoder) -@bp.route('/user//qrcode.png') -@login_required -def user_qrcode(auth_hash): - user = User.query.filter_by(auth_hash=auth_hash).first_or_404() - if not current_user.owns_game_played_by(user): - abort(403) - if user.last_login: - flash('After a player has logged in, it is no longer possible to request their QR code.') - abort(403) - img = generate_qr_code(url_for('main.user', auth_hash=auth_hash, _external=True)) - return serve_pil_image(img) - -@bp.route('/user/') -@login_required -def user(auth_hash): - user = User.query.filter_by(auth_hash=auth_hash).first_or_404() - if user.password_hash: - flash('Please login with your username and password!') - abort(404) - return render_template('auth/user.html', title=f'User: {user.name}', user=user) - @bp.route('/game//add_objective', methods=['GET', 'POST']) @login_required def add_objective(game_name): @@ -267,24 +242,6 @@ def delete_objective(objective_hash): db.session.commit() return redirect(url_for('main.game_dashboard', game_name=objective.game.name)) -def generate_qr_code(url): - qr = qrcode.QRCode( - version=None, - error_correction=qrcode.constants.ERROR_CORRECT_M, - box_size=30, - border=4, - ) - qr.add_data(url) - qr.make(fit=True) - return qr.make_image(fill_color='black', back_color='white') - -# Source: https://stackoverflow.com/questions/7877282/how-to-send-image-generated-by-pil-to-browser -def serve_pil_image(pil_img): - img_io = BytesIO() - pil_img.save(img_io, 'PNG', quality=70) - img_io.seek(0) - return send_file(img_io, mimetype='image/png') - @bp.route('/objective//qrcode.png') @login_required def objective_qrcode(objective_hash): diff --git a/app/models/user.py b/app/models/user.py index 0032b41..3df2ad8 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -67,12 +67,20 @@ class User(UserMixin, db.Model): return None return gameplayer.role - def owns_game_played_by(self, player): - '''Self is an owner of a game the player participates in''' + def owns_game_played_by(self, user): + '''Self is an owner of a game the user participates in''' return self in [gameplayer.user for gameplayers in - [game.players for game in player.games] + [game.players for game in user.games] for gameplayer in gameplayers if gameplayer.role == Role.owner] + def owned_game_played_by(self, user): + '''Return first game owned by self that the user participates in''' + return next(iter([gameplayer.game for gameplayers in + [game.players for game in user.games] + for gameplayer in gameplayers + if gameplayer.role == Role.owner and gameplayer.user == self]), + None) + def player_in(self, game): # pylint: disable=not-an-iterable '''Returns GamePlayer object for given game, or None if user does not participate in given game''' diff --git a/app/templates/auth/user.html b/app/templates/auth/user.html deleted file mode 100644 index 5b936dd..0000000 --- a/app/templates/auth/user.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} -{% import 'bootstrap/wtf.html' as wtf %} - -{% block app_content %} -

User

- This page is is progress, it should enable you to claim a player account using the authhash, as long as the player hasnt logged in yet. -{% endblock %} \ No newline at end of file diff --git a/app/templates/auth/user_hash_login.html b/app/templates/auth/user_hash_login.html new file mode 100644 index 0000000..cf1785f --- /dev/null +++ b/app/templates/auth/user_hash_login.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

Welcome, {{ user.name }}!

+

+ If you found this page, it probably means someone who is organising a hunt invited you by + sending a link or QR-code to this page. You can start playing right away, if and if you get + logged out just visit this page again. However, if you want to be sure other people can't + steal this account, please set a password. +

+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/game_player.html b/app/templates/game_player.html index 924dfc5..ae12f28 100644 --- a/app/templates/game_player.html +++ b/app/templates/game_player.html @@ -18,7 +18,9 @@ {% if player.user.auth_hash and not player.user.last_login %}
- qr_code_failed + + qr_code_failed +
{% elif not player.user.last_login %}
diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..3be0b79 --- /dev/null +++ b/app/utils.py @@ -0,0 +1,21 @@ +from io import BytesIO +import qrcode +from flask import send_file + +def generate_qr_code(url): + qr = qrcode.QRCode( + version=None, + error_correction=qrcode.constants.ERROR_CORRECT_M, + box_size=30, + border=4, + ) + qr.add_data(url) + qr.make(fit=True) + return qr.make_image(fill_color='black', back_color='white') + +def serve_pil_image(pil_img): + # Source: https://stackoverflow.com/questions/7877282/how-to-send-image-generated-by-pil-to-browser + img_io = BytesIO() + pil_img.save(img_io, 'PNG', quality=70) + img_io.seek(0) + return send_file(img_io, mimetype='image/png')