Skip to content

Commit

Permalink
api - api keys + fix dates in scheduler + change organization to bill…
Browse files Browse the repository at this point in the history
…_organization
  • Loading branch information
dcordz committed Jan 24, 2025
1 parent a630217 commit c0a910d
Show file tree
Hide file tree
Showing 55 changed files with 1,160 additions and 775 deletions.
1 change: 0 additions & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
3.3.6
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

source "https://rubygems.org"

ruby "3.3.6"
ruby "3.4.1"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
# gem "rails", "~> 8"
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ DEPENDENCIES
webauthn

RUBY VERSION
ruby 3.3.6p108
ruby 3.4.1p0

BUNDLED WITH
2.5.23
2.6.3
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ Sway was created to solve several issues of democracy:

Because of these difficulties, it can be challenging to hold elected representatives accountable for their actions.

- [Participating](#participating)
- [Locales](#locales)
- [Current Supported Locales](#current-supported-locales)
- [Onboard a New Locale](#onboard-a-new-locale)
- [Development](#development)
- [Environment Variables](#environment-variables)
- [Running Sway](#running-sway)
- [Participating](#participating)
- [Locales](#locales)
- [Current Supported Locales](#current-supported-locales)
- [Onboard a New Locale](#onboard-a-new-locale)
- [Development](#development)
- [Environment Variables](#environment-variables)
- [Running Sway](#running-sway)

## Participating

Expand All @@ -37,31 +37,31 @@ Sway relies on people like you to support this venture. To get started, please s

tl;dr

- Create an issue.
- Fork this repo.
- Update code as needed.
- Open a pull request with your changes linked to the aforementioned issue.
- Create an issue.
- Fork this repo.
- Update code as needed.
- Open a pull request with your changes linked to the aforementioned issue.

## Adding Your City, Region, and/or Country to Sway

Sway is designed to work with and be extended to multiple locations regardless of city, region or country, and anyone and everyone is free to onboard a new municipality or `"locale"` into Sway.

### Current Supported Locales

- Baltimore City, Maryland, United States - `baltimore-maryland-united_states`
- United States Congress - `congress-congress-united_states`
- Baltimore City, Maryland, United States - `baltimore-maryland-united_states`
- United States Congress - `congress-congress-united_states`

### Onboarding

To add a new locale, create a new [Issue](https://github.com/Plebeian-Technology/sway/issues) and label it as `locale` and include the below files:

- An SVG image avatar for the locale, usually a flag representing that municipality. See [/public/images/avatars/baltimore-maryland-united_states.svg](/public/images/avatars/baltimore-maryland-united_states.svg) as an example. Wikipedia and Twitter are good sources to get these from.
- An SVG image avatar for the locale, usually a flag representing that municipality. See [/public/images/avatars/baltimore-maryland-united_states.svg](/public/images/avatars/baltimore-maryland-united_states.svg) as an example. Wikipedia and Twitter are good sources to get these from.

- A `.geojson` file named `<city>-<region>-<country>.geojson` with locations of each district corresponding to the respective legislator. For example, GeoJSON data for Baltimore City can be found [here](https://data.baltimorecity.gov/datasets/council-district-2021) - other cities may have similar sources.
- A `.geojson` file named `<city>-<region>-<country>.geojson` with locations of each district corresponding to the respective legislator. For example, GeoJSON data for Baltimore City can be found [here](https://data.baltimorecity.gov/datasets/council-district-2021) - other cities may have similar sources.

- Adding new [Bills](/app/models/bill.rb), [Organizations](/app/models/organization.rb) and [LegislatorVotes](/app/models/legislator_vote.rb) requires administrative access to Sway. More importantly it requires a commmitment to selecting, researching and summarizing a [Bill of the Week](/app/controllers/bill_of_the_week_controller.rb) each week for your Sway locale.
- Adding new [Bills](/app/models/bill.rb), [BillOrganizations](/app/models/organization.rb) and [LegislatorVotes](/app/models/legislator_vote.rb) requires administrative access to Sway. More importantly it requires a commmitment to selecting, researching and summarizing a [Bill of the Week](/app/controllers/bill_of_the_week_controller.rb) each week for your Sway locale.

- To add and/or update Legislators in Sway, please provide a `legislators.json` file. For an example of the file structure, see [the Baltimore legislators.json file.](/storage/seeds/data/united_states/maryland/baltimore/legislators.json)
- To add and/or update Legislators in Sway, please provide a `legislators.json` file. For an example of the file structure, see [the Baltimore legislators.json file.](/storage/seeds/data/united_states/maryland/baltimore/legislators.json)

Once the above have been assembled, we will work with you to get them into Sway!

Expand Down Expand Up @@ -147,11 +147,11 @@ Copy the full key, including the `=` at the end into each environment variable a

You can read more about web push notifications here:

- [https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices](https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices)
- [https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices](https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices)

- [https://web.dev/articles/push-notifications-web-push-protocol](https://web.dev/articles/push-notifications-web-push-protocol)
- [https://web.dev/articles/push-notifications-web-push-protocol](https://web.dev/articles/push-notifications-web-push-protocol)

- [https://medium.com/@dejanvu.developer/implementing-web-push-notifications-in-a-ruby-on-rails-application-dcd829e02df0](https://medium.com/@dejanvu.developer/implementing-web-push-notifications-in-a-ruby-on-rails-application-dcd829e02df0)
- [https://medium.com/@dejanvu.developer/implementing-web-push-notifications-in-a-ruby-on-rails-application-dcd829e02df0](https://medium.com/@dejanvu.developer/implementing-web-push-notifications-in-a-ruby-on-rails-application-dcd829e02df0)

#### Add your phone number as an Admin phone number by setting the below key in the same format:

Expand Down
44 changes: 44 additions & 0 deletions app/controllers/api_keys_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class ApiKeysController < ApplicationController
# include ApiKeyAuthenticatable
# Require API key authentication
# prepend_before_action :authenticate_with_api_key!, only: %i[index destroy]

def index
render_component(Pages::API_KEYS, {
apiKeys: current_user.api_keys
})
end

def create
if current_user.api_keys.blank?
api_key = current_user.api_keys.create! token: SecureRandom.hex
flash[:notice] = "API Key Created!"

render json: {
**api_key.attributes,
token: api_key.token
}, status: :ok
else
flash[:alert] = "You may only have 1 API Key."
route_component(api_keys_path)
end
end

def update
current_user.api_keys.find_by(id: params[:id])&.update!(update_params)
flash[:notice] = "API Key Name Updated"
route_component(api_keys_path)
end

def destroy
current_user.api_keys.find_by(id: params[:id])&.destroy
flash[:notice] = "API Key Deleted!"
route_component(api_keys_path)
end

private

def update_params
params.require(:api_keys_update_params).permit(:name)
end
end
10 changes: 9 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
class ApplicationController < ActionController::Base
extend T::Sig
include RelyingParty
include ApiKeyAuthenticatable
include SwayProps
include Pages
include SwayRoutes
Expand All @@ -12,6 +13,7 @@ class ApplicationController < ActionController::Base

# newrelic_ignore_enduser

before_action :is_api_request_and_is_route_api_accessible?
before_action :redirect_if_no_current_user
before_action :set_sway_locale_id_in_session

Expand Down Expand Up @@ -134,14 +136,20 @@ def sign_out
sig { returns(T.nilable(User)) }
def current_user
@current_user ||=
User.find_by(id: session[:user_id])
User.find_by(id: session[:user_id]) ||
authenticate_with_api_key # ApiKeyAuthenticatable
end

sig { returns(T.nilable(SwayLocale)) }
def current_sway_locale
@current_sway_locale ||= SwayLocale.find_by(id: session[:sway_locale_id]) || current_user&.default_sway_locale
end

sig { void }
def is_api_request_and_is_route_api_accessible?
# TODO
end

sig { void }
def redirect_if_no_current_user
u = current_user
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# frozen_string_literal: true
# typed: true

class OrganizationsController < ApplicationController
class BillOrganizationsController < ApplicationController
include SwayGoogleCloudStorage

before_action :verify_is_admin, only: %i[create]
before_action :set_organization, only: %i[show]
before_action :set_bill, only: %i[create]

def index
render json: Organization.where(sway_locale_id: current_sway_locale&.id).map(&:to_sway_json), status: :ok
render json: BillOrganization.where(sway_locale_id: current_sway_locale&.id).map(&:to_sway_json), status: :ok
end

def show
Expand All @@ -23,7 +23,7 @@ def show
def create
errored = T.let(false, T::Boolean)
errors = {
organizations: organizations_params[:organizations].map do |_|
bill_organization: bill_organization_params[:bill_organization].map do |_|
{
label: nil,
value: nil,
Expand All @@ -34,38 +34,38 @@ def create
end
}

organizations_params[:organizations].each_with_index do |param, index|
organization = find_or_initialize_organization_from_param(param)
current_icon_path = organization.icon_path.freeze
organization.icon_path = param[:icon_path]
organizations_params[:bill_organization].each_with_index do |param, index|
bill_organization = find_or_initialize_organization_from_param(param)
current_icon_path = bill_organization.icon_path.freeze
bill_organization.icon_path = param[:icon_path]

if organization.save
organization.remove_icon(current_icon_path)
if bill_organization.save
bill_organization.remove_icon(current_icon_path)

position = OrganizationBillPosition.find_or_initialize_by(organization:, bill: @bill)
position = OrganizationBillPosition.find_or_initialize_by(bill_organization:, bill: @bill)
position.support = param[:support]
position.summary = param[:summary]

unless position.save
errored = true
position.errors.each do |e|
if errors[:organizations][index].key?(e.attribute)
errors[:organizations][index][e.attribute] = "#{e.attribute.capitalize} #{e.message}"
if errors[:bill_organization][index].key?(e.attribute)
errors[:bill_organization][index][e.attribute] = "#{e.attribute.capitalize} #{e.message}"
end
end
end
else
errored = true
organization.errors.each do |e|
bill_organization.errors.each do |e|
attribute_by_key = {
name: :label,
id: :value,
icon_path: :icon_path
}

attr = attribute_by_key[e.attribute]
if attr.present? && errors[:organizations][index].key?(attr)
errors[:organizations][index][attr] = "#{attr} #{e.message.capitalize}"
if attr.present? && errors[:bill_organization][index].key?(attr)
errors[:bill_organization][index][attr] = "#{attr} #{e.message.capitalize}"
end
end
end
Expand All @@ -85,11 +85,11 @@ def set_bill
end

def organizations_params
params.permit(:bill_id, organizations: %i[label value summary support icon_path])
params.permit(:bill_id, bill_organization: %i[label value summary support icon_path])
end

def find_or_initialize_organization_from_param(param)
o = param[:value].blank? ? nil : Organization.find_by(id: param[:value]).presence
o || Organization.find_or_initialize_by(name: param[:label], sway_locale: @bill.sway_locale)
o = param[:value].blank? ? nil : BillOrganization.find_by(id: param[:value]).presence
o || BillOrganization.find_or_initialize_by(name: param[:label], sway_locale: @bill.sway_locale)
end
end
9 changes: 5 additions & 4 deletions app/controllers/bills_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def new
bill: Bill.new.attributes,
legislators: current_sway_locale&.legislators&.map(&:to_sway_json),
legislatorVotes: [],
organizations: Organization.where(sway_locale: current_sway_locale).map(&:to_sway_json),
bill_organization: BillOrganization.where(sway_locale: current_sway_locale).map(&:to_sway_json),
tabKey: params[:tab_key]
})
end
Expand All @@ -51,7 +51,7 @@ def edit
end,
legislators: current_sway_locale&.legislators&.map(&:to_sway_json),
legislatorVotes: @bill.legislator_votes.map(&:to_sway_json),
organizations: Organization.where(sway_locale: current_sway_locale).map(&:to_sway_json),
bill_organization: BillOrganization.where(sway_locale: current_sway_locale).map(&:to_sway_json),
tabKey: params[:tab_key]
})
end
Expand Down Expand Up @@ -184,8 +184,9 @@ def params
super.transform_keys(&:underscore)
end

def remove_icon(organization, current_icon_path)
return unless organization.icon_path != current_icon_path
sig { params(bill_organization: BillOrganization, current_icon_path: T.nilable(String)) }
def remove_icon(bill_organization, current_icon_path)
return unless bill_organization.icon_path != current_icon_path

delete_file(bucket_name: SwayGoogleCloudStorage::BUCKETS[:ASSETS], file_name: current_icon_path)
end
Expand Down
37 changes: 37 additions & 0 deletions app/controllers/concerns/api_key_authenticatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# typed: true

module ApiKeyAuthenticatable
extend ActiveSupport::Concern
extend T::Sig

include ActionController::HttpAuthentication::Basic::ControllerMethods
include ActionController::HttpAuthentication::Token::ControllerMethods

attr_reader :current_api_key
attr_reader :current_bearer

# Use this to raise an error and automatically respond with a 401 HTTP status
# code when API key authentication fails
def authenticate_with_api_key!
@current_bearer = authenticate_or_request_with_http_token(&method(:authenticator))
end

# Use this for optional API key authentication
def authenticate_with_api_key
@current_bearer = authenticate_with_http_token(&method(:authenticator))
end

private

attr_writer :current_api_key
attr_writer :current_bearer

def authenticator(http_token, options)
@current_api_key = ApiKey.find_by(token: http_token)
@current_api_key = ApiKey.authenticate_by_token(http_token)

@current_api_key.update(last_used_on_utc: Time.zone.now)

current_api_key&.bearer
end
end
9 changes: 5 additions & 4 deletions app/controllers/concerns/pages.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
module Pages
HOME = "Home"
LEGISLATORS = "Legislators"
REGISTRATION = "Registration"
API_KEYS = "ApiKeys"
BILL = "Bill"
BILLS = "Bills"
BILL_OF_THE_WEEK = "BillOfTheWeek"
BILL_CREATOR = "BillOfTheWeekCreatorPage"
BILL_OF_THE_WEEK = "BillOfTheWeek"
HOME = "Home"
INFLUENCE = "Influence"
INVITE = "Invite"
LEGISLATORS = "Legislators"
NOTIFICATIONS = "Notifications"
REGISTRATION = "Registration"
end
Loading

0 comments on commit c0a910d

Please sign in to comment.