-
Notifications
You must be signed in to change notification settings - Fork 166
LG-3204: Create Image Upload API #3985
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
Changes from all commits
324af20
ed40895
f9bf1c9
1870b3a
1e1ef4e
a9a58e0
ec7a44f
519a058
0870a2d
f4abfbe
7fd72f6
80b6467
e5f3c9c
190ba85
fc6654c
2bb4deb
ad271a0
d6fda5f
b220a13
55e1fcc
f8d26ed
a3acc63
de321a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| module Idv | ||
| class ImageUploadController < ApplicationController | ||
| include IdvSession | ||
|
|
||
| def upload | ||
| validation_error = validate_request(request) | ||
| response = validation_error || upload_and_check_images | ||
| render json: response | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def upload_and_check_images | ||
| doc_response = client.post_images(front_image: @front_image, | ||
| back_image: @back_image, | ||
| selfie_image: @selfie_image, | ||
| liveness_checking_enabled: liveness_checking_enabled?) | ||
| return error_json(doc_response.errors.first) unless doc_response.success? | ||
| upload_info = { | ||
| documents: doc_response.to_h, | ||
| } | ||
| store_pii(doc_response) | ||
| user_session['idv/doc_auth']['api_upload'] = upload_info | ||
| success_json('Uploaded images') | ||
| end | ||
|
|
||
| def store_pii(doc_response) | ||
| user_session['idv/doc_auth'][:pii_from_doc] = doc_response.pii_from_doc.merge( | ||
| uuid: current_user.uuid, | ||
| phone: current_user.phone_configurations.take&.phone, | ||
| ) | ||
| end | ||
|
|
||
| def validate_request(request) | ||
| steps = %i[check_content_type check_image_fields] | ||
| steps.each do |step| | ||
| err = method(step).call(request) | ||
| return error_json(err) if err | ||
| end | ||
| nil | ||
| end | ||
|
|
||
| def check_content_type(request) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I marked my original comment as resolved, but flagging @zachmargolis's comment at #3985 (comment) for follow-up.
|
||
| "Invalid content type #{request.content_type}" if request.content_type != 'application/json' | ||
| end | ||
aduth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def check_image_fields(request) | ||
| data = request.body.read | ||
| @front_image = data['front'] | ||
| @back_image = data['back'] | ||
| @selfie_image = data['selfie'] | ||
| 'Missing image keys' unless [@front_image, @back_image, @selfie_image].all? | ||
| end | ||
|
Comment on lines
+47
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has a side effect of setting the instance variables, so I think the name |
||
|
|
||
| def error_json(reason) | ||
| { | ||
| status: 'error', | ||
| message: reason, | ||
| } | ||
| end | ||
|
Comment on lines
+55
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What status code are we returning in the error case? Is it still going to be Asking because it's something where we should probably want the status code to match the response status here, and in doing so, we could perform success / error handling on the basis of status code alone. i.e. we wouldn't need |
||
|
|
||
| def success_json(reason) | ||
| { | ||
| status: 'success', | ||
| message: reason, | ||
| } | ||
| end | ||
|
|
||
| def client | ||
| @client ||= DocAuthClient.client | ||
| end | ||
| end | ||
| end | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import React, { createContext } from 'react'; | ||
| import defaultUpload from '../services/upload'; | ||
|
|
||
| const UploadContext = createContext(defaultUpload); | ||
|
|
||
| /** @typedef {import('react').ReactNode} ReactNode */ | ||
|
|
||
| /** | ||
| * @typedef {(payload:Record<string,any>,csrf?:string)=>Promise<any>} UploadImplementation | ||
| */ | ||
|
|
||
| /** | ||
| * @typedef UploadContextProviderProps | ||
| * | ||
| * @prop {UploadImplementation=} upload Custom upload implementation. | ||
| * @prop {string=} csrf CSRF token to send as parameter to upload implementation. | ||
| * @prop {ReactNode} children Child elements. | ||
| */ | ||
|
|
||
| /** | ||
| * @param {UploadContextProviderProps} props Props object. | ||
| */ | ||
| function UploadContextProvider({ upload = defaultUpload, csrf, children }) { | ||
| const uploadWithCSRF = (payload) => upload(payload, csrf); | ||
|
|
||
| return <UploadContext.Provider value={uploadWithCSRF}>{children}</UploadContext.Provider>; | ||
| } | ||
|
|
||
| export default UploadContext; | ||
| export { UploadContextProvider as Provider }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,30 @@ | ||
| function upload(payload) { | ||
| return new Promise((resolve, reject) => { | ||
| const isFailure = window.location.search === '?fail'; | ||
| setTimeout(isFailure ? reject : () => resolve({ ...payload, saved: true }), 2000); | ||
| }); | ||
| /** | ||
| * Endpoint to which payload is submitted. | ||
| * | ||
| * @type {string} | ||
| */ | ||
| const ENDPOINT = '/api/verify/upload'; | ||
|
|
||
| /** | ||
| * @type {import('../context/upload').UploadImplementation} | ||
| */ | ||
| function upload(payload, csrf) { | ||
| return fetch(ENDPOINT, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'X-CSRF-Token': csrf, | ||
| }, | ||
| body: JSON.stringify(payload), | ||
| }) | ||
| .then((response) => response.json()) | ||
| .then((result) => { | ||
| if (!['success', 'error'].includes(result.status)) { | ||
| throw Error('Malformed response'); | ||
| } | ||
|
|
||
| return result; | ||
| }); | ||
| } | ||
|
|
||
| export default upload; |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,18 +2,28 @@ module Idv | |||||||
| module Steps | ||||||||
| class DocumentCaptureStep < DocAuthBaseStep | ||||||||
| def call | ||||||||
| response = post_images | ||||||||
| if response.success? | ||||||||
| api_upload = flow_session['api_upload'] | ||||||||
| response = (api_upload && api_upload['documents']) || post_form_images | ||||||||
|
Comment on lines
+5
to
+6
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can take advantage of
Suggested change
|
||||||||
| handle_response(response) | ||||||||
| end | ||||||||
|
|
||||||||
| private | ||||||||
|
|
||||||||
| def post_form_images | ||||||||
| selfie = selfie_image ? selfie_image.read : nil | ||||||||
| post_images(front_image.read, back_image.read, selfie) | ||||||||
|
Comment on lines
+13
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do the nil-safe operator (or
Suggested change
|
||||||||
| end | ||||||||
|
|
||||||||
| def handle_response(response) | ||||||||
| if response.to_h[:success] | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. usually our response objects have
Suggested change
|
||||||||
| save_proofing_components | ||||||||
| extract_pii_from_doc(response) | ||||||||
| extract_pii_from_doc(response) unless flow_session[:pii_from_doc] | ||||||||
| response | ||||||||
| else | ||||||||
| handle_document_verification_failure(response) | ||||||||
| end | ||||||||
| end | ||||||||
|
|
||||||||
| private | ||||||||
|
|
||||||||
| def handle_document_verification_failure(response) | ||||||||
| mark_step_incomplete(:document_capture) | ||||||||
| notice = if liveness_checking_enabled? | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,9 @@ | |
| # Twilio Request URL for inbound SMS | ||
| post '/api/sms/receive' => 'sms#receive' | ||
|
|
||
| # Image upload API for doc auth | ||
| post '/api/verify/upload' => 'idv/image_upload#upload' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sometimes we try to stick to built-in rails method names/actions to be "RESTful"1 as much as we can, WDYT of renaming this to
|
||
|
|
||
| post '/api/service_provider' => 'service_provider#update' | ||
| match '/api/voice/otp' => 'voice/otp#show', | ||
| via: %i[get post], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With only two steps.... I don't think this is a great use for this level of dynamic behavior, there are a few alternatives I would prefer (in order of most to least preferred)
Let's make a Form object -- we do this is many other controllers, make a form object that performs the validations and returns useful error structures the controllers know how to render (LMK if you need a more detailed example)
Move these into before actions, so that way they can just render and stop the chain (instead of having a loop to check for errors and stop)
Just call the methods directly: