add voting portal (#7)

de huidige opzet hier houdt in dat de tokens van leden voor een vergadering, voorheen alleen gebruikt voor hun persoonlijke links tot de digitale vergadering (die hun naam instelt in BigBlueButton en ze als aanwezig markeerde voor stemmingen), nu ook gebruikt worden voor de link naar de stem portaal.

implicatie hiervan is dat Ingang admins zo ook toegang krijgen tot de stem credentials, in plaats van enkel de systeembeheerders met toegang tot de Helios database.

waar die kennis iemand wel in staat stelt om namens een ander te stemmen, wordt dit risico in check gehouden doordat dit niet ongemerkt gaat: na het uitbrengen van een stem wordt er naar het bijbehorende email adres een bevestiging gestuurd.
het risico bij anderszins kwaadwillenden die de info verkrijgen is hetzelfde; gebruik ervan gaat niet ongemerkt.

waar het mogelijk zou zijn om van de bestaande token af te wijken voor dit doeleinde naar een hash niet zichtbaar voor Ingang admins, zou dit ook nadelen hebben.
af en toe hebben we leden voor wie de mailtjes uit Ingang niet makkelijk aankomen, in welk geval we nu handmatig een inlog link kunnen doorgeven.
mochten de nodige info hiervoor niet meer beschikbaar zijn voor degene die een ALV technisch runt, dan worden juist dit soort situaties weer lastiger.

Co-authored-by: Kiara Grouwstra <kiara@bij1.org>
Reviewed-on: #7
This commit is contained in:
kiara 2023-01-27 14:47:10 +00:00
parent 2082cace93
commit 9d67656f44
19 changed files with 285 additions and 29 deletions

View File

@ -1,5 +1,5 @@
class MainController < ApplicationController
before_action :set_user_room, only: [:join, :users, :stream]
before_action :set_user_room, only: [:join, :users, :stream, :stemmen]
def index
end
@ -32,10 +32,19 @@ class MainController < ApplicationController
end
end
def stemmen
@votes = Vote.where(room_id: @room.id, user_id: @user.id).order(created_at: :desc)
render :votes
end
private
def set_user_room
@user = User.find_by token: params[:token]
@room = Room.find(@user.room_id)
if @user.nil?
redirect_to 'https://bij1.org/', status: :unauthorized
else
@room = Room.find(@user.room_id)
end
end
end

View File

@ -130,6 +130,9 @@ class UsersController < ApplicationController
# DELETE /users/1
# DELETE /users/1.json
def destroy
Vote.where(user_id: @user.id).each do |vote|
vote.destroy
end
@user.destroy
respond_to do |format|
format.html { redirect_to room_users_url(@user.room_id), notice: 'User was successfully destroyed.' }
@ -141,6 +144,9 @@ class UsersController < ApplicationController
# DELETE /rooms/1/users.json
def destroy_all
@users.each do |user|
Vote.where(user_id: user.id).each do |vote|
vote.destroy
end
user.destroy
end
respond_to do |format|
@ -162,11 +168,12 @@ class UsersController < ApplicationController
require 'csv'
CSV.parse(users_csv, :headers => true) do |row|
users = CSV.parse(users_csv, :headers => true).map { |row|
fields = row.to_hash
fields[:room_id] = room_id
User.create!(fields)
end
fields
}
User.upsert_all(users, unique_by: [:room_id, :email])
respond_to do |format|
format.html { redirect_to room_users_url(room_id), notice: 'Users were successfully created.' }

View File

@ -0,0 +1,81 @@
class VotesController < ApplicationController
http_basic_authenticate_with name: Rails.application.config.admin_name,
password: Rails.application.config.admin_password
before_action :set_room, only: [:index, :bulk, :destroy_for_room]
before_action :set_votes, only: [:index, :bulk, :destroy_for_room]
# GET /rooms/:room_id/votes
# GET /rooms/:room_id/votes.json
def index
respond_to do |format|
format.html { render :index }
# let's protect voter credentials
format.json { render json: [], status: :unauthorized }
end
end
# GET /rooms/:room_id/votes/bulk
def bulk
@sample = "info@bij1.org,mijn-stemming,1,abcdABCD1234"
end
# POST /rooms/:room_id/votes/bulk
# POST /rooms/:room_id/votes/bulk.json
def create_bulk
room_id = params[:room_id]
votes_csv = params[:votes_csv]
require 'csv'
headers = %i[
voter_email
short_name
voter_login_id
voter_password
]
votes = CSV.parse(votes_csv, headers: headers).map { |row|
csv_fields = row.to_hash
email = csv_fields[:voter_email]
user = User.find_by(room_id: room_id, email: email)
{
:room_id => room_id,
:user_id => user.id,
:election_slug => csv_fields[:short_name],
:voter_login_id => csv_fields[:voter_login_id],
:voter_password => csv_fields[:voter_password],
}
}
Vote.upsert_all(votes, unique_by: [:user_id, :election_slug])
respond_to do |format|
format.html { redirect_to room_users_url(room_id), notice: 'Votes were successfully created.' }
format.json { render :show, status: :created, location: @room }
end
end
# DELETE /rooms/:room_id/votes
# DELETE /rooms/:room_id/votes.json
def destroy_for_room
@votes.each do |vote|
vote.destroy
end
respond_to do |format|
format.html { redirect_to room_users_url(@room.id), notice: 'Votes were successfully destroyed.' }
format.json { render :show, status: :created, location: @room }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_room
@room = Room.find(params[:room_id])
end
def set_votes
@votes = Vote.where(room_id: @room.id)
end
end

View File

@ -6,4 +6,7 @@ class User < ApplicationRecord
attribute :invited, :boolean, default: false
attribute :vote, :boolean, default: true
attribute :proxy, :boolean, default: false
validates :email, uniqueness: { scope: :room_id }
validates :room_id, uniqueness: { scope: :email }
end

6
app/models/vote.rb Normal file
View File

@ -0,0 +1,6 @@
class Vote < ApplicationRecord
belongs_to :user
validates :election_slug, uniqueness: { scope: :user_id }
validates :user_id, uniqueness: { scope: :election_slug }
end

View File

@ -0,0 +1,18 @@
<p id="notice"><%= notice %></p>
<h1>Stemmingen</h1>
<ul>
<% @votes.each do |vote| %>
<li>
<a href="https://stemmen.bij1.org/helios/e/<%= vote.election_slug %>/vote#id_voter_id=<%= vote.voter_login_id %>&id_password=<%= vote.voter_password %>">
<%= vote.election_slug %>
</a>
</li>
<% end %>
</ul>
<br>
<%= link_to 'Stream', user_stream_path %> |
<%= link_to 'Inbellen', join_room_path %> via BigBlueButton (voor toelichtingen)

View File

@ -32,5 +32,7 @@
<%= link_to 'Show Users', room_users_path(@room) %> |
<%= link_to 'Edit', edit_room_path(@room) %> |
<%= link_to 'Import Users', bulk_new_room_users_path(@room), method: :get %> |
<%= link_to 'Import Votes', bulk_new_room_votes_path(@room), method: :get %> |
<%= link_to 'Present Users', room_present_users_path(@room) %> |
<%= link_to 'Back', rooms_path %>

View File

@ -1,7 +1,7 @@
<h1>Bulk Users: <%= @room.name %></h1>
<p>
csv formaat voor bulk import: email,name,moderator,vote,proxy,invited,presence. kolom volgorde maakt niet uit. komma-separated met header.
csv formaat voor bulk import: <code>email,name,moderator,vote,proxy,invited,presence</code>. kolom volgorde maakt niet uit. komma-separated met header.
<% if false %>
download een sample <%= link_to "hier", "/bbb.csv" %>.
<% end %>

View File

@ -50,4 +50,6 @@
<%= link_to 'Present Users', room_present_users_path(id: params[:room_id], v: 1) %> |
<%= link_to 'Absent Users', room_present_users_path(id: params[:room_id], v: 0) %> |
<%= link_to 'Destroy all', destroy_room_users_path(params[:room_id]), method: :delete %> |
<%= link_to 'Import Votes', bulk_new_room_votes_path(params[:room_id]), method: :get %> |
<%= link_to 'Destroy Votes', room_votes_path(params[:room_id]), method: :delete %> |
<%= link_to 'Back', room_path(params[:room_id]) %>

View File

@ -0,0 +1,46 @@
<h1>Bulk Votes: <%= @room.name %></h1>
<p>
csv formaat voor bulk import, in die volgorde, komma-separated en zonder header: <code>voter_email,short_name,voter_login_id,voter_password</code>
<% if false %>
download een sample <%= link_to "hier", "/votes.csv" %>.
<% end %>
</p>
<ul>
<li><code>voter_email</code> is het email adres van de stemmer, om de credentials terug te koppelen aan het juiste lid.</li>
<li><code>short_name</code> is de naam van de stemming</li>
<li><code>voter_login_id</code> is de login id van de stemmer in het stem systeem</li>
<li><code>voter_password</code> is het wachtwoord van de stemmer in het stem systeem</li>
</ul>
<p>
De gegevens hiervoor zijn te verkrijgen via commando, gegeven SSH toegang tot de database server:
</p>
<ul>
<li>
<code>ssh db.bij1.net "sudo psql -U postgres helios -c \"SELECT voter_email, short_name, voter_login_id, voter_password FROM helios_voter JOIN helios_election ON election_id = helios_election.id WHERE helios_election.uuid IN ('UUID1', 'UUID2', ...);\" -tAF,"</code>
</li>
</ul>
<p>
... waar <code>'UUID1', 'UUID2', ...</code> vervangen dient te worden door de UUIDs van de stemmingen van deze kamer, in de URLs te vinden in <a href="https://stemmen.bij1.org/helios/elections/administered">Helios</a>.
</p>
<p>
Gebruikers vinden vervolgens hun stem links op de volgende URL, waar <code>TOKEN</code> dient te worden vervangen door hun Ingang token:
</p>
<ul>
<li>
<code>https://vergadering.bij1.org/ingang/TOKEN/stemmen</code>
</li>
</ul>
<%= form_with(local: true) do |form| %>
<div class="field">
<%= form.text_area :votes_csv, value: @sample %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<%= link_to 'Back', room_users_path %>
<% end %>

View File

@ -0,0 +1,29 @@
<p id="notice"><%= notice %></p>
<h1>Votes: <%= @room.name %></h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>User ID</th>
<th>Election Slug</th>
<th>Login ID</th>
</tr>
</thead>
<tbody>
<% @votes.each do |vote| %>
<tr>
<td><%= vote.id %></td>
<td><%= vote.user_id %></td>
<td><%= vote.election_slug %></td>
<td><%= vote.voter_login_id %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'Back', room_path(@room) %>

View File

@ -1,4 +1,8 @@
RAILS_LOG_TO_STDOUT: 'any value will do, it just cares that this env var is present'
SMTP_USERNAME: ""
SMTP_PASSWORD: ""
INGANG_ADMIN_PASSWORD: ""
BIGBLUEBUTTON_SECRET: ""
DATABASE_PASSWORD_PRODUCTION: ""
DATABASE_PASSWORD_TEST: ""
DATABASE_PASSWORD_DEV: ""

View File

@ -1,35 +1,29 @@
# SQLite. Versions 3.8.0 and up are supported.
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
# upsert does not work with sqlite
adapter: postgresql
encoding: unicode
host: db.internal.bij1.net
port: 5432
development:
<<: *default
adapter: sqlite3
database: db/development.sqlite3
database: ingang_dev
username: ingang_dev
password: <%= ENV.fetch("DATABASE_PASSWORD_DEV") { } %>
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
adapter: sqlite3
database: db/test.sqlite3
database: ingang_test
username: ingang_test
password: <%= ENV.fetch("DATABASE_PASSWORD_TEST") { } %>
production:
<<: *default
adapter: sqlite3
database: db/production.sqlite3
# adapter: postgresql
# encoding: unicode
# database: ingang
# username: ingang
# password: <%= ENV.fetch("DATABASE_PASSWORD") { } %>
# host: db.internal.bij1.net
# port: 5432
database: ingang
username: ingang
password: <%= ENV.fetch("DATABASE_PASSWORD_PRODUCTION") { } %>

View File

@ -1,18 +1,24 @@
Rails.application.routes.draw do
resources :rooms do
resources :users, shallow: true
resources :rooms, shallow: true do
resources :users do
# resources :votes
end
end
get 'rooms/:room_id/users/bulk', to: 'users#bulk', as: 'bulk_new_room_users'
post 'rooms/:room_id/users/bulk', to: 'users#create_bulk', as: 'bulk_create_room_users'
post 'rooms/:room_id/users/:id/test_invite', to: 'users#test_invite', as: 'test_invite_user'
post 'rooms/:room_id/users/invite', to: 'users#invite', as: 'invite_room_users'
delete 'rooms/:room_id/users', to: 'users#destroy_all', as: 'destroy_room_users'
delete 'rooms/:room_id/votes', to: 'votes#destroy_for_room', as: 'room_votes'
delete 'rooms/:room_id/users/invite', to: 'users#uninvite', as: 'uninvite_room_users'
post 'rooms/:room_id/users/mark_invited', to: 'users#mark_invited', as: 'mark_invited_room_users'
post 'rooms/:room_id/users/mark_presence', to: 'users#mark_presence', as: 'mark_presence_room_users'
get 'rooms/:room_id/votes/bulk', to: 'votes#bulk', as: 'bulk_new_room_votes'
post 'rooms/:room_id/votes/bulk', to: 'votes#create_bulk', as: 'bulk_create_room_votes'
get 'rooms/:id/voters.csv', to: 'rooms#voters', as: 'room_export_voters'
get 'rooms/:id/aanwezig.csv', to: 'rooms#present', as: 'room_present_users'
get ':token/stream', to: 'main#stream'
get ':token/stemmen', to: 'main#stemmen', as: 'user_elections'
get ':token/stream', to: 'main#stream', as: 'user_stream'
get ':token', to: 'main#join', as: 'join_room'
root 'main#index'
end

View File

@ -0,0 +1,16 @@
class CreateVotes < ActiveRecord::Migration[6.0]
def change
create_table :votes do |t|
t.references :room, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.string :election_slug
t.string :voter_login_id
t.string :voter_password
t.timestamps
end
change_column_default :votes, :created_at, from: nil, to: ->{ 'now()' }
change_column_default :votes, :updated_at, from: nil, to: ->{ 'now()' }
add_index :votes, [:user_id, :election_slug], unique: true
end
end

View File

@ -0,0 +1,8 @@
class UpsertableUsers < ActiveRecord::Migration[6.0]
def change
change_column_default :users, :created_at, from: nil, to: ->{ 'now()' }
change_column_default :users, :updated_at, from: nil, to: ->{ 'now()' }
add_index :users, [:room_id, :email], unique: true
end
end

1
public/votes.csv Normal file
View File

@ -0,0 +1 @@
info@bij1.org,mijn-stemming,1,abcdABCD1234
1 info@bij1.org mijn-stemming 1 abcdABCD1234

17
test/fixtures/votes.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
id:
room_id: 1
user_id: 1
election_slug: MyString
voter_login_id: MyString
voter_password: MyString
two:
id:
room_id: 1
user_id: 1
election_slug: MyString
voter_login_id: MyString
voter_password: MyString

7
test/models/vote_test.rb Normal file
View File

@ -0,0 +1,7 @@
require 'test_helper'
class VoteTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end