diff options
32 files changed, 282 insertions, 183 deletions
diff --git a/service/pixelated/resources/account_recovery_resource.py b/service/pixelated/resources/account_recovery_resource.py index ccfdc580..8cb10fc8 100644 --- a/service/pixelated/resources/account_recovery_resource.py +++ b/service/pixelated/resources/account_recovery_resource.py @@ -18,19 +18,20 @@ import os  from pixelated.resources import BaseResource  from twisted.python.filepath import FilePath -from pixelated.resources import get_protected_static_folder +from pixelated.resources import get_public_static_folder  from twisted.web.http import OK  from twisted.web.template import Element, XMLFile, renderElement  class AccountRecoveryPage(Element): -    loader = XMLFile(FilePath(os.path.join(get_protected_static_folder(), 'account_recovery.html'))) +    loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), 'account_recovery.html')))      def __init__(self):          super(AccountRecoveryPage, self).__init__()  class AccountRecoveryResource(BaseResource): +    BASE_URL = 'account-recovery'      isLeaf = True      def __init__(self, services_factory): diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py index 3e1200d7..45942ea6 100644 --- a/service/pixelated/resources/login_resource.py +++ b/service/pixelated/resources/login_resource.py @@ -20,6 +20,7 @@ from xml.sax import SAXParseException  from pixelated.authentication import Authenticator  from pixelated.config.leap import BootstrapUserServices  from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession +from pixelated.resources.account_recovery_resource import AccountRecoveryResource  from pixelated.resources import get_public_static_folder, respond_json  from twisted.cred.error import UnauthorizedLogin  from twisted.internet import defer @@ -101,6 +102,8 @@ class LoginResource(BaseResource):              return self          if path == 'status':              return LoginStatusResource(self._services_factory) +        if path == AccountRecoveryResource.BASE_URL: +            return AccountRecoveryResource(self._services_factory)          if not self.is_logged_in(request):              return UnAuthorizedResource()          return NoResource() diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 02f2fb62..d860c422 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -92,7 +92,7 @@ class RootResource(BaseResource):      def initialize(self, provider=None, disclaimer_banner=None, authenticator=None):          self._child_resources.add('assets', File(self._protected_static_folder)) -        self._child_resources.add('account-recovery', AccountRecoveryResource(self._services_factory)) +        self._child_resources.add(AccountRecoveryResource.BASE_URL, AccountRecoveryResource(self._services_factory))          self._child_resources.add('backup-account', BackupAccountResource(self._services_factory, authenticator))          self._child_resources.add('sandbox', SandboxResource(self._protected_static_folder))          self._child_resources.add('keys', KeysResource(self._services_factory)) diff --git a/service/test/unit/resources/test_login_resource.py b/service/test/unit/resources/test_login_resource.py index 9f940bc6..eaaba1d4 100644 --- a/service/test/unit/resources/test_login_resource.py +++ b/service/test/unit/resources/test_login_resource.py @@ -67,6 +67,16 @@ class TestLoginResource(unittest.TestCase):          d.addCallback(assert_unauthorized_resources)          return d +    def test_account_recovery_resource_does_not_require_login(self): +        request = DummyRequest(['account-recovery']) +        d = self.web.get(request) + +        def assert_successful(_): +            self.assertEqual(200, request.responseCode) + +        d.addCallback(assert_successful) +        return d +      @patch('pixelated.resources.session.PixelatedSession.is_logged_in')      def test_there_are_no_grand_children_resources_when_logged_in(self, mock_is_logged_in):          request = DummyRequest(['/login/grand_children']) diff --git a/web-ui/app/locales/en_US/translation.json b/web-ui/app/locales/en_US/translation.json index 8116aba4..8607f590 100644 --- a/web-ui/app/locales/en_US/translation.json +++ b/web-ui/app/locales/en_US/translation.json @@ -89,23 +89,21 @@          "tip1": "The safest way to do this is in person.",          "tip2": "You can call or text if you need to.",          "tip3": "Don't ever ask for it via email.", -        "input-label": "type here admin's backup code", -        "button": "Next" +        "input-label": "type here admin's backup code"        },        "user-form": {          "image-description": "User Recovery Code - Step 2 of 4",          "title": "Remember your backup account?",          "description": "When you created your account you received a message - it was sent by team@pixelated-project.org. You'll need the recovery code that is in it.", -        "input-label": "type here your backup code", -        "button": "Next" +        "input-label": "type here your backup code"        },        "new-password-form": {          "image-description": "New Password - Step 3 of 4",          "title": "Now, create a new password",          "input-label1": "create new password", -        "input-label2": "confirm your new password", -        "button": "Next" +        "input-label2": "confirm your new password"        }, +      "button-next": "Next",        "back": "Back to previous step"      },      "backup-account": { diff --git a/web-ui/app/locales/pt_BR/translation.json b/web-ui/app/locales/pt_BR/translation.json index d43487a2..c4b3d9e7 100644 --- a/web-ui/app/locales/pt_BR/translation.json +++ b/web-ui/app/locales/pt_BR/translation.json @@ -82,7 +82,29 @@          "tags": "Etiquetas"      },      "account-recovery": { -      "page-title": "Pixelated Recuperação de Conta" +      "page-title": "Pixelated Recuperação de Conta", +      "admin-form": { +        "image-description": "Código de Recuperação do Administrador - Passo 1 de 4", +        "title": "Contate o administrador da sua conta e peça seu código", +        "tip1": "A maneira mais segura é fazer isso pessoalmente.", +        "tip2": "Você pode ligar ou mandar mensagem de texto se precisar.", +        "tip3": "Nunca peça por e-mail.", +        "input-label": "digite aqui o código" +      }, +      "user-form": { +        "image-description": "Código de Recuperação do Usuário - Passo 2 de 4", +        "title": "Lembra do seu e-mail de recuperação?", +        "description": "Quando você criou uma conta você recebeu uma mensagem do team@pixelated-project.org. Você precisará do código que está neste e-mail.", +        "input-label": "digite aqui o código" +      }, +      "new-password-form": { +        "image-description": "Nova Senha - Passo 3 de 4", +        "title": "Agora, crie uma nova senha", +        "input-label1": "digite a nova senha", +        "input-label2": "confirme a nova senha" +      }, +      "button-next": "Próximo", +      "back": "Voltar ao passo anterior"      },      "backup-account": {        "page-title": "Pixelated E-mail de Recuperação", diff --git a/web-ui/config/public-assets-webpack.js b/web-ui/config/public-assets-webpack.js index 28dff566..13364e2c 100644 --- a/web-ui/config/public-assets-webpack.js +++ b/web-ui/config/public-assets-webpack.js @@ -3,6 +3,7 @@ var CopyWebpackPlugin = require('copy-webpack-plugin');  module.exports = new CopyWebpackPlugin([    { context: 'src/login/', from: '*.html' },    { context: 'src/login/', from: '*.css' }, +  { context: 'src/account_recovery/', from: 'account_recovery.html' },    { context: 'src/interstitial/', from: '*' },    { context: 'app/', from: 'fonts/*' },    { context: 'app/', from: 'locales/**/*' }, diff --git a/web-ui/src/account_recovery/account_recovery.html b/web-ui/src/account_recovery/account_recovery.html index f4601239..35054455 100644 --- a/web-ui/src/account_recovery/account_recovery.html +++ b/web-ui/src/account_recovery/account_recovery.html @@ -9,6 +9,6 @@    </head>    <body>      <div id="root"/> -    <script type="text/javascript" src="/assets/account_recovery.js"></script> +    <script type="text/javascript" src="/public/account_recovery.js"></script>    </body>  </html> diff --git a/web-ui/src/account_recovery/forms/admin_recovery_code_form.js b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js index 3d97b191..5b9da350 100644 --- a/web-ui/src/account_recovery/forms/admin_recovery_code_form.js +++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js @@ -21,7 +21,7 @@ import { translate } from 'react-i18next';  import InputField from 'src/common/input_field/input_field';  import SubmitButton from 'src/common/submit_button/submit_button'; -import './forms.scss'; +import './admin_recovery_code_form.scss';  export const AdminRecoveryCodeForm = ({ t, next }) => (    <form className='account-recovery-form admin-code' onSubmit={next}> @@ -32,7 +32,7 @@ export const AdminRecoveryCodeForm = ({ t, next }) => (      />      <h1>{t('account-recovery.admin-form.title')}</h1>      <img -      className='admin-codes-image' +      className='admin-code-image'        src='/public/images/account-recovery/admins_contact.svg'        alt=''      /> @@ -42,7 +42,7 @@ export const AdminRecoveryCodeForm = ({ t, next }) => (        <li>{t('account-recovery.admin-form.tip3')}</li>      </ul>      <InputField name='admin-code' label={t('account-recovery.admin-form.input-label')} /> -    <SubmitButton buttonText={t('account-recovery.admin-form.button')} /> +    <SubmitButton buttonText={t('account-recovery.button-next')} />    </form>  ); diff --git a/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss new file mode 100644 index 00000000..6dcbcc67 --- /dev/null +++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +.admin-code-image { +  margin: 1em 0; +  align-self: center; +  height: 2.7em; +} diff --git a/web-ui/src/account_recovery/forms/admin_recovery_code_form.spec.js b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js index 0e922212..73c4c1e0 100644 --- a/web-ui/src/account_recovery/forms/admin_recovery_code_form.spec.js +++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js @@ -1,7 +1,7 @@  import { shallow } from 'enzyme';  import expect from 'expect';  import React from 'react'; -import { AdminRecoveryCodeForm } from 'src/account_recovery/forms/admin_recovery_code_form'; +import { AdminRecoveryCodeForm } from './admin_recovery_code_form';  describe('AdminRecoveryCodeForm', () => {    let adminRecoveryCodeForm; @@ -28,7 +28,7 @@ describe('AdminRecoveryCodeForm', () => {    });    it('renders button for next step', () => { -    expect(adminRecoveryCodeForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.admin-form.button'); +    expect(adminRecoveryCodeForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');    });    it('submits form to next step', () => { diff --git a/web-ui/src/account_recovery/forms/forms.scss b/web-ui/src/account_recovery/forms/forms.scss deleted file mode 100644 index 09d8f2ce..00000000 --- a/web-ui/src/account_recovery/forms/forms.scss +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2017 ThoughtWorks, Inc. - * - * Pixelated is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Pixelated is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. - */ - -.account-recovery-form { -  display: flex; -  flex-direction: column; - -  img { -    margin: 1em 0; -    align-self: center; -  } - -  .user-code-form-content { -    display: flex; -    flex-direction: column; -    align-items: center; -  } - -  .account-recovery-progress { -    width: 100%; -  } - -  .admin-code-image { -    height: 2.7em; -  } - -  .user-code-image { -    height: 4em; -  } - -  .input-field-group { -    margin-top: 0; -  } -} - -.new-password { -  .input-field-group:first-of-type { -    margin-bottom: 0; -  } -} - -@media only screen and (min-width : 500px) { -  .account-recovery-form { -    align-items: center; - -    .account-recovery-progress, h1 { -      width: 80%; -    } - -    .user-code-form-content { -      flex-direction: row; -      width: 80%; - -      img { -        margin: 1.6em; -      } -    } -  } -} - -@media only screen and (min-width : 960px) { -  .account-recovery-form { -    .account-recovery-progress { -      width: 80%; -      margin-top: 0; -    } - -    h1 { -      max-width: 80%; -      width: auto; -    } - -    .input-field-group, .submit-button { -      width: 60%; -      align-self: center; -    } -  } -} diff --git a/web-ui/src/account_recovery/forms/new_password_form.js b/web-ui/src/account_recovery/new_password_form/new_password_form.js index 114366b3..f1097b0b 100644 --- a/web-ui/src/account_recovery/forms/new_password_form.js +++ b/web-ui/src/account_recovery/new_password_form/new_password_form.js @@ -22,12 +22,14 @@ import InputField from 'src/common/input_field/input_field';  import SubmitButton from 'src/common/submit_button/submit_button';  import BackLink from 'src/common/back_link/back_link'; +import './new_password_form.scss'; +  export const NewPasswordForm = ({ t, previous }) => (    <form className='account-recovery-form new-password'>      <img        className='account-recovery-progress'        src='/public/images/account-recovery/step_3.svg' -      alt={t('account-recovery.new-password.image-description')} +      alt={t('account-recovery.new-password-form.image-description')}      />      <h1>{t('account-recovery.new-password-form.title')}</h1>      <InputField @@ -38,12 +40,8 @@ export const NewPasswordForm = ({ t, previous }) => (        type='password' name='confirm-password'        label={t('account-recovery.new-password-form.input-label2')}      /> -    <SubmitButton buttonText={t('account-recovery.new-password-form.button')} /> -    <BackLink -      text={t('account-recovery.back')} -      onClick={previous} onKeyDown={previous} -      role='button' -    /> +    <SubmitButton buttonText={t('account-recovery.button-next')} /> +    <BackLink text={t('account-recovery.back')} onClick={previous} />    </form>  ); diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.scss b/web-ui/src/account_recovery/new_password_form/new_password_form.scss new file mode 100644 index 00000000..77623262 --- /dev/null +++ b/web-ui/src/account_recovery/new_password_form/new_password_form.scss @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +.new-password { +  .input-field-group:first-of-type { +    margin-bottom: 0; +  } +} diff --git a/web-ui/src/account_recovery/forms/new_password_form.spec.js b/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js index 5ac96b40..d2bd350c 100644 --- a/web-ui/src/account_recovery/forms/new_password_form.spec.js +++ b/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js @@ -1,7 +1,7 @@  import { shallow } from 'enzyme';  import expect from 'expect';  import React from 'react'; -import { NewPasswordForm } from 'src/account_recovery/forms/new_password_form'; +import { NewPasswordForm } from './new_password_form';  describe('NewPasswordForm', () => {    let newPasswordForm; @@ -30,16 +30,11 @@ describe('NewPasswordForm', () => {    });    it('renders submit button', () => { -    expect(newPasswordForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.new-password-form.button'); +    expect(newPasswordForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');    });    it('returns to previous step on link click', () => {      newPasswordForm.find('BackLink').simulate('click');      expect(mockPrevious).toHaveBeenCalled();    }); - -  it('returns to previous step on key down', () => { -    newPasswordForm.find('BackLink').simulate('keyDown'); -    expect(mockPrevious).toHaveBeenCalled(); -  });  }); diff --git a/web-ui/src/account_recovery/page.js b/web-ui/src/account_recovery/page.js index 3043a38b..579f17cc 100644 --- a/web-ui/src/account_recovery/page.js +++ b/web-ui/src/account_recovery/page.js @@ -19,9 +19,9 @@ import React from 'react';  import { translate } from 'react-i18next';  import DocumentTitle from 'react-document-title';  import Header from 'src/common/header/header'; -import AdminRecoveryCodeForm from 'src/account_recovery/forms/admin_recovery_code_form'; -import UserRecoveryCodeForm from 'src/account_recovery/forms/user_recovery_code_form'; -import NewPasswordForm from 'src/account_recovery/forms/new_password_form'; +import AdminRecoveryCodeForm from 'src/account_recovery/admin_recovery_code_form/admin_recovery_code_form'; +import UserRecoveryCodeForm from 'src/account_recovery/user_recovery_code_form/user_recovery_code_form'; +import NewPasswordForm from 'src/account_recovery/new_password_form/new_password_form';  import Footer from 'src/common/footer/footer';  import 'font-awesome/scss/font-awesome.scss'; diff --git a/web-ui/src/account_recovery/page.scss b/web-ui/src/account_recovery/page.scss index 20604b70..c61c855e 100644 --- a/web-ui/src/account_recovery/page.scss +++ b/web-ui/src/account_recovery/page.scss @@ -66,10 +66,37 @@ p {    margin-bottom: 0.5em;  } +.account-recovery-form { +  display: flex; +  flex-direction: column; + +  .account-recovery-progress { +    align-self: center; +    width: 100%; +  } + +  .input-field-group { +    margin-top: 0; +  } +} +  @media only screen and (min-width : 500px) {    body {      font-size: 1.3em;    } + +  .account-recovery-form { +    align-items: center; + +    .account-recovery-progress { +      margin-bottom: 1.4em; +      width: 80%; +    } + +    h1 { +      width: 80%; +    } +  }  }  @media only screen and (min-width : 960px) { @@ -78,4 +105,16 @@ p {      max-width: 700px;      padding: 3em;    } + +  .account-recovery-form { +    h1 { +      max-width: 80%; +      width: auto; +    } + +    .input-field-group, .submit-button { +      width: 60%; +      align-self: center; +    } +  }  } diff --git a/web-ui/src/account_recovery/page.spec.js b/web-ui/src/account_recovery/page.spec.js index 32e1477c..68debba0 100644 --- a/web-ui/src/account_recovery/page.spec.js +++ b/web-ui/src/account_recovery/page.spec.js @@ -4,11 +4,12 @@ import React from 'react';  import { Page } from 'src/account_recovery/page';  import Header from 'src/common/header/header'; -import AdminRecoveryCodeForm from 'src/account_recovery/forms/admin_recovery_code_form'; -import UserRecoveryCodeForm from 'src/account_recovery/forms/user_recovery_code_form'; -import NewPasswordForm from 'src/account_recovery/forms/new_password_form';  import Footer from 'src/common/footer/footer'; +import AdminRecoveryCodeFormWrapper from './admin_recovery_code_form/admin_recovery_code_form'; +import UserRecoveryCodeFormWrapper from './user_recovery_code_form/user_recovery_code_form'; +import NewPasswordFormWrapper from './new_password_form/new_password_form'; +  describe('Account Recovery Page', () => {    let page; @@ -21,8 +22,8 @@ describe('Account Recovery Page', () => {      expect(page.props().title).toEqual('account-recovery.page-title');    }); -  it('renders header', () => { -    expect(page.find(Header).length).toEqual(1); +  it('renders header without logout button', () => { +    expect(page.find(Header).props().renderLogout).toEqual(false);    });    it('renders footer', () => { @@ -37,29 +38,29 @@ describe('Account Recovery Page', () => {      });      it('renders admin recovery code form as default form', () => { -      expect(page.find(AdminRecoveryCodeForm).length).toEqual(1); -      expect(page.find(UserRecoveryCodeForm).length).toEqual(0); -      expect(page.find(NewPasswordForm).length).toEqual(0); +      expect(page.find(AdminRecoveryCodeFormWrapper).length).toEqual(1); +      expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(0); +      expect(page.find(NewPasswordFormWrapper).length).toEqual(0);      });      it('renders user recovery code form when admin code submitted', () => {        pageInstance.nextStep({ preventDefault: () => {} }); -      expect(page.find(UserRecoveryCodeForm).length).toEqual(1); +      expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(1);      });      it('returns to admin code form on user code form back link', () => {        pageInstance.nextStep({ preventDefault: () => {} });        pageInstance.previousStep(); -      expect(page.find(AdminRecoveryCodeForm).length).toEqual(1); +      expect(page.find(AdminRecoveryCodeFormWrapper).length).toEqual(1);      });      it('renders new password form when user code submitted', () => {        pageInstance.nextStep({ preventDefault: () => {} });        pageInstance.nextStep({ preventDefault: () => {} }); -      expect(page.find(NewPasswordForm).length).toEqual(1); +      expect(page.find(NewPasswordFormWrapper).length).toEqual(1);      });      it('returns to user code form on new password form back link', () => { @@ -67,7 +68,7 @@ describe('Account Recovery Page', () => {        pageInstance.nextStep({ preventDefault: () => {} });        pageInstance.previousStep(); -      expect(page.find(UserRecoveryCodeForm).length).toEqual(1); +      expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(1);      });    });  }); diff --git a/web-ui/src/account_recovery/forms/user_recovery_code_form.js b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js index 30525cdf..a4119885 100644 --- a/web-ui/src/account_recovery/forms/user_recovery_code_form.js +++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js @@ -22,7 +22,7 @@ import InputField from 'src/common/input_field/input_field';  import SubmitButton from 'src/common/submit_button/submit_button';  import BackLink from 'src/common/back_link/back_link'; -import './forms.scss'; +import './user_recovery_code_form.scss';  export const UserRecoveryCodeForm = ({ t, previous, next }) => (    <form className='account-recovery-form user-code' onSubmit={next}> @@ -34,19 +34,15 @@ export const UserRecoveryCodeForm = ({ t, previous, next }) => (      <h1>{t('account-recovery.user-form.title')}</h1>      <div className='user-code-form-content'>        <img -        className='user-codes-image' +        className='user-code-image'          src='/public/images/account-recovery/codes.svg'          alt=''        />        <p>{t('account-recovery.user-form.description')}</p>      </div>      <InputField name='admin-code' label={t('account-recovery.user-form.input-label')} /> -    <SubmitButton buttonText={t('account-recovery.user-form.button')} /> -    <BackLink -      text={t('account-recovery.back')} -      onClick={previous} onKeyDown={previous} -      role='button' -    /> +    <SubmitButton buttonText={t('account-recovery.button-next')} /> +    <BackLink text={t('account-recovery.back')} onClick={previous} />    </form>  ); diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss new file mode 100644 index 00000000..55f5621b --- /dev/null +++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +.user-code-form-content { +  display: flex; +  flex-direction: column; +  align-items: center; +} + +.user-code-image { +  margin: 1em 0; +  align-self: center; +  height: 4em; +} + +@media only screen and (min-width : 500px) { +  .user-code-form-content { +    flex-direction: row; +    width: 80%; +  } + +  .user-code-image { +    margin-right: 1.6em; +  } +} diff --git a/web-ui/src/account_recovery/forms/user_recovery_code_form.spec.js b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js index 1aebb814..e47f2e6c 100644 --- a/web-ui/src/account_recovery/forms/user_recovery_code_form.spec.js +++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js @@ -1,7 +1,7 @@  import { shallow } from 'enzyme';  import expect from 'expect';  import React from 'react'; -import { UserRecoveryCodeForm } from 'src/account_recovery/forms/user_recovery_code_form'; +import { UserRecoveryCodeForm } from './user_recovery_code_form';  describe('UserRecoveryCodeForm', () => {    let userRecoveryCodeForm; @@ -32,7 +32,7 @@ describe('UserRecoveryCodeForm', () => {    });    it('renders submit button', () => { -    expect(userRecoveryCodeForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.user-form.button'); +    expect(userRecoveryCodeForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');    });    it('submits form to next step', () => { @@ -44,9 +44,4 @@ describe('UserRecoveryCodeForm', () => {      userRecoveryCodeForm.find('BackLink').simulate('click');      expect(mockPrevious).toHaveBeenCalled();    }); - -  it('returns to previous step on key down', () => { -    userRecoveryCodeForm.find('BackLink').simulate('keyDown'); -    expect(mockPrevious).toHaveBeenCalled(); -  });  }); diff --git a/web-ui/src/backup_account/confirmation/confirmation.js b/web-ui/src/backup_account/confirmation/confirmation.js index 41637dab..49b0d19c 100644 --- a/web-ui/src/backup_account/confirmation/confirmation.js +++ b/web-ui/src/backup_account/confirmation/confirmation.js @@ -18,6 +18,7 @@  import React from 'react';  import { translate } from 'react-i18next';  import SubmitButton from 'src/common/submit_button/submit_button'; +import BackLink from 'src/common/back_link/back_link';  import './confirmation.scss'; @@ -29,12 +30,10 @@ export const Confirmation = ({ t }) => (      <form action='/'>        <SubmitButton buttonText={t('backup-account.confirmation.button')} type='submit' />      </form> -    <div className='link-content'> -      <a href='/backup-account' className='link'> -        <i className='fa fa-angle-left' aria-hidden='true' /> -        <span>{t('backup-account.confirmation.retry-button')}</span> -      </a> -    </div> +    <BackLink +      href='/backup-account' +      text={t('backup-account.confirmation.retry-button')} +    />    </div>  ); diff --git a/web-ui/src/backup_account/confirmation/confirmation.spec.js b/web-ui/src/backup_account/confirmation/confirmation.spec.js index 291d156d..7a6f38ca 100644 --- a/web-ui/src/backup_account/confirmation/confirmation.spec.js +++ b/web-ui/src/backup_account/confirmation/confirmation.spec.js @@ -20,10 +20,10 @@ describe('Confirmation', () => {    });    it('renders confirmation retry button', () => { -    expect(page.find('a').text()).toEqual('backup-account.confirmation.retry-button'); +    expect(page.find('BackLink').props().text).toEqual('backup-account.confirmation.retry-button');    });    it('retries button redirects to backup account', () => { -    expect(page.find('a').props().href).toEqual('/backup-account'); +    expect(page.find('BackLink').props().href).toEqual('/backup-account');    });  }); diff --git a/web-ui/src/backup_account/page.js b/web-ui/src/backup_account/page.js index be3bae36..e7663205 100644 --- a/web-ui/src/backup_account/page.js +++ b/web-ui/src/backup_account/page.js @@ -56,7 +56,7 @@ export class Page extends React.Component {      return (        <DocumentTitle title={t('backup-account.page-title')}>          <div className='page'> -          <Header /> +          <Header renderLogout />            <section>              {this.mainContent()}            </section> diff --git a/web-ui/src/backup_account/page.spec.js b/web-ui/src/backup_account/page.spec.js index 2933a64e..8c014ee4 100644 --- a/web-ui/src/backup_account/page.spec.js +++ b/web-ui/src/backup_account/page.spec.js @@ -5,6 +5,7 @@ import { Page } from 'src/backup_account/page';  import BackupEmail from 'src/backup_account/backup_email/backup_email';  import Confirmation from 'src/backup_account/confirmation/confirmation';  import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification'; +import Header from 'src/common/header/header';  describe('BackupAccount', () => {    let page; @@ -18,6 +19,10 @@ describe('BackupAccount', () => {      expect(page.props().title).toEqual('backup-account.page-title');    }); +  it('renders header with logout button', () => { +    expect(page.find(Header).props().renderLogout).toEqual(true); +  }); +    describe('save backup email', () => {      let pageInstance; diff --git a/web-ui/src/common/back_link/back_link.js b/web-ui/src/common/back_link/back_link.js index f3bdb2b5..bb5ffbea 100644 --- a/web-ui/src/common/back_link/back_link.js +++ b/web-ui/src/common/back_link/back_link.js @@ -19,12 +19,19 @@ import React from 'react';  import './back_link.scss'; +const icon = <i className='fa fa-angle-left' aria-hidden='true' />; + +const button = (text, options) => ( +  <button className='link' {...options}>{icon}<span>{text}</span></button> +); + +const link = (text, options) => ( +  <a className='link' {...options}>{icon}<span>{text}</span></a> +); +  const BackLink = ({ text, ...other }) => (    <div className='link-content'> -    <a className='link' tabIndex='0' {...other}> -      <i className='fa fa-angle-left' aria-hidden='true' /> -      <span>{text}</span> -    </a> +    { other.href ? link(text, other) : button(text, other) }    </div>  ); diff --git a/web-ui/src/common/back_link/back_link.scss b/web-ui/src/common/back_link/back_link.scss index a799a710..5541d9d9 100644 --- a/web-ui/src/common/back_link/back_link.scss +++ b/web-ui/src/common/back_link/back_link.scss @@ -21,6 +21,10 @@    color: $dark_blue;    font-style: italic;    font-size: 0.8em; +  margin: 0; +  padding: 0; +  border: 0; +  background: transparent;    .fa {      font-size: 1.6em; diff --git a/web-ui/src/common/back_link/back_link.spec.js b/web-ui/src/common/back_link/back_link.spec.js index ee659267..5f49a6f9 100644 --- a/web-ui/src/common/back_link/back_link.spec.js +++ b/web-ui/src/common/back_link/back_link.spec.js @@ -4,17 +4,38 @@ import React from 'react';  import BackLink from 'src/common/back_link/back_link';  describe('BackLink', () => { -  let backLink; +  context('as link', () => { +    let backLink; -  beforeEach(() => { -    backLink = shallow(<BackLink text='Back to inbox' href='/' />); -  }); +    beforeEach(() => { +      backLink = shallow(<BackLink text='Back to inbox' href='/' />); +    }); + +    it('renders link with text', () => { +      expect(backLink.find('a').text()).toEqual('Back to inbox'); +    }); -  it('renders link with text', () => { -    expect(backLink.find('a').text()).toEqual('Back to inbox'); +    it('adds link action', () => { +      expect(backLink.find('a').props().href).toEqual('/'); +    });    }); -  it('adds link action', () => { -    expect(backLink.find('a').props().href).toEqual('/'); +  context('as button', () => { +    let backLink; +    let mockClick; + +    beforeEach(() => { +      mockClick = expect.createSpy(); +      backLink = shallow(<BackLink text='Back to inbox' onClick={mockClick} />); +    }); + +    it('renders button with text', () => { +      expect(backLink.find('button').text()).toEqual('Back to inbox'); +    }); + +    it('adds button click event', () => { +      backLink.find('button').simulate('click'); +      expect(mockClick).toHaveBeenCalled(); +    });    });  }); diff --git a/web-ui/src/common/header/header.js b/web-ui/src/common/header/header.js index 715d54c6..3ad924c0 100644 --- a/web-ui/src/common/header/header.js +++ b/web-ui/src/common/header/header.js @@ -19,7 +19,7 @@ import React from 'react';  import Logout from 'src/common/logout/logout';  import './header.scss'; -export const Header = () => ( +export const Header = ({ renderLogout }) => (    <header className='header-wrapper'>      <div className='header-content'>        <a href='/'> @@ -30,10 +30,18 @@ export const Header = () => (          />        </a>        <div className='header-icons'> -        <Logout /> +        { renderLogout ? <Logout /> : <div /> }        </div>      </div>    </header>  ); +Header.propTypes = { +  renderLogout: React.PropTypes.bool +}; + +Header.defaultProps = { +  renderLogout: false +}; +  export default Header; diff --git a/web-ui/src/common/header/header.spec.js b/web-ui/src/common/header/header.spec.js index 81a952c7..0c11713b 100644 --- a/web-ui/src/common/header/header.spec.js +++ b/web-ui/src/common/header/header.spec.js @@ -11,11 +11,16 @@ describe('Header', () => {      header = shallow(<Header />);    }); -  it('renders the header containing the logout button', () => { -    expect(header.find('header').find(Logout)).toExist(); -  }); -    it('renders the header pixelated logo', () => {      expect(header.find('header').find('img').props().alt).toEqual('Pixelated');    }); + +  it('renders the header containing the logout button when renderLogout is true', () => { +    header = shallow(<Header renderLogout />); +    expect(header.find('header').find(Logout).length).toEqual(1); +  }); + +  it('hides logout button when renderLogout is false', () => { +    expect(header.find('header').find(Logout).length).toEqual(0); +  });  }); diff --git a/web-ui/webpack.config.js b/web-ui/webpack.config.js index 0470b508..e82cf88b 100644 --- a/web-ui/webpack.config.js +++ b/web-ui/webpack.config.js @@ -21,11 +21,12 @@ var commonConfiguration = {  var publicAssets = Object.assign({}, commonConfiguration, {    entry: {      'login': './src/login/login.js', +    'account_recovery': './src/account_recovery/account_recovery.js'    },    output: {      path: path.join(__dirname, 'dist/public'),      filename: '[name].js', -    publicPath: '/assets/' +    publicPath: '/public/'    },    plugins: [      publicAssetsWebpack, @@ -39,7 +40,6 @@ var publicAssets = Object.assign({}, commonConfiguration, {  var protectedAssets = Object.assign({}, commonConfiguration, {    entry: {      'app': './app/js/index.js', -    'account_recovery': './src/account_recovery/account_recovery.js',      'backup_account': './src/backup_account/backup_account.js',      'sandbox': './app/js/sandbox.js'    }, diff --git a/web-ui/webpack.production.config.js b/web-ui/webpack.production.config.js index 23be0565..c731526a 100644 --- a/web-ui/webpack.production.config.js +++ b/web-ui/webpack.production.config.js @@ -31,11 +31,12 @@ var commonPlugins = [  var publicAssets = Object.assign({}, commonConfiguration, {    entry: {      'login': './src/login/login.js', +    'account_recovery': './src/account_recovery/account_recovery.js'    },    output: {      path: path.join(__dirname, 'dist/public'),      filename: '[name].js', -    publicPath: '/assets/' +    publicPath: '/public/'    },    plugins: commonPlugins.concat([ publicAssetsWebpack ])  }); @@ -43,7 +44,6 @@ var publicAssets = Object.assign({}, commonConfiguration, {  var protectedAssets = Object.assign({}, commonConfiguration, {    entry: {      'app': './app/js/index.js', -    'account_recovery': './src/account_recovery/account_recovery.js',      'backup_account': './src/backup_account/backup_account.js',      'sandbox': './app/js/sandbox.js'    },  | 
