Browse Source

move qr helpers to utils.py, implement user hash login

testing
Burathar 4 years ago
parent
commit
2e5f1a2c2f
  1. 44
      app/auth/routes.py
  2. 49
      app/main/routes.py
  3. 14
      app/models/user.py
  4. 7
      app/templates/auth/user.html
  5. 14
      app/templates/auth/user_hash_login.html
  6. 4
      app/templates/game_player.html
  7. 21
      app/utils.py

44
app/auth/routes.py

@ -1,9 +1,10 @@
from datetime import datetime 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 flask_login import login_user, logout_user, current_user, login_required
from app import db from app import db
from app.auth import bp from app.auth import bp
from app.utils import generate_qr_code, serve_pil_image
from app.models import User from app.models import User
from app.auth.forms import LoginForm, RegistrationForm 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): if user is None or not user.check_password(form.password.data):
flash('Invalid username or password') flash('Invalid username or password')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data) return user_login(user, form.remember_me.data)
user.last_login = datetime.utcnow()
return redirect(url_for('main.index'))
return render_template('auth/login.html', title='Sign In', form=form) 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') @bp.route('/logout')
@login_required @login_required
def logout(): def logout():
@ -58,3 +62,35 @@ def generate_auth_hash(username):
user.set_auth_hash() user.set_auth_hash()
db.session.commit() db.session.commit()
return 'nothing' return 'nothing'
@bp.route('/user/<auth_hash>')
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 <a href='{request.url}'>login link</a> 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/<auth_hash>/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)

49
app/main/routes.py

@ -3,8 +3,7 @@ import json
from os import listdir from os import listdir
from pathlib import Path from pathlib import Path
from datetime import datetime 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 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 flask_login import current_user, login_required
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@ -13,12 +12,10 @@ from sqlalchemy import and_
from app import db from app import db
from app.main import bp 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.models import User, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder, PlayerCaughtPlayer
from app.main.forms import CreateGameForm, ObjectiveForm, PlayerAddForm, UserCreateForm, PlayerUpdateForm, CatchBunnyForm from app.main.forms import CreateGameForm, ObjectiveForm, PlayerAddForm, UserCreateForm, PlayerUpdateForm, CatchBunnyForm
import os
@bp.before_app_request @bp.before_app_request
def before_request(): def before_request():
if current_user.is_authenticated: if current_user.is_authenticated:
@ -36,7 +33,6 @@ def index():
@bp.route('/create_game', methods=['GET', 'POST']) @bp.route('/create_game', methods=['GET', 'POST'])
@login_required @login_required
def create_game(): def create_game():
print(current_user.is_authenticated)
form = CreateGameForm() form = CreateGameForm()
if form.validate_on_submit(): if form.validate_on_submit():
game = Game(name=form.game_name.data, start_time=form.start_time.data, end_time=form.end_time.data) 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.choices = [(role.value, role.name) for role in Role]
form.role.default = player.role.value form.role.default = player.role.value
form.process() form.process()
if form.validate_on_submit(): if form.validate_on_submit():
player.role = Role[form.role.data] player.role = Role[form.role.data]
db.session.commit() db.session.commit()
return redirect(url_for('main.game_dashboard', game_name=game.name)) 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) 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/<auth_hash>/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/<auth_hash>')
@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/<game_name>/add_objective', methods=['GET', 'POST']) @bp.route('/game/<game_name>/add_objective', methods=['GET', 'POST'])
@login_required @login_required
def add_objective(game_name): def add_objective(game_name):
@ -267,24 +242,6 @@ def delete_objective(objective_hash):
db.session.commit() db.session.commit()
return redirect(url_for('main.game_dashboard', game_name=objective.game.name)) 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/<objective_hash>/qrcode.png') @bp.route('/objective/<objective_hash>/qrcode.png')
@login_required @login_required
def objective_qrcode(objective_hash): def objective_qrcode(objective_hash):

14
app/models/user.py

@ -67,12 +67,20 @@ class User(UserMixin, db.Model):
return None return None
return gameplayer.role return gameplayer.role
def owns_game_played_by(self, player): def owns_game_played_by(self, user):
'''Self is an owner of a game the player participates in''' '''Self is an owner of a game the user participates in'''
return self in [gameplayer.user for gameplayers 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] 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): def player_in(self, game):
# pylint: disable=not-an-iterable # pylint: disable=not-an-iterable
'''Returns GamePlayer object for given game, or None if user does not participate in given game''' '''Returns GamePlayer object for given game, or None if user does not participate in given game'''

7
app/templates/auth/user.html

@ -1,7 +0,0 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>User</h1>
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 %}

14
app/templates/auth/user_hash_login.html

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Welcome, {{ user.name }}!</h1>
<p>
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.
</p>
<a href="{{ url_for('main.index') }}"><button class="btn btn-primary">Set Password</button></a>
<a href="{{ url_for('auth.user_hash_login', auth_hash=user.auth_hash, login='true') }}"><button class="btn btn-primary">Start Playing!</button></a>
{% endblock %}

4
app/templates/game_player.html

@ -18,7 +18,9 @@
{% if player.user.auth_hash and not player.user.last_login %} {% if player.user.auth_hash and not player.user.last_login %}
<div class="row"> <div class="row">
<img src="{{ url_for('main.user_qrcode', auth_hash=player.user.auth_hash) }}" alt="qr_code_failed" , width="80%"> <a href="{{ url_for('auth.user_hash_login', auth_hash=player.user.auth_hash) }}">
<img src="{{ url_for('auth.user_qrcode', auth_hash=player.user.auth_hash) }}" alt="qr_code_failed" width="80%" title="login code for {{ player.user.name }}">
</a>
</div> </div>
{% elif not player.user.last_login %} {% elif not player.user.last_login %}
<br> <br>

21
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')
Loading…
Cancel
Save