Skip to content

Commit 1f2e9b6

Browse files
matiasperrone-exosmarcet
authored andcommitted
feat: Add Login UI MFA Flow
1 parent 349deb8 commit 1f2e9b6

19 files changed

Lines changed: 1966 additions & 868 deletions

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"clean": "find . -name \"node_modules\" -type d -prune -exec rm -rf '{}' + && yarn",
99
"build-dev": "./node_modules/.bin/webpack --config webpack.dev.js",
1010
"build": "./node_modules/.bin/webpack --config webpack.prod.js",
11-
"serve": "webpack-dev-server --open --port=8888 --https --config webpack.dev.js",
11+
"serve": "webpack-dev-server --open --port=8888 --server-type https --config webpack.dev.js",
1212
"test": "jest --watch"
1313
},
1414
"devDependencies": {
@@ -81,6 +81,7 @@
8181
"bootstrap-tagsinput": "^0.7.1",
8282
"chosen-js": "^1.8.7",
8383
"crypto-js": "^3.1.9-1",
84+
"dompurify": "^3.4.11",
8485
"easymde": "^2.18.0",
8586
"font-awesome": "^4.7.0",
8687
"formik": "^2.2.9",
@@ -95,6 +96,7 @@
9596
"moment": "^2.29.4",
9697
"moment-timezone": "^0.5.21",
9798
"popper.js": "^1.14.3",
99+
"prop-types": "^15.8.1",
98100
"pure": "^2.85.0",
99101
"pwstrength-bootstrap": "^3.0.10",
100102
"react-otp-input": "^3.1.1",

resources/js/base_actions.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,40 @@ export const postRawRequest = (endpoint) => (params, headers = {}) => {
9292
})
9393
}
9494

95+
// Like postRawRequest, but also surfaces the final URL the browser landed on after the
96+
// XHR transparently followed any 3xx redirects (res.xhr.responseURL) and the HTTP status.
97+
// Used by flows that complete via a server-side redirect (e.g. 2FA verify) so the SPA can
98+
// navigate the top window to the post-login destination.
99+
export const postRawRequestFull = (endpoint) => (params, headers = {}, queryParams = {}) => {
100+
let url = URI(endpoint);
101+
102+
if (!isObjectEmpty(queryParams))
103+
url = url.query(queryParams);
104+
105+
let key = url.toString();
106+
107+
cancel(key);
108+
109+
let req = http.post(url.toString());
110+
111+
schedule(key, req);
112+
113+
return req.set(headers).send(params).timeout({
114+
response: 60000,
115+
deadline: 60000,
116+
}).then((res) => {
117+
end(key);
118+
return Promise.resolve({
119+
response: res.body,
120+
status: res.status,
121+
finalUrl: (res.xhr && res.xhr.responseURL) ? res.xhr.responseURL : null,
122+
});
123+
}).catch((error) => {
124+
end(key);
125+
return Promise.reject(error);
126+
})
127+
}
128+
95129
export const putRawRequest = (endpoint) => (payload = null, params={}, headers = {}) => {
96130
let url = URI(endpoint);
97131

resources/js/login/actions.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {postRawRequest} from '../base_actions'
1+
import {postRawRequest, postRawRequestFull } from '../base_actions'
22

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

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

2828
return postRawRequest(window.RESEND_VERIFICATION_EMAIL_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
2929
}
30+
31+
// verify / recovery complete login via a server-side redirect, so use the *Full helper to
32+
// recover the final URL for top-window navigation.
33+
export const verify2FA = (otpValue, method, trustDevice, token) => {
34+
const params = {
35+
otp_value: otpValue,
36+
method: method,
37+
trust_device: trustDevice ? 1 : 0
38+
};
39+
40+
return postRawRequestFull(window.VERIFY_2FA_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
41+
}
42+
43+
export const resend2FA = (method, token) => {
44+
const params = {
45+
method: method
46+
};
47+
48+
return postRawRequestFull(window.RESEND_2FA_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
49+
}
50+
51+
export const verifyRecoveryCode = (recoveryCode, token) => {
52+
const params = {
53+
recovery_code: recoveryCode
54+
};
55+
56+
return postRawRequestFull(window.RECOVERY_2FA_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
57+
}
58+
59+
export const authenticateWithPassword = (formData, token) => {
60+
const params = Object.fromEntries(formData.entries());
61+
return postRawRequestFull(window.FORM_ACTION_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
62+
}
63+
64+
export const cancelLogin = (token) => {
65+
return postRawRequest(window.CANCEL_LOGIN_ENDPOINT)({}, {'X-CSRF-TOKEN': token});
66+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from "react";
2+
import Grid from "@material-ui/core/Grid";
3+
import Button from "@material-ui/core/Button";
4+
import styles from "../login.module.scss";
5+
6+
const EmailErrorActions = ({
7+
emitOtpAction,
8+
createAccountAction,
9+
onValidateEmail,
10+
disableInput,
11+
}) => {
12+
return (
13+
<Grid container spacing={1}>
14+
<Grid
15+
container
16+
item
17+
spacing={1}
18+
justifyContent="center"
19+
alignItems="center"
20+
>
21+
<Grid item>
22+
<Button
23+
variant="contained"
24+
onClick={emitOtpAction}
25+
type="button"
26+
className={styles.secondary_btn}
27+
color="primary"
28+
>
29+
Email me a one time use code
30+
</Button>
31+
</Grid>
32+
<Grid item>
33+
<Button
34+
variant="contained"
35+
href={createAccountAction}
36+
type="button"
37+
target="_self"
38+
className={styles.secondary_btn}
39+
color="primary"
40+
>
41+
Register and set a password
42+
</Button>
43+
</Grid>
44+
<Grid item>
45+
<Button
46+
variant="text"
47+
onClick={onValidateEmail}
48+
disabled={disableInput}
49+
className={styles.secondary_btn}
50+
color="primary"
51+
>
52+
Adjust email above and try again
53+
</Button>
54+
</Grid>
55+
</Grid>
56+
</Grid>
57+
);
58+
};
59+
60+
export default EmailErrorActions;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from "react";
2+
import Paper from "@material-ui/core/Paper";
3+
import TextField from "@material-ui/core/TextField";
4+
import Button from "@material-ui/core/Button";
5+
import styles from "../login.module.scss";
6+
import HTMLRender from "../../shared/HTMLRender";
7+
8+
const EmailInputForm = ({
9+
value,
10+
onValidateEmail,
11+
onHandleUserNameChange,
12+
disableInput,
13+
emailError,
14+
}) => {
15+
return (
16+
<>
17+
<Paper
18+
elevation={0}
19+
component="form"
20+
target="_self"
21+
className={styles.paper_root}
22+
onSubmit={onValidateEmail}
23+
>
24+
<TextField
25+
id="email"
26+
name="email"
27+
value={value}
28+
autoComplete="email"
29+
variant="outlined"
30+
margin="normal"
31+
required
32+
fullWidth
33+
disabled={disableInput}
34+
label="Email Address"
35+
autoFocus={true}
36+
onChange={onHandleUserNameChange}
37+
error={emailError != ""}
38+
/>
39+
{emailError == "" && (
40+
<Button
41+
variant="contained"
42+
color="primary"
43+
title="Continue"
44+
className={styles.apply_button}
45+
disabled={disableInput}
46+
onClick={onValidateEmail}
47+
>
48+
&gt;
49+
</Button>
50+
)}
51+
</Paper>
52+
{emailError != "" && (
53+
<HTMLRender component="p" className={styles.error_label}>
54+
{emailError}
55+
</HTMLRender>
56+
)}
57+
</>
58+
);
59+
};
60+
61+
export default EmailInputForm;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from "react";
2+
import Grid from "@material-ui/core/Grid";
3+
import Button from "@material-ui/core/Button";
4+
import Link from "@material-ui/core/Link";
5+
import styles from "../login.module.scss";
6+
7+
const ExistingAccountActions = ({
8+
emitOtpAction,
9+
forgotPasswordAction,
10+
userName,
11+
disableInput,
12+
}) => {
13+
let forgotPasswordActionHref = forgotPasswordAction;
14+
15+
if (userName) {
16+
forgotPasswordActionHref = `${forgotPasswordAction}?email=${encodeURIComponent(userName)}`;
17+
}
18+
19+
return (
20+
<Grid container spacing={1} style={{ marginTop: "30px" }}>
21+
<Grid item xs={12}>
22+
<Button
23+
variant="contained"
24+
onClick={emitOtpAction}
25+
type="button"
26+
disabled={disableInput}
27+
className={styles.secondary_btn}
28+
color="primary"
29+
>
30+
Sign in by emailing me a single-use code
31+
</Button>
32+
</Grid>
33+
<Grid item xs={12}>
34+
<Link
35+
onClick={disableInput ? (e) => e.preventDefault() : undefined}
36+
href={forgotPasswordActionHref}
37+
target="_self"
38+
variant="body2"
39+
>
40+
Reset your password
41+
</Link>
42+
</Grid>
43+
</Grid>
44+
);
45+
};
46+
47+
export default ExistingAccountActions;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React, { useMemo } from "react";
2+
import Link from "@material-ui/core/Link";
3+
import styles from "../login.module.scss";
4+
5+
const HelpLinks = ({
6+
userName,
7+
showEmitOtpAction,
8+
forgotPasswordAction,
9+
showForgotPasswordAction,
10+
showVerifyEmailAction,
11+
verifyEmailAction,
12+
showHelpAction,
13+
helpAction,
14+
appName,
15+
emitOtpAction,
16+
}) => {
17+
const actions = useMemo(() => {
18+
let forgotPasswordActionHref = forgotPasswordAction;
19+
if (userName) {
20+
const separator = forgotPasswordAction.includes("?") ? "&" : "?";
21+
forgotPasswordActionHref = `${forgotPasswordAction}${separator}email=${encodeURIComponent(userName)}`;
22+
}
23+
24+
return [
25+
{
26+
show: showEmitOtpAction,
27+
href: "#",
28+
onClick: emitOtpAction,
29+
label: "Get A Single-use Code emailed to you",
30+
},
31+
{
32+
show: showForgotPasswordAction,
33+
href: forgotPasswordActionHref,
34+
label: "Reset your password",
35+
},
36+
{
37+
show: showVerifyEmailAction,
38+
href: verifyEmailAction,
39+
label: `Verify ${appName}`,
40+
},
41+
{
42+
show: showHelpAction,
43+
href: helpAction,
44+
label: "Having trouble?",
45+
},
46+
].filter((action) => action.show);
47+
}, [
48+
showEmitOtpAction,
49+
showForgotPasswordAction,
50+
showVerifyEmailAction,
51+
showHelpAction,
52+
userName,
53+
forgotPasswordAction,
54+
verifyEmailAction,
55+
helpAction,
56+
appName,
57+
emitOtpAction,
58+
]);
59+
60+
return (
61+
<>
62+
<hr className={styles.separator} />
63+
{actions.map((action, index) => (
64+
<Link
65+
key={index}
66+
href={action.href}
67+
onClick={action.onClick}
68+
variant="body2"
69+
target="_self"
70+
>
71+
{action.label}
72+
</Link>
73+
))}
74+
</>
75+
);
76+
};
77+
78+
export default HelpLinks;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from "react";
2+
import Link from "@material-ui/core/Link";
3+
import styles from "../login.module.scss";
4+
5+
const OTPHelpLinks = ({ emitOtpAction }) => {
6+
return (
7+
<>
8+
<hr className={styles.separator} />
9+
<p className={styles.otp_p}>Didn't receive it ?</p>
10+
<p className={styles.otp_p}>
11+
Check your spam folder or{" "}
12+
<Link href="#" onClick={emitOtpAction} variant="body2" target="_self">
13+
resend email.
14+
</Link>
15+
</p>
16+
</>
17+
);
18+
};
19+
20+
export default OTPHelpLinks;

0 commit comments

Comments
 (0)