Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5e33cc2
Remove unused UserAccessKeyEncryptor (#6327)
May 10, 2022
4fcdd08
i18n: Support array of string values in JavaScript implementation (#6…
aduth May 11, 2022
c3cea3b
Add feature flag for including SLO in SAML metadata (#6330)
orenyk May 11, 2022
1f74e88
Update knapsack_rspec_report.json (#6332)
aduth May 11, 2022
ee696de
LG-6139: Implement submission form for password confirmation step (#6…
aduth May 11, 2022
2ff6ebe
LG-5934 Document the rest of analytics events (#6331)
stevegsa May 11, 2022
700f883
Describe changelog requirement in contributing guide (#6333)
aduth May 11, 2022
ceab799
Regenerate personal key for users with malformed recovery data (LG-6…
zachmargolis May 11, 2022
61c54f6
Fix margins for "Forgot Password" form (#6334)
aduth May 11, 2022
f25da61
LG-6111-back-event-tracking-personal-key-dl (#6335)
gsa-manish May 11, 2022
0ba9c5f
LG-6271: Validate IdV app values to push user to the correct first st…
aduth May 12, 2022
5e82072
Permit GET requests for SAML Remote logout (#6344)
orenyk May 12, 2022
e02c665
LG 5930 Document Analytics 12 (#6346)
theabrad May 12, 2022
877665b
LG-6139: Handle and display password confirm errors (#6337)
aduth May 13, 2022
f8c279f
Support non-input element ref assignment for FormSteps (#6340)
aduth May 13, 2022
f3ed9d5
Show "Login.gov" app name in IdV app password confirmation step (#6342)
aduth May 13, 2022
7e51b93
LG-6291: Add password field with toggled visibility to IdV app passwo…
aduth May 13, 2022
0e8c7e1
Update sp-logos archive to include original names for fallback (#6343)
julialeague May 13, 2022
a36bb75
Remove default wrapper and margin from hidden inputs (#6347)
aduth May 16, 2022
cc6364b
LG-6314: Add Start Over and Cancel links to password confirmation (#6…
aduth May 16, 2022
48d8568
Partial KMS session encryption (#6315)
May 16, 2022
1f9583f
Merge remote-tracking branch 'origin/stages/prod' into stages/rc-2022…
zachmargolis May 17, 2022
a702399
Import custom element definitions directly (#6357)
aduth May 16, 2022
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
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ pull request is about.
- If the pull request is in response to a Jira ticket, include the ticket ID in
the commit title (e.g. "LG-1234 Add the stuff to the thing")

- Include a changelog message which describes the changes in human-readable
terms. These messages are included in release notes, so they should be easy to
understand for our partners and users. In the rare case that a change should
not be included in release notes, add `[skip changelog]` to the commit.

Example:

```
Expand All @@ -41,8 +46,15 @@ and making development less efficient.
meant to change, and so that only one database call is made.
- To prevent the data from being wiped out after each spec, configure
Database Cleaner to ignore those static tables.

changelog: Internal, Automated Testing, Improve performance of test suite
```

Refer to the [changelog check script] for a complete list of acceptable
changelog categories.

[changelog check script]: https://github.com/18F/identity-idp/blob/main/scripts/changelog_check.rb

### Style, Readability, and OO
- Rubocop or Reek offenses are not disabled unless they are false positives.
If you're not sure, please ask a teammate.
Expand Down
2 changes: 1 addition & 1 deletion app/components/clipboard_button_component.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import '@18f/identity-clipboard-button';
import '@18f/identity-clipboard-button/clipboard-button-element';
4 changes: 1 addition & 3 deletions app/components/password_toggle_component.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import { PasswordToggleElement } from '@18f/identity-password-toggle-element';

customElements.define('lg-password-toggle', PasswordToggleElement);
import '@18f/identity-password-toggle/password-toggle-element';
2 changes: 1 addition & 1 deletion app/components/print_button_component.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import '@18f/identity-print-button';
import '@18f/identity-print-button/print-button-element';
2 changes: 1 addition & 1 deletion app/components/spinner_button_component.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import '@18f/identity-spinner-button';
import '@18f/identity-spinner-button/spinner-button-element';
2 changes: 1 addition & 1 deletion app/components/validated_field_component.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import '@18f/identity-validated-field';
import '@18f/identity-validated-field/validated-field-element';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Api
module Verify
class CompleteController < Api::BaseController
class PasswordConfirmController < Api::BaseController
def create
result, personal_key = Api::ProfileCreationForm.new(
password: verify_params[:password],
Expand Down
1 change: 1 addition & 0 deletions app/controllers/frontend_log_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class FrontendLogController < ApplicationController
'IdV: personal key submitted' => :idv_personal_key_submitted,
'IdV: personal key confirm visited' => :idv_personal_key_confirm_visited,
'IdV: personal key confirm submitted' => :idv_personal_key_confirm_submitted,
'IdV: download personal key' => :idv_personal_key_downloaded,
}.transform_values { |method| AnalyticsEvents.instance_method(method) }.freeze

def create
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/idv/forgot_password_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ class ForgotPasswordController < ApplicationController
before_action :confirm_idv_needed

def new
analytics.track_event(Analytics::IDV_FORGOT_PASSWORD)
analytics.idv_forgot_password
end

def update
analytics.track_event(Analytics::IDV_FORGOT_PASSWORD_CONFIRMED)
analytics.idv_forgot_password_confirmed
request_id = sp_session[:request_id]
email = current_user.email
reset_password(email, request_id)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/idv/otp_delivery_method_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class OtpDeliveryMethodController < ApplicationController
before_action :set_idv_phone

def new
analytics.track_event(Analytics::IDV_PHONE_OTP_DELIVERY_SELECTION_VISIT)
analytics.idv_phone_otp_delivery_selection_visit
render :new, locals: { gpo_letter_available: gpo_letter_available }
end

Expand Down
6 changes: 2 additions & 4 deletions app/controllers/idv/phone_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ class PhoneController < ApplicationController
before_action :set_idv_form

def new
if params[:step]
analytics.track_event(Analytics::IDV_PHONE_USE_DIFFERENT, step: params[:step])
end
analytics.idv_phone_use_different(step: params[:step]) if params[:step]

redirect_to failure_url(:fail) and return if throttle.throttled?

async_state = step.async_state
if async_state.none?
analytics.track_event(Analytics::IDV_PHONE_RECORD_VISIT)
analytics.idv_phone_of_record_visited
render :new, locals: { gpo_letter_available: gpo_letter_available }
elsif async_state.in_progress?
render :wait
Expand Down
10 changes: 8 additions & 2 deletions app/controllers/idv/review_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class ReviewController < ApplicationController

before_action :confirm_idv_steps_complete
before_action :confirm_idv_phone_confirmed
before_action :redirect_to_idv_app_if_enabled
before_action :confirm_current_password, only: [:create]

def confirm_idv_steps_complete
Expand All @@ -30,7 +31,7 @@ def confirm_current_password
def new
@applicant = idv_session.applicant
@step_indicator_steps = step_indicator_steps
analytics.track_event(Analytics::IDV_REVIEW_VISIT)
analytics.idv_review_info_visited

gpo_mail_service = Idv::GpoMail.new(current_user)
flash_now = flash.now
Expand All @@ -45,7 +46,7 @@ def create
init_profile
user_session[:need_personal_key_confirmation] = true
redirect_to next_step
analytics.track_event(Analytics::IDV_REVIEW_COMPLETE)
analytics.idv_review_complete
analytics.idv_final(success: true)

return unless FeatureManagement.reveal_gpo_code?
Expand All @@ -54,6 +55,11 @@ def create

private

def redirect_to_idv_app_if_enabled
return if !IdentityConfig.store.idv_api_enabled_steps.include?('password_confirm')
redirect_to idv_app_path
end

def step_indicator_steps
steps = Idv::Flows::DocAuthFlow::STEP_INDICATOR_STEPS
return steps if idv_session.address_verification_mechanism != 'gpo'
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/idv_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def sp_over_quota_limit?
end

def verify_identity
analytics.track_event(Analytics::IDV_INTRO_VISIT)
analytics.idv_intro_visit
redirect_to idv_doc_auth_url
end

Expand Down
10 changes: 3 additions & 7 deletions app/controllers/verify_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ class VerifyController < ApplicationController

check_or_render_not_found -> { FeatureManagement.idv_api_enabled? }, only: [:show]

before_action :redirect_root_path_to_first_step
before_action :validate_step
before_action :confirm_two_factor_authenticated
before_action :confirm_idv_vendor_session_started
Expand All @@ -16,20 +15,17 @@ def show

private

def redirect_root_path_to_first_step
redirect_to idv_app_path(step: first_step) if params[:step].blank?
end

def validate_step
render_not_found if !enabled_steps.include?(params[:step])
render_not_found if params[:step].present? && !enabled_steps.include?(params[:step])
end

def app_data
user_session[:idv_api_store_key] ||= Base64.strict_encode64(random_encryption_key)

{
base_path: idv_app_path,
app_name: APP_NAME,
start_over_url: idv_session_path,
cancel_url: idv_cancel_path,
completion_url: completion_url,
initial_values: initial_values,
enabled_step_names: IdentityConfig.store.idv_api_enabled_steps,
Expand Down
14 changes: 7 additions & 7 deletions app/forms/api/profile_creation_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def submit

response = FormResponse.new(
success: form_valid?,
errors: errors.to_hash,
errors: errors,
extra: extra_attributes,
)
[response, personal_key]
Expand Down Expand Up @@ -105,20 +105,20 @@ def session

def valid_jwt
@user_bundle = Api::UserBundleDecorator.new(user_bundle: jwt, public_key: public_key)
rescue JWT::DecodeError => err
errors.add(:jwt, "decode error: #{err.message}", type: :invalid)
rescue ::Api::UserBundleError => err
errors.add(:jwt, "malformed user bundle: #{err.message}", type: :invalid)
rescue JWT::DecodeError
errors.add(:jwt, I18n.t('idv.failure.exceptions.internal_error'), type: :decode_error)
rescue ::Api::UserBundleError
errors.add(:jwt, I18n.t('idv.failure.exceptions.internal_error'), type: :user_bundle_error)
end

def valid_user
return if user
errors.add(:user, 'user not found', type: :invalid)
errors.add(:user, I18n.t('devise.failure.unauthenticated'), type: :invalid_user)
end

def valid_password
return if user&.valid_password?(password)
errors.add(:password, 'invalid password', type: :invalid)
errors.add(:password, I18n.t('idv.errors.incorrect_password'), type: :invalid_password)
end

def form_valid?
Expand Down
20 changes: 0 additions & 20 deletions app/javascript/packages/analytics/index.js

This file was deleted.

20 changes: 20 additions & 0 deletions app/javascript/packages/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getConfigValue } from '@18f/identity-config';

/**
* Logs an event.
*
* @param event Event name.
* @param payload Payload object.
*
* @return Promise resolving once event has been logged.
*/
export async function trackEvent(event: string, payload: object = {}): Promise<void> {
const endpoint = getConfigValue('analyticsEndpoint');
if (endpoint) {
await window.fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, payload }),
});
}
}
4 changes: 2 additions & 2 deletions app/javascript/packages/clipboard-button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ Custom element and React implementation for a clipboard button component.

### Custom Element

Importing the package will register the `<lg-clipboard-button>` custom element:
Importing the element will register the `<lg-clipboard-button>` custom element:

```ts
import '@18f/identity-clipboard-button';
import '@18f/identity-clipboard-button/clipboard-button-element';
```

The custom element will implement the copying behavior, but all markup must already exist, rendered server-side or by the included React component.
Expand Down
2 changes: 0 additions & 2 deletions app/javascript/packages/clipboard-button/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import './clipboard-button-element';

export { default as ClipboardButton } from './clipboard-button';
67 changes: 67 additions & 0 deletions app/javascript/packages/components/button-to.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import sinon from 'sinon';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ButtonTo from './button-to';

describe('ButtonTo', () => {
beforeEach(() => {
const csrf = document.createElement('meta');
csrf.name = 'csrf-token';
csrf.content = 'token-value';
document.body.appendChild(csrf);
});

it('renders props passed through to Button', () => {
const { getByRole } = render(
<ButtonTo url="" method="" isUnstyled>
Click me
</ButtonTo>,
);

const button = getByRole('button', { name: 'Click me' }) as HTMLButtonElement;

expect(button.type).to.equal('button');
expect(button.classList.contains('usa-button')).to.be.true();
expect(button.classList.contains('usa-button--unstyled')).to.be.true();
});

it('creates a form in the body outside the root container', () => {
const { container, getByRole } = render(
<ButtonTo url="/" method="delete" isUnstyled>
Click me
</ButtonTo>,
);

const form = document.querySelector('form')!;
expect(form).to.be.ok();
expect(container.contains(form)).to.be.false();
return Promise.all([
new Promise<void>((resolve) => {
form.addEventListener('submit', (event) => {
event.preventDefault();
expect(Object.fromEntries(new window.FormData(form))).to.deep.equal({
_method: 'delete',
authenticity_token: 'token-value',
});
resolve();
});
}),
userEvent.click(getByRole('button')),
]);
});

it('submits to form on click', async () => {
const { getByRole } = render(
<ButtonTo url="" method="" isUnstyled>
Click me
</ButtonTo>,
);

const form = document.querySelector('form')!;
sinon.stub(form, 'submit');

await userEvent.click(getByRole('button'));

expect(form.submit).to.have.been.calledOnce();
});
});
49 changes: 49 additions & 0 deletions app/javascript/packages/components/button-to.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useRef } from 'react';
import { createPortal } from 'react-dom';
import Button from './button';
import type { ButtonProps } from './button';

interface ButtonToProps extends ButtonProps {
/**
* URL to which the user should navigate.
*/
url: string;

/**
* Form method button should submit as.
*/
method: string;
}

/**
* Component which renders a button that navigates to the specified URL via form, with method
* parameterized as a hidden input and including authenticity token. The form is rendered to the
* document root, to avoid conflicts with nested forms.
*/
function ButtonTo({ url, method, children, ...buttonProps }: ButtonToProps) {
const formRef = useRef<HTMLFormElement>(null);
const csrfRef = useRef<HTMLInputElement>(null);

function submitForm() {
const csrf = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
if (csrf && csrfRef.current) {
csrfRef.current.value = csrf;
}
formRef.current?.submit();
}

return (
<Button {...buttonProps} onClick={submitForm}>
{children}
{createPortal(
<form ref={formRef} method="post" action={url}>
<input type="hidden" name="_method" value={method} />
<input ref={csrfRef} type="hidden" name="authenticity_token" />
</form>,
document.body,
)}
</Button>
);
}

export default ButtonTo;
Loading