Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to unlock Florida St door #800

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//= link admin.css
//= link door.js
//= link dues.js
//= link membership_note.js
//
Expand Down
60 changes: 60 additions & 0 deletions app/assets/javascripts/door.js
Original file line number Diff line number Diff line change
@@ -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}`);
}
});
});
15 changes: 15 additions & 0 deletions app/controllers/members/access_controller.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions app/views/members/key_members/edit.html.haml
Original file line number Diff line number Diff line change
@@ -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 [email protected] 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
Expand Down
1 change: 0 additions & 1 deletion app/views/members/users/_bookmarks.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -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" } &mdash; 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" } &mdash; 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/[email protected]&ctz=America/Los_Angeles", target: "_blank" } &mdash; 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" }
41 changes: 31 additions & 10 deletions app/views/members/users/index.html.haml
Original file line number Diff line number Diff line change
@@ -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:&nbsp;
%td= ENV["WIFI_NETWORK_NAME"]
%tr
%td.bold.text-right
password:&nbsp;
%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
Expand Down
9 changes: 9 additions & 0 deletions config/application.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion config/environments/staging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down