From 906abf09d3e0c5d30301d819f0a1a0639f882dd8 Mon Sep 17 00:00:00 2001 From: Matt Gardner Date: Tue, 7 May 2024 16:48:53 -0400 Subject: [PATCH 1/4] Include CSRF token as part of API request --- app/controllers/api/argyle_controller.rb | 2 - app/javascript/utilities/argyle.js | 8 +++- app/javascript/utilities/csrf.ts | 36 ++++++++++++++++++ db/schema.rb | 5 ++- doc/compliance/rendered/apps/data.logical.pdf | Bin 29392 -> 29419 bytes 5 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 app/javascript/utilities/csrf.ts diff --git a/app/controllers/api/argyle_controller.rb b/app/controllers/api/argyle_controller.rb index 795f7e375..2de0cd9d8 100644 --- a/app/controllers/api/argyle_controller.rb +++ b/app/controllers/api/argyle_controller.rb @@ -1,6 +1,4 @@ class Api::ArgyleController < ApplicationController - skip_before_action :verify_authenticity_token - USER_TOKEN_ENDPOINT = 'https://api-sandbox.argyle.com/v2/user-tokens'; def update_token diff --git a/app/javascript/utilities/argyle.js b/app/javascript/utilities/argyle.js index 7a2347208..d564e00c7 100644 --- a/app/javascript/utilities/argyle.js +++ b/app/javascript/utilities/argyle.js @@ -1,5 +1,6 @@ import loadScript from 'load-script'; import metaContent from "./meta"; +import CSRF from './csrf'; const ARGYLE_TOKENS_REFRESH = '/api/argyle/tokens'; @@ -24,7 +25,12 @@ export function initializeArgyle(Argyle, userToken, callbacks) { } export const updateToken = async updateToken => { - const response = await fetch(ARGYLE_TOKENS_REFRESH, { method: 'post' }).then(response => response.json()); + const response = await fetch(ARGYLE_TOKENS_REFRESH, { + method: 'post', + headers: { + 'X-CSRF-Token': CSRF.token, + }, + }).then(response => response.json()); updateToken(response.token); } diff --git a/app/javascript/utilities/csrf.ts b/app/javascript/utilities/csrf.ts new file mode 100644 index 000000000..54a53355f --- /dev/null +++ b/app/javascript/utilities/csrf.ts @@ -0,0 +1,36 @@ +// Borrowed from https://github.com/18F/identity-idp/blob/59bc8bb6c47402f386d9248bfad3c0803f68187e/app/javascript/packages/request/index.ts#L25-L59 +export default class CSRF { + static get token(): string | null { + return this.#tokenMetaElement?.content || null; + } + + static set token(value: string | null) { + if (!value) { + return; + } + + if (this.#tokenMetaElement) { + this.#tokenMetaElement.content = value; + } + + this.#paramInputElements.forEach((input) => { + input.value = value; + }); + } + + static get param(): string | undefined { + return this.#paramMetaElement?.content; + } + + static get #tokenMetaElement(): HTMLMetaElement | null { + return document.querySelector('meta[name="csrf-token"]'); + } + + static get #paramMetaElement(): HTMLMetaElement | null { + return document.querySelector('meta[name="csrf-param"]'); + } + + static get #paramInputElements(): NodeListOf { + return document.querySelectorAll(`input[name="${this.param}"]`); + } +} diff --git a/db/schema.rb b/db/schema.rb index ae447aed5..bbada8ef8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -27,10 +27,11 @@ t.date "payroll_data_available_from" end - create_table "connected_argyle_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + create_table "connected_argyle_accounts", force: :cascade do |t| t.uuid "user_id", null: false t.uuid "account_id", null: false - t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["user_id", "account_id"], name: "index_connected_argyle_accounts_on_user_id_and_account_id", unique: true end diff --git a/doc/compliance/rendered/apps/data.logical.pdf b/doc/compliance/rendered/apps/data.logical.pdf index 0b07fc5358a234814e471b1c1985720e744d1bca..3909a52097676bccfbaea1cedc78fb550522f832 100644 GIT binary patch delta 1691 zcmV;M24wlr~G=<^pV_nfg8X=1b+Znnk6K?G|q+9Ln& zAnkHinqUXSp@=9gN`9Y|3;aK=lg(cJ0*gKacnSZ1$m_S1b&PHaXY@C!Smzk0T<}f{ zNiS-GWUBLKKkli2hwgX1p7=NnUc;l zi-22=v2cjeP7#qolRRz5%=sJ@EZoHsDoV?is%+iUKL?kam{2PDcY)S)Hh8sy zt!(G5(MBZy7K4j?4?d!s>lPdZhh57pxEgm&-wFw?#~C<(9ZY<@?=Mz#&iavCWJY7!_i z2TC|d2b!e+K?Mcekg>-U+|FqgKOuA_#GwP)j8>A5qF0Wi7nZ(h!ii0g^&Jve_lVD#HNIp%%6@)~q`D7m6<^a=f1=^T@B>Rd|U^-0&2lix*v|BM&W$VSs zR{C(tquDlB5Li+`l>8ig5L50|pnblVW0mur5q@xnOi1Qgm|~STIfFr9JMU(L1>*!W z?0NXQ!;HHNDq=KaAY;UTz?t`j9cM6}wkV8I1SM_pn_qcW(+WzgunU7qkp z7V>g`S`FTNgf^88@c5U(L};CmQ{IJ-WS~7s;RJ(d*YTq|KD=gK#Ar&4iunq}nO9~xNglr7^Ody$(4>b>= zOmb{8Z)3lic1w^9>STsQ$S@zL#{GzbCLv|VY_rV_SODt}HxSfbRke6iSW+rZ}&{LJ?P^*xLims^cy;tHsSSC0Tr7CFO+9 zeFxY}?`bH`v}GFTNB3J$7)!nR@m>2lkMu#0*8(F=ZYom;6Eg z1JF_Y5R=FR9|AWxv*`p%Ykxs&gfI|=@A(yTDb+(YiMnNt@IFLaD`$d ziKYARJJHora+;4fdG9d{D9IRPlyG1fBN>MzPf1Zg{C*o1#I@*@B>)h=_N|2v;6R~) z&l#0^8XPckJkA@n(d{Iafk2AhXkZb0jwl3I=SF*o)37 zaTo^h_kTEN!xwN#W`k^Q8Ra7=IVZUt4*gR$$!eo)N(N;&nB)^!%*v$r0*uG=Wcl@8 zhWC9$#Pz;r0)$NI+>WG!p#)GX*Melc;hZ2{twgB_%~qMhf$H8sh)} delta 1702 zcmV;X23h&*X7n*OS5x~r<|YEg0ctpp6hiK@F||1nOq+so?NhF6y#*^g@` zWE(d!ES`ln_kef=KB zO!))mrrr?co+!64s`{EJyJi(&c(;Cqn~JjO^A{`sobwlHVQ?B9wxy483tnF}W&eM0 zxBcvHk{{WQwurmMoj)YukpJg(vf1lj;iE4AUcvug_U2v1TV8FM;MMPy=B?+V5>m8A zs_JS=P+Ygc9>!gNr36O`>sf@2EW(U259O0FCF9@_vI!o15b_ChWaZ6BrUbi8V40G1 znnl2^hgf<->7-|P9~Dr-4Ow?e!0nt=aSEZUAPzmyW~5SL5WQ*`z3}PV8a1}K*AGPCIDOm9 zZUowiype%_%vkg`a6(!tH@NgpgitM)^E%(5FgL~g8#F(vOm`&9ort4%BG0-L#g_>9 z)Gh_WN*PES2KcmSrGcoL5G*7JDGtG|vXC%H84Lp32R|Du6epBn zFL8EH8Fz;wQZ!Q_Q^bFyl@EoTRw$mf4Lcf4UlZbB!39D`Sz<_&FeJwwCAw^uC%l1$ zf*N;!gAa<(W?}tIfKkCSAe14-cwgBah6lQKTOK?Jw0Li4-tsw^7_Wi3xi z_|!_k;#(1r2@d$cP0{(jzn^QwJBVi!J$7oC&A3{BZdh{mWGKo6v_#&W6GOV*3o85h zAPHujv4lV8o~`8No{#AK@Se{ztC9zE&9`fRcZ_yR1bk|~4i(ONo8C7b%G(eH!_zBr zJnsP6eg&#TdRfX0z)vi)4N#uLlZ``tpTM3Mo-Q2wfqVp19^y~uC)iC1S?*bgZHo1L z#Nf(%atMn!6GDQFRLN^m$$~)rdJQ+1WPzT?Gh~YE8b6frYufhi$h*@EpWdD5F&QPpE^LUrH08~CxRg>P z#R?b4>(T=4``C}&&vq^vWH=|-Jv`k{*ZI35OX|GlqEQ}-M*FpuXJE1Kv>)&g2@5)w zr|qs_QVO!Wx6(t1!3`86Ni5xO-$YkS$!UJP$$O8X zfP_(CNHU;|5|%-}DM(pDdc6-4(uQ}^5&%fwRBPb_GC(-P=Y%Rf_6{gH9Hx!N=ynv+ zKqR;_8fZed2|{#pZm|0_ooHwHa8tr2+JAI$Jbz;(cS`p)cM_s{pJ7%Y!<^>G*!BgB zXT*3h`HKZl-a70z+{ln4B%8pyY!yH1A0HR}+wFY04P8U`&dV(ntNvv4I6zs3AF}N)bQR8!Asm1{0h#Da+xZxs^zy5@_af5=rad?mR65ONDKMudGU+F&C#vD!W6%H z6?gngcaL`d0*`WD^b0CIZrhV#aZUjo037<4JLU77PB%b-T?2w#v3@_Z!LfzA0sV_yQtjFuk*Cg7k^pfMJn3$o4>CLk^&rClydyfYz0~ zKzm7cT%h{}PLvp+UnLG0%z&F9bFwA8=Dzu95{yl Date: Tue, 7 May 2024 17:41:10 -0400 Subject: [PATCH 2/4] Partially functional implementation of employer search Add permissive data filter Allow clearing Only include verified/mappe Updated WIP Use Hotwire Switch to cards Alt formaction Fix modal bug Remove unused Fixes USWDS initialization bug Rework the form --- app/assets/images/uswds.js | 1 + app/controllers/cbv_flows_controller.rb | 11 +++- app/javascript/application.js | 8 ++- .../controllers/cbv_flows_controller.js | 35 +++--------- app/views/cbv_flows/_employer.html.erb | 31 ++++++++++ app/views/cbv_flows/employer_search.html.erb | 56 +++++++++---------- app/views/pages/home.html.erb | 2 +- 7 files changed, 85 insertions(+), 59 deletions(-) create mode 100644 app/views/cbv_flows/_employer.html.erb diff --git a/app/assets/images/uswds.js b/app/assets/images/uswds.js index c3227067a..1ea55bd84 100644 --- a/app/assets/images/uswds.js +++ b/app/assets/images/uswds.js @@ -4,3 +4,4 @@ //= link @uswds/uswds/dist/img/icon-dot-gov.svg //= link @uswds/uswds/dist/img/icon-https.svg //= link @uswds/uswds/dist/img/usa-icons/close.svg +//= link @uswds/uswds/dist/img/usa-icons-bg/search--white.svg diff --git a/app/controllers/cbv_flows_controller.rb b/app/controllers/cbv_flows_controller.rb index d7abde93e..9c3a2aea4 100644 --- a/app/controllers/cbv_flows_controller.rb +++ b/app/controllers/cbv_flows_controller.rb @@ -10,7 +10,8 @@ def entry def employer_search @argyle_user_token = fetch_and_store_argyle_token - @companies = fetch_employers + @query = search_params[:query] + @employers = @query.blank? ? [] : fetch_employers(@query) end def summary @@ -71,8 +72,8 @@ def fetch_and_store_argyle_token parsed['user_token'] end - def fetch_employers - res = Net::HTTP.get(URI.parse(ITEMS_ENDPOINT), {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"}) + def fetch_employers(query = '') + res = Net::HTTP.get(URI.parse("#{ITEMS_ENDPOINT}?mapping_status=verified,mapped&q=#{query}"), {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"}) parsed = JSON.parse(res) parsed['results'] @@ -84,4 +85,8 @@ def fetch_payroll parsed['results'] end + + def search_params + params.permit(:query) + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index d887608a7..b2d64b4dd 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -7,15 +7,21 @@ import "@uswds/uswds" // make sure USWDS components are wired to their behavior after a Turbo navigation import components from "@uswds/uswds/src/js/components" let initialLoad = true; + document.addEventListener("turbo:load", () => { if (initialLoad) { // initial domready is handled by `import "uswds"` code initialLoad = false return } + const target = document.body Object.keys(components).forEach((key) => { const behavior = components[key] behavior.on(target) }) -}) +}); + +document.addEventListener("turbo:frame-render", () => { + initialLoad = true; +}); diff --git a/app/javascript/controllers/cbv_flows_controller.js b/app/javascript/controllers/cbv_flows_controller.js index e1ecb2d26..330967288 100644 --- a/app/javascript/controllers/cbv_flows_controller.js +++ b/app/javascript/controllers/cbv_flows_controller.js @@ -4,14 +4,13 @@ import * as ActionCable from '@rails/actioncable' import metaContent from "../utilities/meta"; import { loadArgyle, initializeArgyle, updateToken } from "../utilities/argyle" -function toOptionHTML({ value }) { - return ``; -} - export default class extends Controller { - static targets = ["options", "continue", "userAccountId", "fullySynced", "form", "modal"]; - - selection = null; + static targets = [ + "form", + "searchTerms", + "userAccountId", + "modal" + ]; argyle = null; @@ -19,10 +18,6 @@ export default class extends Controller { cable = ActionCable.createConsumer(); - // TODO: information stored on the CbvFlow model can infer whether the paystubs are sync'd - // by checking the value of payroll_data_available_from. We should make that the initial value. - fullySynced = false; - connect() { // check for this value when connected this.argyleUserToken = metaContent('argyle_user_token'); @@ -34,10 +29,7 @@ export default class extends Controller { console.log("Disconnected"); }, received: (data) => { - console.log("Received some data:", data); if (data.event === 'paystubs.fully_synced' || data.event === 'paystubs.partially_synced') { - this.fullySynced = true; - this.formTarget.submit(); } } @@ -54,23 +46,14 @@ export default class extends Controller { console.log(event); } - search(event) { - const input = event.target.value; - this.optionsTarget.innerHTML = [this.optionsTarget.innerHTML, toOptionHTML({ value: input })].join(''); - } - select(event) { - this.selection = event.detail; - - this.continueTarget.disabled = false; + this.submit(event.target.dataset.itemId); } - submit(event) { - event.preventDefault(); - + submit(itemId) { loadArgyle() .then(Argyle => initializeArgyle(Argyle, this.argyleUserToken, { - items: [this.selection.value], + items: [itemId], onAccountConnected: this.onSignInSuccess.bind(this), onAccountError: this.onAccountError.bind(this), // Unsure what these are for! diff --git a/app/views/cbv_flows/_employer.html.erb b/app/views/cbv_flows/_employer.html.erb new file mode 100644 index 000000000..ec3bbb3a0 --- /dev/null +++ b/app/views/cbv_flows/_employer.html.erb @@ -0,0 +1,31 @@ +<%= turbo_frame_tag 'employers' do %> +
+ <% @employers.each do |employer| %> +
+
+
+

<%= employer['name'] %>

+
+ + +
+
+ <% end %> +
+<% end %> diff --git a/app/views/cbv_flows/employer_search.html.erb b/app/views/cbv_flows/employer_search.html.erb index 90107ae4c..81dac0488 100644 --- a/app/views/cbv_flows/employer_search.html.erb +++ b/app/views/cbv_flows/employer_search.html.erb @@ -6,39 +6,39 @@

Get payment info from your employer.

- <%= form_with url: next_path, method: :post, data: { 'cbv-flows-target': "form" } do |f| %> - -
- - -
- - <%= f.submit "Continue", - class: "usa-button usa-button--outline", - disabled: "disabled", - type: "submit", - data: { action: "click->cbv-flows#submit", 'cbv-flows-target': "continue" } - %> - +

Search for an employer

+ <%= form_with url: cbv_flow_employer_search_path, method: :get, class: 'usa-search usa-search--big', html: { role: 'search' }, data: { turbo_frame: 'employers', turbo_action: 'advance' } do |f| %> + <%= f.label :query, "Search for your employer", class: "usa-sr-only" %> + <%= f.text_field :query, value: @query, class: "usa-input", type: 'search', data: { "cbv-flows-target": "searchTerms" } %> + + <% end %> + +

Results

+ <%= render partial: 'employer', locals: { employer: @employers } %> + + <%= form_with url: cbv_flow_summary_path, method: :post, class: 'display-none', data: { 'cbv-flows-target': "form" } do |f| %> + <% end %> +
Welcome to Verify.gov -

This website can help you get approved for Medicaid

+

This website can help you get approved for your benefits.

Get started here by requesting a link from your caseworker.

From ab5c530d66cf105b3c10f85287a772f93327422a Mon Sep 17 00:00:00 2001 From: Matt Gardner Date: Tue, 14 May 2024 11:57:32 -0400 Subject: [PATCH 3/4] Use flow path helper --- app/views/cbv_flows/employer_search.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/cbv_flows/employer_search.html.erb b/app/views/cbv_flows/employer_search.html.erb index 81dac0488..e05d426a0 100644 --- a/app/views/cbv_flows/employer_search.html.erb +++ b/app/views/cbv_flows/employer_search.html.erb @@ -24,7 +24,7 @@

Results

<%= render partial: 'employer', locals: { employer: @employers } %> - <%= form_with url: cbv_flow_summary_path, method: :post, class: 'display-none', data: { 'cbv-flows-target': "form" } do |f| %> + <%= form_with url: next_path, method: :post, class: 'display-none', data: { 'cbv-flows-target': "form" } do |f| %> <% end %> From 90d17e39ba7cafb62b41a364cdd9b0af83645608 Mon Sep 17 00:00:00 2001 From: Matt Gardner Date: Tue, 14 May 2024 13:44:40 -0400 Subject: [PATCH 4/4] Update app/controllers/cbv_flows_controller.rb Co-authored-by: Tom Dooner --- app/controllers/cbv_flows_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/cbv_flows_controller.rb b/app/controllers/cbv_flows_controller.rb index 9c3a2aea4..59fece5ff 100644 --- a/app/controllers/cbv_flows_controller.rb +++ b/app/controllers/cbv_flows_controller.rb @@ -73,7 +73,11 @@ def fetch_and_store_argyle_token end def fetch_employers(query = '') - res = Net::HTTP.get(URI.parse("#{ITEMS_ENDPOINT}?mapping_status=verified,mapped&q=#{query}"), {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"}) + request_params = URI.encode_www_form( + mapping_status: 'verified,mapped', + q: query + ) + res = Net::HTTP.get(URI(ITEMS_ENDPOINT).tap { |u| u.query = request_params }, {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"}) parsed = JSON.parse(res) parsed['results']