Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions resources/js/login/error_handling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const SESSION_EXPIRED_MESSAGE =
'Your session has expired or your browser is blocking cookies. Please reload the page and try again.';
export const GENERIC_RETRY_MESSAGE = 'Something went wrong. Please try again.';
export const TOO_MANY_REQUESTS_MESSAGE = 'Too many requests. Try it later.';

export const ACCOUNT_NOT_FOUND = 'account_not_found';
export const SESSION_EXPIRED = 'session_expired';
export const TOO_MANY_REQUESTS = 'too_many_requests';
export const GENERIC_ERROR = 'generic_error';

/**
* Classifies a failed POST /auth/login/account-verify.
* Only a real 404 means the account does not exist; anything else must never
* switch the page to the sign-up state.
* @param {number|undefined} status HTTP status (undefined on network errors)
* @returns {{type: string, message: string}}
*/
export const classifyAccountVerifyError = (status) => {
switch (Number(status)) {
case 404:
// non-empty on purpose: errors.email only has to be truthy for this branch
return {type: ACCOUNT_NOT_FOUND, message: ' '};
case 419:
return {type: SESSION_EXPIRED, message: SESSION_EXPIRED_MESSAGE};
case 429:
return {type: TOO_MANY_REQUESTS, message: TOO_MANY_REQUESTS_MESSAGE};
default:
return {type: GENERIC_ERROR, message: GENERIC_RETRY_MESSAGE};
}
};

export const isSessionExpired = (error) => Number(error?.status) === 419;
42 changes: 42 additions & 0 deletions resources/js/login/error_handling.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
classifyAccountVerifyError,
isSessionExpired,
ACCOUNT_NOT_FOUND,
SESSION_EXPIRED,
TOO_MANY_REQUESTS,
GENERIC_ERROR,
SESSION_EXPIRED_MESSAGE,
GENERIC_RETRY_MESSAGE,
TOO_MANY_REQUESTS_MESSAGE,
} from './error_handling';

describe('classifyAccountVerifyError', () => {
test('404 is the only status that means account not found', () => {
const r = classifyAccountVerifyError(404);
expect(r.type).toBe(ACCOUNT_NOT_FOUND);
expect(r.message.trim()).toBe('');
expect(r.message).toBeTruthy();
});

test('419 is a session/cookies problem, not account not found', () => {
expect(classifyAccountVerifyError(419)).toEqual({type: SESSION_EXPIRED, message: SESSION_EXPIRED_MESSAGE});
});

test('429 keeps the too many requests message', () => {
expect(classifyAccountVerifyError(429)).toEqual({type: TOO_MANY_REQUESTS, message: TOO_MANY_REQUESTS_MESSAGE});
expect(TOO_MANY_REQUESTS_MESSAGE).toBe('Too many requests. Try it later.');
});

test.each([500, 502, 503, 400, undefined, null])('status %p is a generic error', (status) => {
expect(classifyAccountVerifyError(status)).toEqual({type: GENERIC_ERROR, message: GENERIC_RETRY_MESSAGE});
});
});

describe('isSessionExpired', () => {
test('detects 419 only', () => {
expect(isSessionExpired({status: 419})).toBe(true);
expect(isSessionExpired({status: 404})).toBe(false);
expect(isSessionExpired({})).toBe(false);
expect(isSessionExpired(undefined)).toBe(false);
});
});
58 changes: 50 additions & 8 deletions resources/js/login/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ import CustomSnackbar from "../components/custom_snackbar";
import Banner from '../components/banner/banner';
import OtpInput from 'react-otp-input';
import {handleErrorResponse, handleThirdPartyProvidersVerbiage} from '../utils';
import {
classifyAccountVerifyError,
isSessionExpired,
ACCOUNT_NOT_FOUND,
SESSION_EXPIRED,
SESSION_EXPIRED_MESSAGE
} from './error_handling';

import styles from './login.module.scss'
import "./third_party_identity_providers.scss";
Expand Down Expand Up @@ -453,6 +460,8 @@ class LoginPage extends React.Component {
user_verified: props.hasOwnProperty('user_verified') ? props.user_verified : false,
user_active: props.hasOwnProperty('user_active') ? props.user_active : null,
email_verified: props.hasOwnProperty('email_verified') ? props.email_verified : null,
account_not_found: false,
account_verify_error: null,
errors: {
email: '',
otp: props.authError != '' ? props.authError : '',
Expand Down Expand Up @@ -530,6 +539,10 @@ class LoginPage extends React.Component {
this.showAlert(errors[0], 'error');
return;
}
if (isSessionExpired(error)) {
this.showAlert(SESSION_EXPIRED_MESSAGE, 'error');
return;
}
this.showAlert('Oops... Something went wrong!', 'error');
});
return false;
Expand Down Expand Up @@ -628,6 +641,8 @@ class LoginPage extends React.Component {
user_pic: response.pic,
user_fullname: response.full_name,
user_verified: true,
account_not_found: false,
account_verify_error: null,
user_active: response.is_active,
email_verified: response.is_verified,
authFlow: response.has_password_set ? password_flow : otp_flow,
Expand All @@ -648,20 +663,20 @@ class LoginPage extends React.Component {

let { response, status, message } = error;

const {type, message: errorMessage} = classifyAccountVerifyError(status);

let newErrors = {};

newErrors['password'] = '';
newErrors['email'] = " ";

if (status == 429) {
newErrors['email'] = "Too many requests. Try it later.";
}
newErrors['email'] = errorMessage;

this.setState({
...this.state,
user_pic: null,
user_fullname: null,
user_verified: false,
account_not_found: type === ACCOUNT_NOT_FOUND,
account_verify_error: type === ACCOUNT_NOT_FOUND ? null : type,
errors: newErrors,
disableInput: false
});
Expand All @@ -686,6 +701,10 @@ class LoginPage extends React.Component {
'We\'ve sent you a verification email. Please check your inbox and click the link to verify your account.',
'success');
}, (error) => {
if (isSessionExpired(error)) {
this.showAlert(SESSION_EXPIRED_MESSAGE, 'error');
return;
}
handleErrorResponse(error, (title, messageLines, type) => {
const message = (messageLines ?? []).join(', ')
this.showAlert(`${title}: ${message}`, type);
Expand All @@ -701,6 +720,8 @@ class LoginPage extends React.Component {
user_pic: null,
user_fullname: null,
user_verified: false,
account_not_found: false,
account_verify_error: null,
user_active: null,
email_verified: null,
authFlow: "password",
Expand Down Expand Up @@ -728,9 +749,9 @@ class LoginPage extends React.Component {
}

getSignUpSignInTitle() {
const { errors, user_active } = this.state;
const { account_not_found } = this.state;

if (errors.email && this.existingUserCanContinue()) {
if (account_not_found && this.existingUserCanContinue()) {
return 'Create an account for:';
}
return 'Sign in';
Expand Down Expand Up @@ -801,7 +822,28 @@ class LoginPage extends React.Component {
// we already had an interaction and got an user error...
this.state.errors.email !== '' &&
<>
{this.existingUserCanContinue() &&
{this.state.account_verify_error === SESSION_EXPIRED &&
<Button variant="contained"
color="primary"
title="Reload page"
className={styles.apply_button}
onClick={() => window.location.reload()}>
Reload page
</Button>
}
{this.state.account_verify_error !== null &&
this.state.account_verify_error !== SESSION_EXPIRED &&
this.state.account_verify_error !== 'too_many_requests' &&
<Button variant="contained"
color="primary"
title="Try again"
className={styles.apply_button}
disabled={this.state.disableInput}
onClick={this.onValidateEmail}>
Try again
</Button>
}
{this.state.account_not_found && this.existingUserCanContinue() &&
<EmailErrorActions
emitOtpAction={this.handleEmitOtpAction}
onValidateEmail={this.onValidateEmail}
Expand Down
Loading