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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"clean": "find . -name \"node_modules\" -type d -prune -exec rm -rf '{}' + && yarn",
"build-dev": "./node_modules/.bin/webpack --config webpack.dev.js",
"build": "./node_modules/.bin/webpack --config webpack.prod.js",
"serve": "webpack-dev-server --open --port=8888 --https --config webpack.dev.js",
"serve": "webpack-dev-server --open --port=8888 --server-type https --config webpack.dev.js",
"test": "jest --watch"
},
"devDependencies": {
Expand Down Expand Up @@ -81,6 +81,7 @@
"bootstrap-tagsinput": "^0.7.1",
"chosen-js": "^1.8.7",
"crypto-js": "^3.1.9-1",
"dompurify": "^3.4.11",
"easymde": "^2.18.0",
"font-awesome": "^4.7.0",
"formik": "^2.2.9",
Expand All @@ -95,6 +96,7 @@
"moment": "^2.29.4",
"moment-timezone": "^0.5.21",
"popper.js": "^1.14.3",
"prop-types": "^15.8.1",
"pure": "^2.85.0",
"pwstrength-bootstrap": "^3.0.10",
"react-otp-input": "^3.1.1",
Expand Down
34 changes: 34 additions & 0 deletions resources/js/base_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,40 @@ export const postRawRequest = (endpoint) => (params, headers = {}) => {
})
}

// Like postRawRequest, but also surfaces the final URL the browser landed on after the
// XHR transparently followed any 3xx redirects (res.xhr.responseURL) and the HTTP status.
// Used by flows that complete via a server-side redirect (e.g. 2FA verify) so the SPA can
// navigate the top window to the post-login destination.
export const postRawRequestFull = (endpoint) => (params, headers = {}, queryParams = {}) => {
let url = URI(endpoint);

if (!isObjectEmpty(queryParams))
url = url.query(queryParams);

let key = url.toString();

cancel(key);

let req = http.post(url.toString());

schedule(key, req);

return req.set(headers).send(params).timeout({
response: 60000,
deadline: 60000,
}).then((res) => {
end(key);
return Promise.resolve({
response: res.body,
status: res.status,
finalUrl: (res.xhr && res.xhr.responseURL) ? res.xhr.responseURL : null,
});
}).catch((error) => {
end(key);
return Promise.reject(error);
})
}

export const putRawRequest = (endpoint) => (payload = null, params={}, headers = {}) => {
let url = URI(endpoint);

Expand Down
39 changes: 38 additions & 1 deletion resources/js/login/actions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {postRawRequest} from '../base_actions'
import {postRawRequest, postRawRequestFull } from '../base_actions'

export const verifyAccount = (email, token) => {

Expand Down Expand Up @@ -27,3 +27,40 @@ export const resendVerificationEmail = (email, token) => {

return postRawRequest(window.RESEND_VERIFICATION_EMAIL_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
}

// verify / recovery complete login via a server-side redirect, so use the *Full helper to
// recover the final URL for top-window navigation.
export const verify2FA = (otpValue, method, trustDevice, token) => {
const params = {
otp_value: otpValue,
method: method,
trust_device: trustDevice ? 1 : 0
};

return postRawRequestFull(window.VERIFY_2FA_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
}

export const resend2FA = (method, token) => {
const params = {
method: method
};

return postRawRequestFull(window.RESEND_2FA_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
}

export const verifyRecoveryCode = (recoveryCode, token) => {
const params = {
recovery_code: recoveryCode
};

return postRawRequestFull(window.RECOVERY_2FA_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
}

export const authenticateWithPassword = (formData, token) => {
const params = Object.fromEntries(formData.entries());
return postRawRequestFull(window.FORM_ACTION_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
}

export const cancelLogin = (token) => {
return postRawRequest(window.CANCEL_LOGIN_ENDPOINT)({}, {'X-CSRF-TOKEN': token});
}
60 changes: 60 additions & 0 deletions resources/js/login/components/email_error_actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from "react";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import styles from "../login.module.scss";

const EmailErrorActions = ({
emitOtpAction,
createAccountAction,
onValidateEmail,
disableInput,
}) => {
return (
<Grid container spacing={1}>
<Grid
container
item
spacing={1}
justifyContent="center"
alignItems="center"
>
<Grid item>
<Button
variant="contained"
onClick={emitOtpAction}
type="button"
className={styles.secondary_btn}
color="primary"
>
Email me a one time use code
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
href={createAccountAction}
type="button"
target="_self"
className={styles.secondary_btn}
color="primary"
>
Register and set a password
</Button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</Grid>
<Grid item>
<Button
variant="text"
onClick={onValidateEmail}
disabled={disableInput}
className={styles.secondary_btn}
color="primary"
>
Adjust email above and try again
</Button>
</Grid>
</Grid>
</Grid>
);
};

export default EmailErrorActions;
61 changes: 61 additions & 0 deletions resources/js/login/components/email_input_form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";
import Paper from "@material-ui/core/Paper";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";
import styles from "../login.module.scss";
import HTMLRender from "../../shared/HTMLRender";

const EmailInputForm = ({
value,
onValidateEmail,
onHandleUserNameChange,
disableInput,
emailError,
}) => {
return (
<>
<Paper
elevation={0}
component="form"
target="_self"
className={styles.paper_root}
onSubmit={onValidateEmail}
>
<TextField
id="email"
name="email"
value={value}
autoComplete="email"
variant="outlined"
margin="normal"
required
fullWidth
disabled={disableInput}
label="Email Address"
autoFocus={true}
onChange={onHandleUserNameChange}
error={emailError != ""}
/>
{emailError == "" && (
<Button
variant="contained"
color="primary"
title="Continue"
className={styles.apply_button}
disabled={disableInput}
onClick={onValidateEmail}
>
&gt;
</Button>
)}
</Paper>
{emailError != "" && (
<HTMLRender component="p" className={styles.error_label}>
{emailError}
</HTMLRender>
)}
</>
);
};

export default EmailInputForm;
47 changes: 47 additions & 0 deletions resources/js/login/components/existing_account_actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import Link from "@material-ui/core/Link";
import styles from "../login.module.scss";

const ExistingAccountActions = ({
emitOtpAction,
forgotPasswordAction,
userName,
disableInput,
}) => {
let forgotPasswordActionHref = forgotPasswordAction;

if (userName) {
forgotPasswordActionHref = `${forgotPasswordAction}?email=${encodeURIComponent(userName)}`;
}

return (
<Grid container spacing={1} style={{ marginTop: "30px" }}>
<Grid item xs={12}>
<Button
variant="contained"
onClick={emitOtpAction}
type="button"
disabled={disableInput}
className={styles.secondary_btn}
color="primary"
>
Sign in by emailing me a single-use code
</Button>
</Grid>
<Grid item xs={12}>
<Link
onClick={disableInput ? (e) => e.preventDefault() : undefined}
href={forgotPasswordActionHref}
target="_self"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
variant="body2"
>
Reset your password
</Link>
</Grid>
</Grid>
);
};

export default ExistingAccountActions;
78 changes: 78 additions & 0 deletions resources/js/login/components/help_links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useMemo } from "react";
import Link from "@material-ui/core/Link";
import styles from "../login.module.scss";

const HelpLinks = ({
userName,
showEmitOtpAction,
forgotPasswordAction,
showForgotPasswordAction,
showVerifyEmailAction,
verifyEmailAction,
showHelpAction,
helpAction,
appName,
emitOtpAction,
}) => {
const actions = useMemo(() => {
let forgotPasswordActionHref = forgotPasswordAction;
if (userName) {
const separator = forgotPasswordAction.includes("?") ? "&" : "?";
forgotPasswordActionHref = `${forgotPasswordAction}${separator}email=${encodeURIComponent(userName)}`;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return [
{
show: showEmitOtpAction,
href: "#",
onClick: emitOtpAction,
label: "Get A Single-use Code emailed to you",
},
{
show: showForgotPasswordAction,
href: forgotPasswordActionHref,
label: "Reset your password",
},
{
show: showVerifyEmailAction,
href: verifyEmailAction,
label: `Verify ${appName}`,
},
{
show: showHelpAction,
href: helpAction,
label: "Having trouble?",
},
].filter((action) => action.show);
}, [
showEmitOtpAction,
showForgotPasswordAction,
showVerifyEmailAction,
showHelpAction,
userName,
forgotPasswordAction,
verifyEmailAction,
helpAction,
appName,
emitOtpAction,
]);

return (
<>
<hr className={styles.separator} />
{actions.map((action, index) => (
<Link
key={index}
href={action.href}
onClick={action.onClick}
variant="body2"
target="_self"
>
{action.label}
</Link>
))}
</>
);
};

export default HelpLinks;
20 changes: 20 additions & 0 deletions resources/js/login/components/otp_help_links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";
import Link from "@material-ui/core/Link";
import styles from "../login.module.scss";

const OTPHelpLinks = ({ emitOtpAction }) => {
return (
<>
<hr className={styles.separator} />
<p className={styles.otp_p}>Didn't receive it ?</p>
<p className={styles.otp_p}>
Check your spam folder or{" "}
<Link href="#" onClick={emitOtpAction} variant="body2" target="_self">
resend email.
</Link>
</p>
</>
);
};

export default OTPHelpLinks;
Loading
Loading