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 on lines +22 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Disable all recovery actions while input is locked.

Two buttons in this block ignore disableInput, so users can still trigger actions while the UI is in a disabled state.

Suggested fix
         <Grid item>
           <Button
             variant="contained"
             onClick={emitOtpAction}
+            disabled={disableInput}
             type="button"
             className={styles.secondary_btn}
             color="primary"
           >
             Email me a one time use code
           </Button>
         </Grid>
         <Grid item>
           <Button
             variant="contained"
             href={createAccountAction}
+            disabled={disableInput}
             type="button"
             target="_self"
             className={styles.secondary_btn}
             color="primary"
           >
             Register and set a password
           </Button>
         </Grid>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<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>
<Button
variant="contained"
onClick={emitOtpAction}
disabled={disableInput}
type="button"
className={styles.secondary_btn}
color="primary"
>
Email me a one time use code
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
href={createAccountAction}
disabled={disableInput}
type="button"
target="_self"
className={styles.secondary_btn}
color="primary"
>
Register and set a password
</Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/components/email_error_actions.js` around lines 22 - 42,
The two Button components in this block are not respecting the disableInput
state, allowing users to trigger actions even when the UI should be locked. Add
the disabled prop set to disableInput on both buttons: the button with
onClick={emitOtpAction} and the button with href={createAccountAction}. This
will prevent users from clicking these recovery action buttons when the input is
disabled.

</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
disabled={disableInput}
href={forgotPasswordActionHref}
target="_self"
Comment on lines +34 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and check the package.json to determine MUI version
cd /
fd -t f "package.json" -x head -50 {} \; 2>/dev/null | head -100

Repository: OpenStackweb/openstackid

Length of output: 2974


🏁 Script executed:

# Find the specific file mentioned in the review
fd -t f "existing_account_actions.js"

Repository: OpenStackweb/openstackid

Length of output: 220


🏁 Script executed:

# Check for package.json at repo root
cat package.json 2>/dev/null | grep -A5 -B5 "`@material-ui`"

Repository: OpenStackweb/openstackid

Length of output: 698


🏁 Script executed:

# Read the existing_account_actions.js file to see the full context
cat -n resources/js/login/components/existing_account_actions.js

Repository: OpenStackweb/openstackid

Length of output: 1678


🏁 Script executed:

# Search for other usages of Link component in the codebase to understand pattern
rg "from.*`@material-ui/core`" resources/js/login/components/existing_account_actions.js -A 10

Repository: OpenStackweb/openstackid

Length of output: 506


🏁 Script executed:

# Check what disableInput is and how it's defined
rg "disableInput" resources/js/login/components/existing_account_actions.js -B 2 -A 2

Repository: OpenStackweb/openstackid

Length of output: 585


🌐 Web query:

Material-UI v4 Link component disabled prop behavior

💡 Result:

In Material-UI (MUI) v4, the Link component does not have a built-in "disabled" state or default styling for a disabled appearance [1][2]. While you can pass a disabled prop to the Link component—as it is built on top of the Typography component and supports spreading arbitrary props—the Link component itself does not natively interpret or style this prop [3][1][4]. Consequently, simply adding the disabled prop to a Link will not prevent user interaction, nor will it change the component's appearance [1][2]. To achieve a disabled effect in MUI v4, you must implement custom logic and styling [1]: 1. Accessibility and Interaction: If you need to stop interaction, you should manually disable the link's functionality (e.g., by preventing the onClick handler) and manage its focusability, such as by setting tabIndex={-1} [1]. For accessibility, it is recommended to add the aria-disabled="true" attribute [1]. 2. Styling: Since there is no default "disabled" class, you need to apply custom CSS to visually indicate that the link is disabled (e.g., changing the text color, setting cursor to default or not-allowed, and disabling text-decoration) [1]. 3. Using component="button": If you change the underlying element to a button using component="button", the standard HTML disabled attribute will function; however, you will still need to define custom styles to achieve the desired disabled visual appearance, as MUI v4 does not provide default disabled styling for the Link component even when rendered as a button [1][2].

Citations:


🌐 Web query:

MUI v4.11 Link API disabled attribute navigation

💡 Result:

In MUI v4.11, setting the disabled prop on MuiLink only prevents navigation when the rendered root element is a button (component="button"); otherwise it won’t reliably stop link navigation because the underlying element is an . What the code shows (v4.11.4): - MuiLink defaults component="a" (anchor). [1] - The Link implementation applies a “button” class only when component === 'button'. [1] So for disabled navigation you must do one of these: 1) Render it as a button and use disabled Use component="button". This makes disabled behave like a real disabled button: the browser won’t trigger navigation for href-based routing and MUI applies disabled semantics/styles for the button variant. - Example pattern: ... [2] 2) Keep it as an anchor () and block navigation yourself If you must use href with the default component="a", disabled does not inherently stop the default anchor click. A workaround is to intercept the click and call preventDefault when you want “disabled navigation”. (This is essentially the pattern described as a working workaround.) [2] 3) For accessibility If you implement “disabled-like” behavior via preventDefault, also add aria-disabled and remove from tab order as needed (as suggested in the workaround). [2] Practical takeaway - If you’re using MuiLink for navigation and want a true “disabled” state in MUI v4.11: set component="button" (and provide onClick only if needed) rather than relying on disabled while keeping it as an . [2][1] - If you keep href and the element: you need an onClick handler to preventDefault; disabled alone won’t be sufficient. [2]

Citations:


Link is not actually disabled here.

Passing disabled to MUI v4 Link does not prevent navigation when using the default component="a", so this action can still fire when input is locked.

Suggested fix
       <Grid item xs={12}>
         <Link
-          disabled={disableInput}
           href={forgotPasswordActionHref}
+          onClick={disableInput ? (ev) => ev.preventDefault() : undefined}
+          aria-disabled={disableInput}
+          className={disableInput ? styles.disabled_link : undefined}
           target="_self"
           variant="body2"
         >
           Reset your password
         </Link>
       </Grid>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/components/existing_account_actions.js` around lines 34 -
37, The `Link` component with `disabled={disableInput}` prop does not actually
prevent navigation in MUI v4 when using the default anchor element. To fix this,
add an onClick handler to the Link component that prevents the default
navigation behavior when disableInput is true. The onClick handler should call
preventDefault() on the event whenever disableInput is true, effectively
blocking the navigation while the input is locked.

variant="body2"
>
Reset your password
</Link>
</Grid>
</Grid>
);
};

export default ExistingAccountActions;
77 changes: 77 additions & 0 deletions resources/js/login/components/help_links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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) {
forgotPasswordActionHref = `${forgotPasswordAction}?email=${encodeURIComponent(userName)}`;
}
Comment on lines +18 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle existing query params when appending email.

Current construction always uses ?email=..., which breaks when forgotPasswordAction already contains query parameters.

Suggested fix
     let forgotPasswordActionHref = forgotPasswordAction;
     if (userName) {
-      forgotPasswordActionHref = `${forgotPasswordAction}?email=${encodeURIComponent(userName)}`;
+      const separator = forgotPasswordAction.includes("?") ? "&" : "?";
+      forgotPasswordActionHref = `${forgotPasswordAction}${separator}email=${encodeURIComponent(userName)}`;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let forgotPasswordActionHref = forgotPasswordAction;
if (userName) {
forgotPasswordActionHref = `${forgotPasswordAction}?email=${encodeURIComponent(userName)}`;
}
let forgotPasswordActionHref = forgotPasswordAction;
if (userName) {
const separator = forgotPasswordAction.includes("?") ? "&" : "?";
forgotPasswordActionHref = `${forgotPasswordAction}${separator}email=${encodeURIComponent(userName)}`;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/login/components/help_links.js` around lines 18 - 21, The URL
construction for forgotPasswordActionHref does not account for existing query
parameters in the forgotPasswordAction base URL. When appending the email
parameter, the code always uses `?email=...`, which will create invalid URLs if
forgotPasswordAction already contains a query string. Modify the conditional
block where forgotPasswordActionHref is assigned (when userName exists) to check
if forgotPasswordAction already contains a `?` character. If it does, use `&` to
append the email parameter; otherwise use `?`. This ensures proper URL
formatting regardless of whether the base URL already has query parameters.


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