diff --git a/client/src/javascript/actions/AuthActions.ts b/client/src/javascript/actions/AuthActions.ts index bd8491c1..5df79bf8 100644 --- a/client/src/javascript/actions/AuthActions.ts +++ b/client/src/javascript/actions/AuthActions.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, {AxiosError} from 'axios'; import type { AuthAuthenticationOptions, @@ -33,7 +33,7 @@ const AuthActions = { // server's response. let errorMessage; - if (error.response) { + if (error.response && error.response.data.message != null) { errorMessage = error.response.data.message; } else if (error.message) { errorMessage = error.message; @@ -133,11 +133,8 @@ const AuthActions = { data, }); }, - (error) => { - AppDispatcher.dispatchServerAction({ - type: 'AUTH_REGISTER_ERROR', - error: error.response.data.message, - }); + (error: AxiosError) => { + throw error; }, ), diff --git a/client/src/javascript/components/auth/AuthForm.tsx b/client/src/javascript/components/auth/AuthForm.tsx index 5e47d0fe..4233400c 100644 --- a/client/src/javascript/components/auth/AuthForm.tsx +++ b/client/src/javascript/components/auth/AuthForm.tsx @@ -7,9 +7,7 @@ import type {Credentials} from '@shared/schema/Auth'; import {Button, Form, FormError, FormRow, Panel, PanelContent, PanelHeader, PanelFooter, Textbox} from '../../ui'; import AuthActions from '../../actions/AuthActions'; -import AuthStore from '../../stores/AuthStore'; import ClientConnectionSettingsForm from '../general/connection-settings/ClientConnectionSettingsForm'; -import connectStores from '../../util/connectStores'; import history from '../../util/history'; import type {ClientConnectionSettingsFormType} from '../general/connection-settings/ClientConnectionSettingsForm'; @@ -19,11 +17,11 @@ type RegisterFormData = Pick; interface AuthFormProps extends WrappedComponentProps { mode: 'login' | 'register'; - error?: Error; } -interface AuthFormStates extends Record { +interface AuthFormStates { isSubmitting: boolean; + errorMessage?: string; } class AuthForm extends React.Component { @@ -73,20 +71,22 @@ class AuthForm extends React.Component { this.setState({isSubmitting: true}); - if (this.props.mode === 'login') { - const credentials = submission.formData as Partial; + const {intl, mode} = this.props; - if ( - credentials.username == null || - credentials.username === '' || - credentials.password == null || - credentials.password === '' - ) { - this.setState({isSubmitting: false}, () => { - // do nothing. - }); - return; - } + const formData = submission.formData as Partial | Partial; + + if (formData.username == null || formData.username === '') { + this.setState({isSubmitting: false, errorMessage: intl.formatMessage({id: 'auth.error.username.empty'})}); + return; + } + + if (formData.password == null || formData.password === '') { + this.setState({isSubmitting: false, errorMessage: intl.formatMessage({id: 'auth.error.password.empty'})}); + return; + } + + if (mode === 'login') { + const credentials = formData as LoginFormData; AuthActions.authenticate({ username: credentials.username, @@ -95,26 +95,20 @@ class AuthForm extends React.Component { .then(() => { this.setState({isSubmitting: false}, () => history.replace('overview')); }) - .catch(() => { - this.setState({isSubmitting: false}, () => history.replace('login')); + .catch((error: Error) => { + this.setState({isSubmitting: false, errorMessage: error.message}, () => history.replace('login')); }); } else { - const config = submission.formData as Partial; + const config = formData as RegisterFormData; - if ( - config.username == null || - config.username === '' || - config.password == null || - config.password === '' || - this.settingsFormRef.current == null - ) { - this.setState({isSubmitting: false}); + if (this.settingsFormRef.current == null) { + this.setState({isSubmitting: false, errorMessage: intl.formatMessage({id: 'connection.settings.error.empty'})}); return; } const connectionSettings = this.settingsFormRef.current.getConnectionSettings(); if (connectionSettings == null) { - this.setState({isSubmitting: false}); + this.setState({isSubmitting: false, errorMessage: intl.formatMessage({id: 'connection.settings.error.empty'})}); return; } @@ -123,15 +117,20 @@ class AuthForm extends React.Component { password: config.password, client: connectionSettings, level: AccessLevel.ADMINISTRATOR, - }).then(() => { - this.setState({isSubmitting: false}, () => history.replace('overview')); - }); + }).then( + () => { + this.setState({isSubmitting: false}, () => history.replace('overview')); + }, + (error: Error) => { + this.setState({isSubmitting: false, errorMessage: error.message}); + }, + ); } }; render() { - const {isSubmitting} = this.state; - const {error, intl, mode} = this.props; + const {errorMessage, isSubmitting} = this.state; + const {intl, mode} = this.props; const isLoginMode = mode === 'login'; return ( @@ -147,9 +146,9 @@ class AuthForm extends React.Component {

{this.getIntroText()}

- {error != null ? ( + {errorMessage != null ? ( - {error} + {errorMessage} ) : null} @@ -198,18 +197,4 @@ class AuthForm extends React.Component { } } -const ConnectedAuthForm = connectStores, AuthFormStates>(injectIntl(AuthForm), () => { - return [ - { - store: AuthStore, - event: ['AUTH_LOGIN_ERROR', 'AUTH_REGISTER_ERROR'], - getValue: ({payload}) => { - return { - error: payload as AuthFormProps['error'], - }; - }, - }, - ]; -}); - -export default ConnectedAuthForm; +export default injectIntl(AuthForm); diff --git a/client/src/javascript/constants/EventTypes.ts b/client/src/javascript/constants/EventTypes.ts index 66303c43..78842247 100644 --- a/client/src/javascript/constants/EventTypes.ts +++ b/client/src/javascript/constants/EventTypes.ts @@ -6,7 +6,6 @@ export type EventType = | 'AUTH_LIST_USERS_SUCCESS' | 'AUTH_LOGIN_ERROR' | 'AUTH_LOGIN_SUCCESS' - | 'AUTH_REGISTER_ERROR' | 'AUTH_REGISTER_SUCCESS' | 'AUTH_VERIFY_ERROR' | 'AUTH_VERIFY_SUCCESS' diff --git a/client/src/javascript/constants/ServerActions.ts b/client/src/javascript/constants/ServerActions.ts index 859d9248..54c38dda 100644 --- a/client/src/javascript/constants/ServerActions.ts +++ b/client/src/javascript/constants/ServerActions.ts @@ -12,7 +12,6 @@ import type {Feeds, Items, Rules} from '../stores/FeedsStore'; type ErrorType = | 'AUTH_LOGIN_ERROR' | 'AUTH_LOGOUT_ERROR' - | 'AUTH_REGISTER_ERROR' | 'AUTH_VERIFY_ERROR' | 'CLIENT_ADD_TORRENT_ERROR' | 'FLOOD_CLEAR_NOTIFICATIONS_ERROR' diff --git a/client/src/javascript/stores/AuthStore.ts b/client/src/javascript/stores/AuthStore.ts index db80bf6b..aee884e8 100644 --- a/client/src/javascript/stores/AuthStore.ts +++ b/client/src/javascript/stores/AuthStore.ts @@ -100,10 +100,6 @@ class AuthStoreClass extends BaseStore { FloodActions.restartActivityStream(); } - handleRegisterError(error?: Error): void { - this.emit('AUTH_REGISTER_ERROR', error); - } - handleAuthVerificationSuccess(response: AuthVerificationResponse): void { if (response.initialUser === true) { this.currentUser.isInitialUser = response.initialUser; @@ -160,9 +156,6 @@ AuthStore.dispatcherID = AppDispatcher.register((payload) => { case 'AUTH_REGISTER_SUCCESS': AuthStore.handleRegisterSuccess(action.data); break; - case 'AUTH_REGISTER_ERROR': - AuthStore.handleRegisterError(action.error); - break; case 'AUTH_VERIFY_SUCCESS': AuthStore.handleAuthVerificationSuccess(action.data); break; diff --git a/cypress/integration/login.spec.ts b/cypress/integration/login.spec.ts new file mode 100644 index 00000000..63d8d437 --- /dev/null +++ b/cypress/integration/login.spec.ts @@ -0,0 +1,54 @@ +context('Login', () => { + beforeEach(() => { + cy.server(); + cy.route({method: 'GET', url: 'http://127.0.0.1:4200/api/auth/verify?*', response: {}, status: 401}).as( + 'verify-request', + ); + cy.visit('http://127.0.0.1:4200/login'); + cy.url().should('include', 'login'); + }); + + it('Login without username', () => { + cy.get('.input[name="password"]').type('test'); + cy.get('.button[type="submit"]').click(); + cy.get('.application__view--auth-form').should('be.visible'); + cy.get('.application__content').should('not.be.visible'); + cy.get('.application__loading-overlay').should('not.be.visible'); + cy.get('.error').should('be.visible'); + }); + + it('Login without password', () => { + cy.get('.input[name="username"]').type('test'); + cy.get('.button[type="submit"]').click(); + cy.get('.application__view--auth-form').should('be.visible'); + cy.get('.application__content').should('not.be.visible'); + cy.get('.application__loading-overlay').should('not.be.visible'); + cy.get('.error').should('be.visible'); + }); + + it('Login, server error occurred', () => { + cy.get('.input[name="username"]').type('test'); + cy.get('.input[name="password"]').type('test'); + + cy.server(); + cy.route({ + method: 'POST', + url: 'http://127.0.0.1:4200/api/auth/authenticate', + status: 500, + }).as('verify-request'); + + cy.get('.button[type="submit"]').click(); + cy.get('.application__view--auth-form').should('be.visible'); + cy.get('.application__content').should('not.be.visible'); + cy.get('.application__loading-overlay').should('not.be.visible'); + cy.get('.error').should('be.visible'); + }); + + it('Clear', () => { + cy.get('.input[name="username"]').type('test').as('password'); + cy.get('.input[name="password"]').type('test').as('username'); + cy.get('.button__content').contains('Clear').parent().click(); + cy.get('@username').should('have.value', ''); + cy.get('@password').should('have.value', ''); + }); +}); diff --git a/cypress/integration/register.spec.ts b/cypress/integration/register.spec.ts index 143aa730..347962b1 100644 --- a/cypress/integration/register.spec.ts +++ b/cypress/integration/register.spec.ts @@ -1,5 +1,12 @@ context('Register', () => { beforeEach(() => { + cy.server(); + cy.route({ + method: 'GET', + url: 'http://127.0.0.1:4200/api/auth/verify?*', + response: {initialUser: true}, + status: 200, + }).as('verify-request'); cy.visit('http://127.0.0.1:4200/register'); cy.url().should('include', 'register'); }); @@ -38,6 +45,7 @@ context('Register', () => { cy.get('.application__view--auth-form').should('be.visible'); cy.get('.application__content').should('not.be.visible'); cy.get('.application__loading-overlay').should('not.be.visible'); + cy.get('.error').should('be.visible'); }); it('Register without password', () => { @@ -51,6 +59,7 @@ context('Register', () => { cy.get('.application__view--auth-form').should('be.visible'); cy.get('.application__content').should('not.be.visible'); cy.get('.application__loading-overlay').should('not.be.visible'); + cy.get('.error').should('be.visible'); }); it('Register without connection settings', () => { @@ -60,9 +69,10 @@ context('Register', () => { cy.get('.application__view--auth-form').should('be.visible'); cy.get('.application__content').should('not.be.visible'); cy.get('.application__loading-overlay').should('not.be.visible'); + cy.get('.error').should('be.visible'); }); - it('Register with socket connection settings', () => { + it('Register with socket connection settings, server error occurred', () => { cy.get('.input[name="username"]').type('test'); cy.get('.input[name="password"]').type('test'); cy.get('.select').click(); @@ -71,13 +81,16 @@ context('Register', () => { cy.get('.input--text[name="socket"]').type('/data/rtorrent.sock'); cy.server(); - cy.route({method: 'POST', url: 'http://127.0.0.1:4200/api/auth/register', response: {}, status: 403}).as( + cy.route({method: 'POST', url: 'http://127.0.0.1:4200/api/auth/register', response: {}, status: 500}).as( 'register-request', ); cy.get('.button[type="submit"]').click(); - cy.get('.application__view--auth-form').should('not.be.visible'); - cy.get('.application__content').should('be.visible'); + cy.get('.application__view--auth-form').should('be.visible'); + cy.get('.application__content').should('not.be.visible'); + cy.get('.application__loading-overlay').should('not.be.visible'); + + cy.get('.error').should('be.visible'); }); });