mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Fleet UI: No role = no access, refactor jsx class components to typescript functional components (#12953)
This commit is contained in:
parent
2e6589b66c
commit
3477178758
1
changes/bug-11388-no-access-to-ui
Normal file
1
changes/bug-11388-no-access-to-ui
Normal file
@ -0,0 +1 @@
|
||||
- Users with no global or team role cannot access the UI
|
1
changes/bug-12632-login-viewport
Normal file
1
changes/bug-12632-login-viewport
Normal file
@ -0,0 +1 @@
|
||||
- Fix login form cut off when viewport is too short
|
@ -4,7 +4,6 @@
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
padding: $pad-medium 0;
|
||||
height: 100vh;
|
||||
|
||||
&__logo {
|
||||
width: 120px;
|
||||
|
@ -1,98 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from "react-router";
|
||||
import classnames from "classnames";
|
||||
|
||||
import CloseIcon from "../../../assets/images/icon-close-fleet-black-16x16@2x.png";
|
||||
|
||||
const baseClass = "stacked-white-boxes";
|
||||
|
||||
class StackedWhiteBoxes extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
headerText: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
leadText: PropTypes.string,
|
||||
onLeave: PropTypes.func,
|
||||
previousLocation: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
isLoaded: false,
|
||||
isLeaving: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { didLoad } = this;
|
||||
didLoad();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
didLoad = () => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
isLoaded: true,
|
||||
});
|
||||
};
|
||||
|
||||
renderBackButton = () => {
|
||||
const { previousLocation } = this.props;
|
||||
|
||||
if (!previousLocation) return false;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__back`}>
|
||||
<Link to={previousLocation} className={`${baseClass}__back-link`}>
|
||||
<img src={CloseIcon} alt="close icon" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderHeader = () => {
|
||||
const { headerText } = this.props;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__header`}>
|
||||
<p className={`${baseClass}__header-text`}>{headerText}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, className, leadText } = this.props;
|
||||
const { isLoading, isLoaded, isLeaving } = this.state;
|
||||
const { renderBackButton, renderHeader } = this;
|
||||
|
||||
const boxClass = classnames(baseClass, className, {
|
||||
[`${baseClass}--loading`]: isLoading,
|
||||
[`${baseClass}--loaded`]: isLoaded,
|
||||
[`${baseClass}--leaving`]: isLeaving,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={boxClass}>
|
||||
<div className={`${baseClass}__box`}>
|
||||
{renderBackButton()}
|
||||
{renderHeader()}
|
||||
<p className={`${baseClass}__box-text`}>{leadText}</p>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StackedWhiteBoxes;
|
70
frontend/components/StackedWhiteBoxes/StackedWhiteBoxes.tsx
Normal file
70
frontend/components/StackedWhiteBoxes/StackedWhiteBoxes.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { InjectedRouter, Link } from "react-router";
|
||||
import classnames from "classnames";
|
||||
|
||||
import paths from "router/paths";
|
||||
|
||||
import Icon from "components/Icon/Icon";
|
||||
|
||||
const baseClass = "stacked-white-boxes";
|
||||
|
||||
interface IStackedWhiteBoxesProps {
|
||||
children?: JSX.Element;
|
||||
headerText?: string;
|
||||
className?: string;
|
||||
leadText?: string;
|
||||
previousLocation?: string;
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const StackedWhiteBoxes = ({
|
||||
children,
|
||||
headerText,
|
||||
className,
|
||||
leadText,
|
||||
previousLocation,
|
||||
router,
|
||||
}: IStackedWhiteBoxesProps): JSX.Element => {
|
||||
const boxClass = classnames(baseClass, className);
|
||||
|
||||
useEffect(() => {
|
||||
const closeWithEscapeKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
router.push(paths.LOGIN);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", closeWithEscapeKey);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", closeWithEscapeKey);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderBackButton = () => {
|
||||
if (!previousLocation) return false;
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__back`}>
|
||||
<Link to={previousLocation} className={`${baseClass}__back-link`}>
|
||||
<Icon name="ex" color="core-fleet-black" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={boxClass}>
|
||||
<div className={`${baseClass}__box`}>
|
||||
{renderBackButton()}
|
||||
{headerText && (
|
||||
<p className={`${baseClass}__header-text`}>{headerText}</p>
|
||||
)}
|
||||
{leadText && <p className={`${baseClass}__box-text`}>{leadText}</p>}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StackedWhiteBoxes;
|
@ -3,46 +3,32 @@
|
||||
margin: 2.5rem auto;
|
||||
width: 516px;
|
||||
|
||||
&--loading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&--loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&--leaving {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&__box {
|
||||
background-color: $core-white;
|
||||
border-radius: 10px;
|
||||
min-height: 330px;
|
||||
box-sizing: border-box;
|
||||
padding: $pad-xxlarge;
|
||||
font-weight: $regular;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
|
||||
&-text {
|
||||
color: $core-fleet-black;
|
||||
font-size: $small;
|
||||
margin: $pad-xxlarge 0 $pad-large;
|
||||
p {
|
||||
font-size: $x-small;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
|
||||
&-text {
|
||||
font-size: $large;
|
||||
font-weight: 300;
|
||||
&__header-text {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
color: $core-fleet-black;
|
||||
line-height: 32px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__back {
|
||||
text-align: right;
|
||||
|
@ -1,125 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from "react-router";
|
||||
import classnames from "classnames";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Form from "components/forms/Form";
|
||||
import formFieldInterface from "interfaces/form_field";
|
||||
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon";
|
||||
import paths from "router/paths";
|
||||
import validate from "components/forms/LoginForm/validate";
|
||||
import ssoSettingsInterface from "interfaces/ssoSettings";
|
||||
|
||||
const baseClass = "login-form";
|
||||
const formFields = ["email", "password"];
|
||||
|
||||
class LoginForm extends Component {
|
||||
static propTypes = {
|
||||
baseError: PropTypes.string,
|
||||
fields: PropTypes.shape({
|
||||
password: formFieldInterface.isRequired,
|
||||
email: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func,
|
||||
isHidden: PropTypes.bool,
|
||||
ssoSettings: ssoSettingsInterface,
|
||||
handleSSOSignOn: PropTypes.func,
|
||||
};
|
||||
|
||||
showLegendWithImage = (image, idpName) => {
|
||||
let legend = "Single sign-on";
|
||||
if (idpName !== "") {
|
||||
legend = `Sign on with ${idpName}`;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<img src={image} alt={idpName} className={`${baseClass}__sso-image`} />
|
||||
<span className={`${baseClass}__sso-legend`}>{legend}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
showSingleSignOnButton = () => {
|
||||
const { ssoSettings, handleSSOSignOn } = this.props;
|
||||
const { idp_name: idpName, idp_image_url: imageURL } = ssoSettings;
|
||||
const { showLegendWithImage } = this;
|
||||
|
||||
let legend = "Single sign-on";
|
||||
if (idpName !== "") {
|
||||
legend = `Sign on with ${idpName}`;
|
||||
}
|
||||
if (imageURL !== "") {
|
||||
legend = showLegendWithImage(imageURL, idpName);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`${baseClass}__sso-btn`}
|
||||
type="button"
|
||||
title="Single sign-on"
|
||||
variant="inverse"
|
||||
onClick={handleSSOSignOn}
|
||||
>
|
||||
<div>{legend}</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
baseError,
|
||||
fields,
|
||||
handleSubmit,
|
||||
isHidden,
|
||||
ssoSettings,
|
||||
} = this.props;
|
||||
const { sso_enabled: ssoEnabled } = ssoSettings || {};
|
||||
const { showSingleSignOnButton } = this;
|
||||
|
||||
const loginFormClass = classnames(baseClass, {
|
||||
[`${baseClass}--hidden`]: isHidden,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={loginFormClass}>
|
||||
<div className={`${baseClass}__container`}>
|
||||
{baseError && <div className="form__base-error">{baseError}</div>}
|
||||
<InputFieldWithIcon
|
||||
{...fields.email}
|
||||
autofocus
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<InputFieldWithIcon
|
||||
{...fields.password}
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
/>
|
||||
<div className={`${baseClass}__forgot-wrap`}>
|
||||
<Link
|
||||
className={`${baseClass}__forgot-link`}
|
||||
to={paths.FORGOT_PASSWORD}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
className={`${baseClass}__submit-btn button button--brand`}
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
{ssoEnabled && showSingleSignOnButton()}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form(LoginForm, {
|
||||
fields: formFields,
|
||||
validate,
|
||||
});
|
@ -16,7 +16,7 @@ describe("LoginForm - component", () => {
|
||||
it("renders the base error", () => {
|
||||
render(
|
||||
<LoginForm
|
||||
serverErrors={{ base: baseError }}
|
||||
baseError={baseError}
|
||||
handleSubmit={submitSpy}
|
||||
ssoSettings={settings}
|
||||
/>
|
||||
@ -34,7 +34,7 @@ describe("LoginForm - component", () => {
|
||||
it("renders 2 InputField components", () => {
|
||||
render(<LoginForm handleSubmit={submitSpy} ssoSettings={settings} />);
|
||||
|
||||
expect(screen.getByRole("textbox", { name: "Email" })).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("Email")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("Password")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -69,7 +69,7 @@ describe("LoginForm - component", () => {
|
||||
<LoginForm handleSubmit={submitSpy} ssoSettings={settings} />
|
||||
);
|
||||
|
||||
await user.type(screen.getByRole("textbox", { name: "Email" }), validEmail);
|
||||
await user.type(screen.getByPlaceholderText("Email"), validEmail);
|
||||
|
||||
// try to log in without entering a password
|
||||
await user.click(screen.getByRole("button", { name: "Login" }));
|
||||
@ -95,7 +95,7 @@ describe("LoginForm - component", () => {
|
||||
<LoginForm handleSubmit={submitSpy} ssoSettings={settings} />
|
||||
);
|
||||
|
||||
await user.type(screen.getByRole("textbox", { name: "Email" }), validEmail);
|
||||
await user.type(screen.getByPlaceholderText("Email"), validEmail);
|
||||
await user.type(screen.getByPlaceholderText("Password"), password);
|
||||
await user.click(screen.getByRole("button", { name: "Login" }));
|
||||
|
165
frontend/components/forms/LoginForm/LoginForm.tsx
Normal file
165
frontend/components/forms/LoginForm/LoginForm.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import React, { FormEvent, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { size } from "lodash";
|
||||
import classnames from "classnames";
|
||||
import { ILoginUserData } from "interfaces/user";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon";
|
||||
import paths from "router/paths";
|
||||
import { ISSOSettings } from "interfaces/ssoSettings";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
import validateEmail from "components/forms/validators/valid_email";
|
||||
|
||||
const baseClass = "login-form";
|
||||
|
||||
interface ILoginFormProps {
|
||||
baseError?: string;
|
||||
handleSubmit: (formData: ILoginUserData) => Promise<false | void>;
|
||||
ssoSettings?: ISSOSettings;
|
||||
handleSSOSignOn?: () => void;
|
||||
}
|
||||
|
||||
const LoginForm = ({
|
||||
baseError,
|
||||
handleSubmit,
|
||||
ssoSettings,
|
||||
handleSSOSignOn,
|
||||
}: ILoginFormProps): JSX.Element => {
|
||||
const {
|
||||
idp_name: idpName,
|
||||
idp_image_url: imageURL,
|
||||
sso_enabled: ssoEnabled,
|
||||
} = ssoSettings || {}; // TODO: Consider refactoring ssoSettings undefined
|
||||
|
||||
const loginFormClass = classnames(baseClass);
|
||||
|
||||
const [errors, setErrors] = useState<any>({});
|
||||
const [formData, setFormData] = useState<ILoginUserData>({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const validate = () => {
|
||||
const { password, email } = formData;
|
||||
|
||||
const validationErrors: { [key: string]: string } = {};
|
||||
|
||||
if (!validatePresence(email)) {
|
||||
validationErrors.email = "Email field must be completed";
|
||||
} else if (!validateEmail(email)) {
|
||||
validationErrors.email = "Email must be a valid email address";
|
||||
}
|
||||
|
||||
if (!validatePresence(password)) {
|
||||
validationErrors.password = "Password field must be completed";
|
||||
}
|
||||
|
||||
setErrors(validationErrors);
|
||||
const valid = !size(validationErrors);
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
||||
const onFormSubmit = (evt: FormEvent): Promise<false | void> | boolean => {
|
||||
evt.preventDefault();
|
||||
const valid = validate();
|
||||
|
||||
if (valid) {
|
||||
return handleSubmit(formData);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const showLegendWithImage = () => {
|
||||
let legend = "Single sign-on";
|
||||
if (idpName !== "") {
|
||||
legend = `Sign on with ${idpName}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={imageURL}
|
||||
alt={idpName}
|
||||
className={`${baseClass}__sso-image`}
|
||||
/>
|
||||
<span className={`${baseClass}__sso-legend`}>{legend}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSingleSignOnButton = () => {
|
||||
let legend: string | JSX.Element = "Single sign-on";
|
||||
if (idpName !== "") {
|
||||
legend = `Sign on with ${idpName}`;
|
||||
}
|
||||
if (imageURL !== "") {
|
||||
legend = showLegendWithImage();
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`${baseClass}__sso-btn`}
|
||||
type="button"
|
||||
title="Single sign-on"
|
||||
variant="inverse"
|
||||
onClick={handleSSOSignOn}
|
||||
>
|
||||
<div>{legend}</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const onInputChange = (formField: string): ((value: string) => void) => {
|
||||
return (value: string) => {
|
||||
setErrors({});
|
||||
setFormData({
|
||||
...formData,
|
||||
[formField]: value,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onFormSubmit} className={loginFormClass}>
|
||||
<div className={`${baseClass}__container`}>
|
||||
{baseError && <div className="form__base-error">{baseError}</div>}
|
||||
<InputFieldWithIcon
|
||||
error={errors.email}
|
||||
autofocus
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
value={formData.email}
|
||||
onChange={onInputChange("email")}
|
||||
/>
|
||||
<InputFieldWithIcon
|
||||
error={errors.password}
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={onInputChange("password")}
|
||||
/>
|
||||
<div className={`${baseClass}__forgot-wrap`}>
|
||||
<Link
|
||||
className={`${baseClass}__forgot-link`}
|
||||
to={paths.FORGOT_PASSWORD}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
className={`${baseClass}__submit-btn button button--brand`}
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
{ssoEnabled && renderSingleSignOnButton()}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
@ -4,7 +4,6 @@
|
||||
width: 516px;
|
||||
align-self: center;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
&--hidden {
|
||||
opacity: 0;
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { size } from "lodash";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
import validateEmail from "components/forms/validators/valid_email";
|
||||
|
||||
const validate = (formData) => {
|
||||
const errors = {};
|
||||
const { password, email } = formData;
|
||||
|
||||
if (!validatePresence(email)) {
|
||||
errors.email = "Email field must be completed";
|
||||
} else if (!validateEmail(email)) {
|
||||
errors.email = "Email must be a valid email address";
|
||||
}
|
||||
|
||||
if (!validatePresence(password)) {
|
||||
errors.password = "Password field must be completed";
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
export default validate;
|
@ -1,65 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Form from "components/forms/Form";
|
||||
import formFieldInterface from "interfaces/form_field";
|
||||
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon";
|
||||
import validate from "components/forms/ResetPasswordForm/validate";
|
||||
|
||||
const baseClass = "reset-password-form";
|
||||
const formFields = ["new_password", "new_password_confirmation"];
|
||||
|
||||
class ResetPasswordForm extends Component {
|
||||
static propTypes = {
|
||||
baseError: PropTypes.string,
|
||||
handleSubmit: PropTypes.func,
|
||||
fields: PropTypes.shape({
|
||||
new_password: formFieldInterface.isRequired,
|
||||
new_password_confirmation: formFieldInterface.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
render() {
|
||||
const { baseError, fields, handleSubmit } = this.props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={baseClass}>
|
||||
{baseError && <div className="form__base-error">{baseError}</div>}
|
||||
<InputFieldWithIcon
|
||||
{...fields.new_password}
|
||||
autofocus
|
||||
label="New password"
|
||||
placeholder="New password"
|
||||
className={`${baseClass}__input`}
|
||||
type="password"
|
||||
hint={[
|
||||
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
|
||||
]}
|
||||
/>
|
||||
<InputFieldWithIcon
|
||||
{...fields.new_password_confirmation}
|
||||
label="Confirm password"
|
||||
placeholder="Confirm password"
|
||||
className={`${baseClass}__input`}
|
||||
type="password"
|
||||
/>
|
||||
<div className={`${baseClass}__button-wrap`}>
|
||||
<Button
|
||||
variant="brand"
|
||||
onClick={handleSubmit}
|
||||
className={`${baseClass}__btn`}
|
||||
type="submit"
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form(ResetPasswordForm, {
|
||||
fields: formFields,
|
||||
validate,
|
||||
});
|
@ -11,8 +11,8 @@ describe("ResetPasswordForm - component", () => {
|
||||
it("renders correctly", () => {
|
||||
render(<ResetPasswordForm handleSubmit={submitSpy} />);
|
||||
|
||||
expect(screen.getByLabelText("New password")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Confirm password")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("New password")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("Confirm password")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Reset password" })
|
||||
@ -42,7 +42,7 @@ describe("ResetPasswordForm - component", () => {
|
||||
<ResetPasswordForm handleSubmit={submitSpy} />
|
||||
);
|
||||
|
||||
await user.type(screen.getByLabelText("New password"), newPassword);
|
||||
await user.type(screen.getByPlaceholderText("New password"), newPassword);
|
||||
await user.click(screen.getByRole("button", { name: "Reset password" }));
|
||||
|
||||
const passwordError = screen.getByText(
|
||||
@ -58,7 +58,10 @@ describe("ResetPasswordForm - component", () => {
|
||||
<ResetPasswordForm handleSubmit={submitSpy} />
|
||||
);
|
||||
|
||||
await user.type(screen.getByLabelText("Confirm password"), newPassword);
|
||||
await user.type(
|
||||
screen.getByPlaceholderText("Confirm password"),
|
||||
newPassword
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Reset password" }));
|
||||
|
||||
const passwordError = screen.getByText(
|
||||
@ -73,9 +76,9 @@ describe("ResetPasswordForm - component", () => {
|
||||
<ResetPasswordForm handleSubmit={submitSpy} />
|
||||
);
|
||||
|
||||
await user.type(screen.getByLabelText("New password"), newPassword);
|
||||
await user.type(screen.getByPlaceholderText("New password"), newPassword);
|
||||
await user.type(
|
||||
screen.getByLabelText("Confirm password"),
|
||||
screen.getByPlaceholderText("Confirm password"),
|
||||
"not my new password"
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Reset password" }));
|
||||
@ -91,8 +94,14 @@ describe("ResetPasswordForm - component", () => {
|
||||
<ResetPasswordForm handleSubmit={submitSpy} />
|
||||
);
|
||||
|
||||
await user.type(screen.getByLabelText("New password"), invalidPassword);
|
||||
await user.type(screen.getByLabelText("Confirm password"), invalidPassword);
|
||||
await user.type(
|
||||
screen.getByPlaceholderText("New password"),
|
||||
invalidPassword
|
||||
);
|
||||
await user.type(
|
||||
screen.getByPlaceholderText("Confirm password"),
|
||||
invalidPassword
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Reset password" }));
|
||||
|
||||
const passwordError = screen.getByText(
|
||||
@ -107,8 +116,11 @@ describe("ResetPasswordForm - component", () => {
|
||||
<ResetPasswordForm handleSubmit={submitSpy} />
|
||||
);
|
||||
|
||||
await user.type(screen.getByLabelText("New password"), newPassword);
|
||||
await user.type(screen.getByLabelText("Confirm password"), newPassword);
|
||||
await user.type(screen.getByPlaceholderText("New password"), newPassword);
|
||||
await user.type(
|
||||
screen.getByPlaceholderText("Confirm password"),
|
||||
newPassword
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Reset password" }));
|
||||
|
||||
expect(submitSpy).toHaveBeenCalledWith({
|
||||
|
@ -0,0 +1,130 @@
|
||||
import React, { FormEvent, useState } from "react";
|
||||
import { size } from "lodash";
|
||||
|
||||
import { IResetPasswordForm, IResetPasswordFormErrors } from "interfaces/user";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
import validatePassword from "components/forms/validators/valid_password";
|
||||
import validateEquality from "components/forms/validators/validate_equality";
|
||||
import { IOldApiError } from "interfaces/errors";
|
||||
|
||||
const baseClass = "reset-password-form";
|
||||
|
||||
export interface IFormData {
|
||||
new_password: string;
|
||||
new_password_confirmation: string;
|
||||
}
|
||||
|
||||
interface IResetPasswordFormProps {
|
||||
serverErrors: IOldApiError;
|
||||
handleSubmit: (formData: IFormData) => void;
|
||||
}
|
||||
const ResetPasswordForm = ({
|
||||
serverErrors,
|
||||
handleSubmit,
|
||||
}: IResetPasswordFormProps): JSX.Element => {
|
||||
const [errors, setErrors] = useState<IResetPasswordFormErrors>({});
|
||||
const [formData, setFormData] = useState<IResetPasswordForm>({
|
||||
new_password: "",
|
||||
new_password_confirmation: "",
|
||||
});
|
||||
|
||||
const validate = (): boolean => {
|
||||
const {
|
||||
new_password: newPassword,
|
||||
new_password_confirmation: newPasswordConfirmation,
|
||||
} = formData;
|
||||
|
||||
const noMatch =
|
||||
newPassword &&
|
||||
newPasswordConfirmation &&
|
||||
!validateEquality(newPassword, newPasswordConfirmation);
|
||||
|
||||
const validationErrors: { [key: string]: string } = {};
|
||||
|
||||
if (!validatePassword(newPassword)) {
|
||||
validationErrors.new_password = "Password must meet the criteria below";
|
||||
}
|
||||
|
||||
if (!validatePresence(newPasswordConfirmation)) {
|
||||
validationErrors.new_password_confirmation =
|
||||
"New password confirmation field must be completed";
|
||||
}
|
||||
|
||||
if (!validatePresence(newPassword)) {
|
||||
validationErrors.new_password = "New password field must be completed";
|
||||
}
|
||||
|
||||
if (noMatch) {
|
||||
validationErrors.new_password_confirmation = "Passwords do not match";
|
||||
}
|
||||
|
||||
setErrors(validationErrors);
|
||||
const valid = !size(validationErrors);
|
||||
return valid;
|
||||
};
|
||||
|
||||
const onFormSubmit = (evt: FormEvent): void => {
|
||||
evt.preventDefault();
|
||||
const valid = validate();
|
||||
|
||||
if (valid) {
|
||||
return handleSubmit(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const onInputChange = (formField: string): ((value: string) => void) => {
|
||||
return (value: string) => {
|
||||
setErrors({});
|
||||
setFormData({
|
||||
...formData,
|
||||
[formField]: value,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={baseClass}>
|
||||
{serverErrors?.base && (
|
||||
<div className="form__base-error">{serverErrors.base}</div>
|
||||
)}
|
||||
<InputFieldWithIcon
|
||||
error={errors.new_password}
|
||||
autofocus
|
||||
label="New password"
|
||||
placeholder="New password"
|
||||
onChange={onInputChange("new_password")}
|
||||
value={formData.new_password || ""}
|
||||
className={`${baseClass}__input`}
|
||||
type="password"
|
||||
hint={[
|
||||
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
|
||||
]}
|
||||
/>
|
||||
<InputFieldWithIcon
|
||||
error={errors.new_password_confirmation}
|
||||
label="Confirm password"
|
||||
placeholder="Confirm password"
|
||||
onChange={onInputChange("new_password_confirmation")}
|
||||
value={formData.new_password_confirmation || ""}
|
||||
className={`${baseClass}__input`}
|
||||
type="password"
|
||||
/>
|
||||
<div className={`${baseClass}__button-wrap`}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={onFormSubmit}
|
||||
className={`${baseClass}__btn`}
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordForm;
|
@ -13,6 +13,5 @@
|
||||
&__btn {
|
||||
margin-top: $pad-xxlarge;
|
||||
width: 160px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
import { size } from "lodash";
|
||||
import validateEquality from "components/forms/validators/validate_equality";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
import validPassword from "components/forms/validators/valid_password";
|
||||
|
||||
const validate = (formData) => {
|
||||
const errors = {};
|
||||
const {
|
||||
new_password: newPassword,
|
||||
new_password_confirmation: newPasswordConfirmation,
|
||||
} = formData;
|
||||
|
||||
const noMatch =
|
||||
newPassword &&
|
||||
newPasswordConfirmation &&
|
||||
!validateEquality(newPassword, newPasswordConfirmation);
|
||||
|
||||
if (!validPassword(newPassword)) {
|
||||
errors.new_password = "Password must meet the criteria below";
|
||||
}
|
||||
|
||||
if (!validatePresence(newPasswordConfirmation)) {
|
||||
errors.new_password_confirmation =
|
||||
"New password confirmation field must be completed";
|
||||
}
|
||||
|
||||
if (!validatePresence(newPassword)) {
|
||||
errors.new_password = "New password field must be completed";
|
||||
}
|
||||
|
||||
if (noMatch) {
|
||||
errors.new_password_confirmation = "Passwords do not match";
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
export default validate;
|
@ -1,5 +1,5 @@
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
export default (actual, expected) => {
|
||||
export default (actual: any, expected: any) => {
|
||||
return isEqual(actual, expected);
|
||||
};
|
@ -5,6 +5,7 @@ export default PropTypes.shape({
|
||||
base: PropTypes.string,
|
||||
});
|
||||
|
||||
// Response created by utilities/format_error_response
|
||||
export interface IOldApiError {
|
||||
http_status: number;
|
||||
base: string;
|
||||
@ -15,7 +16,9 @@ export interface IError {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
// Response returned by API when there is an error
|
||||
export interface IApiError {
|
||||
message: string;
|
||||
errors: IError[];
|
||||
uuid?: string;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export default PropTypes.shape({
|
||||
});
|
||||
|
||||
export interface ISSOSettings {
|
||||
idp_image_url: string;
|
||||
idp_name: string;
|
||||
idp_image_url?: string;
|
||||
idp_name?: string;
|
||||
sso_enabled: boolean;
|
||||
}
|
||||
|
@ -73,6 +73,20 @@ export interface IUserFormErrors {
|
||||
password?: string | null;
|
||||
sso_enabled?: boolean | null;
|
||||
}
|
||||
export interface IResetPasswordFormErrors {
|
||||
new_password?: string | null;
|
||||
new_password_confirmation?: string | null;
|
||||
}
|
||||
|
||||
export interface IResetPasswordForm {
|
||||
new_password: string;
|
||||
new_password_confirmation: string;
|
||||
}
|
||||
|
||||
export interface ILoginUserData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ICreateUserFormData {
|
||||
email: string;
|
||||
|
@ -1,4 +1,8 @@
|
||||
.gated-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
|
||||
.flash-message {
|
||||
border: 0;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { InjectedRouter } from "react-router";
|
||||
import PATHS from "router/paths";
|
||||
import usersAPI from "services/entities/users";
|
||||
import formatErrorResponse from "utilities/format_error_response";
|
||||
@ -12,7 +12,11 @@ import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
|
||||
import Spinner from "components/Spinner";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
const ForgotPasswordPage = () => {
|
||||
interface IForgotPasswordPage {
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const ForgotPasswordPage = ({ router }: IForgotPasswordPage) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -81,7 +85,7 @@ const ForgotPasswordPage = () => {
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<StackedWhiteBoxes previousLocation={PATHS.LOGIN}>
|
||||
<StackedWhiteBoxes previousLocation={PATHS.LOGIN} router={router}>
|
||||
<div className={baseClass}>{renderContent()}</div>
|
||||
</StackedWhiteBoxes>
|
||||
</AuthenticationFormWrapper>
|
||||
|
@ -1,7 +1,7 @@
|
||||
.forgot-password {
|
||||
p,
|
||||
a {
|
||||
font-size: 1rem;
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
&__text {
|
||||
|
@ -9,6 +9,7 @@ import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { RoutingContext } from "context/routing";
|
||||
import { ISSOSettings } from "interfaces/ssoSettings";
|
||||
import { ILoginUserData } from "interfaces/user";
|
||||
import local from "utilities/local";
|
||||
import configAPI from "services/entities/config";
|
||||
import sessionsAPI, { ISSOSettingsResponse } from "services/entities/sessions";
|
||||
@ -28,11 +29,6 @@ interface ILoginPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
interface ILoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface IStatusMessages {
|
||||
account_disabled: string;
|
||||
account_invalid: string;
|
||||
@ -65,7 +61,6 @@ const LoginPage = ({ router, location }: ILoginPageProps) => {
|
||||
const { redirectLocation } = useContext(RoutingContext);
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [loginVisible, setLoginVisible] = useState(true);
|
||||
|
||||
const {
|
||||
data: ssoSettings,
|
||||
@ -83,17 +78,6 @@ const LoginPage = ({ router, location }: ILoginPageProps) => {
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
availableTeams &&
|
||||
config &&
|
||||
currentUser &&
|
||||
!currentUser.force_password_reset
|
||||
) {
|
||||
router.push(redirectLocation || paths.DASHBOARD);
|
||||
}
|
||||
}, [availableTeams, config, currentUser, redirectLocation, router]);
|
||||
|
||||
useEffect(() => {
|
||||
// this only needs to run once so we can wrap it in useEffect to avoid unneccesary third-party
|
||||
// API calls
|
||||
@ -111,6 +95,17 @@ const LoginPage = ({ router, location }: ILoginPageProps) => {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
availableTeams &&
|
||||
config &&
|
||||
currentUser &&
|
||||
!currentUser.force_password_reset
|
||||
) {
|
||||
router.push(redirectLocation || paths.DASHBOARD);
|
||||
}
|
||||
}, [availableTeams, config, currentUser, redirectLocation, router]);
|
||||
|
||||
// TODO: Fix this. If renderFlash is added as a dependency it causes infinite re-renders.
|
||||
useEffect(() => {
|
||||
let status = new URLSearchParams(location.search).get("status");
|
||||
@ -120,36 +115,28 @@ const LoginPage = ({ router, location }: ILoginPageProps) => {
|
||||
}
|
||||
}, [location?.search]);
|
||||
|
||||
const onChange = useCallback(() => {
|
||||
if (size(errors)) {
|
||||
setErrors({});
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [errors]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (formData: ILoginData) => {
|
||||
const { DASHBOARD, RESET_PASSWORD } = paths;
|
||||
async (formData: ILoginUserData) => {
|
||||
const { DASHBOARD, RESET_PASSWORD, NO_ACCESS } = paths;
|
||||
|
||||
try {
|
||||
const { user, available_teams, token } = await sessionsAPI.create(
|
||||
formData
|
||||
);
|
||||
const response = await sessionsAPI.create(formData);
|
||||
const { user, available_teams, token } = response;
|
||||
|
||||
local.setItem("auth_token", token);
|
||||
|
||||
setLoginVisible(false);
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(user, available_teams);
|
||||
setCurrentTeam(undefined);
|
||||
|
||||
if (!user.global_role && user.teams.length === 0) {
|
||||
return router.push(NO_ACCESS);
|
||||
}
|
||||
// Redirect to password reset page if user is forced to reset password.
|
||||
// Any other requests will fail.
|
||||
if (user.force_password_reset) {
|
||||
else if (user.force_password_reset) {
|
||||
return router.push(RESET_PASSWORD);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
} else if (!config) {
|
||||
const configResponse = await configAPI.loadAll();
|
||||
setConfig(configResponse);
|
||||
}
|
||||
@ -201,10 +188,8 @@ const LoginPage = ({ router, location }: ILoginPageProps) => {
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<LoginForm
|
||||
onChangeFunc={onChange}
|
||||
handleSubmit={onSubmit}
|
||||
isHidden={!loginVisible}
|
||||
serverErrors={errors}
|
||||
baseError={errors.base}
|
||||
ssoSettings={ssoSettings}
|
||||
handleSSOSignOn={ssoSignOn}
|
||||
/>
|
||||
|
@ -26,7 +26,6 @@ const LoginPreviewPage = ({ router }: ILoginPreviewPageProps): JSX.Element => {
|
||||
setCurrentUser,
|
||||
setCurrentTeam,
|
||||
} = useContext(AppContext);
|
||||
const [loginVisible, setLoginVisible] = useState(true);
|
||||
|
||||
const onSubmit = async (formData: ILoginData) => {
|
||||
const { DASHBOARD } = paths;
|
||||
@ -37,7 +36,6 @@ const LoginPreviewPage = ({ router }: ILoginPreviewPageProps): JSX.Element => {
|
||||
);
|
||||
local.setItem("auth_token", token);
|
||||
|
||||
setLoginVisible(false);
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(user, available_teams);
|
||||
setCurrentTeam(undefined);
|
||||
@ -61,7 +59,7 @@ const LoginPreviewPage = ({ router }: ILoginPreviewPageProps): JSX.Element => {
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<LoginSuccessfulPage />
|
||||
<LoginForm handleSubmit={onSubmit} isHidden={!loginVisible} />
|
||||
<LoginForm handleSubmit={onSubmit} />
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
};
|
||||
|
69
frontend/pages/NoAccessPage/NoAccessPage.tsx
Normal file
69
frontend/pages/NoAccessPage/NoAccessPage.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
// Page returned when a user has no access because they have no global or team role
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import Button from "components/buttons/Button/Button";
|
||||
// @ts-ignore
|
||||
import StackedWhiteBoxes from "components/StackedWhiteBoxes";
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
|
||||
import CustomLink from "components/CustomLink/CustomLink";
|
||||
|
||||
const baseClass = "no-access-page";
|
||||
|
||||
interface INoAccessPageProps {
|
||||
router: InjectedRouter;
|
||||
orgContactUrl?: string;
|
||||
}
|
||||
|
||||
const NoAccessPage = ({ router, orgContactUrl }: INoAccessPageProps) => {
|
||||
const onBackToLogin = () => {
|
||||
router.push(PATHS.LOGIN);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (onBackToLogin) {
|
||||
const closeOrSaveWithEnterKey = (event: KeyboardEvent) => {
|
||||
if (event.code === "Enter" || event.code === "NumpadEnter") {
|
||||
event.preventDefault();
|
||||
onBackToLogin();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", closeOrSaveWithEnterKey);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", closeOrSaveWithEnterKey);
|
||||
};
|
||||
}
|
||||
}, [onBackToLogin]);
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<StackedWhiteBoxes
|
||||
router={router}
|
||||
headerText="This account does not currently have access to Fleet."
|
||||
>
|
||||
<>
|
||||
<p>
|
||||
To get access,{" "}
|
||||
<CustomLink
|
||||
url={orgContactUrl || "https://fleetdm.com/contact"}
|
||||
text="contact your administrator"
|
||||
/>
|
||||
.
|
||||
</p>
|
||||
<Button
|
||||
variant="brand"
|
||||
onClick={onBackToLogin}
|
||||
className={`${baseClass}__btn`}
|
||||
>
|
||||
Back to login
|
||||
</Button>
|
||||
</>
|
||||
</StackedWhiteBoxes>
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoAccessPage;
|
5
frontend/pages/NoAccessPage/_styles.scss
Normal file
5
frontend/pages/NoAccessPage/_styles.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.no-access-page {
|
||||
&__btn {
|
||||
margin-top: $pad-small;
|
||||
}
|
||||
}
|
1
frontend/pages/NoAccessPage/index.ts
Normal file
1
frontend/pages/NoAccessPage/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./NoAccessPage";
|
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState, useContext } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { size } from "lodash";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
@ -30,11 +29,12 @@ const ResetPasswordPage = ({ location, router }: IResetPasswordPageProps) => {
|
||||
}
|
||||
}, [currentUser, token]);
|
||||
|
||||
const onResetErrors = () => {
|
||||
if (size(errors)) {
|
||||
setErrors({});
|
||||
// No access prompt if API errors due to no role or currentUser data has no role
|
||||
useEffect(() => {
|
||||
if (!currentUser?.global_role && currentUser?.teams.length === 0) {
|
||||
router.push(PATHS.NO_ACCESS);
|
||||
}
|
||||
};
|
||||
}, [errors, currentUser]);
|
||||
|
||||
const continueWithLoggedInUser = async (formData: any) => {
|
||||
const { new_password } = formData;
|
||||
@ -44,7 +44,15 @@ const ResetPasswordPage = ({ location, router }: IResetPasswordPageProps) => {
|
||||
const config = await configAPI.loadAll();
|
||||
setConfig(config);
|
||||
return router.push(PATHS.DASHBOARD);
|
||||
} catch (response) {
|
||||
} catch (response: any) {
|
||||
if (
|
||||
response.data.message.includes(
|
||||
"either global role or team role needs to be defined"
|
||||
)
|
||||
) {
|
||||
router.push(PATHS.NO_ACCESS);
|
||||
}
|
||||
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
@ -73,12 +81,11 @@ const ResetPasswordPage = ({ location, router }: IResetPasswordPageProps) => {
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<StackedWhiteBoxes leadText="Create a new password. Your new password must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)">
|
||||
<ResetPasswordForm
|
||||
handleSubmit={onSubmit}
|
||||
onChangeFunc={onResetErrors}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
<StackedWhiteBoxes
|
||||
router={router}
|
||||
leadText="Create a new password. Your new password must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)"
|
||||
>
|
||||
<ResetPasswordForm handleSubmit={onSubmit} serverErrors={errors} />
|
||||
</StackedWhiteBoxes>
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
|
@ -33,6 +33,7 @@ import ManageSoftwarePage from "pages/software/ManageSoftwarePage";
|
||||
import ManageQueriesPage from "pages/queries/ManageQueriesPage";
|
||||
import ManagePacksPage from "pages/packs/ManagePacksPage";
|
||||
import ManagePoliciesPage from "pages/policies/ManagePoliciesPage";
|
||||
import NoAccessPage from "pages/NoAccessPage";
|
||||
import PackComposerPage from "pages/packs/PackComposerPage";
|
||||
import PolicyPage from "pages/policies/PolicyPage";
|
||||
import QueryPage from "pages/queries/QueryPage";
|
||||
@ -100,6 +101,7 @@ const routes = (
|
||||
/>
|
||||
<Route path="login/forgot" component={ForgotPasswordPage} />
|
||||
<Route path="login/reset" component={ResetPasswordPage} />
|
||||
<Route path="login/denied" component={NoAccessPage} />
|
||||
<Route path="mdm/sso/callback" component={MDMAppleSSOCallbackPage} />
|
||||
<Route path="mdm/sso" component={MDMAppleSSOPage} />
|
||||
</Route>
|
||||
|
@ -55,6 +55,7 @@ export default {
|
||||
}`;
|
||||
},
|
||||
FORGOT_PASSWORD: `${URL_PREFIX}/login/forgot`,
|
||||
NO_ACCESS: `${URL_PREFIX}/login/denied`,
|
||||
API_ONLY_USER: `${URL_PREFIX}/apionlyuser`,
|
||||
FLEET_403: `${URL_PREFIX}/403`,
|
||||
LOGIN: `${URL_PREFIX}/login`,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { get, join } from "lodash";
|
||||
import { IError } from "interfaces/errors";
|
||||
import { IError, IOldApiError } from "interfaces/errors";
|
||||
|
||||
const formatServerErrors = (errors: IError[]) => {
|
||||
if (!errors || !errors.length) {
|
||||
@ -18,7 +18,7 @@ const formatServerErrors = (errors: IError[]) => {
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return result; // TODO: Typing {base: string}
|
||||
};
|
||||
|
||||
const formatErrorResponse = (errorResponse: any) => {
|
||||
@ -30,7 +30,7 @@ const formatErrorResponse = (errorResponse: any) => {
|
||||
return {
|
||||
...formatServerErrors(errors),
|
||||
http_status: errorResponse.status,
|
||||
} as any;
|
||||
} as any; // TODO: Fix type to IOldApiError
|
||||
};
|
||||
|
||||
export default formatErrorResponse;
|
||||
|
Loading…
Reference in New Issue
Block a user