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:
parent
f92c7640d5
commit
20abbdae78
@ -149,7 +149,7 @@ class DateTimeField(Field):
|
|||||||
if is_python:
|
if is_python:
|
||||||
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
|
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
|
||||||
else:
|
else:
|
||||||
return Date.parse(value)
|
return __pragma__('js', '{}', 'new Date(value)')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def now():
|
def now():
|
||||||
|
@ -96,6 +96,7 @@ def setup_test_election():
|
|||||||
election.voters.append(Voter(name='Alice'))
|
election.voters.append(Voter(name='Alice'))
|
||||||
election.voters.append(Voter(name='Bob'))
|
election.voters.append(Voter(name='Bob'))
|
||||||
election.voters.append(Voter(name='Charlie'))
|
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'))
|
||||||
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
|
@using_election
|
||||||
def election_booth(election):
|
def election_booth(election):
|
||||||
selection_model_view_map = EosObject.to_json({key._name: val for key, val in model_view_map.items()}) # ewww
|
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')
|
@app.route('/election/<election_id>/view/questions')
|
||||||
@using_election
|
@using_election
|
||||||
@ -193,20 +195,24 @@ def election_view_trustees(election):
|
|||||||
@app.route('/election/<election_id>/cast_ballot', methods=['POST'])
|
@app.route('/election/<election_id>/cast_ballot', methods=['POST'])
|
||||||
@using_election
|
@using_election
|
||||||
def election_api_cast_vote(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
|
# 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)
|
data = json.loads(flask.request.data)
|
||||||
|
|
||||||
|
if 'user' not in flask.session:
|
||||||
|
# User is not authenticated
|
||||||
|
return flask.Response('Not authenticated', 403)
|
||||||
|
|
||||||
voter = None
|
voter = None
|
||||||
for election_voter in election.voters:
|
for election_voter in election.voters:
|
||||||
if election_voter.name == data['auth']['username']:
|
if election_voter.name == flask.session['user'].username:
|
||||||
voter = election_voter
|
voter = election_voter
|
||||||
break
|
break
|
||||||
|
|
||||||
if voter is None:
|
if voter is None:
|
||||||
# User is not authenticated
|
# Invalid user
|
||||||
return flask.Response('Invalid credentials', 403)
|
return flask.Response('Invalid credentials', 403)
|
||||||
|
|
||||||
# Cast the vote
|
# Cast the vote
|
||||||
|
@ -21,18 +21,25 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="cast_prompt">
|
<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>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>
|
<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>
|
||||||
<div class="ui form">
|
|
||||||
<div class="inline field">
|
{% if username %}
|
||||||
<label for="booth_login_name">Name</label>
|
<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>
|
||||||
<input type="text" id="booth_login_name">
|
{% else %}
|
||||||
</div>
|
<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>
|
||||||
<div class="ui error message" id="error_invalid_id">
|
{% 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>
|
<i class="close icon"></i>
|
||||||
<div class="header">Error</div>
|
<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>
|
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui hidden error message" id="error_unknown">
|
<div class="ui hidden error message" id="error_unknown">
|
||||||
<i class="close icon"></i>
|
<i class="close icon"></i>
|
||||||
@ -48,7 +55,7 @@
|
|||||||
|
|
||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
<button class="ui left floated button" onclick="prevTemplate(2);">Back</a>
|
<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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block after %}
|
{% block after %}
|
||||||
@ -57,6 +64,14 @@
|
|||||||
$(this).closest(".message").addClass("hidden");
|
$(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() {
|
function castBallot() {
|
||||||
$("#cast_prompt").hide();
|
$("#cast_prompt").hide();
|
||||||
$("#casting").show();
|
$("#casting").show();
|
||||||
@ -65,8 +80,7 @@
|
|||||||
url: "{{ election_base_url }}cast_ballot",
|
url: "{{ election_base_url }}cast_ballot",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: eosjs.eos.core.objects.__all__.EosObject.to_json({
|
data: eosjs.eos.core.objects.__all__.EosObject.to_json({
|
||||||
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(booth.ballot, null),
|
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(booth.ballot, null)
|
||||||
"auth": { "username": $("#booth_login_name").val() }
|
|
||||||
}),
|
}),
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
dataType: "text"
|
dataType: "text"
|
||||||
@ -83,14 +97,14 @@
|
|||||||
})
|
})
|
||||||
.fail(function(xhr, status, err) {
|
.fail(function(xhr, status, err) {
|
||||||
if (xhr.status === 403) { // Forbidden
|
if (xhr.status === 403) { // Forbidden
|
||||||
$("#error_invalid_id").addClass("visible");
|
$("#error_invalid_id").removeClass("hidden");
|
||||||
|
|
||||||
$("#error_unknown").addClass("hidden");
|
$("#error_unknown").addClass("hidden");
|
||||||
} else {
|
} else {
|
||||||
$("#error_unknown_tech").text("Technical details: " + err);
|
$("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText);
|
||||||
$("#error_unknown").removeClass("hidden");
|
$("#error_unknown").removeClass("hidden");
|
||||||
|
|
||||||
$("#error_invalid_id").removeClass("visible");
|
$("#error_invalid_id").addClass("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#casting").hide();
|
$("#casting").hide();
|
||||||
|
@ -29,9 +29,9 @@
|
|||||||
|
|
||||||
<p>Please check that the ballot fingerprint above matches the ballot fingerprint you recorded earlier.</p>
|
<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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block buttons %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
@ -27,6 +27,8 @@
|
|||||||
}
|
}
|
||||||
booth.ballot = eosjs.eos.base.election.__all__.Ballot();
|
booth.ballot = eosjs.eos.base.election.__all__.Ballot();
|
||||||
booth.ballot.encrypted_answers = encryptedAnswers;
|
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();
|
nextTemplate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -21,9 +21,9 @@
|
|||||||
{% block content %}
|
{% 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>
|
<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>
|
<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 %}
|
{% 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>
|
<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>
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
boothError("Question template unable to save selections");
|
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() {
|
function previousQuestion() {
|
||||||
saveSelections();
|
saveSelections();
|
||||||
@ -41,17 +41,17 @@
|
|||||||
prevTemplate();
|
prevTemplate();
|
||||||
} else {
|
} else {
|
||||||
booth.questionNum--;
|
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() {
|
function nextQuestion() {
|
||||||
saveSelections();
|
saveSelections();
|
||||||
if (booth.questionNum == election.questions.length - 1) {
|
if (booth.questionNum == election.questions.__len__() - 1) {
|
||||||
nextTemplate();
|
nextTemplate();
|
||||||
} else {
|
} else {
|
||||||
booth.questionNum++;
|
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>
|
</script>
|
||||||
|
@ -16,13 +16,13 @@
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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 id="question-choices" class="ui form" style="margin-bottom: 1em;">
|
||||||
<div class="grouped fields">
|
<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="field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input type="checkbox" id="question-choice-{{ loop.index0 }}" onchange="choicesChanged();">
|
<input type="checkbox" id="question-choice-{{ loop.index0 }}" onchange="choicesChanged();">
|
||||||
@ -40,7 +40,7 @@
|
|||||||
<script>
|
<script>
|
||||||
function choicesChanged() {
|
function choicesChanged() {
|
||||||
var numChoices = $("#question-choices input:checked").length;
|
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
|
// Prevent making any more selections
|
||||||
$("#question-choices input:not(:checked)").prop("disabled", true);
|
$("#question-choices input:not(:checked)").prop("disabled", true);
|
||||||
$("#message-max-choices").removeClass("hidden");
|
$("#message-max-choices").removeClass("hidden");
|
||||||
@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
// Fill in ballot with previous selections
|
// Fill in ballot with previous selections
|
||||||
if (booth.answers[booth.questionNum]) {
|
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);
|
$("#question-choice-" + selection).prop("checked", true);
|
||||||
}
|
}
|
||||||
choicesChanged();
|
choicesChanged();
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
{% for choice in booth.answers[loop.index0].value.choices %}
|
{% for choice in booth.answers[loop.index0].value.choices %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="checkmark icon"></i>
|
<i class="checkmark icon"></i>
|
||||||
<div class="content">{{ question.choices[choice] }}</div>
|
<div class="content">{{ question.choices.__getitem__(choice) }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<p>You are not currently logged in. Please select an option from the list below to log in.</p>
|
<p>You are not currently logged in. Please select an option from the list below to log in.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul>
|
<ul class="ui list">
|
||||||
{% for auth_method in eosweb.app.config['AUTH_METHODS'] %}
|
{% 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>
|
<li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -39,6 +39,9 @@
|
|||||||
var currentBoothTask = 0;
|
var currentBoothTask = 0;
|
||||||
var selection_model_view_map = {{ selection_model_view_map|safe }}; {# :rooWut: #}
|
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() {
|
function resetBooth() {
|
||||||
booth = {
|
booth = {
|
||||||
"questionNum": 0,
|
"questionNum": 0,
|
||||||
@ -143,7 +146,9 @@
|
|||||||
"election": election,
|
"election": election,
|
||||||
"booth": booth,
|
"booth": booth,
|
||||||
"eosjs": eosjs,
|
"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);
|
}, opts);
|
||||||
$(destination).html(templates[template].render(opts));
|
$(destination).html(templates[template].render(opts));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -62,7 +62,9 @@ def main(app):
|
|||||||
user.oauth_token = resp['access_token']
|
user.oauth_token = resp['access_token']
|
||||||
flask.session['user'] = user
|
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']
|
user.username = me.data['name']
|
||||||
|
|
||||||
return flask.redirect(flask.url_for('login_complete'))
|
return flask.redirect(flask.url_for('login_complete'))
|
||||||
|
Loading…
Reference in New Issue
Block a user