Merge pull request #1033 from dod-ccpo/ajax-uploads

Get presigned cloud upload token through an ajax call
This commit is contained in:
richard-dds 2019-08-29 10:07:40 -04:00 committed by GitHub
commit b3e48aa6e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 80 additions and 52 deletions

View File

@ -1,9 +1,9 @@
{ {
"exclude": { "exclude": {
"files": null, "files": "^.secrets.baseline$",
"lines": null "lines": null
}, },
"generated_at": "2019-08-21T18:40:15Z", "generated_at": "2019-08-28T19:50:39Z",
"plugins_used": [ "plugins_used": [
{ {
"base64_limit": 4.5, "base64_limit": 4.5,
@ -38,7 +38,7 @@
"hashed_secret": "d141ce86b0584abb29ee7c24af9afb1e3d871f04", "hashed_secret": "d141ce86b0584abb29ee7c24af9afb1e3d871f04",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 145, "line_number": 156,
"type": "Secret Keyword" "type": "Secret Keyword"
} }
], ],
@ -60,6 +60,22 @@
"type": "Hex High Entropy String" "type": "Hex High Entropy String"
} }
], ],
"alembic/versions/fda6bd7e1b65_clin_delete_cascade.py": [
{
"hashed_secret": "61d8937fb12b982e07b933c083d9014c34159723",
"is_secret": false,
"is_verified": false,
"line_number": 13,
"type": "Hex High Entropy String"
},
{
"hashed_secret": "999a22300a564f9d2bdca555c2170465fd760ae3",
"is_secret": false,
"is_verified": false,
"line_number": 14,
"type": "Hex High Entropy String"
}
],
"atst.ini.example": [ "atst.ini.example": [
{ {
"hashed_secret": "abcdb568713c255c81376829da20004ba9463fd3", "hashed_secret": "abcdb568713c255c81376829da20004ba9463fd3",
@ -178,10 +194,11 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 481, "line_number": 523,
"type": "Hex High Entropy String" "type": "Hex High Entropy String"
} }
] ]
}, },
"version": "0.12.5" "version": "0.12.5"
} }

View File

@ -4,7 +4,8 @@ from flask import (
render_template, render_template,
request as http_request, request as http_request,
url_for, url_for,
current_app, current_app as app,
jsonify,
) )
from . import task_orders_bp from . import task_orders_bp
@ -17,9 +18,7 @@ from atst.utils.flash import formatted_flash as flash
def render_task_orders_edit(template, portfolio_id=None, task_order_id=None, form=None): def render_task_orders_edit(template, portfolio_id=None, task_order_id=None, form=None):
(token, object_name) = current_app.csp.files.get_token() render_args = {}
render_args = {"token": token, "object_name": object_name}
if task_order_id: if task_order_id:
task_order = TaskOrders.get(task_order_id) task_order = TaskOrders.get(task_order_id)
portfolio_id = task_order.portfolio_id portfolio_id = task_order.portfolio_id
@ -73,6 +72,15 @@ def update_task_order(
) )
@task_orders_bp.route("/task_orders/<portfolio_id>/upload-token")
@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form")
def upload_token(portfolio_id):
(token, object_name) = app.csp.files.get_token()
render_args = {"token": token, "objectName": object_name}
return jsonify(render_args)
@task_orders_bp.route("/task_orders/<task_order_id>/edit") @task_orders_bp.route("/task_orders/<task_order_id>/edit")
@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form") @user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form")
def edit(task_order_id): def edit(task_order_id):

View File

@ -1,6 +1,3 @@
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
import { conformToMask } from 'vue-text-mask'
import { emitEvent } from '../lib/emitters' import { emitEvent } from '../lib/emitters'
import FormMixin from '../mixins/form' import FormMixin from '../mixins/form'
import textinput from './text_input' import textinput from './text_input'
@ -20,12 +17,6 @@ export default {
props: { props: {
name: String, name: String,
token: {
type: Object,
},
objectName: {
type: String,
},
initialData: { initialData: {
type: String, type: String,
}, },
@ -40,6 +31,9 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
portfolioId: {
type: String,
},
}, },
data: function() { data: function() {
@ -52,8 +46,7 @@ export default {
} }
}, },
created: function() { created: async function() {
this.uploader = buildUploader(this.token)
emitEvent('field-mount', this, { emitEvent('field-mount', this, {
optional: this.optional, optional: this.optional,
name: this.name, name: this.name,
@ -71,13 +64,12 @@ export default {
return return
} }
const response = await this.uploader.upload(file, this.objectName) const uploader = await this.getUploader()
const response = await uploader.upload(file)
if (response.ok) { if (response.ok) {
this.attachment = e.target.value this.attachment = e.target.value
this.$refs.attachmentFilename.value = file.name this.$refs.attachmentFilename.value = file.name
this.$refs.attachmentObjectName.value = this.objectName this.$refs.attachmentObjectName.value = response.objectName
this.$refs.attachmentInput.disabled = true
} else { } else {
this.uploadError = true this.uploadError = true
} }
@ -111,6 +103,13 @@ export default {
this.uploadError = false this.uploadError = false
this.sizeError = false this.sizeError = false
}, },
getUploader: async function() {
return fetch(`/task_orders/${this.portfolioId}/upload-token`, {
credentials: 'include',
})
.then(response => response.json())
.then(({ token, objectName }) => buildUploader(token, objectName))
},
}, },
computed: { computed: {

View File

@ -2,13 +2,14 @@ import Azure from 'azure-storage'
import 'whatwg-fetch' import 'whatwg-fetch'
class AzureUploader { class AzureUploader {
constructor(accountName, containerName, sasToken) { constructor(accountName, containerName, sasToken, objectName) {
this.accountName = accountName this.accountName = accountName
this.containerName = containerName this.containerName = containerName
this.sasToken = sasToken.token this.sasToken = sasToken.token
this.objectName = objectName
} }
async upload(file, objectName) { async upload(file) {
const blobService = Azure.createBlobServiceWithSas( const blobService = Azure.createBlobServiceWithSas(
`https://${this.accountName}.blob.core.windows.net`, `https://${this.accountName}.blob.core.windows.net`,
this.sasToken this.sasToken
@ -27,14 +28,14 @@ class AzureUploader {
fileReader.addEventListener('load', f => { fileReader.addEventListener('load', f => {
blobService.createBlockBlobFromText( blobService.createBlockBlobFromText(
this.containerName, this.containerName,
`${objectName}`, `${this.objectName}`,
f.target.result, f.target.result,
options, options,
function(err, result) { (err, result) => {
if (err) { if (err) {
resolve({ ok: false }) resolve({ ok: false })
} else { } else {
resolve({ ok: true }) resolve({ ok: true, objectName: this.objectName })
} }
} }
) )
@ -45,11 +46,12 @@ class AzureUploader {
} }
class AwsUploader { class AwsUploader {
constructor(presignedPost) { constructor(presignedPost, objectName) {
this.presignedPost = presignedPost this.presignedPost = presignedPost
this.objectName = objectName
} }
async upload(file, objectName) { async upload(file) {
const form = new FormData() const form = new FormData()
Object.entries(this.presignedPost.fields).forEach(([k, v]) => { Object.entries(this.presignedPost.fields).forEach(([k, v]) => {
form.append(k, v) form.append(k, v)
@ -57,34 +59,38 @@ class AwsUploader {
form.append('file', file) form.append('file', file)
form.set('x-amz-meta-filename', file.name) form.set('x-amz-meta-filename', file.name)
return fetch(this.presignedPost.url, { const response = await fetch(this.presignedPost.url, {
method: 'POST', method: 'POST',
body: form, body: form,
}) })
return { ok: response.ok, objectName: this.objectName }
} }
} }
class MockUploader { class MockUploader {
constructor(token) { constructor(token, objectName) {
this.token = token this.token = token
this.objectName = objectName
} }
async upload(file, objectName) { async upload(file, objectName) {
return Promise.resolve({ ok: true }) return Promise.resolve({ ok: true, objectName: this.objectName })
} }
} }
export const buildUploader = token => { export const buildUploader = (token, objectName) => {
const cloudProvider = process.env.CLOUD_PROVIDER || 'mock' const cloudProvider = process.env.CLOUD_PROVIDER || 'mock'
if (cloudProvider === 'aws') { if (cloudProvider === 'aws') {
return new AwsUploader(token) return new AwsUploader(token, objectName)
} else if (cloudProvider === 'azure') { } else if (cloudProvider === 'azure') {
return new AzureUploader( return new AzureUploader(
process.env.AZURE_ACCOUNT_NAME, process.env.AZURE_ACCOUNT_NAME,
process.env.AZURE_CONTAINER_NAME, process.env.AZURE_CONTAINER_NAME,
token token,
objectName
) )
} else { } else {
return new MockUploader(token) return new MockUploader(token, objectName)
} }
} }

View File

@ -4,10 +4,9 @@
v-bind:initial-errors='true' v-bind:initial-errors='true'
v-bind:watch='false' v-bind:watch='false'
v-bind:portfolio-id="''"
name='pdf' name='pdf'
:optional='false' :optional='false'
v-bind:token='token'
v-bind:object-name='"object_name"'
> >
<div> <div>
<div v-show="hasAttachment" class="uploaded-file"> <div v-show="hasAttachment" class="uploaded-file">
@ -41,6 +40,9 @@
<template v-if="uploadError"> <template v-if="uploadError">
<span class="usa-input__message">There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO.</span> <span class="usa-input__message">There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO.</span>
</template> </template>
<template v-if="sizeError">
<span class="usa-input__message">The file you have selected is too large. Please choose a file no larger than 64MB.</span>
</template>
<span class="usa-input__message">Test Error Message</span> <span class="usa-input__message">Test Error Message</span>

View File

@ -4,10 +4,9 @@
v-bind:initial-data='initialvalue' v-bind:initial-data='initialvalue'
v-bind:watch='false' v-bind:watch='false'
v-bind:portfolio-id="''"
name='pdf' name='pdf'
:optional='false' :optional='false'
v-bind:token='token'
v-bind:object-name='"object_name"'
> >
<div> <div>
<div v-show="hasAttachment" class="uploaded-file"> <div v-show="hasAttachment" class="uploaded-file">
@ -41,6 +40,9 @@
<template v-if="uploadError"> <template v-if="uploadError">
<span class="usa-input__message">There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO.</span> <span class="usa-input__message">There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO.</span>
</template> </template>
<template v-if="sizeError">
<span class="usa-input__message">The file you have selected is too large. Please choose a file no larger than 64MB.</span>
</template>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% macro UploadInput(field, show_label=False, watch=False, token="", object_name="") -%} {% macro UploadInput(field, portfolio_id, show_label=False, watch=False) -%}
<uploadinput <uploadinput
inline-template inline-template
{% if not field.errors %} {% if not field.errors %}
@ -9,10 +9,9 @@
v-bind:initial-errors='true' v-bind:initial-errors='true'
{% endif %} {% endif %}
v-bind:watch='{{ watch | string | lower }}' v-bind:watch='{{ watch | string | lower }}'
v-bind:portfolio-id="'{{ portfolio_id }}'"
name='{{ field.name }}' name='{{ field.name }}'
:optional='false' :optional='false'
v-bind:token='{{ token | tojson }}'
v-bind:object-name='"{{ object_name | string }}"'
> >
<div> <div>
<div v-show="hasAttachment" class="uploaded-file"> <div v-show="hasAttachment" class="uploaded-file">

View File

@ -14,8 +14,7 @@
{% set next_button_text = "Next: Add TO Number" %} {% set next_button_text = "Next: Add TO Number" %}
{% set step = "1" %} {% set step = "1" %}
{% block to_builder_form_field %} {% block to_builder_form_field %}
{{ TOFormStepHeader('task_orders.form.supporting_docs_header' | translate, 'task_orders.form.supporting_docs_text' | translate) }} {{ TOFormStepHeader('task_orders.form.supporting_docs_header' | translate, 'task_orders.form.supporting_docs_text' | translate) }}
{{ UploadInput(form.pdf, watch=True, token=token, object_name=object_name) }} {{ UploadInput(form.pdf, portfolio.id, watch=True) }}
{% endblock %} {% endblock %}

View File

@ -74,15 +74,11 @@ def test_make_checkbox_input_template(checkbox_input_macro, initial_value_form):
def test_make_upload_input_template(upload_input_macro, task_order_form): def test_make_upload_input_template(upload_input_macro, task_order_form):
rendered_upload_macro = upload_input_macro( rendered_upload_macro = upload_input_macro(task_order_form.pdf)
task_order_form.pdf, token="token", object_name="object_name"
)
write_template(rendered_upload_macro, "upload_input_template.html") write_template(rendered_upload_macro, "upload_input_template.html")
def test_make_upload_input_error_template(upload_input_macro, task_order_form): def test_make_upload_input_error_template(upload_input_macro, task_order_form):
task_order_form.validate() task_order_form.validate()
rendered_upload_macro = upload_input_macro( rendered_upload_macro = upload_input_macro(task_order_form.pdf)
task_order_form.pdf, token="token", object_name="object_name"
)
write_template(rendered_upload_macro, "upload_input_error_template.html") write_template(rendered_upload_macro, "upload_input_error_template.html")