From 9fba1860b054cd008320d9385600c6b420274e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Ramos?= Date: Thu, 18 Jan 2018 11:56:01 +0000 Subject: [PATCH] fix(my-joy-beta): sanitize and validate instance name fixes #1009 --- packages/icons/src/mocks/__aliases__.js | 2 +- .../__tests__/__snapshots__/name.spec.js.snap | 336 ++++++++++++++---- .../src/components/create-instance/name.js | 53 ++- .../src/containers/create-instance/name.js | 120 ++++++- .../src/graphql/get-instance-small.gql | 5 + .../src/graphql/get-random-name.gql | 3 + packages/navigation/src/mocks/__aliases__.js | 2 +- packages/ui-toolkit/src/form/meta.js | 4 + packages/ui-toolkit/src/mocks/__aliases__.js | 2 +- 9 files changed, 437 insertions(+), 90 deletions(-) create mode 100644 packages/my-joy-beta/src/graphql/get-instance-small.gql create mode 100644 packages/my-joy-beta/src/graphql/get-random-name.gql diff --git a/packages/icons/src/mocks/__aliases__.js b/packages/icons/src/mocks/__aliases__.js index a0995453..f053ebf7 100644 --- a/packages/icons/src/mocks/__aliases__.js +++ b/packages/icons/src/mocks/__aliases__.js @@ -1 +1 @@ -module.exports = {}; \ No newline at end of file +module.exports = {}; diff --git a/packages/my-joy-beta/src/components/create-instance/__tests__/__snapshots__/name.spec.js.snap b/packages/my-joy-beta/src/components/create-instance/__tests__/__snapshots__/name.spec.js.snap index 929c301b..3a92aca6 100644 --- a/packages/my-joy-beta/src/components/create-instance/__tests__/__snapshots__/name.spec.js.snap +++ b/packages/my-joy-beta/src/components/create-instance/__tests__/__snapshots__/name.spec.js.snap @@ -11,40 +11,52 @@ exports[`renders without throwing 1`] = ` margin-bottom: 1rem; } -.c8 { +.c10 { + margin-left: 0.25rem; +} + +.c14 { margin-top: 0.5rem; margin-bottom: 2rem; } -.c0 { - box-sizing: border-box; +.c4 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex: 0 1 auto; - -ms-flex: 0 1 auto; - flex: 0 1 auto; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; - -webkit-flex-wrap: wrap; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - margin-right: -0.5rem; - margin-left: -0.5rem; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; } -.c1 { - box-sizing: border-box; - -webkit-flex: 0 0 auto; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - padding-right: 0.5rem; - padding-left: 0.5rem; +.c5 { + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-flex-shrink: 1; + -ms-flex-negative: 1; + flex-shrink: 1; } -.c11 { +.c13 { font-family: sans-serif; font-size: 100%; line-height: 1.15; @@ -57,22 +69,22 @@ exports[`renders without throwing 1`] = ` min-width: 7.5rem; } -.c11::-moz-focus-inner, -.c11[type='button']::-moz-focus-inner, -.c11[type='reset']::-moz-focus-inner, -.c11[type='submit']::-moz-focus-inner { +.c13::-moz-focus-inner, +.c13[type='button']::-moz-focus-inner, +.c13[type='reset']::-moz-focus-inner, +.c13[type='submit']::-moz-focus-inner { border-style: none; padding: 0; } -.c11:-moz-focusring, -.c11[type='button']:-moz-focusring, -.c11[type='reset']:-moz-focusring, -.c11[type='submit']:-moz-focusring { +.c13:-moz-focusring, +.c13[type='button']:-moz-focusring, +.c13[type='reset']:-moz-focusring, +.c13[type='submit']:-moz-focusring { outline: 0.0625rem dotted ButtonText; } -.c11 + button { +.c13 + button { margin-left: 0.375rem; } @@ -96,11 +108,116 @@ exports[`renders without throwing 1`] = ` padding-bottom: 2.25rem; } -.c10 { +.c12 { display: inline-block; } -.c9 { +.c11 { + box-sizing: border-box; + display: inline-block; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + min-height: 3rem; + height: 3rem; + min-width: 7.5rem; + margin-bottom: 0.5rem; + margin-top: 0.5rem; + padding: 0.9375rem 1.125rem; + position: relative; + font-size: 0.9375rem; + text-align: center; + font-style: normal; + font-stretch: normal; + line-height: normal; + -webkit-letter-spacing: normal; + -moz-letter-spacing: normal; + -ms-letter-spacing: normal; + letter-spacing: normal; + text-decoration: none; + white-space: nowrap; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + color: rgb(255,255,255); + -webkit-text-fill-color: currentcolor; + background-image: none; + background-color: rgb(59,70,204); + border-radius: 0.25rem; + border: solid 0.0625rem rgb(45,56,132); + color: rgb(70,70,70); + -webkit-text-fill-color: currentcolor; + background-color: rgb(255,255,255); + border-color: rgb(216,216,216); + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0; + margin-top: 0.5rem; +} + +.c11:focus { + outline: 0; + text-decoration: none; + background-color: rgb(59,70,204); + border-color: rgb(45,56,132); +} + +.c11:hover { + background-color: rgb(72,83,217); + border: solid 0.0625rem rgb(45,56,132); +} + +.c11:active, +.c11:active:hover, +.c11:active:focus { + background-image: none; + outline: 0; + background-color: rgb(45,56,132); + border-color: rgb(45,56,132); +} + +.c11[disabled] { + cursor: not-allowed; + pointer-events: none; +} + +.c11:focus { + background-color: rgb(255,255,255); + border-color: rgb(216,216,216); +} + +.c11:hover { + background-color: rgb(247,247,247); + border-color: rgb(216,216,216); +} + +.c11:active, +.c11:active:hover, +.c11:active:focus { + background-color: rgb(230,230,230); + border-color: rgb(216,216,216); +} + +.c11 svg + span { + margin-left: 0.75rem; +} + +.c11 svg { + max-height: 1.125rem; +} + +.c15 { box-sizing: border-box; display: inline-block; -webkit-box-pack: center; @@ -140,33 +257,33 @@ exports[`renders without throwing 1`] = ` border: solid 0.0625rem rgb(45,56,132); } -.c9:focus { +.c15:focus { outline: 0; text-decoration: none; background-color: rgb(59,70,204); border-color: rgb(45,56,132); } -.c9:hover { +.c15:hover { background-color: rgb(72,83,217); border: solid 0.0625rem rgb(45,56,132); } -.c9:active, -.c9:active:hover, -.c9:active:focus { +.c15:active, +.c15:active:hover, +.c15:active:focus { background-image: none; outline: 0; background-color: rgb(45,56,132); border-color: rgb(45,56,132); } -.c9[disabled] { +.c15[disabled] { cursor: not-allowed; pointer-events: none; } -.c5 { +.c7 { font-size: 0.9375rem; font-style: normal; font-stretch: normal; @@ -179,7 +296,7 @@ exports[`renders without throwing 1`] = ` font-size: 0.8125rem; } -.c7 { +.c9 { font-size: 0.9375rem; font-style: normal; font-stretch: normal; @@ -190,9 +307,38 @@ exports[`renders without throwing 1`] = ` font-size: 0.8125rem; float: none; margin-left: 1.75rem; + margin-left: 0; } -.c4 { +.c0 { + box-sizing: border-box; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -0.5rem; + margin-left: -0.5rem; +} + +.c1 { + box-sizing: border-box; + -webkit-flex: 0 0 auto; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + padding-right: 0.5rem; + padding-left: 0.5rem; +} + +.c6 { display: inline-block; margin: 0; padding: 0; @@ -208,7 +354,7 @@ exports[`renders without throwing 1`] = ` width: 100%; } -.c6 { +.c8 { box-sizing: border-box; width: 18.75rem; height: 3rem; @@ -232,41 +378,41 @@ exports[`renders without throwing 1`] = ` outline: 0; } -.c6::-webkit-input-placeholder { +.c8::-webkit-input-placeholder { color: rgba(73,73,73,0.5); } -.c6::-moz-placeholder { +.c8::-moz-placeholder { color: rgba(73,73,73,0.5); } -.c6:-ms-input-placeholder { +.c8:-ms-input-placeholder { color: rgba(73,73,73,0.5); } -.c6:invalid { +.c8:invalid { box-shadow: none; } -.c6:disabled { +.c8:disabled { background-color: rgb(250,250,250); color: rgb(216,216,216); cursor: not-allowed; } -.c6:disabled::-webkit-input-placeholder { +.c8:disabled::-webkit-input-placeholder { color: rgba(73,73,73,0.5); } -.c6:disabled::-moz-placeholder { +.c8:disabled::-moz-placeholder { color: rgba(73,73,73,0.5); } -.c6:disabled:-ms-input-placeholder { +.c8:disabled:-ms-input-placeholder { color: rgba(73,73,73,0.5); } -.c6:focus { +.c8:focus { border-color: rgb(59,70,204); outline: 0; } @@ -312,32 +458,86 @@ exports[`renders without throwing 1`] = `
- - -
+
+ +
+ +
+
+ + + - diff --git a/packages/my-joy-beta/src/containers/create-instance/name.js b/packages/my-joy-beta/src/containers/create-instance/name.js index fc87ded9..d130219a 100644 --- a/packages/my-joy-beta/src/containers/create-instance/name.js +++ b/packages/my-joy-beta/src/containers/create-instance/name.js @@ -1,29 +1,54 @@ import React, { Fragment } from 'react'; -import { compose } from 'react-apollo'; +import { compose, graphql } from 'react-apollo'; +import { set } from 'react-redux-values'; import ReduxForm from 'declarative-redux-form'; +import { change } from 'redux-form'; import { connect } from 'react-redux'; +import intercept from 'apr-intercept'; import get from 'lodash.get'; +import punycode from 'punycode'; import { NameIcon } from 'joyent-ui-toolkit'; import Name from '@components/create-instance/name'; import Title from '@components/create-instance/title'; +import GetInstance from '@graphql/get-instance-small.gql'; +import GetRandomName from '@graphql/get-random-name.gql'; +import { client } from '@state/store'; +import parseError from '@state/parse-error'; -const NameContainer = ({ expanded, name, handleSubmit, handleCancel }) => ( +const FORM_NAME = 'CREATE_INSTANCE_NAME'; + +const NameContainer = ({ + expanded, + name, + placeholderName, + randomizing, + handleAsyncValidation, + shouldAsyncValidate, + handleNext, + handleCancel, + handleRandomize +}) => ( }>Instance name {props => ( )} @@ -31,14 +56,89 @@ const NameContainer = ({ expanded, name, handleSubmit, handleCancel }) => ( ); export default compose( + graphql(GetRandomName, { + fetchPolicy: 'network-only', + props: ({ data }) => ({ + placeholderName: data.rndName || '' + }) + }), connect( - (state, ownProps) => ({ - ...ownProps, - name: get(state, 'form.create-instance-name.values.name') - }), + ({ form, values }, ownProps) => { + const randomizing = get( + values, + 'create-instance-name-randomizing', + false + ); + const name = get(form, `${FORM_NAME}.values.name`, ''); + + return { + ...ownProps, + randomizing, + name + }; + }, (dispatch, { history }) => ({ - handleSubmit: () => history.push(`/instances/~create/image`), - handleCancel: () => history.push(`/instances/~create/name`) + shouldAsyncValidate: ({ trigger }) => trigger === 'submit', + handleAsyncValidation: async ({ name }) => { + const sanitized = punycode.encode(name).replace(/\-$/, ''); + + if (sanitized !== name) { + throw { + name: 'Special characters are not accepted' + }; + } + + const [err, res] = await intercept( + client.query({ + fetchPolicy: 'network-only', + query: GetInstance, + variables: { name } + }) + ); + + if (err) { + throw { + name: parseError(err) + }; + } + + const { data } = res; + const { machines = [] } = data; + + if (machines.length) { + throw { + name: `${name} already exists` + }; + } + }, + handleNext: () => history.push(`/instances/~create/image`), + handleCancel: () => history.push(`/instances/~create/name`), + handleRandomize: async () => { + dispatch( + set({ name: 'create-instance-name-randomizing', value: true }) + ); + + const [err, res] = await intercept( + client.query({ + fetchPolicy: 'network-only', + query: GetRandomName + }) + ); + + dispatch( + set({ name: 'create-instance-name-randomizing', value: false }) + ); + + if (err) { + console.error(err); + return; + } + + const { data } = res; + const { rndName } = data; + + return dispatch(change(FORM_NAME, 'name', rndName)); + } }) ) )(NameContainer); diff --git a/packages/my-joy-beta/src/graphql/get-instance-small.gql b/packages/my-joy-beta/src/graphql/get-instance-small.gql new file mode 100644 index 00000000..1f026571 --- /dev/null +++ b/packages/my-joy-beta/src/graphql/get-instance-small.gql @@ -0,0 +1,5 @@ +query instance($name: String) { + machines(name: $name) { + id + } +} diff --git a/packages/my-joy-beta/src/graphql/get-random-name.gql b/packages/my-joy-beta/src/graphql/get-random-name.gql new file mode 100644 index 00000000..100af5a9 --- /dev/null +++ b/packages/my-joy-beta/src/graphql/get-random-name.gql @@ -0,0 +1,3 @@ +query rndName { + rndName +} diff --git a/packages/navigation/src/mocks/__aliases__.js b/packages/navigation/src/mocks/__aliases__.js index a0995453..f053ebf7 100644 --- a/packages/navigation/src/mocks/__aliases__.js +++ b/packages/navigation/src/mocks/__aliases__.js @@ -1 +1 @@ -module.exports = {}; \ No newline at end of file +module.exports = {}; diff --git a/packages/ui-toolkit/src/form/meta.js b/packages/ui-toolkit/src/form/meta.js index ec05418e..02c572b2 100644 --- a/packages/ui-toolkit/src/form/meta.js +++ b/packages/ui-toolkit/src/form/meta.js @@ -30,6 +30,10 @@ const StyledLabel = Label.extend` font-size: ${remcalc(13)}; float: none; margin-left: ${remcalc(28)}; + + ${is('marginless')` + margin-left: 0; + `}; `; const Meta = props => { diff --git a/packages/ui-toolkit/src/mocks/__aliases__.js b/packages/ui-toolkit/src/mocks/__aliases__.js index a0995453..f053ebf7 100644 --- a/packages/ui-toolkit/src/mocks/__aliases__.js +++ b/packages/ui-toolkit/src/mocks/__aliases__.js @@ -1 +1 @@ -module.exports = {}; \ No newline at end of file +module.exports = {};