From ebe65305571a33626a3b8beb9d51907a393186d2 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 28 Jan 2018 22:28:10 +1100 Subject: [PATCH] Implement login fix for browsers without window.opener support #11 --- eosweb/core/main.py | 42 ++++++- eosweb/core/static/nunjucks/booth/cast.html | 91 +++++++++++++-- eosweb/core/templates/auth/login.html | 10 +- .../core/templates/auth/login_complete.html | 39 ++++--- .../core/templates/election/view/booth.html | 109 +++++++++++------- requirements.txt | 1 + 6 files changed, 218 insertions(+), 74 deletions(-) diff --git a/eosweb/core/main.py b/eosweb/core/main.py index 2492c14..48f76fe 100644 --- a/eosweb/core/main.py +++ b/eosweb/core/main.py @@ -16,6 +16,7 @@ import click import flask +import flask_session import timeago from eos.core.objects import * @@ -62,6 +63,16 @@ if 'EOSWEB_SETTINGS' in os.environ: # Connect to database db_connect(app.config['DB_NAME'], app.config['DB_URI'], app.config['DB_TYPE']) +# Configure sessions +if app.config['DB_TYPE'] == 'mongodb': + app.config['SESSION_TYPE'] = 'mongodb' + app.config['SESSION_MONGODB'] = dbinfo.provider.client + app.config['SESSION_MONGODB_DB'] = dbinfo.provider.db_name +elif app.config['DB_TYPE'] == 'postgresql': + app.config['SESSION_TYPE'] = 'sqlalchemy' + app.config['SQLALCHEMY_DATABASE_URI'] = dbinfo.provider.conn.dsn +flask_session.Session(app) + # Set configs User.admins = app.config['ADMINS'] @@ -298,6 +309,12 @@ def election_admin_schedule_task(election): return flask.redirect(flask.url_for('election_admin_summary', election_id=election._id)) +@app.route('/election//stage_ballot', methods=['POST']) +@using_election +def election_api_stage_ballot(election): + flask.session['staged_ballot'] = json.loads(flask.request.data) + return 'OK' + @app.route('/election//cast_ballot', methods=['POST']) @using_election def election_api_cast_vote(election): @@ -305,7 +322,7 @@ def election_api_cast_vote(election): # Voting is not yet open or has closed return flask.Response('Voting is not yet open or has closed', 409) - data = json.loads(flask.request.data) + data = flask.session['staged_ballot'] if 'user' not in flask.session: # User is not authenticated @@ -336,6 +353,8 @@ def election_api_cast_vote(election): vote.save() + del flask.session['staged_ballot'] + return flask.Response(json.dumps({ 'voter': EosObject.serialise_and_wrap(voter, None, SerialiseOptions(should_protect=True)), 'vote': EosObject.serialise_and_wrap(vote, None, SerialiseOptions(should_protect=True)) @@ -365,14 +384,29 @@ def debug(): @app.route('/auth/login') def login(): + flask.session['login_next'] = flask.request.referrer return flask.render_template('auth/login.html') +@app.route('/auth/stage_next', methods=['POST']) +def auth_stage_next(): + flask.session['login_next'] = flask.request.data + return 'OK' + @app.route('/auth/logout') def logout(): flask.session['user'] = None - #return flask.redirect(flask.request.args['next'] if 'next' in flask.request.args else '/') - # I feel like there's some kind of exploit here, so we'll leave this for now - return flask.redirect('/') + if flask.request.referrer: + return flask.redirect(flask.request.referrer) + else: + return flask.redirect('/') + +@app.route('/auth/login_callback') +def login_callback(): + print(flask.session) + if 'login_next' in flask.session and flask.session['login_next']: + return flask.redirect(flask.session['login_next']) + else: + return flask.redirect('/') @app.route('/auth/login_complete') def login_complete(): diff --git a/eosweb/core/static/nunjucks/booth/cast.html b/eosweb/core/static/nunjucks/booth/cast.html index 9482f15..1a00120 100644 --- a/eosweb/core/static/nunjucks/booth/cast.html +++ b/eosweb/core/static/nunjucks/booth/cast.html @@ -59,8 +59,13 @@ {% endblock %} {% block buttons %} - - + {% if is_cast %} + Reset + + {% else %} + + + {% endif %} {% endblock %} {% block after %} @@ -75,30 +80,90 @@ }); function login(el) { - window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600"); + // Stage the vote for casting in case we change page + stageBallot(function(data) { + // Stage the next page in case we change page + stageNext("{{ election_base_url }}booth?cast", function(data) { + window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600"); + }); + }); } + function callback_complete(username) { $("#cast_button").show(); $("#booth_logged_in_as").text("You are currently logged in as " + username + "."); + // Ballot was staged when we clicked the login button castBallot(); return true; } + function stageBallot(callback) { + // Prepare ballot + var deauditedBallot = booth.ballot.deaudit(); + + $.ajax({ + url: "{{ election_base_url }}stage_ballot", + type: "POST", + data: eosjs.eos.core.objects.__all__.EosObject.to_json({ + "ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(deauditedBallot, null), + "fingerprint": booth.fingerprint || null + }), + contentType: "application/json", + dataType: "text", + async: false // so window.open happens in main thread + }) + .done(function(data) { + callback(data); + }) + .fail(function(xhr, status, err) { + if (xhr.responseText && xhr.responseText.length < 100) { + $("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText); + } else { + $("#error_unknown_tech").text("Technical details: " + err); + } + $("#error_unknown").removeClass("hidden"); + + $("#error_invalid_id").addClass("hidden"); + + console.error(xhr); + throw err; + }); + } + + function stageNext(url, callback) { + $.ajax({ + url: "/auth/stage_next", + type: "POST", + data: url, + contentType: "text/plain", + dataType: "text", + async: false // so window.open happens in main thread + }) + .done(function(data) { + callback(data); + }) + .fail(function(xhr, status, err) { + if (xhr.responseText && xhr.responseText.length < 100) { + $("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText); + } else { + $("#error_unknown_tech").text("Technical details: " + err); + } + $("#error_unknown").removeClass("hidden"); + + $("#error_invalid_id").addClass("hidden"); + + console.error(xhr); + throw err; + }); + } + function castBallot() { $("#cast_prompt").hide(); $("#casting").show(); - // Prepare ballot - booth.ballot = booth.ballot.deaudit(); - $.ajax({ url: "{{ election_base_url }}cast_ballot", type: "POST", - data: eosjs.eos.core.objects.__all__.EosObject.to_json({ - "ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(booth.ballot, null), - "fingerprint": booth.fingerprint || null - }), - contentType: "application/json", dataType: "text" }) .done(function(data) { @@ -134,6 +199,10 @@ throw err; }); } + + {% if is_cast %} + castBallot(); + {% endif %} {% endblock %} diff --git a/eosweb/core/templates/auth/login.html b/eosweb/core/templates/auth/login.html index a6979a8..b00c72f 100644 --- a/eosweb/core/templates/auth/login.html +++ b/eosweb/core/templates/auth/login.html @@ -31,6 +31,12 @@
  • {{ auth_method[1] }}
  • {% endfor %} + + {% endblock %} {% block basecontent %} @@ -40,8 +46,8 @@ window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600"); } - function callback_complete(name) { - window.location = "/"; + function callback_complete() { + window.location = "{{ url_for('login_callback') }}"; return true; } diff --git a/eosweb/core/templates/auth/login_complete.html b/eosweb/core/templates/auth/login_complete.html index 0733faa..9620873 100644 --- a/eosweb/core/templates/auth/login_complete.html +++ b/eosweb/core/templates/auth/login_complete.html @@ -23,33 +23,46 @@ {% block basecontent %}
    -
    {% endblock %} diff --git a/eosweb/core/templates/election/view/booth.html b/eosweb/core/templates/election/view/booth.html index 907d73e..0ac4c9f 100644 --- a/eosweb/core/templates/election/view/booth.html +++ b/eosweb/core/templates/election/view/booth.html @@ -192,67 +192,88 @@ // === BOOTH TASKS === // TODO: Make modular - boothTasks.append({ - activate: function(fromLeft) { - showTemplate('booth/welcome.html'); - } - }); templates['booth/base.html'] = null; - templates['booth/welcome.html'] = null; - boothTasks.append({ - activate: function(fromLeft) { - showTemplate('booth/selections.html'); - } - }); - templates['booth/selections.html'] = null; - boothTasks.append({ - activate: function(fromLeft) { - if (fromLeft) { - showTemplate('booth/encrypt.html'); - } else { - prevTemplate(); - } - } - }); - templates['booth/encrypt.html'] = null; - if (location.search.indexOf('?prepoll') >= 0) { - // Pre-poll + if (location.search.indexOf('?cast') < 0) { + // Normal booth boothTasks.append({ activate: function(fromLeft) { - showTemplate('booth/review_prepoll.html', {ballot: booth.ballot}); + showTemplate('booth/welcome.html'); } }); - templates['booth/review_prepoll.html'] = null; + templates['booth/welcome.html'] = null; boothTasks.append({ activate: function(fromLeft) { - showTemplate('booth/audit.html', {ballot: booth.ballot}); + showTemplate('booth/selections.html'); } }); - templates['booth/audit.html'] = null; + templates['booth/selections.html'] = null; boothTasks.append({ activate: function(fromLeft) { - showTemplate('booth/cast_prepoll.html', {ballot: booth.ballot}); + if (fromLeft) { + showTemplate('booth/encrypt.html'); + } else { + prevTemplate(); + } } }); - templates['booth/cast_prepoll.html'] = null; + templates['booth/encrypt.html'] = null; + + if (location.search.indexOf('?prepoll') >= 0) { + // Pre-poll + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/review_prepoll.html', {ballot: booth.ballot}); + } + }); + templates['booth/review_prepoll.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/audit.html', {ballot: booth.ballot}); + } + }); + templates['booth/audit.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/cast_prepoll.html', {ballot: booth.ballot}); + } + }); + templates['booth/cast_prepoll.html'] = null; + } else { + // Real voting booth + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/review.html', {ballot: booth.ballot}); + } + }); + templates['booth/review.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/audit.html', {ballot: booth.ballot}); + } + }); + templates['booth/audit.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: false}); + } + }); + templates['booth/cast.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote}); + } + }); + templates['booth/complete.html'] = null; + } } else { - // Real voting booth + // Cast immediately + {% if session.staged_ballot %} + booth.ballot = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json('{{ eos.core.objects.EosObject.to_json(session.staged_ballot.ballot)|safe }}'), null); + {% endif %} boothTasks.append({ activate: function(fromLeft) { - showTemplate('booth/review.html', {ballot: booth.ballot}); - } - }); - templates['booth/review.html'] = null; - boothTasks.append({ - activate: function(fromLeft) { - showTemplate('booth/audit.html', {ballot: booth.ballot}); - } - }); - templates['booth/audit.html'] = null; - boothTasks.append({ - activate: function(fromLeft) { - showTemplate('booth/cast.html', {ballot: booth.ballot}); + showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: true}); } }); templates['booth/cast.html'] = null; diff --git a/requirements.txt b/requirements.txt index 4c536b1..bdb7948 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ coverage==4.4.1 Flask==0.12.2 Flask-Mail==0.9.1 Flask-OAuthlib==0.9.4 +Flask-Session==0.3.1 gunicorn==19.7.1 libsass==0.13.4 premailer==3.1.1