Integrate authentication into voting booth

Also fix a number (several too many) bugs that found their way into the system following the previous few commits
This commit is contained in:
RunasSudo 2017-11-25 23:16:29 +11:00
parent f92c7640d5
commit 20abbdae78
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
12 changed files with 69 additions and 40 deletions

View File

@ -149,7 +149,7 @@ class DateTimeField(Field):
if is_python:
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
else:
return Date.parse(value)
return __pragma__('js', '{}', 'new Date(value)')
@staticmethod
def now():

View File

@ -96,6 +96,7 @@ def setup_test_election():
election.voters.append(Voter(name='Alice'))
election.voters.append(Voter(name='Bob'))
election.voters.append(Voter(name='Charlie'))
election.voters.append(Voter(name='RunasSudo'))
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
@ -172,8 +173,9 @@ def election_view(election):
@using_election
def election_booth(election):
selection_model_view_map = EosObject.to_json({key._name: val for key, val in model_view_map.items()}) # ewww
auth_methods = EosObject.to_json(app.config['AUTH_METHODS'])
return flask.render_template('election/booth.html', election=election, selection_model_view_map=selection_model_view_map)
return flask.render_template('election/booth.html', election=election, selection_model_view_map=selection_model_view_map, auth_methods=auth_methods)
@app.route('/election/<election_id>/view/questions')
@using_election
@ -193,20 +195,24 @@ def election_view_trustees(election):
@app.route('/election/<election_id>/cast_ballot', methods=['POST'])
@using_election
def election_api_cast_vote(election):
if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status >= WorkflowTask.Status.EXITED or election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status <= WorkflowTask.Status.READY:
if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status < WorkflowTask.Status.EXITED or election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status > WorkflowTask.Status.READY:
# Voting is not yet open or has closed
return flask.Response('Voting is not yet open or has closed', 405)
return flask.Response('Voting is not yet open or has closed', 409)
data = json.loads(flask.request.data)
if 'user' not in flask.session:
# User is not authenticated
return flask.Response('Not authenticated', 403)
voter = None
for election_voter in election.voters:
if election_voter.name == data['auth']['username']:
if election_voter.name == flask.session['user'].username:
voter = election_voter
break
if voter is None:
# User is not authenticated
# Invalid user
return flask.Response('Invalid credentials', 403)
# Cast the vote

View File

@ -21,17 +21,24 @@
{% block content %}
<div id="cast_prompt">
<p>Your vote has <b>not yet been cast</b>. If you have not already done so, please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
<p>This election requires you to log in to vote. Please enter your name below, then click ‘Cast ballot’ to cast your ballot.</p>
<div class="ui form">
<div class="inline field">
<label for="booth_login_name">Name</label>
<input type="text" id="booth_login_name">
</div>
<div class="ui error message" id="error_invalid_id">
<i class="close icon"></i>
<div class="header">Error</div>
<p>The log in details you entered are not valid for this election. Please check your username and password and try again. If the issue persists, contact your election administrator.</p>
</div>
<p>This election requires you to log in to vote. If you disconnected your internet connection earlier, you must now reconnect it before proceeding.</p>
{% if username %}
<p>You are currently logged in as {{ username }}. Please select an option from the list below if you would like to switch accounts. Otherwise, click ‘Cast ballot’ to continue.</p>
{% else %}
<p>You are not currently logged in. Please select an option from the list below to log in. Your ballot will be automatically cast once you have logged in.</p>
{% endif %}
<ul class="ui list">
{% for auth_method in auth_methods %}
<li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li>
{% endfor %}
</ul>
<div class="ui hidden error message" id="error_invalid_id">
<i class="close icon"></i>
<div class="header">Error</div>
<p>The log in details you entered are not valid for this election. Please check your username and password and try again. If the issue persists, contact your election administrator.</p>
</div>
<div class="ui hidden error message" id="error_unknown">
@ -48,7 +55,7 @@
{% block buttons %}
<button class="ui left floated button" onclick="prevTemplate(2);">Back</a>
<button class="ui right floated primary button" onclick="castBallot();">Cast ballot</button>
<button class="ui right floated primary{% if not username %} hidden{% endif %} button" id="cast_button" onclick="castBallot();">Cast ballot</button>
{% endblock %}
{% block after %}
@ -57,6 +64,14 @@
$(this).closest(".message").addClass("hidden");
});
function login(el) {
window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600");
}
function callback_complete() {
$("#cast_button").removeClass("hidden");
castBallot();
}
function castBallot() {
$("#cast_prompt").hide();
$("#casting").show();
@ -65,8 +80,7 @@
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),
"auth": { "username": $("#booth_login_name").val() }
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(booth.ballot, null)
}),
contentType: "application/json",
dataType: "text"
@ -83,14 +97,14 @@
})
.fail(function(xhr, status, err) {
if (xhr.status === 403) { // Forbidden
$("#error_invalid_id").addClass("visible");
$("#error_invalid_id").removeClass("hidden");
$("#error_unknown").addClass("hidden");
} else {
$("#error_unknown_tech").text("Technical details: " + err);
$("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText);
$("#error_unknown").removeClass("hidden");
$("#error_invalid_id").removeClass("visible");
$("#error_invalid_id").addClass("hidden");
}
$("#casting").hide();

View File

@ -29,9 +29,9 @@
<p>Please check that the ballot fingerprint above matches the ballot fingerprint you recorded earlier.</p>
<p>To confirm that your ballot was cast correctly, please go to the <a href="{{ election_base_url }}ballots">‘Voters and ballots’ page</a> for the election or click ‘Finish’, and confirm that the above ballot fingerprint appears next to your name.</p>
<p>To confirm that your ballot was cast correctly, please go to the <a href="{{ election_base_url }}view/ballots">‘Voters and ballots’ page</a> for the election or click ‘Finish’, and confirm that the above ballot fingerprint appears next to your name.</p>
{% endblock %}
{% block buttons %}
<a href="{{ election_base_url }}ballots" class="ui right floated primary button">Finish</a>
<a href="{{ election_base_url }}view/ballots" class="ui right floated primary button">Finish</a>
{% endblock %}

View File

@ -27,6 +27,8 @@
}
booth.ballot = eosjs.eos.base.election.__all__.Ballot();
booth.ballot.encrypted_answers = encryptedAnswers;
booth.ballot.election_id = election._id;
booth.ballot.election_hash = eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64();
nextTemplate();
} catch (err) {

View File

@ -21,9 +21,9 @@
{% block content %}
<p>Your vote has <b>not yet been cast</b>. Your selections are shown below. Please review your selections and ensure you are happy with them before continuing.</p>
{% for question in election.questions %}
{% for question in election.questions.impl %}
<h2>{{ loop.index }}. {{ question.prompt }}</h2>
{% include templates[selection_model_view_map[election.questions[loop.index0]._name]["selections_review"]] %}
{% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %}
{% endfor %}
<p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>

View File

@ -33,7 +33,7 @@
boothError("Question template unable to save selections");
}
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
function previousQuestion() {
saveSelections();
@ -41,17 +41,17 @@
prevTemplate();
} else {
booth.questionNum--;
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
}
}
function nextQuestion() {
saveSelections();
if (booth.questionNum == election.questions.length - 1) {
if (booth.questionNum == election.questions.__len__() - 1) {
nextTemplate();
} else {
booth.questionNum++;
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
}
}
</script>

View File

@ -16,13 +16,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
<h2>{{ questionNum + 1 }}. {{ election.questions[questionNum].prompt }}</h2>
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
<p><small>Vote for between {{ election.questions[questionNum].min_choices }} and {{ election.questions[questionNum].max_choices }} candidates. Click the check-boxes to the left of the candidates' names to make your selection, then click the ‘Continue’ button. If you make a mistake, click the check-boxes again to clear your selection.</small></p>
<p><small>Vote for between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }} candidates. Click the check-boxes to the left of the candidates' names to make your selection, then click the ‘Continue’ button. If you make a mistake, click the check-boxes again to clear your selection.</small></p>
<div id="question-choices" class="ui form" style="margin-bottom: 1em;">
<div class="grouped fields">
{% for choice in election.questions[questionNum].choices %}
{% for choice in election.questions.__getitem__(questionNum).choices.impl %}
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="question-choice-{{ loop.index0 }}" onchange="choicesChanged();">
@ -40,7 +40,7 @@
<script>
function choicesChanged() {
var numChoices = $("#question-choices input:checked").length;
if (numChoices >= election.questions[booth.questionNum].max_choices) {
if (numChoices >= election.questions.__getitem__(booth.questionNum).max_choices) {
// Prevent making any more selections
$("#question-choices input:not(:checked)").prop("disabled", true);
$("#message-max-choices").removeClass("hidden");
@ -52,7 +52,7 @@
// Fill in ballot with previous selections
if (booth.answers[booth.questionNum]) {
for (var selection of booth.answers[booth.questionNum].value.choices) { // Answer already serialised
for (var selection of booth.answers.__getitem__(booth.questionNum).value.choices) { // Answer already serialised
$("#question-choice-" + selection).prop("checked", true);
}
choicesChanged();

View File

@ -20,7 +20,7 @@
{% for choice in booth.answers[loop.index0].value.choices %}
<div class="item">
<i class="checkmark icon"></i>
<div class="content">{{ question.choices[choice] }}</div>
<div class="content">{{ question.choices.__getitem__(choice) }}</div>
</div>
{% else %}
<div class="item">

View File

@ -26,7 +26,7 @@
{% else %}
<p>You are not currently logged in. Please select an option from the list below to log in.</p>
{% endif %}
<ul>
<ul class="ui list">
{% for auth_method in eosweb.app.config['AUTH_METHODS'] %}
<li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li>
{% endfor %}

View File

@ -39,6 +39,9 @@
var currentBoothTask = 0;
var selection_model_view_map = {{ selection_model_view_map|safe }}; {# :rooWut: #}
var username = {% if session.user %}"{{ session.user.username }}"{% else %}null{% endif %};
var auth_methods = {{ auth_methods|safe }};
function resetBooth() {
booth = {
"questionNum": 0,
@ -143,7 +146,9 @@
"election": election,
"booth": booth,
"eosjs": eosjs,
"selection_model_view_map": selection_model_view_map
"selection_model_view_map": selection_model_view_map,
"username": username,
"auth_methods": auth_methods
}, opts);
$(destination).html(templates[template].render(opts));
} catch (err) {

View File

@ -62,7 +62,9 @@ def main(app):
user.oauth_token = resp['access_token']
flask.session['user'] = user
me = reddit.get('https://oauth.reddit.com/api/v1/me')
me = reddit.get('https://oauth.reddit.com/api/v1/me', headers={
'User-Agent': app.config['REDDIT_USER_AGENT']
})
user.username = me.data['name']
return flask.redirect(flask.url_for('login_complete'))