From 75a350418ec45e20de35829479515de2ab8fe66c Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Sun, 19 Nov 2023 17:58:54 -0800 Subject: [PATCH] Add ability to unlock florida st door (#800) --- Gemfile | 1 + Gemfile.lock | 1 + app/assets/config/manifest.js | 1 + app/assets/javascripts/door.js | 60 ++++++++++++++++++++ app/controllers/members/access_controller.rb | 15 +++++ app/models/user.rb | 4 ++ app/views/members/key_members/edit.html.haml | 10 ++-- app/views/members/users/_bookmarks.html.haml | 1 - app/views/members/users/index.html.haml | 41 +++++++++---- config/application.example.yml | 9 +++ config/environments/production.rb | 2 +- config/environments/staging.rb | 2 +- config/routes.rb | 2 + 13 files changed, 131 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/door.js create mode 100644 app/controllers/members/access_controller.rb diff --git a/Gemfile b/Gemfile index a2fcb8d6..9f10967a 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,7 @@ gem "uglifier" gem "coffee-rails", ">= 4.2.2" gem "bootstrap-sass" gem "jquery-datatables-rails", ">= 3.4.0" +gem 'jwt' # Avoid low-severity security issue: https://github.com/advisories/GHSA-vr8q-g5c7-m54m gem "nokogiri", ">= 1.11.0.rc4" diff --git a/Gemfile.lock b/Gemfile.lock index 0d4ba7d5..d72d2e77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -450,6 +450,7 @@ DEPENDENCIES jbuilder jquery-datatables-rails (>= 3.4.0) jquery-rails (>= 4.3.5) + jwt kaminari (>= 1.2.1) launchy nokogiri (>= 1.11.0.rc4) diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 786b9305..7e8bbed6 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,4 +1,5 @@ //= link admin.css +//= link door.js //= link dues.js //= link membership_note.js // diff --git a/app/assets/javascripts/door.js b/app/assets/javascripts/door.js new file mode 100644 index 00000000..7a069496 --- /dev/null +++ b/app/assets/javascripts/door.js @@ -0,0 +1,60 @@ +$(document).ready(() => { + const checkDoorAccessibility = async () => { + try { + const response = await fetch(`${window.accessControlUri}/api/v1/status`); + if (!response.ok) { + throw 'door control not ready'; + } + $("#unlock-door").removeClass('disabled'); + } catch (e) { + $("#unlock-door").addClass('disabled'); + } + }; + + // Poll if door control is available and update button state + checkDoorAccessibility(); + setInterval(checkDoorAccessibility, 5000); + + $("#unlock-door").click(async () => { + if ($("#unlock-door").hasClass('disabled')) { + alert('Door control not accessible. Are you on the space Wi-Fi?'); + return; + } + + try { + // Fetch a short-lived token to authenticate with door control + const tokenResponse = await fetch(`/members/users/${window.userId}/access_control_token`); + if (!tokenResponse.ok) { + throw 'failed to get door token'; + } + const tokenJson = await tokenResponse.json(); + + // Unlock the door + const openResponse = await fetch(`${window.accessControlUri}/api/v1/unlock`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${tokenJson.token}` + }, + body: JSON.stringify({ + seconds: window.accessControlUnlockSeconds, + }), + }); + const openJson = await openResponse.json(); + if (!openResponse.ok) { + throw openJson.message; + } + + $("#unlock-error").addClass('hidden') + $("#unlock-success").removeClass('hidden'); + + // Hide the success message when the door should be locked again + clearTimeout(window.successTimeoutId); + window.successTimeoutId = setTimeout(() => { + $("#unlock-success").addClass('hidden'); + }, window.accessControlUnlockSeconds * 1000); + } catch (e) { + $("#unlock-error").removeClass('hidden'); + $("#error-text").text(`Failed to unlock door: ${e}`); + } + }); +}); \ No newline at end of file diff --git a/app/controllers/members/access_controller.rb b/app/controllers/members/access_controller.rb new file mode 100644 index 00000000..cf8f40e3 --- /dev/null +++ b/app/controllers/members/access_controller.rb @@ -0,0 +1,15 @@ +require 'jwt' + +class Members::AccessController < Members::MembersController + def token + return head :unprocessable_entity unless current_user.space_access? + + payload = { + sub: current_user.email, + nbf: Time.now.to_i - 30, + exp: Time.now.to_i + 30 + } + + render json: { token: JWT.encode(payload, ENV['ACCESS_CONTROL_SIGNING_KEY'], 'HS256') } + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 4c2f89d6..c5eac94e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -136,6 +136,10 @@ def general_member? member? || key_member? || voting_member? end + def space_access? + key_member? || voting_member? + end + def gravatar_url(size = 200) email = gravatar_email || self.email hash = email ? Digest::MD5.hexdigest(email.downcase) : nil diff --git a/app/views/members/key_members/edit.html.haml b/app/views/members/key_members/edit.html.haml index c9b071fb..facd74ca 100644 --- a/app/views/members/key_members/edit.html.haml +++ b/app/views/members/key_members/edit.html.haml @@ -1,26 +1,26 @@ %h1 Become a key member! %p - Key members receive a door code that enables you to access Double Union - whenever you want to (24/7), host events, and bring guests of any gender. + Key members can access Double Union whenever they want to (24/7), + host events, and bring guests of any gender. %p To become a key member, first complete a - #{ link_to "Key Membership Orientation for 77 Falmouth", "https://docs.google.com/presentation/d/1ygUBN_4SqZSjDi3Sx8MPviAXQz5KdI6_IOstW9qtWzQ/edit#slide=id.g111195ab4e6_0_0", target: "_blank" }. + #{ link_to "Key Membership Orientation for 650 Florida #M", "https://docs.google.com/presentation/d/1ygUBN_4SqZSjDi3Sx8MPviAXQz5KdI6_IOstW9qtWzQ/edit#slide=id.g111195ab4e6_0_0", target: "_blank" }. %p You can also learn more in the #{ link_to "Members Handbook section on key membership", "https://docs.google.com/document/d/1yYXAj8rzQMiYt2xFdzm2xEOe0LCHohWNrMWzad-4mtQ/edit#heading=h.lsg2zdao236h", target: "_blank" }. %p - When you submit this form, you will receive a randomly-assigned key code. You + When you submit this form, you will be able to unlock the space from the app. You can email the Membership Coordinator at membership@doubleunion.org with any questions. = form_tag members_user_key_members_path(current_user.id), method: "patch" do %p = check_box_tag "agreements[attended_events]", 1, false, required: true - = label_tag "agreements[attended_events]", "I have completed a Key Member Orientation for 77 Falmouth." + = label_tag "agreements[attended_events]", "I have completed a Key Member Orientation for 650 Florida #M." %p = check_box_tag "agreements[kick_out]", 1, false, required: true diff --git a/app/views/members/users/_bookmarks.html.haml b/app/views/members/users/_bookmarks.html.haml index a856282c..00b71764 100644 --- a/app/views/members/users/_bookmarks.html.haml +++ b/app/views/members/users/_bookmarks.html.haml @@ -3,7 +3,6 @@ %ul %li #{ link_to "Members mailing list", "https://groups.google.com/a/doubleunion.org/forum/#!forum/members", target: "_blank" } %li #{ link_to "Members folder in Google Drive", "https://drive.google.com/folderview?id=0B6a_aDP-2fOVV2FQLW5FVTZ2Mjg", target: "_blank" } — For easy access, #{ link_to "add a shortcut in your own Google Drive", "https://support.google.com/drive/answer/2375057?hl=en", target: "_blank" }. - %li #{ link_to "Key code orientation", "https://docs.google.com/presentation/d/1ygUBN_4SqZSjDi3Sx8MPviAXQz5KdI6_IOstW9qtWzQ/edit", target: "_blank" } — Describes how to get access to the 77 Falmouth space, and how to enter the space. %li #{ link_to "Members calendar", "https://www.google.com/calendar/embed?src=br12b81lfe63rggddlg0k92mko@group.calendar.google.com&ctz=America/Los_Angeles", target: "_blank" } — This is a view-only version of the calendar. To add or edit events, go to #{ link_to "Google Calendar", "https://calendar.google.com/calendar/", target: "_blank" } (it should show up among your calendars). %li #{ link_to "Members Slack chat", "https://doubleunion.slack.com/", target: "_blank" } %li #{ link_to "DU web application code on GitHub", "https://github.com/doubleunion", target: "_blank" } diff --git a/app/views/members/users/index.html.haml b/app/views/members/users/index.html.haml index 57259ebc..95124049 100644 --- a/app/views/members/users/index.html.haml +++ b/app/views/members/users/index.html.haml @@ -1,20 +1,41 @@ +:javascript + window.userId = #{current_user.id}; + window.accessControlUri = '#{ENV["ACCESS_CONTROL_URI"]}'; + window.accessControlUnlockSeconds = #{ENV["ACCESS_CONTROL_UNLOCK_SECONDS"]}; + += content_for :js do + = javascript_include_tag :door + - if current_user.member? || current_user.key_member? .mt-20 - unless current_user.voting_policy_agreement = link_to "Become a voting member", edit_members_user_voting_members_path(current_user), class: "btn btn-default" -= render 'bookmarks' - %h3 Space Access -- if current_user.member? - = link_to "Become a key member", edit_members_user_key_members_path(current_user), class: "btn btn-default" -- if current_user.door_code.present? - %p Your door code is #{current_user.door_code.code}* -- elsif current_user.key_member? +- if current_user.space_access? + %p Click the button below to unlock the door and access the space. + %p The door will automatically re-lock after #{ENV["ACCESS_CONTROL_UNLOCK_SECONDS"]} seconds. + #unlock-door.btn.btn-default.disabled Unlock Door + #unlock-success.hidden.bold + %p The door was unlocked! + #unlock-error.hidden + %p#error-text.text-danger.bold %p - You are a key member, but you don't seem to have a door code set. If you need one, contact - =mail_to MEMBERSHIP_EMAIL - for help. + %i Note: + You must be at the space and connected to the Wi-Fi to unlock the door. + %table + %tr + %td.bold.text-right + ssid:  + %td= ENV["WIFI_NETWORK_NAME"] + %tr + %td.bold.text-right + password:  + %td= ENV["WIFI_NETWORK_PASSWORD"] +- elsif current_user.member? + = link_to "Become a key member", edit_members_user_key_members_path(current_user), class: "btn btn-default" + += render 'bookmarks' - if @all_admins.any? %h3 Admins diff --git a/config/application.example.yml b/config/application.example.yml index 6b3f0096..cb279ba0 100644 --- a/config/application.example.yml +++ b/config/application.example.yml @@ -9,6 +9,15 @@ # SECRET_TOKEN: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +# If you want to test the access control system, you'll need to populate the +# signing key and local URI here. +ACCESS_CONTROL_SIGNING_KEY: +ACCESS_CONTROL_URI: +ACCESS_CONTROL_UNLOCK_SECONDS: '10' + +WIFI_NETWORK_NAME: wifinetwork +WIFI_NETWORK_PASSWORD: wifipassword + # If you want to try sending email locally, you'll need make an AWS account and populate these values # It's easier to just use Mailcatcher and Mailer previews, though! AWS_ACCESS_KEY_ID: diff --git a/config/environments/production.rb b/config/environments/production.rb index 1577e9a1..211127de 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -25,7 +25,7 @@ config.serve_static_files = ENV["RAILS_SERVE_STATIC_FILES"].present? # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + config.assets.js_compressor = Uglifier.new(harmony: true) # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. diff --git a/config/environments/staging.rb b/config/environments/staging.rb index edd14859..458372b8 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -26,7 +26,7 @@ config.serve_static_files = true # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + config.assets.js_compressor = Uglifier.new(harmony: true) # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. diff --git a/config/routes.rb b/config/routes.rb index a6b4fb99..5cadccf1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,6 +12,8 @@ delete "cancel" => "dues#cancel" post "scholarship_request" => "dues#scholarship_request" + get "access_control_token" => "access#token" + resource :key_members, only: [:edit, :update] resource :voting_members, only: [:edit, :update] end