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 class MainController < ApplicationController
before_action :set_user_room, only: [:join, :users, :stream] before_action :set_user_room, only: [:join, :users, :stream, :stemmen]
def index def index
end end
@ -32,10 +32,19 @@ class MainController < ApplicationController
end end
end end
def stemmen
@votes = Vote.where(room_id: @room.id, user_id: @user.id).order(created_at: :desc)
render :votes
end
private private
def set_user_room def set_user_room
@user = User.find_by token: params[:token] @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
end end

View File

@ -130,6 +130,9 @@ class UsersController < ApplicationController
# DELETE /users/1 # DELETE /users/1
# DELETE /users/1.json # DELETE /users/1.json
def destroy def destroy
Vote.where(user_id: @user.id).each do |vote|
vote.destroy
end
@user.destroy @user.destroy
respond_to do |format| respond_to do |format|
format.html { redirect_to room_users_url(@user.room_id), notice: 'User was successfully destroyed.' } 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 # DELETE /rooms/1/users.json
def destroy_all def destroy_all
@users.each do |user| @users.each do |user|
Vote.where(user_id: user.id).each do |vote|
vote.destroy
end
user.destroy user.destroy
end end
respond_to do |format| respond_to do |format|
@ -162,11 +168,12 @@ class UsersController < ApplicationController
require 'csv' require 'csv'
CSV.parse(users_csv, :headers => true) do |row| users = CSV.parse(users_csv, :headers => true).map { |row|
fields = row.to_hash fields = row.to_hash
fields[:room_id] = room_id fields[:room_id] = room_id
User.create!(fields) fields
end }
User.upsert_all(users, unique_by: [:room_id, :email])
respond_to do |format| respond_to do |format|
format.html { redirect_to room_users_url(room_id), notice: 'Users were successfully created.' } 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 :invited, :boolean, default: false
attribute :vote, :boolean, default: true attribute :vote, :boolean, default: true
attribute :proxy, :boolean, default: false attribute :proxy, :boolean, default: false
validates :email, uniqueness: { scope: :room_id }
validates :room_id, uniqueness: { scope: :email }
end 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 'Show Users', room_users_path(@room) %> |
<%= link_to 'Edit', edit_room_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 'Present Users', room_present_users_path(@room) %> |
<%= link_to 'Back', rooms_path %> <%= link_to 'Back', rooms_path %>

View File

@ -1,7 +1,7 @@
<h1>Bulk Users: <%= @room.name %></h1> <h1>Bulk Users: <%= @room.name %></h1>
<p> <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 %> <% if false %>
download een sample <%= link_to "hier", "/bbb.csv" %>. download een sample <%= link_to "hier", "/bbb.csv" %>.
<% end %> <% end %>

View File

@ -50,4 +50,6 @@
<%= link_to 'Present Users', room_present_users_path(id: params[:room_id], v: 1) %> | <%= 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 '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 '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]) %> <%= 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_USERNAME: ""
SMTP_PASSWORD: "" SMTP_PASSWORD: ""
INGANG_ADMIN_PASSWORD: "" INGANG_ADMIN_PASSWORD: ""
BIGBLUEBUTTON_SECRET: "" 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 default: &default
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000 timeout: 5000
# upsert does not work with sqlite
adapter: postgresql
encoding: unicode
host: db.internal.bij1.net
port: 5432
development: development:
<<: *default <<: *default
adapter: sqlite3 database: ingang_dev
database: db/development.sqlite3 username: ingang_dev
password: <%= ENV.fetch("DATABASE_PASSWORD_DEV") { } %>
# Warning: The database defined as "test" will be erased and # Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake". # re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production. # Do not set this db to the same as development or production.
test: test:
<<: *default <<: *default
adapter: sqlite3 database: ingang_test
database: db/test.sqlite3 username: ingang_test
password: <%= ENV.fetch("DATABASE_PASSWORD_TEST") { } %>
production: production:
<<: *default <<: *default
adapter: sqlite3 database: ingang
database: db/production.sqlite3 username: ingang
# adapter: postgresql password: <%= ENV.fetch("DATABASE_PASSWORD_PRODUCTION") { } %>
# encoding: unicode
# database: ingang
# username: ingang
# password: <%= ENV.fetch("DATABASE_PASSWORD") { } %>
# host: db.internal.bij1.net
# port: 5432

View File

@ -1,18 +1,24 @@
Rails.application.routes.draw do Rails.application.routes.draw do
resources :rooms do resources :rooms, shallow: true do
resources :users, shallow: true resources :users do
# resources :votes
end
end end
get 'rooms/:room_id/users/bulk', to: 'users#bulk', as: 'bulk_new_room_users' 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/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/:id/test_invite', to: 'users#test_invite', as: 'test_invite_user'
post 'rooms/:room_id/users/invite', to: 'users#invite', as: 'invite_room_users' 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/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' 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_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' 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/voters.csv', to: 'rooms#voters', as: 'room_export_voters'
get 'rooms/:id/aanwezig.csv', to: 'rooms#present', as: 'room_present_users' 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' get ':token', to: 'main#join', as: 'join_room'
root 'main#index' root 'main#index'
end 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