Auth Redux Removal (#4924)
* all login methods no longer use redux * removed redux from registration * redirect user from registration * removed redux from sso invite * removed redundant component * refactored user settings page * removed redux from logout * cleaned up unused redux calls * lint fixes * removed test * removed old config interface * fixed registration bug * team permission fix * removed remaining redux references from pages - #4436 * better way to set config
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
import React from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import { IUser } from "interfaces/user";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import paths from "router/paths";
|
||||
|
||||
interface IAccessRoutes {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
interface IRootState {
|
||||
auth: {
|
||||
user: IUser;
|
||||
};
|
||||
}
|
||||
|
||||
const { FLEET_403 } = paths;
|
||||
|
||||
const AccessRoutes = ({ children }: IAccessRoutes): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector((state: IRootState) => state.auth.user);
|
||||
|
||||
// user is an empty object here. The API result has not come back
|
||||
// so render nothing.
|
||||
if (Object.keys(user).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (permissionUtils.isNoAccess(user)) {
|
||||
dispatch(push(FLEET_403));
|
||||
return null;
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AccessRoutes;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./AccessRoutes";
|
||||
@@ -4,11 +4,13 @@ import { useQuery } from "react-query";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
import configAPI from "services/entities/config";
|
||||
import { AppContext } from "context/app"; // @ts-ignore
|
||||
import { stringToClipboard } from "utilities/copy_text";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IEnrollSecret } from "interfaces/enroll_secret";
|
||||
|
||||
import configAPI from "services/entities/config";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton"; // @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
@@ -80,10 +82,10 @@ const PlatformWrapper = ({
|
||||
}
|
||||
);
|
||||
|
||||
let tlsHostname = config?.server_url || "";
|
||||
let tlsHostname = config?.server_settings.server_url || "";
|
||||
|
||||
try {
|
||||
const serverUrl = new URL(config?.server_url || "");
|
||||
const serverUrl = new URL(config?.server_settings.server_url || "");
|
||||
tlsHostname = serverUrl.hostname;
|
||||
if (serverUrl.port) {
|
||||
tlsHostname += `:${serverUrl.port}`;
|
||||
@@ -225,12 +227,14 @@ const PlatformWrapper = ({
|
||||
|
||||
const renderInstallerString = (platform: string) => {
|
||||
return platform === "advanced"
|
||||
? `fleetctl package --type=rpm --fleet-url=${config?.server_url}
|
||||
? `fleetctl package --type=rpm --fleet-url=${config?.server_settings.server_url}
|
||||
--enroll-secret=${enrollSecret}
|
||||
--fleet-certificate=PATH_TO_YOUR_CERTIFICATE/fleet.pem`
|
||||
: `fleetctl package --type=${platform} ${
|
||||
includeFleetDesktop ? "--fleet-desktop " : ""
|
||||
}--fleet-url=${config?.server_url} --enroll-secret=${enrollSecret}`;
|
||||
}--fleet-url=${
|
||||
config?.server_settings.server_url
|
||||
} --enroll-secret=${enrollSecret}`;
|
||||
};
|
||||
|
||||
const renderLabel = (platform: string, installerString: string) => {
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import classnames from "classnames";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { authToken } from "utilities/local"; // @ts-ignore
|
||||
import { useDeepEffect } from "utilities/hooks"; // @ts-ignore
|
||||
import { fetchCurrentUser } from "redux/nodes/auth/actions"; // @ts-ignore
|
||||
import { getConfig, getEnrollSecret } from "redux/nodes/app/actions";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IEnrollSecret } from "interfaces/enroll_secret";
|
||||
import { ITeamSummary } from "interfaces/team";
|
||||
import { IUser } from "interfaces/user";
|
||||
import PATHS from "router/paths";
|
||||
import TableProvider from "context/table";
|
||||
import QueryProvider from "context/query";
|
||||
import PolicyProvider from "context/policy";
|
||||
import NotificationProvider from "context/notification";
|
||||
import { AppContext } from "context/app";
|
||||
import { authToken } from "utilities/local"; // @ts-ignore
|
||||
import { useDeepEffect } from "utilities/hooks";
|
||||
|
||||
import usersAPI from "services/entities/users";
|
||||
import configAPI from "services/entities/config";
|
||||
|
||||
import { ErrorBoundary } from "react-error-boundary"; // @ts-ignore
|
||||
import Fleet403 from "pages/errors/Fleet403"; // @ts-ignore
|
||||
@@ -27,62 +24,57 @@ import Spinner from "components/Spinner";
|
||||
|
||||
interface IAppProps {
|
||||
children: JSX.Element;
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
interface ISecretResponse {
|
||||
spec: {
|
||||
secrets: IEnrollSecret[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IRootState {
|
||||
auth: {
|
||||
user: IUser;
|
||||
available_teams: ITeamSummary[];
|
||||
};
|
||||
}
|
||||
|
||||
const App = ({ children }: IAppProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector((state: IRootState) => state.auth.user);
|
||||
const availableTeams = useSelector(
|
||||
(state: IRootState) => state.auth.available_teams
|
||||
);
|
||||
const App = ({ children, router }: IAppProps): JSX.Element => {
|
||||
const queryClient = new QueryClient();
|
||||
const {
|
||||
setAvailableTeams,
|
||||
setCurrentUser,
|
||||
setConfig,
|
||||
setEnrollSecret,
|
||||
currentUser,
|
||||
isGlobalObserver,
|
||||
isOnlyObserver,
|
||||
isAnyTeamMaintainerOrTeamAdmin,
|
||||
setAvailableTeams,
|
||||
setCurrentUser,
|
||||
setConfig,
|
||||
setEnrollSecret,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
useDeepEffect(() => {
|
||||
const fetchCurrentUser = async () => {
|
||||
try {
|
||||
const { user, available_teams } = await usersAPI.me();
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(available_teams);
|
||||
} catch (error) {
|
||||
router.push(PATHS.LOGIN);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const config = await configAPI.loadAll();
|
||||
setConfig(config);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// on page refresh
|
||||
if (!user && authToken()) {
|
||||
// Auth token is not turning to null fast enough so the user is refetched and is making an unneeded API call to enroll_secret
|
||||
dispatch(fetchCurrentUser()).catch(() => false);
|
||||
if (!currentUser && authToken()) {
|
||||
fetchCurrentUser();
|
||||
}
|
||||
|
||||
if (user) {
|
||||
if (currentUser) {
|
||||
setIsLoading(true);
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(availableTeams);
|
||||
dispatch(getConfig())
|
||||
.then((config: IConfig) => {
|
||||
setConfig(config);
|
||||
})
|
||||
.catch(() => false)
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
fetchConfig();
|
||||
}
|
||||
}, [user]);
|
||||
}, [currentUser]);
|
||||
|
||||
useDeepEffect(() => {
|
||||
const canGetEnrollSecret =
|
||||
@@ -94,12 +86,18 @@ const App = ({ children }: IAppProps): JSX.Element => {
|
||||
typeof isAnyTeamMaintainerOrTeamAdmin !== "undefined" &&
|
||||
!isAnyTeamMaintainerOrTeamAdmin;
|
||||
|
||||
const getEnrollSecret = async () => {
|
||||
try {
|
||||
const { spec } = await configAPI.loadEnrollSecret();
|
||||
setEnrollSecret(spec.secrets);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (canGetEnrollSecret) {
|
||||
dispatch(getEnrollSecret())
|
||||
.then((response: ISecretResponse) => {
|
||||
setEnrollSecret(response.spec.secrets);
|
||||
})
|
||||
.catch(() => false);
|
||||
getEnrollSecret();
|
||||
}
|
||||
}, [currentUser, isGlobalObserver, isOnlyObserver]);
|
||||
|
||||
@@ -110,7 +108,7 @@ const App = ({ children }: IAppProps): JSX.Element => {
|
||||
console.error(error);
|
||||
|
||||
const overlayError = error as AxiosResponse;
|
||||
if (overlayError.status === 403) {
|
||||
if (overlayError.status === 403 || overlayError.status === 402) {
|
||||
return <Fleet403 />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import AuthenticatedRoutes from "./index";
|
||||
import helpers from "../../test/helpers";
|
||||
|
||||
describe("AuthenticatedRoutes - component", () => {
|
||||
const redirectToLoginAction = [
|
||||
{
|
||||
payload: { redirectLocation: {} },
|
||||
type: "SET_REDIRECT_LOCATION",
|
||||
},
|
||||
{
|
||||
payload: { args: ["/login"], method: "push" },
|
||||
type: "@@router/CALL_HISTORY_METHOD",
|
||||
},
|
||||
];
|
||||
const redirectToPasswordResetAction = [
|
||||
{
|
||||
payload: { redirectLocation: {} },
|
||||
type: "SET_REDIRECT_LOCATION",
|
||||
},
|
||||
{
|
||||
payload: { args: ["/login"], method: "push" },
|
||||
type: "@@router/CALL_HISTORY_METHOD",
|
||||
},
|
||||
];
|
||||
const renderedText = "This text was rendered";
|
||||
const storeWithUser = {
|
||||
auth: {
|
||||
loading: false,
|
||||
user: {
|
||||
id: 1,
|
||||
email: "hi@thegnar.co",
|
||||
force_password_reset: false,
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
locationBeforeTransitions: {},
|
||||
},
|
||||
};
|
||||
const storeWithUserRequiringPwReset = {
|
||||
auth: {
|
||||
loading: false,
|
||||
user: {
|
||||
id: 1,
|
||||
email: "hi@thegnar.co",
|
||||
force_password_reset: true,
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
locationBeforeTransitions: {},
|
||||
},
|
||||
};
|
||||
const storeLoadingUser = {
|
||||
auth: {
|
||||
loading: true,
|
||||
user: null,
|
||||
},
|
||||
routing: {
|
||||
locationBeforeTransitions: {},
|
||||
},
|
||||
};
|
||||
|
||||
it("renders if there is a user in state", () => {
|
||||
const { reduxMockStore } = helpers;
|
||||
const mockStore = reduxMockStore(storeWithUser);
|
||||
render(
|
||||
<Provider store={mockStore}>
|
||||
<AuthenticatedRoutes>
|
||||
<div>{renderedText}</div>
|
||||
</AuthenticatedRoutes>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(screen.getByText(renderedText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("redirects to reset password is force_password_reset is true", () => {
|
||||
const { reduxMockStore } = helpers;
|
||||
const mockStore = reduxMockStore(storeWithUserRequiringPwReset);
|
||||
render(
|
||||
<Provider store={mockStore}>
|
||||
<AuthenticatedRoutes>
|
||||
<div>{renderedText}</div>
|
||||
</AuthenticatedRoutes>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(mockStore.getActions()).toEqual(redirectToPasswordResetAction);
|
||||
});
|
||||
|
||||
// TODO: Cannot test functional components with state
|
||||
// it("redirects to login without a user", () => {
|
||||
// const { reduxMockStore } = helpers;
|
||||
// const mockStore = reduxMockStore(storeWithoutUser);
|
||||
// const component = mount(
|
||||
// <Provider store={mockStore}>
|
||||
// <AuthenticatedRoutes>
|
||||
// <div>{renderedText}</div>
|
||||
// </AuthenticatedRoutes>
|
||||
// </Provider>
|
||||
// );
|
||||
|
||||
// expect(mockStore.getActions()).toContainEqual(redirectToLoginAction);
|
||||
// expect(component.html()).toBeFalsy();
|
||||
// });
|
||||
|
||||
it("does not redirect to login if the user is loading", () => {
|
||||
const { reduxMockStore } = helpers;
|
||||
const mockStore = reduxMockStore(storeLoadingUser);
|
||||
render(
|
||||
<Provider store={mockStore}>
|
||||
<AuthenticatedRoutes>
|
||||
<div>{renderedText}</div>
|
||||
</AuthenticatedRoutes>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(mockStore.getActions()).not.toContainEqual(redirectToLoginAction);
|
||||
expect(screen.queryByText(renderedText)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,52 +1,43 @@
|
||||
import React from "react";
|
||||
import { push } from "react-router-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import React, { useContext } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { IRedirectLocation } from "interfaces/redirect_location"; // @ts-ignore
|
||||
import { setRedirectLocation } from "redux/nodes/redirectLocation/actions";
|
||||
import { IUser } from "interfaces/user";
|
||||
import { AppContext } from "context/app";
|
||||
import { RoutingContext } from "context/routing";
|
||||
import { useDeepEffect } from "utilities/hooks";
|
||||
import { authToken } from "utilities/local";
|
||||
|
||||
interface IAppProps {
|
||||
children: JSX.Element;
|
||||
location: any; // no type in react-router v3
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
interface IRootState {
|
||||
auth: {
|
||||
user: IUser;
|
||||
};
|
||||
routing: {
|
||||
locationBeforeTransitions: IRedirectLocation;
|
||||
};
|
||||
}
|
||||
|
||||
export const AuthenticatedRoutes = ({ children, location }: IAppProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { user } = useSelector((state: IRootState) => state.auth);
|
||||
const { locationBeforeTransitions } = useSelector(
|
||||
(state: IRootState) => state.routing
|
||||
);
|
||||
export const AuthenticatedRoutes = ({
|
||||
children,
|
||||
location,
|
||||
router,
|
||||
}: IAppProps) => {
|
||||
const { setRedirectLocation } = useContext(RoutingContext);
|
||||
const { currentUser } = useContext(AppContext);
|
||||
|
||||
const redirectToLogin = () => {
|
||||
const { LOGIN } = paths;
|
||||
|
||||
dispatch(setRedirectLocation(locationBeforeTransitions));
|
||||
return dispatch(push(LOGIN));
|
||||
setRedirectLocation(window.location.pathname);
|
||||
return router.push(LOGIN);
|
||||
};
|
||||
|
||||
const redirectToPasswordReset = () => {
|
||||
const { RESET_PASSWORD } = paths;
|
||||
|
||||
return dispatch(push(RESET_PASSWORD));
|
||||
return router.push(RESET_PASSWORD);
|
||||
};
|
||||
|
||||
const redirectToApiUserOnly = () => {
|
||||
const { API_ONLY_USER } = paths;
|
||||
|
||||
return dispatch(push(API_ONLY_USER));
|
||||
return router.push(API_ONLY_USER);
|
||||
};
|
||||
|
||||
useDeepEffect(() => {
|
||||
@@ -56,20 +47,20 @@ export const AuthenticatedRoutes = ({ children, location }: IAppProps) => {
|
||||
return redirectToLogin();
|
||||
}
|
||||
|
||||
if (user && user.force_password_reset) {
|
||||
if (currentUser?.force_password_reset && !authToken()) {
|
||||
return redirectToPasswordReset();
|
||||
}
|
||||
|
||||
if (user && user.api_only) {
|
||||
if (currentUser?.api_only) {
|
||||
return redirectToApiUserOnly();
|
||||
}
|
||||
}, [user]);
|
||||
}, [currentUser]);
|
||||
|
||||
useDeepEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [location]);
|
||||
|
||||
if (!user) {
|
||||
if (!currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import { noop } from "lodash";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import paths from "router/paths";
|
||||
import userInterface from "interfaces/user";
|
||||
|
||||
export default (WrappedComponent) => {
|
||||
class EnsureUnauthenticated extends Component {
|
||||
static propTypes = {
|
||||
currentUser: userInterface,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
isLoadingUser: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
dispatch: noop,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
const { currentUser, dispatch } = this.props;
|
||||
const { HOME } = paths;
|
||||
|
||||
if (currentUser) {
|
||||
dispatch(push(HOME));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { currentUser, dispatch } = nextProps;
|
||||
const { HOME } = paths;
|
||||
|
||||
if (currentUser) {
|
||||
dispatch(push(HOME));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentUser, isLoadingUser } = this.props;
|
||||
|
||||
if (isLoadingUser || currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return <WrappedComponent {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { loading: isLoadingUser, user: currentUser } = state.auth;
|
||||
|
||||
return { currentUser, isLoadingUser };
|
||||
};
|
||||
|
||||
return connect(mapStateToProps)(EnsureUnauthenticated);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./EnsureUnauthenticated";
|
||||
@@ -1,85 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { hideBackgroundImage } from "redux/nodes/app/actions";
|
||||
import { ssoSettings } from "redux/nodes/auth/actions";
|
||||
import LoginPage, { PreviewLoginPage } from "pages/LoginPage";
|
||||
|
||||
export class LoginRoutes extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
dispatch: PropTypes.func,
|
||||
isResetPassPage: PropTypes.bool,
|
||||
isForgotPassPage: PropTypes.bool,
|
||||
isPreviewLoginPage: PropTypes.bool,
|
||||
pathname: PropTypes.string,
|
||||
token: PropTypes.string,
|
||||
router: PropTypes.any, // eslint-disable-line
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(ssoSettings()).catch(() => false);
|
||||
|
||||
dispatch(hideBackgroundImage);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(hideBackgroundImage);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
isResetPassPage,
|
||||
isForgotPassPage,
|
||||
isPreviewLoginPage,
|
||||
pathname,
|
||||
token,
|
||||
router,
|
||||
} = this.props;
|
||||
|
||||
if (isPreviewLoginPage) {
|
||||
return <PreviewLoginPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-routes">
|
||||
{children || (
|
||||
<LoginPage
|
||||
pathname={pathname}
|
||||
token={token}
|
||||
isForgotPassPage={isForgotPassPage}
|
||||
isResetPassPage={isResetPassPage}
|
||||
router={router}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
location: { pathname, query },
|
||||
} = ownProps;
|
||||
const { token } = query;
|
||||
|
||||
const isForgotPassPage = pathname.endsWith("/login/forgot");
|
||||
const isResetPassPage = pathname.endsWith("/login/reset");
|
||||
const isPreviewLoginPage = pathname.endsWith("/previewlogin");
|
||||
|
||||
return {
|
||||
isForgotPassPage,
|
||||
isResetPassPage,
|
||||
isPreviewLoginPage,
|
||||
pathname,
|
||||
token,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(LoginRoutes);
|
||||
@@ -1,9 +0,0 @@
|
||||
.login-routes {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: $gradients-dark-gradient-vertical;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./LoginRoutes";
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import classnames from "classnames";
|
||||
// @ts-ignore
|
||||
import { hideFlash } from "redux/nodes/notifications/actions";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
const baseClass = "modal";
|
||||
|
||||
@@ -19,7 +17,8 @@ const Modal = ({
|
||||
title,
|
||||
className,
|
||||
}: IModalProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { hideFlash } = useContext(NotificationContext);
|
||||
|
||||
useEffect(() => {
|
||||
const closeWithEscapeKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
@@ -27,8 +26,7 @@ const Modal = ({
|
||||
}
|
||||
};
|
||||
|
||||
dispatch(hideFlash);
|
||||
|
||||
hideFlash();
|
||||
document.addEventListener("keydown", closeWithEscapeKey);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import { IConfig } from "interfaces/config";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import paths from "router/paths";
|
||||
|
||||
interface IPremiumTierRoutes {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
interface IRootState {
|
||||
app: {
|
||||
config: IConfig;
|
||||
};
|
||||
}
|
||||
|
||||
const { FLEET_403 } = paths;
|
||||
|
||||
const PremiumTierRoutes = ({
|
||||
children,
|
||||
}: IPremiumTierRoutes): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
const config = useSelector((state: IRootState) => state.app.config);
|
||||
|
||||
// config is an empty object here. The API result has not come back
|
||||
// so render nothing.
|
||||
if (Object.keys(config).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!permissionUtils.isPremiumTier(config)) {
|
||||
dispatch(push(FLEET_403));
|
||||
return null;
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default PremiumTierRoutes;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./PremiumTierRoutes";
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useTable,
|
||||
} from "react-table";
|
||||
import { isString, kebabCase, noop } from "lodash";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { useDeepEffect } from "utilities/hooks";
|
||||
import sort from "utilities/sort";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { IAceEditor } from "react-ace/lib/types";
|
||||
import { noop, size } from "lodash";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { ILabel, ILabelFormData } from "interfaces/label";
|
||||
import Button from "components/buttons/Button"; // @ts-ignore
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IConfigNested } from "interfaces/config";
|
||||
import { IConfig } from "interfaces/config";
|
||||
|
||||
export interface IAppConfigFormProps {
|
||||
appConfig: IConfigNested;
|
||||
appConfig: IConfig;
|
||||
handleSubmit: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
// @ts-ignore
|
||||
import InputField from "../InputField";
|
||||
|
||||
|
||||
@@ -4,16 +4,13 @@ import classnames from "classnames";
|
||||
|
||||
import { IUser } from "interfaces/user";
|
||||
import { IConfig } from "interfaces/config";
|
||||
|
||||
import LinkWithContext from "components/LinkWithContext";
|
||||
import UserMenu from "components/top_nav/UserMenu";
|
||||
// @ts-ignore
|
||||
import OrgLogoIcon from "components/icons/OrgLogoIcon";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import navItems, { INavItem } from "./navItems";
|
||||
import LinkWithContext from "components/LinkWithContext";
|
||||
import UserMenu from "components/top_nav/UserMenu"; // @ts-ignore
|
||||
import OrgLogoIcon from "components/icons/OrgLogoIcon";
|
||||
|
||||
import navItems, { INavItem } from "./navItems";
|
||||
import HostsIcon from "../../../../assets/images/icon-main-hosts@2x-16x16@2x.png";
|
||||
import SoftwareIcon from "../../../../assets/images/icon-software-16x16@2x.png";
|
||||
import QueriesIcon from "../../../../assets/images/icon-main-queries@2x-16x16@2x.png";
|
||||
@@ -45,7 +42,7 @@ const SiteTopNav = ({
|
||||
|
||||
const renderNavItem = (navItem: INavItem) => {
|
||||
const { name, iconName, withContext } = navItem;
|
||||
const orgLogoURL = config.org_logo_url;
|
||||
const orgLogoURL = config.org_info.org_logo_url;
|
||||
const active = navItem.location.regex.test(pathname);
|
||||
|
||||
const navItemBaseClass = "site-nav-item";
|
||||
|
||||
@@ -184,7 +184,7 @@ const reducer = (state: InitialStateType, action: IAction) => {
|
||||
return {
|
||||
...state,
|
||||
config,
|
||||
...setPermissions(state.currentUser, config),
|
||||
...setPermissions(state.currentUser, config, state.currentTeam?.id),
|
||||
};
|
||||
}
|
||||
case ACTIONS.SET_ENROLL_SECRET: {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { createContext, useReducer, ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
type InitialStateType = {
|
||||
redirectLocation: string | null;
|
||||
setRedirectLocation: (pathname: string | null) => void;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
redirectLocation: null,
|
||||
setRedirectLocation: () => null,
|
||||
};
|
||||
|
||||
const actions = {
|
||||
SET_REDIRECT_LOCATION: "SET_REDIRECT_LOCATION",
|
||||
};
|
||||
|
||||
const reducer = (state: any, action: any) => {
|
||||
switch (action.type) {
|
||||
case actions.SET_REDIRECT_LOCATION:
|
||||
return { ...state, redirectLocation: action.pathname };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const RoutingContext = createContext<InitialStateType>(initialState);
|
||||
|
||||
const RoutingProvider = ({ children }: Props) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const value = {
|
||||
redirectLocation: state.redirectLocation,
|
||||
setRedirectLocation: (pathname: string | null) => {
|
||||
dispatch({ type: actions.SET_REDIRECT_LOCATION, pathname });
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<RoutingContext.Provider value={value}>{children}</RoutingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoutingProvider;
|
||||
@@ -3,7 +3,7 @@ import md5 from "js-md5";
|
||||
import { format, formatDistanceToNow, isAfter } from "date-fns";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import { IConfigNested } from "interfaces/config";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IHost } from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { IPack } from "interfaces/pack";
|
||||
@@ -184,7 +184,7 @@ export const formatConfigDataForServer = (config: any): any => {
|
||||
};
|
||||
|
||||
// TODO: Finalize interface for config - see frontend\interfaces\config.ts
|
||||
export const frontendFormattedConfig = (config: IConfigNested) => {
|
||||
export const frontendFormattedConfig = (config: IConfig) => {
|
||||
const {
|
||||
org_info: orgInfo,
|
||||
server_settings: serverSettings,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { IOsqueryPlatform, SUPPORTED_PLATFORMS } from "interfaces/platform";
|
||||
import checkPlatformCompatibility from "utilities/sql_tools";
|
||||
|
||||
@@ -71,73 +71,6 @@ export default PropTypes.shape({
|
||||
}),
|
||||
});
|
||||
|
||||
export interface IConfig {
|
||||
org_name: string;
|
||||
org_logo_url: string;
|
||||
server_url: string;
|
||||
live_query_disabled: boolean;
|
||||
enable_analytics: boolean;
|
||||
enable_smtp: boolean;
|
||||
configured: boolean;
|
||||
sender_address: string;
|
||||
server: string;
|
||||
port: number;
|
||||
authentication_type: string;
|
||||
user_name: string;
|
||||
password: string;
|
||||
enable_ssl_tls: boolean;
|
||||
authentication_method: string;
|
||||
domain: string;
|
||||
verify_sll_certs: boolean;
|
||||
enable_start_tls: boolean;
|
||||
entity_id: string;
|
||||
issuer_uri: string;
|
||||
idp_image_url: string;
|
||||
metadata: string;
|
||||
metadata_url: string;
|
||||
idp_name: string;
|
||||
enable_sso: boolean;
|
||||
enable_sso_idp_login: boolean;
|
||||
host_expiry_enabled: boolean;
|
||||
host_expiry_window: number;
|
||||
agent_options: string;
|
||||
osquery_detail: number;
|
||||
osquery_policy: number;
|
||||
tier: string;
|
||||
organization: string;
|
||||
device_count: number;
|
||||
expiration: string;
|
||||
note: string;
|
||||
// vulnerability_settings: any; TODO
|
||||
enable_host_status_webhook: boolean;
|
||||
destination_url: string;
|
||||
host_percentage: number;
|
||||
days_count: number;
|
||||
debug: boolean;
|
||||
json: boolean;
|
||||
result: {
|
||||
plugin: string;
|
||||
config: {
|
||||
status_log_file: string;
|
||||
result_log_file: string;
|
||||
enable_log_rotation: boolean;
|
||||
enable_log_compression: boolean;
|
||||
};
|
||||
};
|
||||
status: {
|
||||
plugin: string;
|
||||
config: {
|
||||
status_log_file: string;
|
||||
result_log_file: string;
|
||||
enable_log_rotation: boolean;
|
||||
enable_log_compression: boolean;
|
||||
};
|
||||
};
|
||||
webhook_settings: {
|
||||
failing_policies_webhook: IWebhookFailingPolicies;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IConfigFormData {
|
||||
smtpAuthenticationMethod: string;
|
||||
smtpAuthenticationType: string;
|
||||
@@ -173,7 +106,7 @@ export interface IConfigFormData {
|
||||
enableUsageStatistics: boolean;
|
||||
}
|
||||
|
||||
export interface IConfigNested {
|
||||
export interface IConfig {
|
||||
org_info: {
|
||||
org_name: string;
|
||||
org_logo_url: string;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default PropTypes.shape({
|
||||
action: PropTypes.string,
|
||||
pathname: PropTypes.string,
|
||||
});
|
||||
|
||||
export interface IRedirectLocation {
|
||||
action: string;
|
||||
pathname: string;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export default PropTypes.shape({
|
||||
build_user: PropTypes.string,
|
||||
});
|
||||
|
||||
export interface IInvite {
|
||||
export interface IVersionData {
|
||||
version: string;
|
||||
branch: string;
|
||||
revision: string;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { useDispatch } from "react-redux"; // @ts-ignore
|
||||
import { logoutUser } from "redux/nodes/auth/actions";
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { TableContext } from "context/table";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { useDeepEffect } from "utilities/hooks";
|
||||
import FlashMessage from "components/FlashMessage";
|
||||
import SiteTopNav from "components/top_nav/SiteTopNav";
|
||||
@@ -33,11 +32,8 @@ const expirationMessage = (
|
||||
);
|
||||
|
||||
const CoreLayout = ({ children, router }: ICoreLayoutProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { config, currentUser, isPremiumTier } = useContext(AppContext);
|
||||
const { notification, renderFlash, hideFlash } = useContext(
|
||||
NotificationContext
|
||||
);
|
||||
const { notification, hideFlash } = useContext(NotificationContext);
|
||||
const { setResetSelectedRows } = useContext(TableContext);
|
||||
const [
|
||||
showExpirationFlashMessage,
|
||||
@@ -58,17 +54,13 @@ const CoreLayout = ({ children, router }: ICoreLayoutProps) => {
|
||||
}
|
||||
|
||||
setShowExpirationFlashMessage(
|
||||
licenseExpirationWarning(config?.expiration || "")
|
||||
licenseExpirationWarning(config?.license.expiration || "")
|
||||
);
|
||||
}, [notification]);
|
||||
|
||||
const onLogoutUser = async () => {
|
||||
try {
|
||||
dispatch(logoutUser());
|
||||
} catch (error) {
|
||||
renderFlash("error", "Unable to log out of your account");
|
||||
console.log(error);
|
||||
}
|
||||
const { LOGOUT } = paths;
|
||||
router.push(LOGOUT);
|
||||
};
|
||||
|
||||
const onNavItemClick = (path: string) => {
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { push } from "react-router-redux";
|
||||
// @ts-ignore
|
||||
import { fetchCurrentUser, logoutUser } from "redux/nodes/auth/actions";
|
||||
import Button from "components/buttons/Button";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import paths from "router/paths";
|
||||
// @ts-ignore
|
||||
import usersAPI from "services/entities/users";
|
||||
|
||||
import Button from "components/buttons/Button"; // @ts-ignore
|
||||
import fleetLogoText from "../../../assets/images/fleet-logo-text-white.svg";
|
||||
|
||||
interface IApiOnlyUserProps {
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const baseClass = "api-only-user";
|
||||
|
||||
const ApiOnlyUser = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { LOGIN, HOME } = paths;
|
||||
const handleClick = () => dispatch(logoutUser());
|
||||
const ApiOnlyUser = ({ router }: IApiOnlyUserProps): JSX.Element => {
|
||||
const { LOGIN, HOME, LOGOUT } = paths;
|
||||
const handleClick = () => router.push(LOGOUT);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCurrentUser()).then((user: any) => {
|
||||
if (!user) {
|
||||
dispatch(push(LOGIN));
|
||||
} else if (user && !user.payload.user.api_only) {
|
||||
dispatch(push(HOME));
|
||||
const fetchCurrentUser = async () => {
|
||||
try {
|
||||
const { user } = await usersAPI.me();
|
||||
|
||||
if (!user) {
|
||||
router.push(LOGIN);
|
||||
} else if (!user?.api_only) {
|
||||
router.push(HOME);
|
||||
}
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fetchCurrentUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { Params } from "react-router/lib/Router";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { ICreateUserWithInvitationFormData } from "interfaces/user";
|
||||
import paths from "router/paths";
|
||||
import usersAPI from "services/entities/users"; // @ts-ignore
|
||||
import { formatErrorResponse } from "redux/nodes/entities/base/helpers";
|
||||
import usersAPI from "services/entities/users";
|
||||
import formatErrorResponse from "utilities/format_error_response";
|
||||
|
||||
// @ts-ignore
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper"; // @ts-ignore
|
||||
import ConfirmInviteForm from "components/forms/ConfirmInviteForm"; // @ts-ignore
|
||||
import EnsureUnauthenticated from "components/EnsureUnauthenticated";
|
||||
import ConfirmInviteForm from "components/forms/ConfirmInviteForm";
|
||||
|
||||
interface IConfirmInvitePageProps {
|
||||
router: InjectedRouter; // v3
|
||||
location: any; // no type in v3
|
||||
location: any; // no type in react-router v3
|
||||
params: Params;
|
||||
}
|
||||
|
||||
@@ -29,9 +29,17 @@ const ConfirmInvitePage = ({
|
||||
const { email, name } = location.query;
|
||||
const { invite_token } = params;
|
||||
const inviteFormData = { email, invite_token, name };
|
||||
const { currentUser } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [userErrors, setUserErrors] = useState<any>({});
|
||||
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
useEffect(() => {
|
||||
const { HOME } = paths;
|
||||
|
||||
if (currentUser) {
|
||||
return router.push(HOME);
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
const onSubmit = async (formData: ICreateUserWithInvitationFormData) => {
|
||||
const { create } = usersAPI;
|
||||
@@ -73,4 +81,4 @@ const ConfirmInvitePage = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default EnsureUnauthenticated(ConfirmInvitePage);
|
||||
export default ConfirmInvitePage;
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
|
||||
import ConfirmSSOInviteForm from "components/forms/ConfirmSSOInviteForm";
|
||||
import EnsureUnauthenticated from "components/EnsureUnauthenticated";
|
||||
import userActions from "redux/nodes/entities/users/actions";
|
||||
import authActions from "redux/nodes/auth/actions";
|
||||
import paths from "router/paths";
|
||||
|
||||
const baseClass = "confirm-invite-page";
|
||||
|
||||
class ConfirmSSOInvitePage extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
inviteFormData: PropTypes.shape({
|
||||
email: PropTypes.string.isRequired,
|
||||
invite_token: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
userErrors: PropTypes.shape({
|
||||
base: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
const { dispatch } = this.props;
|
||||
const { clearErrors } = userActions;
|
||||
|
||||
dispatch(clearErrors());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onSubmit = (formData) => {
|
||||
const { create } = userActions;
|
||||
const { ssoRedirect } = authActions;
|
||||
const { dispatch } = this.props;
|
||||
const { HOME } = paths;
|
||||
|
||||
formData.sso_invite = true;
|
||||
dispatch(create(formData))
|
||||
.then(() => {
|
||||
// set redirect so that we will get redirected to home page after
|
||||
// the user authenticates with the idp
|
||||
dispatch(ssoRedirect(HOME))
|
||||
.then((result) => {
|
||||
window.location.href = result.payload.ssoRedirectURL;
|
||||
})
|
||||
.catch(() => false);
|
||||
})
|
||||
.catch(() => false);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { inviteFormData, userErrors } = this.props;
|
||||
const { onSubmit } = this;
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<div className={`${baseClass}`}>
|
||||
<div className={`${baseClass}__lead-wrapper`}>
|
||||
<p className={`${baseClass}__lead-text`}>Welcome to Fleet</p>
|
||||
<p className={`${baseClass}__sub-lead-text`}>
|
||||
Before you get started, please take a moment to complete the
|
||||
following information.
|
||||
</p>
|
||||
</div>
|
||||
<ConfirmSSOInviteForm
|
||||
className={`${baseClass}__form`}
|
||||
formData={inviteFormData}
|
||||
handleSubmit={onSubmit}
|
||||
serverErrors={userErrors}
|
||||
/>
|
||||
</div>
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, { location: urlLocation, params }) => {
|
||||
const { email, name } = urlLocation.query;
|
||||
const { invite_token: inviteToken } = params;
|
||||
const inviteFormData = { email, invite_token: inviteToken, name };
|
||||
const { errors: userErrors } = state.entities.users;
|
||||
|
||||
return { inviteFormData, userErrors };
|
||||
};
|
||||
|
||||
const ConnectedComponent = connect(mapStateToProps)(ConfirmSSOInvitePage);
|
||||
export default EnsureUnauthenticated(ConnectedComponent);
|
||||
@@ -1,37 +0,0 @@
|
||||
import { mount } from "enzyme";
|
||||
|
||||
import ConfirmSSOInvitePage from "pages/ConfirmSSOInvitePage";
|
||||
import { connectedComponent, reduxMockStore } from "test/helpers";
|
||||
|
||||
describe("ConfirmSSOInvitePage - component", () => {
|
||||
const inviteToken = "abc123";
|
||||
const location = { query: { email: "hi@gnar.dog", name: "Gnar Dog" } };
|
||||
const params = { invite_token: inviteToken };
|
||||
const mockStore = reduxMockStore({ auth: {}, entities: { users: {} } });
|
||||
const component = connectedComponent(ConfirmSSOInvitePage, {
|
||||
props: { location, params },
|
||||
mockStore,
|
||||
});
|
||||
const page = mount(component);
|
||||
|
||||
it("renders", () => {
|
||||
expect(page.length).toEqual(1);
|
||||
expect(page.find("ConfirmSSOInvitePage").prop("inviteFormData")).toEqual({
|
||||
email: "hi@gnar.dog",
|
||||
invite_token: inviteToken,
|
||||
name: "Gnar Dog",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a ConfirmSSOInviteForm", () => {
|
||||
expect(page.find("ConfirmSSOInviteForm").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("clears errors on unmount", () => {
|
||||
page.unmount();
|
||||
|
||||
expect(mockStore.getActions()).toContainEqual({
|
||||
type: "users_CLEAR_ERRORS",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { Params } from "react-router/lib/Router";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import usersAPI from "services/entities/users";
|
||||
import sessionsAPI from "services/entities/sessions";
|
||||
import formatErrorResponse from "utilities/format_error_response";
|
||||
|
||||
// @ts-ignore
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper"; // @ts-ignore
|
||||
import ConfirmSSOInviteForm from "components/forms/ConfirmSSOInviteForm";
|
||||
|
||||
interface IConfirmSSOInvitePageProps {
|
||||
location: any; // no type in react-router v3
|
||||
params: Params;
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const baseClass = "confirm-invite-page";
|
||||
|
||||
const ConfirmSSOInvitePage = ({
|
||||
location,
|
||||
params,
|
||||
router,
|
||||
}: IConfirmSSOInvitePageProps) => {
|
||||
const { email, name } = location.query;
|
||||
const { invite_token } = params;
|
||||
const inviteFormData = { email, invite_token, name };
|
||||
const { currentUser } = useContext(AppContext);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const { HOME } = paths;
|
||||
|
||||
if (currentUser) {
|
||||
return router.push(HOME);
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
const onSubmit = async (formData: any) => {
|
||||
const { HOME } = paths;
|
||||
|
||||
formData.sso_invite = true;
|
||||
|
||||
try {
|
||||
await usersAPI.create(formData);
|
||||
const { url } = await sessionsAPI.initializeSSO(HOME);
|
||||
window.location.href = url;
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<div className={`${baseClass}`}>
|
||||
<div className={`${baseClass}__lead-wrapper`}>
|
||||
<p className={`${baseClass}__lead-text`}>Welcome to Fleet</p>
|
||||
<p className={`${baseClass}__sub-lead-text`}>
|
||||
Before you get started, please take a moment to complete the
|
||||
following information.
|
||||
</p>
|
||||
</div>
|
||||
<ConfirmSSOInviteForm
|
||||
className={`${baseClass}__form`}
|
||||
formData={inviteFormData}
|
||||
handleSubmit={onSubmit}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
</div>
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmSSOInvitePage;
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
|
||||
import { ForgotPasswordPage } from "./ForgotPasswordPage";
|
||||
|
||||
describe("ForgotPasswordPage - component", () => {
|
||||
it("renders the ForgotPasswordForm when there is no email prop", () => {
|
||||
const page = mount(<ForgotPasswordPage />);
|
||||
|
||||
expect(page.find("ForgotPasswordForm").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders the email sent text when the email state is present", () => {
|
||||
const email = "hi@thegnar.co";
|
||||
const page = mount(<ForgotPasswordPage />).setState({ email });
|
||||
|
||||
expect(page.find("ForgotPasswordForm").length).toEqual(0);
|
||||
expect(page.text()).toContain(`An email was sent to ${email}.`);
|
||||
});
|
||||
});
|
||||
+26
-37
@@ -1,47 +1,36 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import usersAPI from "services/entities/users";
|
||||
|
||||
import { formatErrorResponse } from "redux/nodes/entities/base/helpers";
|
||||
import debounce from "utilities/debounce";
|
||||
import ForgotPasswordForm from "components/forms/ForgotPasswordForm";
|
||||
import StackedWhiteBoxes from "components/StackedWhiteBoxes";
|
||||
import formatErrorResponse from "utilities/format_error_response"; // @ts-ignore
|
||||
import ForgotPasswordForm from "components/forms/ForgotPasswordForm"; // @ts-ignore
|
||||
import StackedWhiteBoxes from "components/StackedWhiteBoxes"; // @ts-ignore
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
|
||||
|
||||
export class ForgotPasswordPage extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
const ForgotPasswordPage = () => {
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
this.state = {
|
||||
email: null,
|
||||
errors: {},
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
setErrors({});
|
||||
}, []);
|
||||
|
||||
componentWillUnmount() {
|
||||
return this.clearErrors();
|
||||
}
|
||||
|
||||
handleSubmit = debounce(async (formData) => {
|
||||
const handleSubmit = async (formData: any) => {
|
||||
try {
|
||||
await usersAPI.forgotPassword(formData);
|
||||
|
||||
const { email } = formData;
|
||||
this.setState({ email, errors: {} });
|
||||
setEmail(formData.email);
|
||||
setErrors({});
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
this.setState({ email: null, errors: errorObject });
|
||||
setEmail("");
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
clearErrors = () => {
|
||||
this.setState({ errors: {} });
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
const { clearErrors, handleSubmit } = this;
|
||||
const { email, errors } = this.state;
|
||||
const renderContent = () => {
|
||||
const baseClass = "forgot-password";
|
||||
|
||||
if (email) {
|
||||
@@ -61,26 +50,26 @@ export class ForgotPasswordPage extends Component {
|
||||
return (
|
||||
<ForgotPasswordForm
|
||||
handleSubmit={handleSubmit}
|
||||
onChangeFunc={clearErrors}
|
||||
onChangeFunc={() => setErrors({})}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const leadText =
|
||||
"Enter your email below and we will email you a link so that you can reset your password.";
|
||||
const leadText =
|
||||
"Enter your email below and we will email you a link so that you can reset your password.";
|
||||
|
||||
return (
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<StackedWhiteBoxes
|
||||
leadText={leadText}
|
||||
previousLocation={PATHS.LOGIN}
|
||||
className="forgot-password"
|
||||
>
|
||||
{this.renderContent()}
|
||||
{renderContent()}
|
||||
</StackedWhiteBoxes>
|
||||
);
|
||||
}
|
||||
}
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPasswordPage;
|
||||
@@ -12,8 +12,7 @@ import sortUtils from "utilities/sort";
|
||||
import { PLATFORM_DROPDOWN_OPTIONS } from "utilities/constants";
|
||||
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import Spinner from "components/Spinner";
|
||||
// @ts-ignore
|
||||
import Spinner from "components/Spinner"; // @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import useInfoCard from "./components/InfoCard";
|
||||
import HostsStatus from "./cards/HostsStatus";
|
||||
@@ -297,7 +296,7 @@ const Homepage = (): JSX.Element => {
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
<div className={`${baseClass}__title`}>
|
||||
{isFreeTier && <h1>{config?.org_name}</h1>}
|
||||
{isFreeTier && <h1>{config?.org_info.org_name}</h1>}
|
||||
{isPremiumTier &&
|
||||
teams &&
|
||||
(teams.length > 1 || isOnGlobalTeam) && (
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import { connectedComponent, reduxMockStore } from "../../test/helpers";
|
||||
import LoginPage from "./LoginPage";
|
||||
|
||||
const ssoSettings = { sso_enabled: false };
|
||||
|
||||
describe("LoginPage - component", () => {
|
||||
describe("when the user is not logged in", () => {
|
||||
const mockStore = reduxMockStore({ auth: { ssoSettings } });
|
||||
|
||||
it("renders the LoginForm", () => {
|
||||
const { container } = render(
|
||||
connectedComponent(LoginPage, { mockStore })
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll(".login-form").length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the users session is not recognized", () => {
|
||||
const mockStore = reduxMockStore({
|
||||
auth: {
|
||||
errors: { base: "Unable to authenticate the current user" },
|
||||
ssoSettings,
|
||||
},
|
||||
});
|
||||
|
||||
it("renders the LoginForm base errors", () => {
|
||||
const { container } = render(
|
||||
connectedComponent(LoginPage, { mockStore })
|
||||
);
|
||||
const loginForm = container.querySelectorAll(".login-form");
|
||||
|
||||
expect(loginForm.length).toEqual(1);
|
||||
expect(
|
||||
screen.getByText("Unable to authenticate the current user")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,123 +1,110 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { connect } from "react-redux";
|
||||
import { size } from "lodash";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import { RoutingContext } from "context/routing";
|
||||
import { ISSOSettings } from "interfaces/ssoSettings";
|
||||
import local from "utilities/local";
|
||||
import sessionsAPI from "services/entities/sessions";
|
||||
import formatErrorResponse from "utilities/format_error_response";
|
||||
|
||||
// @ts-ignore
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
|
||||
import {
|
||||
clearAuthErrors,
|
||||
loginUser,
|
||||
ssoRedirect, // @ts-ignore
|
||||
} from "redux/nodes/auth/actions"; // @ts-ignore
|
||||
import { clearRedirectLocation } from "redux/nodes/redirectLocation/actions"; // @ts-ignore
|
||||
import debounce from "utilities/debounce"; // @ts-ignore
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper"; // @ts-ignore
|
||||
import LoginForm from "components/forms/LoginForm"; // @ts-ignore
|
||||
import LoginSuccessfulPage from "pages/LoginSuccessfulPage"; // @ts-ignore
|
||||
import ForgotPasswordPage from "pages/ForgotPasswordPage"; // @ts-ignore
|
||||
import ResetPasswordPage from "pages/ResetPasswordPage";
|
||||
import paths from "router/paths";
|
||||
import { IRedirectLocation } from "interfaces/redirect_location";
|
||||
import { IUser } from "interfaces/user";
|
||||
import { ISSOSettings } from "interfaces/ssoSettings";
|
||||
import { ITeamSummary } from "interfaces/team";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
interface ILoginPageProps {
|
||||
dispatch: Dispatch;
|
||||
errors: {
|
||||
base: string;
|
||||
};
|
||||
pathname: string;
|
||||
isForgotPassPage: boolean;
|
||||
isResetPassPage: boolean;
|
||||
token: string;
|
||||
redirectLocation: IRedirectLocation;
|
||||
user: IUser;
|
||||
ssoSettings: ISSOSettings;
|
||||
router: InjectedRouter; // v3
|
||||
}
|
||||
|
||||
export interface ILoginUserResponse {
|
||||
user: IUser;
|
||||
available_teams: ITeamSummary[];
|
||||
interface ILoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const LoginPage = ({
|
||||
dispatch,
|
||||
errors,
|
||||
pathname,
|
||||
isForgotPassPage,
|
||||
isResetPassPage,
|
||||
token,
|
||||
redirectLocation,
|
||||
user,
|
||||
ssoSettings,
|
||||
router,
|
||||
}: ILoginPageProps) => {
|
||||
const { setAvailableTeams, setCurrentUser, setCurrentTeam } = useContext(
|
||||
AppContext
|
||||
);
|
||||
const LoginPage = ({ router }: ILoginPageProps) => {
|
||||
const {
|
||||
currentUser,
|
||||
setAvailableTeams,
|
||||
setCurrentUser,
|
||||
setCurrentTeam,
|
||||
} = useContext(AppContext);
|
||||
const { redirectLocation } = useContext(RoutingContext);
|
||||
const [loginVisible, setLoginVisible] = useState<boolean>(true);
|
||||
const [ssoSettings, setSSOSettings] = useState<ISSOSettings>();
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const { HOME, LOGIN } = paths;
|
||||
const { HOME } = paths;
|
||||
const getSSO = async () => {
|
||||
try {
|
||||
const { settings } = await sessionsAPI.ssoSettings();
|
||||
setSSOSettings(settings);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (user && pathname === LOGIN) {
|
||||
if (currentUser) {
|
||||
router?.push(HOME);
|
||||
} else {
|
||||
getSSO();
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const onChange = () => {
|
||||
if (size(errors)) {
|
||||
return dispatch(clearAuthErrors);
|
||||
setErrors({});
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const onSubmit = debounce((formData: any) => {
|
||||
const { HOME } = paths;
|
||||
const redirectTime = 1500;
|
||||
return dispatch(loginUser(formData))
|
||||
.then(({ user: returnedUser, available_teams }: ILoginUserResponse) => {
|
||||
setLoginVisible(false);
|
||||
const onSubmit = async (formData: ILoginData) => {
|
||||
const { HOME, RESET_PASSWORD } = paths;
|
||||
|
||||
// Redirect to password reset page if user is forced to reset password.
|
||||
// Any other requests will fail.
|
||||
if (returnedUser.force_password_reset) {
|
||||
return router.push(paths.RESET_PASSWORD);
|
||||
}
|
||||
try {
|
||||
const { user, available_teams, token } = await sessionsAPI.create(
|
||||
formData
|
||||
);
|
||||
local.setItem("auth_token", token);
|
||||
|
||||
// transitioning to context API - 9/1/21 MP
|
||||
setCurrentUser(returnedUser);
|
||||
setAvailableTeams(available_teams);
|
||||
setLoginVisible(false);
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(available_teams);
|
||||
setCurrentTeam(undefined);
|
||||
|
||||
// Ensure team is undefined on login
|
||||
setCurrentTeam(undefined);
|
||||
// Redirect to password reset page if user is forced to reset password.
|
||||
// Any other requests will fail.
|
||||
if (user.force_password_reset) {
|
||||
return router.push(RESET_PASSWORD);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const nextLocation = redirectLocation || HOME;
|
||||
dispatch(clearRedirectLocation);
|
||||
return router.push(nextLocation);
|
||||
}, redirectTime);
|
||||
})
|
||||
.catch(() => false);
|
||||
});
|
||||
return router.push(redirectLocation || HOME);
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ssoSignOn = () => {
|
||||
const ssoSignOn = async () => {
|
||||
const { HOME } = paths;
|
||||
let returnToAfterAuth = HOME;
|
||||
if (redirectLocation != null) {
|
||||
returnToAfterAuth = redirectLocation.pathname;
|
||||
returnToAfterAuth = redirectLocation;
|
||||
}
|
||||
|
||||
dispatch(ssoRedirect(returnToAfterAuth))
|
||||
.then((result: any) => {
|
||||
window.location.href = result.payload.ssoRedirectURL;
|
||||
})
|
||||
.catch(() => false);
|
||||
try {
|
||||
const { url } = await sessionsAPI.initializeSSO(returnToAfterAuth);
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -131,23 +118,8 @@ const LoginPage = ({
|
||||
ssoSettings={ssoSettings}
|
||||
handleSSOSignOn={ssoSignOn}
|
||||
/>
|
||||
{isForgotPassPage && <ForgotPasswordPage />}
|
||||
{isResetPassPage && <ResetPasswordPage token={token} />}
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const { errors, loading, user, ssoSettings } = state.auth;
|
||||
const { redirectLocation } = state;
|
||||
|
||||
return {
|
||||
errors,
|
||||
loading,
|
||||
redirectLocation,
|
||||
user,
|
||||
ssoSettings,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(LoginPage);
|
||||
export default LoginPage;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { AppContext } from "context/app"; // @ts-ignore
|
||||
import sessionsAPI from "services/entities/sessions";
|
||||
import local from "utilities/local";
|
||||
|
||||
// @ts-ignore
|
||||
import LoginSuccessfulPage from "pages/LoginSuccessfulPage"; // @ts-ignore
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper"; // @ts-ignore
|
||||
import LoginForm from "components/forms/LoginForm"; // @ts-ignore
|
||||
|
||||
interface ILoginPreviewPageProps {
|
||||
router: InjectedRouter; // v3
|
||||
}
|
||||
interface ILoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const LoginPreviewPage = ({ router }: ILoginPreviewPageProps): JSX.Element => {
|
||||
const {
|
||||
isPreviewMode,
|
||||
setAvailableTeams,
|
||||
setCurrentUser,
|
||||
setCurrentTeam,
|
||||
} = useContext(AppContext);
|
||||
const [loginVisible, setLoginVisible] = useState<boolean>(true);
|
||||
|
||||
const onSubmit = async (formData: ILoginData) => {
|
||||
const { HOME } = paths;
|
||||
|
||||
try {
|
||||
const { user, available_teams, token } = await sessionsAPI.create(
|
||||
formData
|
||||
);
|
||||
local.setItem("auth_token", token);
|
||||
|
||||
setLoginVisible(false);
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(available_teams);
|
||||
setCurrentTeam(undefined);
|
||||
|
||||
return router.push(HOME);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreviewMode) {
|
||||
onSubmit({
|
||||
email: "admin@example.com",
|
||||
password: "admin123#",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<LoginSuccessfulPage />
|
||||
<LoginForm handleSubmit={onSubmit} isHidden={!loginVisible} />
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPreviewPage;
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
// @ts-ignore
|
||||
import { loginUser } from "redux/nodes/auth/actions";
|
||||
// @ts-ignore
|
||||
import debounce from "utilities/debounce";
|
||||
|
||||
// @ts-ignore
|
||||
import LoginSuccessfulPage from "pages/LoginSuccessfulPage"; // @ts-ignore
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper"; // @ts-ignore
|
||||
import LoginForm from "components/forms/LoginForm"; // @ts-ignore
|
||||
|
||||
import { ILoginUserResponse } from "./LoginPage";
|
||||
|
||||
interface ILoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const PreviewLoginPage = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { isPreviewMode, setAvailableTeams, setCurrentUser } = useContext(
|
||||
AppContext
|
||||
);
|
||||
const [loginVisible, setLoginVisible] = useState<boolean>(true);
|
||||
|
||||
const onSubmit = debounce((formData: ILoginData) => {
|
||||
const { HOME } = paths;
|
||||
const redirectTime = 1500;
|
||||
return dispatch(loginUser(formData))
|
||||
.then(({ user: returnedUser, available_teams }: ILoginUserResponse) => {
|
||||
setLoginVisible(false);
|
||||
|
||||
// transitioning to context API - 9/1/21 MP
|
||||
setCurrentUser(returnedUser);
|
||||
setAvailableTeams(available_teams);
|
||||
|
||||
setTimeout(() => {
|
||||
return dispatch(push(HOME));
|
||||
}, redirectTime);
|
||||
})
|
||||
.catch(() => false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreviewMode) {
|
||||
onSubmit({
|
||||
email: "admin@example.com",
|
||||
password: "admin123#",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<LoginSuccessfulPage />
|
||||
<LoginForm handleSubmit={onSubmit} isHidden={!loginVisible} />
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewLoginPage;
|
||||
@@ -1,4 +1,4 @@
|
||||
import LoginPage from "./LoginPage";
|
||||
import PreviewLoginPage from "./PreviewLoginPage";
|
||||
import LoginPreviewPage from "./LoginPreviewPage";
|
||||
|
||||
export { LoginPage as default, PreviewLoginPage };
|
||||
export { LoginPage as default, LoginPreviewPage };
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
// @ts-ignore
|
||||
import { clearToken } from "../../utilities/local"; // @ts-ignore
|
||||
import { useContext, useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
import sessionsAPI from "services/entities/sessions";
|
||||
import { clearToken } from "utilities/local";
|
||||
|
||||
interface ILogoutPageProps {
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const LogoutPage = ({ router }: ILogoutPageProps): boolean => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
useEffect(() => {
|
||||
const logoutUser = async () => {
|
||||
try {
|
||||
await sessionsAPI.destroy();
|
||||
clearToken();
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 500);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
router.goBack();
|
||||
renderFlash("error", "Unable to log out of your account");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
logoutUser();
|
||||
}, []);
|
||||
|
||||
const LogoutPage = (): boolean => {
|
||||
clearToken();
|
||||
window.location.href = "/";
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import { max, noop } from "lodash";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import Breadcrumbs from "pages/RegistrationPage/Breadcrumbs";
|
||||
import paths from "router/paths";
|
||||
import RegistrationForm from "components/forms/RegistrationForm";
|
||||
import { setup } from "redux/nodes/auth/actions";
|
||||
import { showBackgroundImage } from "redux/nodes/app/actions";
|
||||
import EnsureUnauthenticated from "components/EnsureUnauthenticated";
|
||||
|
||||
import fleetLogoText from "../../../assets/images/fleet-logo-text-white.svg";
|
||||
|
||||
export class RegistrationPage extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
dispatch: noop,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
page: 1,
|
||||
pageProgress: 1,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(showBackgroundImage);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onNextPage = () => {
|
||||
const { page, pageProgress } = this.state;
|
||||
const nextPage = page + 1;
|
||||
this.setState({
|
||||
page: nextPage,
|
||||
pageProgress: max([nextPage, pageProgress]),
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onRegistrationFormSubmit = (formData) => {
|
||||
const { dispatch } = this.props;
|
||||
const { MANAGE_HOSTS } = paths;
|
||||
|
||||
return dispatch(setup(formData))
|
||||
.then(() => {
|
||||
return dispatch(push(MANAGE_HOSTS));
|
||||
})
|
||||
.catch(() => {
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
onSetPage = (page) => {
|
||||
const { pageProgress } = this.state;
|
||||
if (page > pageProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState({ page });
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { page, pageProgress } = this.state;
|
||||
const { onRegistrationFormSubmit, onNextPage, onSetPage } = this;
|
||||
|
||||
return (
|
||||
<div className="registration-page">
|
||||
<img
|
||||
alt="Fleet logo"
|
||||
src={fleetLogoText}
|
||||
className="registration-page__logo"
|
||||
/>
|
||||
<Breadcrumbs
|
||||
onClick={onSetPage}
|
||||
page={page}
|
||||
pageProgress={pageProgress}
|
||||
/>
|
||||
<RegistrationForm
|
||||
page={page}
|
||||
onNextPage={onNextPage}
|
||||
onSubmit={onRegistrationFormSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectedComponent = connect()(RegistrationPage);
|
||||
export default EnsureUnauthenticated(ConnectedComponent);
|
||||
@@ -1,97 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { connectedComponent, reduxMockStore } from "test/helpers";
|
||||
import ConnectedRegistrationPage, {
|
||||
RegistrationPage,
|
||||
} from "pages/RegistrationPage/RegistrationPage";
|
||||
|
||||
const baseStore = {
|
||||
app: {},
|
||||
auth: {},
|
||||
};
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "Gnar Dog",
|
||||
email: "hi@gnar.dog",
|
||||
};
|
||||
|
||||
describe("RegistrationPage - component", () => {
|
||||
it("redirects to the home page when a user is logged in", () => {
|
||||
const storeWithUser = {
|
||||
...baseStore,
|
||||
auth: {
|
||||
loading: false,
|
||||
user,
|
||||
},
|
||||
};
|
||||
const mockStore = reduxMockStore(storeWithUser);
|
||||
|
||||
render(connectedComponent(ConnectedRegistrationPage, { mockStore }));
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
const redirectToHomeAction = {
|
||||
type: "@@router/CALL_HISTORY_METHOD",
|
||||
payload: {
|
||||
method: "push",
|
||||
args: [paths.HOME],
|
||||
},
|
||||
};
|
||||
|
||||
expect(dispatchedActions).toContainEqual(redirectToHomeAction);
|
||||
});
|
||||
|
||||
it("displays the Fleet background triangles", () => {
|
||||
const mockStore = reduxMockStore(baseStore);
|
||||
|
||||
render(connectedComponent(ConnectedRegistrationPage, { mockStore }));
|
||||
|
||||
expect(mockStore.getActions()).toContainEqual({
|
||||
type: "SHOW_BACKGROUND_IMAGE",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not render the RegistrationForm if the user is loading", () => {
|
||||
const mockStore = reduxMockStore({
|
||||
app: {},
|
||||
auth: { loading: true },
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
connectedComponent(ConnectedRegistrationPage, { mockStore })
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll(".user-registration").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("renders the RegistrationForm when there is no user", () => {
|
||||
const mockStore = reduxMockStore(baseStore);
|
||||
|
||||
const { container } = render(
|
||||
connectedComponent(ConnectedRegistrationPage, { mockStore })
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll(".user-registration").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("sets the page number to 1", () => {
|
||||
render(<RegistrationPage />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Setup user" })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the setup breadcrumbs", () => {
|
||||
const mockStore = reduxMockStore(baseStore);
|
||||
const { container } = render(
|
||||
connectedComponent(ConnectedRegistrationPage, { mockStore })
|
||||
);
|
||||
|
||||
expect(
|
||||
container.querySelectorAll(".registration-breadcrumbs").length
|
||||
).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { max } from "lodash";
|
||||
|
||||
import paths from "router/paths"; // @ts-ignore
|
||||
import { AppContext } from "context/app";
|
||||
import usersAPI from "services/entities/users";
|
||||
import local from "utilities/local";
|
||||
|
||||
// @ts-ignore
|
||||
import RegistrationForm from "components/forms/RegistrationForm"; // @ts-ignore
|
||||
import Breadcrumbs from "./Breadcrumbs"; // @ts-ignore
|
||||
import fleetLogoText from "../../../assets/images/fleet-logo-text-white.svg";
|
||||
|
||||
interface IRegistrationPageProps {
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const RegistrationPage = ({ router }: IRegistrationPageProps) => {
|
||||
const { currentUser, setCurrentUser, setAvailableTeams } = useContext(
|
||||
AppContext
|
||||
);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [pageProgress, setPageProgress] = useState<number>(1);
|
||||
|
||||
useEffect(() => {
|
||||
const { HOME } = paths;
|
||||
|
||||
if (currentUser) {
|
||||
return router.push(HOME);
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
const onNextPage = () => {
|
||||
const nextPage = page + 1;
|
||||
setPage(nextPage);
|
||||
setPageProgress(max([nextPage, pageProgress]) || 1);
|
||||
};
|
||||
|
||||
const onRegistrationFormSubmit = async (formData: any) => {
|
||||
const { MANAGE_HOSTS } = paths;
|
||||
|
||||
try {
|
||||
const { token } = await usersAPI.setup(formData);
|
||||
local.setItem("auth_token", token);
|
||||
|
||||
const { user, available_teams } = await usersAPI.me();
|
||||
setCurrentUser(user);
|
||||
setAvailableTeams(available_teams);
|
||||
return router.push(MANAGE_HOSTS);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const onSetPage = (pageNum: number) => {
|
||||
if (pageNum > pageProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setPage(pageNum);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="registration-page">
|
||||
<img
|
||||
alt="Fleet logo"
|
||||
src={fleetLogoText}
|
||||
className="registration-page__logo"
|
||||
/>
|
||||
<Breadcrumbs
|
||||
onClick={onSetPage}
|
||||
page={page}
|
||||
pageProgress={pageProgress}
|
||||
/>
|
||||
<RegistrationForm
|
||||
page={page}
|
||||
onNextPage={onNextPage}
|
||||
onSubmit={onRegistrationFormSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegistrationPage;
|
||||
@@ -1,111 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import { noop, size } from "lodash";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import debounce from "utilities/debounce";
|
||||
import {
|
||||
clearResetPasswordErrors,
|
||||
resetPassword,
|
||||
} from "redux/nodes/components/ResetPasswordPage/actions";
|
||||
import ResetPasswordForm from "components/forms/ResetPasswordForm";
|
||||
import StackedWhiteBoxes from "components/StackedWhiteBoxes";
|
||||
import { performRequiredPasswordReset } from "redux/nodes/auth/actions";
|
||||
import userInterface from "interfaces/user";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
export class ResetPasswordPage extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
errors: PropTypes.shape({
|
||||
base: PropTypes.string,
|
||||
new_password: PropTypes.string,
|
||||
}),
|
||||
token: PropTypes.string,
|
||||
user: userInterface,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
dispatch: noop,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
const { dispatch, token, user } = this.props;
|
||||
|
||||
if (!user && !token) {
|
||||
return dispatch(push(PATHS.LOGIN));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onResetErrors = () => {
|
||||
const { dispatch, errors } = this.props;
|
||||
|
||||
if (size(errors)) {
|
||||
dispatch(clearResetPasswordErrors);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onSubmit = debounce((formData) => {
|
||||
const { dispatch, token, user } = this.props;
|
||||
|
||||
if (user) {
|
||||
return this.loggedInUser(formData);
|
||||
}
|
||||
|
||||
const resetPasswordData = {
|
||||
...formData,
|
||||
password_reset_token: token,
|
||||
};
|
||||
|
||||
return dispatch(resetPassword(resetPasswordData))
|
||||
.then(() => {
|
||||
return dispatch(push(PATHS.LOGIN));
|
||||
})
|
||||
.catch(() => false);
|
||||
});
|
||||
|
||||
loggedInUser = (formData) => {
|
||||
const { dispatch } = this.props;
|
||||
const { new_password: password } = formData;
|
||||
const passwordUpdateParams = { password };
|
||||
|
||||
return dispatch(performRequiredPasswordReset(passwordUpdateParams))
|
||||
.then(() => {
|
||||
return dispatch(push(PATHS.HOME));
|
||||
})
|
||||
.catch(() => false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { handleLeave, onResetErrors, onSubmit } = this;
|
||||
const { errors } = this.props;
|
||||
|
||||
return (
|
||||
<StackedWhiteBoxes leadText="Create a new password. Your new password must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)">
|
||||
<ResetPasswordForm
|
||||
handleSubmit={onSubmit}
|
||||
onChangeFunc={onResetErrors}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
</StackedWhiteBoxes>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { ResetPasswordPage: componentState } = state.components;
|
||||
const { user, errors } = state.auth;
|
||||
|
||||
return {
|
||||
...componentState,
|
||||
user,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ResetPasswordPage);
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
|
||||
import ConnectedPage, { ResetPasswordPage } from "./ResetPasswordPage";
|
||||
import testHelpers from "../../test/helpers";
|
||||
|
||||
describe("ResetPasswordPage - component", () => {
|
||||
it("renders a ResetPasswordForm", () => {
|
||||
const { container } = render(<ResetPasswordPage token="ABC123" />);
|
||||
|
||||
expect(container.querySelectorAll(".reset-password-form").length).toEqual(
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
it("Redirects to the login page when there is no token or user", () => {
|
||||
const { connectedComponent, reduxMockStore } = testHelpers;
|
||||
const redirectToLoginAction = {
|
||||
type: "@@router/CALL_HISTORY_METHOD",
|
||||
payload: {
|
||||
method: "push",
|
||||
args: ["/login"],
|
||||
},
|
||||
};
|
||||
const store = {
|
||||
auth: {},
|
||||
components: {
|
||||
ResetPasswordPage: {
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
render(connectedComponent(ConnectedPage, { mockStore }));
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toContainEqual(redirectToLoginAction);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
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";
|
||||
import usersAPI from "services/entities/users";
|
||||
import configAPI from "services/entities/config";
|
||||
import formatErrorResponse from "utilities/format_error_response";
|
||||
|
||||
// @ts-ignore
|
||||
import ResetPasswordForm from "components/forms/ResetPasswordForm"; // @ts-ignore
|
||||
import StackedWhiteBoxes from "components/StackedWhiteBoxes"; // @ts-ignore
|
||||
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
|
||||
|
||||
interface IResetPasswordPageProps {
|
||||
location: any; // no type in react-router v3
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const ResetPasswordPage = ({ location, router }: IResetPasswordPageProps) => {
|
||||
const { token } = location.query;
|
||||
const { currentUser, setConfig } = useContext(AppContext);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUser && !token) {
|
||||
router.push(PATHS.LOGIN);
|
||||
}
|
||||
}, [currentUser, token]);
|
||||
|
||||
const onResetErrors = () => {
|
||||
if (size(errors)) {
|
||||
setErrors({});
|
||||
}
|
||||
};
|
||||
|
||||
const continueWithLoggedInUser = async (formData: any) => {
|
||||
const { new_password } = formData;
|
||||
|
||||
try {
|
||||
await usersAPI.performRequiredPasswordReset(new_password as string);
|
||||
const config = await configAPI.loadAll();
|
||||
setConfig(config);
|
||||
return router.push(PATHS.HOME);
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: any) => {
|
||||
if (currentUser) {
|
||||
return continueWithLoggedInUser(formData);
|
||||
}
|
||||
|
||||
const resetPasswordData = {
|
||||
...formData,
|
||||
password_reset_token: token,
|
||||
};
|
||||
|
||||
try {
|
||||
await usersAPI.resetPassword(resetPasswordData);
|
||||
router.push(PATHS.LOGIN);
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<StackedWhiteBoxes leadText="Create a new password. Your new password must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)">
|
||||
<ResetPasswordForm
|
||||
handleSubmit={onSubmit}
|
||||
onChangeFunc={onResetErrors}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
</StackedWhiteBoxes>
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordPage;
|
||||
@@ -1,450 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import { goBack } from "react-router-redux";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { authToken } from "utilities/local";
|
||||
import { stringToClipboard } from "utilities/copy_text";
|
||||
|
||||
import { noop } from "lodash";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
import Button from "components/buttons/Button";
|
||||
import ChangeEmailForm from "components/forms/ChangeEmailForm";
|
||||
import ChangePasswordForm from "components/forms/ChangePasswordForm";
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import FleetIcon from "components/icons/FleetIcon";
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import { logoutUser, updateUser } from "redux/nodes/auth/actions";
|
||||
import Modal from "components/Modal";
|
||||
import configInterface from "interfaces/config";
|
||||
import versionInterface from "interfaces/version";
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
import userActions from "redux/nodes/entities/users/actions";
|
||||
import versionActions from "redux/nodes/version/actions";
|
||||
import userInterface from "interfaces/user";
|
||||
import UserSettingsForm from "components/forms/UserSettingsForm";
|
||||
import { generateRole, generateTeam, greyCell } from "fleet/helpers";
|
||||
|
||||
const baseClass = "user-settings";
|
||||
|
||||
export class UserSettingsPage extends Component {
|
||||
static propTypes = {
|
||||
config: configInterface,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
version: versionInterface,
|
||||
errors: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
base: PropTypes.string,
|
||||
}),
|
||||
user: userInterface,
|
||||
userErrors: PropTypes.shape({
|
||||
base: PropTypes.string,
|
||||
new_password: PropTypes.string,
|
||||
old_password: PropTypes.string,
|
||||
}),
|
||||
isPremiumTier: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
version: {},
|
||||
dispatch: noop,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pendingEmail: undefined,
|
||||
showEmailModal: false,
|
||||
showPasswordModal: false,
|
||||
updatedUser: {},
|
||||
copyMessage: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(versionActions.getVersion());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onCancel = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(goBack());
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onLogout = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(logoutUser());
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onShowModal = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({ showPasswordModal: true });
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onShowApiTokenModal = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({ showApiTokenModal: true });
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onToggleEmailModal = (updatedUser = {}) => {
|
||||
const { showEmailModal } = this.state;
|
||||
|
||||
this.setState({
|
||||
showEmailModal: !showEmailModal,
|
||||
updatedUser,
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onTogglePasswordModal = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { showPasswordModal } = this.state;
|
||||
|
||||
this.setState({ showPasswordModal: !showPasswordModal });
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onToggleApiTokenModal = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { showApiTokenModal } = this.state;
|
||||
|
||||
this.setState({ showApiTokenModal: !showApiTokenModal });
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onToggleSecret = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { revealSecret } = this.state;
|
||||
|
||||
this.setState({ revealSecret: !revealSecret });
|
||||
return false;
|
||||
};
|
||||
|
||||
onCopySecret = () => {
|
||||
return (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
stringToClipboard(authToken())
|
||||
.then(() => this.setState({ copyMessage: "Copied!" }))
|
||||
.catch(() => this.setState({ copyMessage: "Copy failed" }));
|
||||
|
||||
// Clear message after 1 second
|
||||
setTimeout(() => this.setState({ copyMessage: "" }), 1000);
|
||||
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
handleSubmit = (formData) => {
|
||||
const { dispatch, user, config } = this.props;
|
||||
const updatedUser = deepDifference(formData, user);
|
||||
|
||||
if (updatedUser.email && !updatedUser.password) {
|
||||
return this.onToggleEmailModal(updatedUser);
|
||||
}
|
||||
|
||||
return dispatch(updateUser(user, updatedUser))
|
||||
.then(() => {
|
||||
let accountUpdatedFlashMessage = "Account updated";
|
||||
if (updatedUser.email) {
|
||||
accountUpdatedFlashMessage += `: A confirmation email was sent from ${config.sender_address} to ${updatedUser.email}`;
|
||||
this.setState({ pendingEmail: updatedUser.email });
|
||||
}
|
||||
|
||||
dispatch(renderFlash("success", accountUpdatedFlashMessage));
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((userErrors) => {
|
||||
if (userErrors.base.includes("already exists")) {
|
||||
// TODO: Revamp to create inline error requires jsx > tsx / form fields state/validators
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"error",
|
||||
"A user with this email address already exists."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not edit user. Please try again.")
|
||||
);
|
||||
}
|
||||
this.setState({ showEmailModal: false });
|
||||
});
|
||||
};
|
||||
|
||||
handleSubmitPasswordForm = (formData) => {
|
||||
const { dispatch, user } = this.props;
|
||||
|
||||
return dispatch(userActions.changePassword(user, formData)).then(() => {
|
||||
dispatch(renderFlash("success", "Password changed successfully"));
|
||||
this.setState({ showPasswordModal: false });
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
renderEmailModal = () => {
|
||||
const { errors } = this.props;
|
||||
const { updatedUser, showEmailModal } = this.state;
|
||||
const { handleSubmit, onToggleEmailModal } = this;
|
||||
|
||||
const emailSubmit = (formData) => {
|
||||
handleSubmit(formData).then((r) => {
|
||||
return r ? onToggleEmailModal() : false;
|
||||
});
|
||||
};
|
||||
|
||||
if (!showEmailModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="Confirm email update" onExit={onToggleEmailModal}>
|
||||
<div className={`${baseClass}__confirm-update`}>
|
||||
To update your email you must confirm your password.
|
||||
</div>
|
||||
<ChangeEmailForm
|
||||
formData={updatedUser}
|
||||
handleSubmit={emailSubmit}
|
||||
onCancel={onToggleEmailModal}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
renderPasswordModal = () => {
|
||||
const { userErrors } = this.props;
|
||||
const { showPasswordModal } = this.state;
|
||||
const { handleSubmitPasswordForm, onTogglePasswordModal } = this;
|
||||
|
||||
if (!showPasswordModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="Change password" onExit={onTogglePasswordModal}>
|
||||
<ChangePasswordForm
|
||||
handleSubmit={handleSubmitPasswordForm}
|
||||
onCancel={onTogglePasswordModal}
|
||||
serverErrors={userErrors}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
renderLabel = () => {
|
||||
const { copyMessage } = this.state;
|
||||
const { onCopySecret } = this;
|
||||
|
||||
return (
|
||||
<span className={`${baseClass}__name`}>
|
||||
<span className="buttons">
|
||||
{copyMessage && <span>{`${copyMessage} `}</span>}
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={`${baseClass}__secret-copy-icon`}
|
||||
onClick={onCopySecret(`.${baseClass}__secret-input`)}
|
||||
>
|
||||
<FleetIcon name="clipboard" />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
renderApiTokenModal = () => {
|
||||
const { showApiTokenModal, revealSecret } = this.state;
|
||||
const { onToggleApiTokenModal, onToggleSecret, renderLabel } = this;
|
||||
|
||||
if (!showApiTokenModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="Get API token" onExit={onToggleApiTokenModal}>
|
||||
<p className={`${baseClass}__secret-label`}>
|
||||
Your API token:
|
||||
<a
|
||||
href="#revealSecret"
|
||||
onClick={onToggleSecret}
|
||||
className={`${baseClass}__reveal-secret`}
|
||||
>
|
||||
{revealSecret ? "Hide" : "Reveal"} Token
|
||||
</a>
|
||||
</p>
|
||||
<div className={`${baseClass}__secret-wrapper`}>
|
||||
<InputField
|
||||
disabled
|
||||
inputWrapperClass={`${baseClass}__secret-input`}
|
||||
name="osqueryd-secret"
|
||||
type={revealSecret ? "text" : "password"}
|
||||
value={authToken()}
|
||||
label={renderLabel()}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__button-wrap`}>
|
||||
<Button
|
||||
onClick={onToggleApiTokenModal}
|
||||
className="button button--brand"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
handleSubmit,
|
||||
onCancel,
|
||||
onShowModal,
|
||||
onShowApiTokenModal,
|
||||
renderEmailModal,
|
||||
renderPasswordModal,
|
||||
renderApiTokenModal,
|
||||
} = this;
|
||||
const { version, errors, user, config, isPremiumTier } = this.props;
|
||||
const { pendingEmail } = this.state;
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
global_role: globalRole,
|
||||
updated_at: updatedAt,
|
||||
sso_enabled: ssoEnabled,
|
||||
teams,
|
||||
} = user;
|
||||
|
||||
const roleText = generateRole(teams, globalRole);
|
||||
const teamsText = generateTeam(teams, globalRole);
|
||||
|
||||
const lastUpdatedAt = formatDistanceToNow(new Date(updatedAt), {
|
||||
addSuffix: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__manage body-wrap`}>
|
||||
<h1>My account</h1>
|
||||
<UserSettingsForm
|
||||
formData={user}
|
||||
handleSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
pendingEmail={pendingEmail}
|
||||
serverErrors={errors}
|
||||
smtpConfigured={config.configured}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__additional body-wrap`}>
|
||||
<div className={`${baseClass}__change-avatar`}>
|
||||
<Avatar user={user} className={`${baseClass}__avatar`} />
|
||||
<a href="http://en.gravatar.com/emails/">
|
||||
Change photo at Gravatar
|
||||
</a>
|
||||
</div>
|
||||
{isPremiumTier && (
|
||||
<div className={`${baseClass}__more-info-detail`}>
|
||||
<p className={`${baseClass}__header`}>Teams</p>
|
||||
<p
|
||||
className={`${baseClass}__description ${baseClass}__teams ${greyCell(
|
||||
teamsText
|
||||
)}`}
|
||||
>
|
||||
{teamsText}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__more-info-detail`}>
|
||||
<p className={`${baseClass}__header`}>Role</p>
|
||||
<p
|
||||
className={`${baseClass}__description ${baseClass}__role ${greyCell(
|
||||
roleText
|
||||
)}`}
|
||||
>
|
||||
{roleText}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__more-info-detail`}>
|
||||
<p className={`${baseClass}__header`}>Password</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onShowModal}
|
||||
disabled={ssoEnabled}
|
||||
className={`${baseClass}__button`}
|
||||
>
|
||||
Change password
|
||||
</Button>
|
||||
<p className={`${baseClass}__last-updated`}>
|
||||
Last changed: {lastUpdatedAt}
|
||||
</p>
|
||||
<Button
|
||||
onClick={onShowApiTokenModal}
|
||||
className={`${baseClass}__button`}
|
||||
>
|
||||
Get API token
|
||||
</Button>
|
||||
<span
|
||||
className={`${baseClass}__version`}
|
||||
>{`Fleet ${version.version} • Go ${version.go_version}`}</span>
|
||||
<span className={`${baseClass}__privacy-policy`}>
|
||||
<a
|
||||
href="https://fleetdm.com/legal/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Privacy policy
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
{renderEmailModal()}
|
||||
{renderPasswordModal()}
|
||||
{renderApiTokenModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { data: version } = state.version;
|
||||
const { errors, user } = state.auth;
|
||||
const { config } = state.app;
|
||||
const { errors: userErrors } = state.entities.users;
|
||||
const isPremiumTier = permissionUtils.isPremiumTier(config);
|
||||
|
||||
return { version, errors, user, userErrors, config, isPremiumTier };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(UserSettingsPage);
|
||||
@@ -1,124 +0,0 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import ConnectedPage, {
|
||||
UserSettingsPage,
|
||||
} from "pages/UserSettingsPage/UserSettingsPage";
|
||||
import testHelpers from "test/helpers";
|
||||
import { userStub, configStub, adminUserStub } from "test/stubs";
|
||||
import * as authActions from "redux/nodes/auth/actions";
|
||||
|
||||
const { connectedComponent, fillInFormInput, reduxMockStore } = testHelpers;
|
||||
|
||||
jest.mock("date-fns");
|
||||
|
||||
describe("UserSettingsPage - component", () => {
|
||||
const store = {
|
||||
auth: { user: userStub },
|
||||
app: { config: configStub },
|
||||
entities: { users: {} },
|
||||
version: { data: {} },
|
||||
};
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
it("renders a UserSettingsForm component", () => {
|
||||
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||
|
||||
expect(Page.find("UserSettingsForm").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("contains expected text", () => {
|
||||
const pageWithUser = mount(
|
||||
<UserSettingsPage dispatch={noop} user={userStub} config={configStub} />
|
||||
);
|
||||
const pageWithAdmin = mount(
|
||||
<UserSettingsPage
|
||||
dispatch={noop}
|
||||
user={adminUserStub}
|
||||
config={configStub}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(pageWithUser.find(".user-settings__role").text()).toContain(
|
||||
"Observer"
|
||||
);
|
||||
expect(pageWithAdmin.find(".user-settings__role").text()).toContain(
|
||||
"Admin"
|
||||
);
|
||||
});
|
||||
|
||||
it("updates a user with only the updated attributes", () => {
|
||||
jest.spyOn(authActions, "updateUser");
|
||||
|
||||
const dispatch = () => Promise.resolve();
|
||||
const props = { dispatch, user: userStub, config: configStub };
|
||||
const pageNode = mount(<UserSettingsPage {...props} />).instance();
|
||||
const updatedAttrs = { name: "Updated Name" };
|
||||
const updatedUser = { ...userStub, ...updatedAttrs };
|
||||
|
||||
pageNode.handleSubmit(updatedUser);
|
||||
|
||||
expect(authActions.updateUser).toHaveBeenCalledWith(userStub, updatedAttrs);
|
||||
});
|
||||
|
||||
describe("changing email address", () => {
|
||||
it("renders the ChangeEmailForm when the user changes their email", () => {
|
||||
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||
const UserSettingsForm = Page.find("UserSettingsForm");
|
||||
const emailInput = UserSettingsForm.find({ name: "email" });
|
||||
|
||||
expect(Page.find("ChangeEmailForm").length).toEqual(
|
||||
0,
|
||||
"Expected the ChangeEmailForm to not render"
|
||||
);
|
||||
|
||||
fillInFormInput(emailInput, "new@email.org");
|
||||
UserSettingsForm.simulate("submit");
|
||||
|
||||
expect(Page.find("ChangeEmailForm").length).toEqual(
|
||||
1,
|
||||
"Expected the ChangeEmailForm to render"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not render the ChangeEmailForm when the user does not change their email", () => {
|
||||
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||
const UserSettingsForm = Page.find("UserSettingsForm");
|
||||
const emailInput = UserSettingsForm.find({ name: "email" });
|
||||
|
||||
expect(Page.find("ChangeEmailForm").length).toEqual(
|
||||
0,
|
||||
"Expected the ChangeEmailForm to not render"
|
||||
);
|
||||
|
||||
fillInFormInput(emailInput, userStub.email);
|
||||
UserSettingsForm.simulate("submit");
|
||||
|
||||
expect(Page.find("ChangeEmailForm").length).toEqual(
|
||||
0,
|
||||
"Expected the ChangeEmailForm to not render"
|
||||
);
|
||||
});
|
||||
|
||||
it("displays pending email text when the user is pending an email change", () => {
|
||||
const props = { dispatch: noop, user: userStub, config: configStub };
|
||||
const Page = mount(<UserSettingsPage {...props} />);
|
||||
const UserSettingsForm = () => Page.find("UserSettingsForm");
|
||||
const emailHint = () =>
|
||||
UserSettingsForm().find(".manage-user__email-hint");
|
||||
|
||||
expect(emailHint().length).toEqual(
|
||||
0,
|
||||
"Expected the form to not render an email hint"
|
||||
);
|
||||
|
||||
Page.setState({ pendingEmail: "new@email.org" });
|
||||
|
||||
expect(emailHint().length).toEqual(
|
||||
1,
|
||||
"Expected the form to render an email hint"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,360 @@
|
||||
import React, { useState, useContext, useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { authToken } from "utilities/local"; // @ts-ignore
|
||||
import { stringToClipboard } from "utilities/copy_text";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification"; // @ts-ignore
|
||||
import { IVersionData } from "interfaces/version";
|
||||
import { IUser } from "interfaces/user"; // @ts-ignore
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
import usersAPI from "services/entities/users";
|
||||
import versionAPI from "services/entities/version";
|
||||
import formatErrorResponse from "utilities/format_error_response";
|
||||
import { generateRole, generateTeam, greyCell } from "fleet/helpers";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
import Button from "components/buttons/Button"; // @ts-ignore
|
||||
import ChangeEmailForm from "components/forms/ChangeEmailForm"; // @ts-ignore
|
||||
import ChangePasswordForm from "components/forms/ChangePasswordForm"; // @ts-ignore
|
||||
import FleetIcon from "components/icons/FleetIcon"; // @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Modal from "components/Modal"; // @ts-ignore
|
||||
import UserSettingsForm from "components/forms/UserSettingsForm";
|
||||
|
||||
const baseClass = "user-settings";
|
||||
|
||||
interface IUserSettingsPageProps {
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const UserSettingsPage = ({ router }: IUserSettingsPageProps) => {
|
||||
const { config, currentUser, isPremiumTier } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [pendingEmail, setPendingEmail] = useState<string>("");
|
||||
const [showEmailModal, setShowEmailModal] = useState<boolean>(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState<boolean>(false);
|
||||
const [updatedUser, setUpdatedUser] = useState<Partial<IUser>>({});
|
||||
const [copyMessage, setCopyMessage] = useState<string>("");
|
||||
const [showApiTokenModal, setShowApiTokenModal] = useState<boolean>(false);
|
||||
const [revealSecret, setRevealSecret] = useState<boolean>(false);
|
||||
const [versionData, setVersionData] = useState<IVersionData>();
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
const [userErrors, setUserErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const getVersionData = async () => {
|
||||
try {
|
||||
const data = await versionAPI.load();
|
||||
setVersionData(data);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
getVersionData();
|
||||
}, []);
|
||||
|
||||
const onCancel = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
evt.preventDefault();
|
||||
return router.goBack();
|
||||
};
|
||||
|
||||
const onShowPasswordModal = () => {
|
||||
setShowPasswordModal(true);
|
||||
return false;
|
||||
};
|
||||
|
||||
const onShowApiTokenModal = () => {
|
||||
setShowApiTokenModal(true);
|
||||
return false;
|
||||
};
|
||||
|
||||
const onToggleEmailModal = (updated = {}) => {
|
||||
setShowEmailModal(!showEmailModal);
|
||||
setUpdatedUser(updated);
|
||||
return false;
|
||||
};
|
||||
|
||||
const onTogglePasswordModal = () => {
|
||||
setShowPasswordModal(!showPasswordModal);
|
||||
return false;
|
||||
};
|
||||
|
||||
const onToggleApiTokenModal = () => {
|
||||
setShowApiTokenModal(!showApiTokenModal);
|
||||
return false;
|
||||
};
|
||||
|
||||
const onToggleSecret = () => {
|
||||
setRevealSecret(!revealSecret);
|
||||
return false;
|
||||
};
|
||||
|
||||
// placeholder is needed even though it's not used
|
||||
const onCopySecret = (placeholder: string) => {
|
||||
return (evt: ClipboardEvent) => {
|
||||
evt.preventDefault();
|
||||
|
||||
stringToClipboard(authToken())
|
||||
.then(() => setCopyMessage("Copied!"))
|
||||
.catch(() => setCopyMessage("Copy failed"));
|
||||
|
||||
// Clear message after 1 second
|
||||
setTimeout(() => setCopyMessage(""), 1000);
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async (formData: any) => {
|
||||
if (!currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updated = deepDifference(formData, currentUser);
|
||||
|
||||
if (updated.email && !updated.password) {
|
||||
return onToggleEmailModal(updated);
|
||||
}
|
||||
|
||||
try {
|
||||
await usersAPI.update(currentUser.id, updated);
|
||||
let accountUpdatedFlashMessage = "Account updated";
|
||||
if (updated.email) {
|
||||
accountUpdatedFlashMessage += `: A confirmation email was sent from ${config?.smtp_settings.sender_address} to ${updated.email}`;
|
||||
setPendingEmail(updated.email);
|
||||
}
|
||||
|
||||
renderFlash("success", accountUpdatedFlashMessage);
|
||||
return true;
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setErrors(errorObject);
|
||||
|
||||
if (errorObject.base.includes("already exists")) {
|
||||
renderFlash("error", "A user with this email address already exists.");
|
||||
} else {
|
||||
renderFlash("error", "Could not edit user. Please try again.");
|
||||
}
|
||||
|
||||
setShowEmailModal(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitPasswordForm = async (formData: any) => {
|
||||
try {
|
||||
await usersAPI.changePassword(formData);
|
||||
renderFlash("success", "Password changed successfully");
|
||||
setShowPasswordModal(false);
|
||||
} catch (response) {
|
||||
const errorObject = formatErrorResponse(response);
|
||||
setUserErrors(errorObject);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderEmailModal = () => {
|
||||
const emailSubmit = (formData: any) => {
|
||||
handleSubmit(formData).then((r?: boolean) => {
|
||||
return r ? onToggleEmailModal() : false;
|
||||
});
|
||||
};
|
||||
|
||||
if (!showEmailModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="Confirm email update" onExit={onToggleEmailModal}>
|
||||
<>
|
||||
<div className={`${baseClass}__confirm-update`}>
|
||||
To update your email you must confirm your password.
|
||||
</div>
|
||||
<ChangeEmailForm
|
||||
formData={updatedUser}
|
||||
handleSubmit={emailSubmit}
|
||||
onCancel={onToggleEmailModal}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPasswordModal = () => {
|
||||
if (!showPasswordModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="Change password" onExit={onTogglePasswordModal}>
|
||||
<ChangePasswordForm
|
||||
handleSubmit={handleSubmitPasswordForm}
|
||||
onCancel={onTogglePasswordModal}
|
||||
serverErrors={userErrors}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLabel = () => {
|
||||
return (
|
||||
<span className={`${baseClass}__name`}>
|
||||
<span className="buttons">
|
||||
{copyMessage && <span>{`${copyMessage} `}</span>}
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={`${baseClass}__secret-copy-icon`}
|
||||
onClick={onCopySecret(`.${baseClass}__secret-input`)}
|
||||
>
|
||||
<FleetIcon name="clipboard" />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const renderApiTokenModal = () => {
|
||||
if (!showApiTokenModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="Get API token" onExit={onToggleApiTokenModal}>
|
||||
<>
|
||||
<p className={`${baseClass}__secret-label`}>
|
||||
Your API token:
|
||||
<a
|
||||
href="#revealSecret"
|
||||
onClick={onToggleSecret}
|
||||
className={`${baseClass}__reveal-secret`}
|
||||
>
|
||||
{revealSecret ? "Hide" : "Reveal"} Token
|
||||
</a>
|
||||
</p>
|
||||
<div className={`${baseClass}__secret-wrapper`}>
|
||||
<InputField
|
||||
disabled
|
||||
inputWrapperClass={`${baseClass}__secret-input`}
|
||||
name="osqueryd-secret"
|
||||
type={revealSecret ? "text" : "password"}
|
||||
value={authToken()}
|
||||
label={renderLabel()}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__button-wrap`}>
|
||||
<Button
|
||||
onClick={onToggleApiTokenModal}
|
||||
className="button button--brand"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
if (!currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
global_role: globalRole,
|
||||
updated_at: updatedAt,
|
||||
sso_enabled: ssoEnabled,
|
||||
teams,
|
||||
} = currentUser;
|
||||
|
||||
const roleText = generateRole(teams, globalRole);
|
||||
const teamsText = generateTeam(teams, globalRole);
|
||||
|
||||
const lastUpdatedAt =
|
||||
updatedAt &&
|
||||
formatDistanceToNow(new Date(updatedAt), {
|
||||
addSuffix: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__manage body-wrap`}>
|
||||
<h1>My account</h1>
|
||||
<UserSettingsForm
|
||||
formData={currentUser}
|
||||
handleSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
pendingEmail={pendingEmail}
|
||||
serverErrors={errors}
|
||||
smtpConfigured={config?.smtp_settings.configured}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__additional body-wrap`}>
|
||||
<div className={`${baseClass}__change-avatar`}>
|
||||
<Avatar user={currentUser} className={`${baseClass}__avatar`} />
|
||||
<a href="http://en.gravatar.com/emails/">Change photo at Gravatar</a>
|
||||
</div>
|
||||
{isPremiumTier && (
|
||||
<div className={`${baseClass}__more-info-detail`}>
|
||||
<p className={`${baseClass}__header`}>Teams</p>
|
||||
<p
|
||||
className={`${baseClass}__description ${baseClass}__teams ${greyCell(
|
||||
teamsText
|
||||
)}`}
|
||||
>
|
||||
{teamsText}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__more-info-detail`}>
|
||||
<p className={`${baseClass}__header`}>Role</p>
|
||||
<p
|
||||
className={`${baseClass}__description ${baseClass}__role ${greyCell(
|
||||
roleText
|
||||
)}`}
|
||||
>
|
||||
{roleText}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__more-info-detail`}>
|
||||
<p className={`${baseClass}__header`}>Password</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onShowPasswordModal}
|
||||
disabled={ssoEnabled}
|
||||
className={`${baseClass}__button`}
|
||||
>
|
||||
Change password
|
||||
</Button>
|
||||
<p className={`${baseClass}__last-updated`}>
|
||||
Last changed: {lastUpdatedAt}
|
||||
</p>
|
||||
<Button
|
||||
onClick={onShowApiTokenModal}
|
||||
className={`${baseClass}__button`}
|
||||
>
|
||||
Get API token
|
||||
</Button>
|
||||
<span
|
||||
className={`${baseClass}__version`}
|
||||
>{`Fleet ${versionData?.version} • Go ${versionData?.go_version}`}</span>
|
||||
<span className={`${baseClass}__privacy-policy`}>
|
||||
<a
|
||||
href="https://fleetdm.com/legal/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Privacy policy
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
{renderEmailModal()}
|
||||
{renderPasswordModal()}
|
||||
{renderApiTokenModal()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSettingsPage;
|
||||
@@ -1,15 +1,13 @@
|
||||
import React, { useCallback, useContext } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useQuery } from "react-query";
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification"; // @ts-ignore
|
||||
import { getConfig } from "redux/nodes/app/actions";
|
||||
|
||||
import configAPI from "services/entities/config";
|
||||
|
||||
// @ts-ignore
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
import { IConfig, IConfigNested } from "interfaces/config";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
|
||||
// @ts-ignore
|
||||
@@ -18,25 +16,22 @@ import AppConfigForm from "components/forms/admin/AppConfigForm";
|
||||
export const baseClass = "app-settings";
|
||||
|
||||
const AppSettingsPage = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const { setConfig } = useContext(AppContext);
|
||||
|
||||
const {
|
||||
data: appConfig,
|
||||
isLoading: isLoadingConfig,
|
||||
refetch: refetchConfig,
|
||||
} = useQuery<IConfigNested, Error, IConfigNested>(
|
||||
["config"],
|
||||
() => configAPI.loadAll(),
|
||||
{
|
||||
select: (data: IConfigNested) => data,
|
||||
}
|
||||
);
|
||||
} = useQuery<IConfig, Error, IConfig>(["config"], () => configAPI.loadAll(), {
|
||||
select: (data: IConfig) => data,
|
||||
onSuccess: (data) => {
|
||||
setConfig(data);
|
||||
},
|
||||
});
|
||||
|
||||
const onFormSubmit = useCallback(
|
||||
(formData: IConfigNested) => {
|
||||
(formData: IConfig) => {
|
||||
const diff = deepDifference(formData, appConfig);
|
||||
// send all formData.agent_options because diff overrides all agent options
|
||||
diff.agent_options = formData.agent_options;
|
||||
@@ -63,15 +58,9 @@ const AppSettingsPage = (): JSX.Element => {
|
||||
})
|
||||
.finally(() => {
|
||||
refetchConfig();
|
||||
// Config must be updated in both Redux and AppContext
|
||||
dispatch(getConfig())
|
||||
.then((configState: IConfig) => {
|
||||
setConfig(configState);
|
||||
})
|
||||
.catch(() => false);
|
||||
});
|
||||
},
|
||||
[dispatch, appConfig, getConfig, setConfig]
|
||||
[appConfig]
|
||||
);
|
||||
|
||||
// WHY???
|
||||
|
||||
+16
-20
@@ -1,5 +1,4 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useQuery } from "react-query";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import yaml from "js-yaml";
|
||||
@@ -7,8 +6,10 @@ import yaml from "js-yaml";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import endpoints from "fleet/endpoints";
|
||||
import teamsAPI from "services/entities/teams"; // @ts-ignore
|
||||
import osqueryOptionsActions from "redux/nodes/osquery/actions"; // @ts-ignore
|
||||
import teamsAPI from "services/entities/teams";
|
||||
import osqueryOptionsAPI from "services/entities/osquery_options";
|
||||
|
||||
// @ts-ignore
|
||||
import validateYaml from "components/forms/validators/validate_yaml"; // @ts-ignore
|
||||
import OsqueryOptionsForm from "components/forms/admin/OsqueryOptionsForm";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
@@ -30,7 +31,6 @@ const AgentOptionsPage = ({
|
||||
params: { team_id },
|
||||
}: IAgentOptionsPageProps): JSX.Element => {
|
||||
const teamIdFromURL = parseInt(team_id, 10);
|
||||
const dispatch = useDispatch();
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [formData, setFormData] = useState<{ osquery_options?: string }>({});
|
||||
@@ -56,29 +56,25 @@ const AgentOptionsPage = ({
|
||||
}
|
||||
);
|
||||
|
||||
const onSaveOsqueryOptionsFormSubmit = (updatedForm: {
|
||||
const onSaveOsqueryOptionsFormSubmit = async (updatedForm: {
|
||||
osquery_options: string;
|
||||
}): void | false => {
|
||||
}) => {
|
||||
const { TEAMS_AGENT_OPTIONS } = endpoints;
|
||||
const { error } = validateYaml(updatedForm.osquery_options);
|
||||
if (error) {
|
||||
renderFlash("error", error.reason);
|
||||
return false;
|
||||
return renderFlash("error", error.reason);
|
||||
}
|
||||
dispatch(
|
||||
osqueryOptionsActions.updateOsqueryOptions(
|
||||
|
||||
try {
|
||||
await osqueryOptionsAPI.update(
|
||||
updatedForm,
|
||||
TEAMS_AGENT_OPTIONS(teamIdFromURL)
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
renderFlash("success", "Successfully saved agent options");
|
||||
})
|
||||
.catch((errors: { [key: string]: string }) => {
|
||||
renderFlash("error", errors.stack);
|
||||
});
|
||||
|
||||
return false;
|
||||
);
|
||||
return renderFlash("success", "Successfully saved agent options");
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
return renderFlash("error", "Could not save agent options");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+3
-3
@@ -66,8 +66,8 @@ const MembersPage = ({
|
||||
AppContext
|
||||
);
|
||||
|
||||
const smtpConfigured = config?.configured || false;
|
||||
const canUseSso = config?.enable_sso || false;
|
||||
const smtpConfigured = config?.smtp_settings.configured || false;
|
||||
const canUseSso = config?.sso_settings.enable_sso || false;
|
||||
|
||||
const [showAddMemberModal, setShowAddMemberModal] = useState<boolean>(false);
|
||||
const [showRemoveMemberModal, setShowRemoveMemberModal] = useState<boolean>(
|
||||
@@ -235,7 +235,7 @@ const MembersPage = ({
|
||||
.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
`An invitation email was sent from ${config?.sender_address} to ${formData.email}.`
|
||||
`An invitation email was sent from ${config?.smtp_settings.sender_address} to ${formData.email}.`
|
||||
);
|
||||
fetchUsers(tableQueryData);
|
||||
toggleCreateMemberModal();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useCallback, useContext } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useQuery } from "react-query";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { InjectedRouter, Link, RouteProps } from "react-router";
|
||||
@@ -14,14 +13,13 @@ import { ITeam, ITeamSummary } from "interfaces/team";
|
||||
import teamsAPI from "services/entities/teams";
|
||||
import usersAPI, { IGetMeResponse } from "services/entities/users";
|
||||
import enrollSecretsAPI from "services/entities/enroll_secret";
|
||||
import teamActions from "redux/nodes/entities/teams/actions";
|
||||
import {
|
||||
IEnrollSecret,
|
||||
IEnrollSecretsResponse,
|
||||
} from "interfaces/enroll_secret";
|
||||
import { IOldApiError } from "interfaces/errors";
|
||||
import permissions from "utilities/permissions";
|
||||
import sortUtils from "utilities/sort";
|
||||
import formatErrorResponse from "utilities/format_error_response";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import Button from "components/buttons/Button";
|
||||
@@ -100,7 +98,6 @@ const TeamDetailsWrapper = ({
|
||||
location: { pathname },
|
||||
params: routeParams,
|
||||
}: ITeamDetailsPageProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const handlePageError = useErrorHandler();
|
||||
const teamIdFromURL = parseInt(routeParams.team_id, 10) || 0;
|
||||
@@ -281,48 +278,66 @@ const TeamDetailsWrapper = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteSubmit = useCallback(() => {
|
||||
dispatch(teamActions.destroy(currentTeam?.id))
|
||||
.then(() => {
|
||||
renderFlash("success", "Team removed");
|
||||
router.push(PATHS.ADMIN_TEAMS);
|
||||
// TODO: error handling
|
||||
})
|
||||
.catch(() => null);
|
||||
const onDeleteSubmit = useCallback(async () => {
|
||||
if (!currentTeam) {
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleDeleteTeamModal();
|
||||
|
||||
try {
|
||||
await teamsAPI.destroy(currentTeam.id);
|
||||
renderFlash("success", "Team removed");
|
||||
return router.push(PATHS.ADMIN_TEAMS);
|
||||
} catch (response) {
|
||||
renderFlash("error", "Something went wrong removing the team");
|
||||
console.error(response);
|
||||
return false;
|
||||
}
|
||||
}, [toggleDeleteTeamModal, currentTeam?.id]);
|
||||
|
||||
const onEditSubmit = useCallback(
|
||||
(formData: IEditTeamFormData) => {
|
||||
async (formData: IEditTeamFormData) => {
|
||||
const updatedAttrs =
|
||||
currentTeam && generateUpdateData(currentTeam, formData);
|
||||
|
||||
if (!currentTeam) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// no updates, so no need for a request.
|
||||
if (updatedAttrs === null) {
|
||||
if (!updatedAttrs) {
|
||||
toggleEditTeamModal();
|
||||
return;
|
||||
}
|
||||
dispatch(teamActions.update(currentTeam?.id, updatedAttrs))
|
||||
.then(() => {
|
||||
dispatch(teamActions.loadAll({ perPage: 500 }));
|
||||
renderFlash(
|
||||
"success",
|
||||
`Successfully updated team name to ${updatedAttrs?.name}`
|
||||
);
|
||||
setBackendValidators({});
|
||||
refetchTeams();
|
||||
refetchMe();
|
||||
|
||||
try {
|
||||
await teamsAPI.update(currentTeam.id, updatedAttrs);
|
||||
await teamsAPI.loadAll({ perPage: 500 });
|
||||
|
||||
renderFlash(
|
||||
"success",
|
||||
`Successfully updated team name to ${updatedAttrs?.name}`
|
||||
);
|
||||
setBackendValidators({});
|
||||
refetchTeams();
|
||||
refetchMe();
|
||||
toggleEditTeamModal();
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
const errorObject = formatErrorResponse(response);
|
||||
|
||||
if (errorObject.base.includes("Duplicate")) {
|
||||
setBackendValidators({
|
||||
name: "A team with this name already exists",
|
||||
});
|
||||
} else {
|
||||
renderFlash("error", "Could not create team. Please try again.");
|
||||
toggleEditTeamModal();
|
||||
})
|
||||
.catch((updateError: IOldApiError) => {
|
||||
if (updateError.base.includes("Duplicate")) {
|
||||
setBackendValidators({
|
||||
name: "A team with this name already exists",
|
||||
});
|
||||
} else {
|
||||
renderFlash("error", "Could not create team. Please try again.");
|
||||
toggleEditTeamModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[toggleEditTeamModal, currentTeam, setBackendValidators]
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useCallback, useContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { ITeam } from "interfaces/team";
|
||||
@@ -34,6 +35,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
const handlePageError = useErrorHandler();
|
||||
|
||||
const {
|
||||
data: teams,
|
||||
@@ -45,6 +47,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
() => teamsAPI.loadAll(),
|
||||
{
|
||||
select: (data: ITeamsResponse) => data.teams,
|
||||
onError: (error) => handlePageError(error),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useState, useCallback, useContext } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { useQuery } from "react-query";
|
||||
import memoize from "memoize-one";
|
||||
|
||||
import paths from "router/paths";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
import { IInvite } from "interfaces/invite";
|
||||
import { IUser, IUserFormErrors } from "interfaces/user";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { clearToken } from "utilities/local";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
@@ -15,7 +16,6 @@ import teamsAPI from "services/entities/teams";
|
||||
import usersAPI from "services/entities/users";
|
||||
import invitesAPI from "services/entities/invites";
|
||||
|
||||
import paths from "router/paths";
|
||||
import TableContainer, { ITableQueryData } from "components/TableContainer";
|
||||
import TableDataError from "components/TableDataError";
|
||||
import Modal from "components/Modal";
|
||||
@@ -40,8 +40,6 @@ interface ITeamsResponse {
|
||||
}
|
||||
|
||||
const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { config, currentUser, isPremiumTier } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
@@ -234,7 +232,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
`An invitation email was sent from ${config?.sender_address} to ${formData.email}.`
|
||||
`An invitation email was sent from ${config?.smtp_settings.sender_address} to ${formData.email}.`
|
||||
);
|
||||
toggleCreateUserModal();
|
||||
refetchInvites();
|
||||
@@ -335,7 +333,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
let userUpdatedFlashMessage = `Successfully edited ${formData.name}`;
|
||||
|
||||
if (userData?.email !== formData.email) {
|
||||
userUpdatedFlashMessage += `: A confirmation email was sent from ${config?.sender_address} to ${formData.email}`;
|
||||
userUpdatedFlashMessage += `: A confirmation email was sent from ${config?.smtp_settings.sender_address} to ${formData.email}`;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -405,7 +403,10 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
.deleteSessions(userEditing.id)
|
||||
.then(() => {
|
||||
if (isResettingCurrentUser) {
|
||||
dispatch({ type: "LOGOUT_SUCCESS" });
|
||||
clearToken();
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
renderFlash("success", "Successfully reset sessions.");
|
||||
@@ -454,8 +455,8 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
onSubmit={onEditUser}
|
||||
availableTeams={teams || []}
|
||||
isPremiumTier={isPremiumTier || false}
|
||||
smtpConfigured={config?.configured || false}
|
||||
canUseSso={config?.enable_sso || false}
|
||||
smtpConfigured={config?.smtp_settings.configured || false}
|
||||
canUseSso={config?.sso_settings.enable_sso || false}
|
||||
isSsoEnabled={userData?.sso_enabled}
|
||||
isModifiedByGlobalAdmin
|
||||
isInvitePending={userEditing.type === "invite"}
|
||||
@@ -476,8 +477,8 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
defaultGlobalRole={"observer"}
|
||||
defaultTeams={[]}
|
||||
isPremiumTier={isPremiumTier || false}
|
||||
smtpConfigured={config?.configured || false}
|
||||
canUseSso={config?.enable_sso || false}
|
||||
smtpConfigured={config?.smtp_settings.configured || false}
|
||||
canUseSso={config?.sso_settings.enable_sso || false}
|
||||
isFormSubmitting={isFormSubmitting}
|
||||
isModifiedByGlobalAdmin
|
||||
/>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { noop } from "lodash";
|
||||
import { resetErrors } from "redux/nodes/errors500/actions";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
import fleetLogoText from "../../../../assets/images/fleet-logo-text-white.svg";
|
||||
import backgroundImg from "../../../../assets/images/500.svg";
|
||||
import githubLogo from "../../../../assets/images/github-mark-white-24x24@2x.png";
|
||||
import slackLogo from "../../../../assets/images/logo-slack-24x24@2x.png";
|
||||
|
||||
const baseClass = "fleet-500";
|
||||
|
||||
class Fleet500 extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
dispatch: noop,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showErrorMessage: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(resetErrors());
|
||||
}
|
||||
|
||||
onShowErrorMessage = () => {
|
||||
this.setState({ showErrorMessage: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<header className="primary-header">
|
||||
<Link to={PATHS.HOME}>
|
||||
<img
|
||||
className="primary-header__logo"
|
||||
src={fleetLogoText}
|
||||
alt="Fleet logo"
|
||||
/>
|
||||
</Link>
|
||||
</header>
|
||||
<img
|
||||
className="background-image"
|
||||
src={backgroundImg}
|
||||
alt="500 background"
|
||||
/>
|
||||
<main>
|
||||
<h1>
|
||||
<span>500:</span> Oh, something went wrong.
|
||||
</h1>
|
||||
<p>Please file an issue if you believe this is a bug.</p>
|
||||
<div className={`${baseClass}__button-wrapper`}>
|
||||
<a
|
||||
href="https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="unstyled"
|
||||
className={`${baseClass}__slack-btn`}
|
||||
>
|
||||
<img src={slackLogo} alt="Slack icon" />
|
||||
Get help on Slack
|
||||
</Button>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md&title="
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button type="button">
|
||||
<img src={githubLogo} alt="Github icon" />
|
||||
File an issue
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Fleet500;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import Button from "components/buttons/Button"; // @ts-ignore
|
||||
import fleetLogoText from "../../../../assets/images/fleet-logo-text-white.svg"; // @ts-ignore
|
||||
import backgroundImg from "../../../../assets/images/500.svg";
|
||||
import githubLogo from "../../../../assets/images/github-mark-white-24x24@2x.png";
|
||||
import slackLogo from "../../../../assets/images/logo-slack-24x24@2x.png";
|
||||
|
||||
const baseClass = "fleet-500";
|
||||
|
||||
const Fleet500 = () => (
|
||||
<div className={baseClass}>
|
||||
<header className="primary-header">
|
||||
<Link to={PATHS.HOME}>
|
||||
<img
|
||||
className="primary-header__logo"
|
||||
src={fleetLogoText}
|
||||
alt="Fleet logo"
|
||||
/>
|
||||
</Link>
|
||||
</header>
|
||||
<img
|
||||
className="background-image"
|
||||
src={backgroundImg}
|
||||
alt="500 background"
|
||||
/>
|
||||
<main>
|
||||
<h1>
|
||||
<span>500:</span> Oh, something went wrong.
|
||||
</h1>
|
||||
<p>Please file an issue if you believe this is a bug.</p>
|
||||
<div className={`${baseClass}__button-wrapper`}>
|
||||
<a
|
||||
href="https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="unstyled"
|
||||
className={`${baseClass}__slack-btn`}
|
||||
>
|
||||
<>
|
||||
<img src={slackLogo} alt="Slack icon" />
|
||||
Get help on Slack
|
||||
</>
|
||||
</Button>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md&title="
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button type="button">
|
||||
<>
|
||||
<img src={githubLogo} alt="Github icon" />
|
||||
File an issue
|
||||
</>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Fleet500;
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useContext, useState, useCallback, useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Link } from "react-router";
|
||||
import { Params, InjectedRouter } from "react-router/lib/Router";
|
||||
import { useQuery } from "react-query";
|
||||
@@ -95,7 +94,6 @@ const HostDetailsPage = ({
|
||||
params: { host_id },
|
||||
}: IHostDetailsProps): JSX.Element => {
|
||||
const hostIdFromURL = parseInt(host_id, 10);
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isPremiumTier,
|
||||
@@ -465,9 +463,7 @@ const HostDetailsPage = ({
|
||||
setShowTransferHostModal(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
dispatch(
|
||||
renderFlash("error", "Could not transfer host. Please try again.")
|
||||
);
|
||||
renderFlash("error", "Could not transfer host. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import { VULNERABLE_DROPDOWN_OPTIONS } from "utilities/constants";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { InjectedRouter } from "react-router/lib/Router";
|
||||
import { noop } from "lodash";
|
||||
|
||||
@@ -11,8 +10,8 @@ import { NotificationContext } from "context/notification";
|
||||
import { inMilliseconds, secondsToHms } from "fleet/helpers";
|
||||
import { IPolicyStats, ILoadAllPoliciesResponse } from "interfaces/policy";
|
||||
import { IWebhookFailingPolicies } from "interfaces/webhook";
|
||||
import { IConfig, IConfigNested } from "interfaces/config"; // @ts-ignore
|
||||
import { getConfig } from "redux/nodes/app/actions";
|
||||
import { IConfig } from "interfaces/config";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import configAPI from "services/entities/config";
|
||||
import globalPoliciesAPI from "services/entities/global_policies";
|
||||
@@ -51,8 +50,6 @@ const ManagePolicyPage = ({
|
||||
router,
|
||||
location,
|
||||
}: IManagePoliciesPageProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
availableTeams,
|
||||
config,
|
||||
@@ -93,6 +90,10 @@ const ManagePolicyPage = ({
|
||||
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
|
||||
const [showRemovePoliciesModal, setShowRemovePoliciesModal] = useState(false);
|
||||
const [showInheritedPolicies, setShowInheritedPolicies] = useState(false);
|
||||
const [
|
||||
failingPoliciesWebhook,
|
||||
setFailingPoliciesWebhook,
|
||||
] = useState<IWebhookFailingPolicies>();
|
||||
const [currentAutomatedPolicies, setCurrentAutomatedPolicies] = useState<
|
||||
number[]
|
||||
>();
|
||||
@@ -143,22 +144,19 @@ const ManagePolicyPage = ({
|
||||
const canAddOrRemovePolicy =
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer || isTeamAdmin;
|
||||
|
||||
const {
|
||||
data: failingPoliciesWebhook,
|
||||
isLoading: isLoadingFailingPoliciesWebhook,
|
||||
refetch: refetchFailingPoliciesWebhook,
|
||||
} = useQuery<IConfigNested, Error, IWebhookFailingPolicies>(
|
||||
["config"],
|
||||
() => configAPI.loadAll(),
|
||||
{
|
||||
enabled: canAddOrRemovePolicy,
|
||||
select: (data: IConfigNested) =>
|
||||
data.webhook_settings.failing_policies_webhook,
|
||||
onSuccess: (data) => {
|
||||
setCurrentAutomatedPolicies(data.policy_ids);
|
||||
},
|
||||
}
|
||||
);
|
||||
const { isLoading: isLoadingConfig, refetch: refetchConfig } = useQuery<
|
||||
IConfig,
|
||||
Error
|
||||
>(["config"], () => configAPI.loadAll(), {
|
||||
enabled: canAddOrRemovePolicy,
|
||||
onSuccess: (data) => {
|
||||
setFailingPoliciesWebhook(data.webhook_settings.failing_policies_webhook);
|
||||
setCurrentAutomatedPolicies(
|
||||
data.webhook_settings.failing_policies_webhook.policy_ids
|
||||
);
|
||||
setConfig(data);
|
||||
},
|
||||
});
|
||||
|
||||
const refetchPolicies = (id?: number) => {
|
||||
refetchGlobalPolicies();
|
||||
@@ -230,13 +228,7 @@ const ManagePolicyPage = ({
|
||||
);
|
||||
} finally {
|
||||
toggleManageAutomationsModal();
|
||||
refetchFailingPoliciesWebhook();
|
||||
// Config must be updated in both Redux and AppContext
|
||||
dispatch(getConfig())
|
||||
.then((configState: IConfig) => {
|
||||
setConfig(configState);
|
||||
})
|
||||
.catch(() => false);
|
||||
refetchConfig();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -291,8 +283,9 @@ const ManagePolicyPage = ({
|
||||
};
|
||||
|
||||
const policyUpdateInterval =
|
||||
secondsToHms(inMilliseconds(config?.osquery_policy || 0) / 1000) ||
|
||||
"osquery policy update interval";
|
||||
secondsToHms(
|
||||
inMilliseconds(config?.update_interval.osquery_policy || 0) / 1000
|
||||
) || "osquery policy update interval";
|
||||
|
||||
const showTeamDescription = isPremiumTier && !!teamId;
|
||||
|
||||
@@ -366,7 +359,7 @@ const ManagePolicyPage = ({
|
||||
<div className={`${baseClass} button-wrap`}>
|
||||
{canAddOrRemovePolicy &&
|
||||
teamId === 0 &&
|
||||
!isLoadingFailingPoliciesWebhook &&
|
||||
!isLoadingConfig &&
|
||||
!isLoadingGlobalPolicies && (
|
||||
<Button
|
||||
onClick={() => onManageAutomationsClick()}
|
||||
@@ -423,14 +416,12 @@ const ManagePolicyPage = ({
|
||||
{!!teamId && teamPoliciesError && <TableDataError />}
|
||||
{!!teamId &&
|
||||
!teamPoliciesError &&
|
||||
(isLoadingTeamPolicies && isLoadingFailingPoliciesWebhook ? (
|
||||
(isLoadingTeamPolicies && isLoadingConfig ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<PoliciesListWrapper
|
||||
policiesList={teamPolicies || []}
|
||||
isLoading={
|
||||
isLoadingTeamPolicies && isLoadingFailingPoliciesWebhook
|
||||
}
|
||||
isLoading={isLoadingTeamPolicies && isLoadingConfig}
|
||||
onRemovePoliciesClick={onRemovePoliciesClick}
|
||||
canAddOrRemovePolicy={canAddOrRemovePolicy}
|
||||
currentTeam={currentTeam}
|
||||
@@ -445,9 +436,7 @@ const ManagePolicyPage = ({
|
||||
) : (
|
||||
<PoliciesListWrapper
|
||||
policiesList={globalPolicies || []}
|
||||
isLoading={
|
||||
isLoadingGlobalPolicies && isLoadingFailingPoliciesWebhook
|
||||
}
|
||||
isLoading={isLoadingGlobalPolicies && isLoadingConfig}
|
||||
onRemovePoliciesClick={onRemovePoliciesClick}
|
||||
canAddOrRemovePolicy={canAddOrRemovePolicy}
|
||||
currentTeam={currentTeam}
|
||||
@@ -482,9 +471,7 @@ const ManagePolicyPage = ({
|
||||
<Spinner />
|
||||
) : (
|
||||
<PoliciesListWrapper
|
||||
isLoading={
|
||||
isLoadingGlobalPolicies && isLoadingFailingPoliciesWebhook
|
||||
}
|
||||
isLoading={isLoadingGlobalPolicies && isLoadingConfig}
|
||||
policiesList={globalPolicies || []}
|
||||
onRemovePoliciesClick={noop}
|
||||
resultsTitle="policies"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useContext, useEffect, KeyboardEvent } from "react";
|
||||
import { IAceEditor } from "react-ace/lib/types";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { size } from "lodash";
|
||||
import classnames from "classnames";
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import SockJS from "sockjs-client";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { formatSelectedTargetsForApi } from "fleet/helpers";
|
||||
// @ts-ignore
|
||||
import campaignHelpers from "redux/nodes/entities/campaigns/helpers";
|
||||
|
||||
import campaignHelpers from "utilities/campaign_helpers";
|
||||
import queryAPI from "services/entities/queries"; // @ts-ignore
|
||||
import debounce from "utilities/debounce"; // @ts-ignore
|
||||
import { BASE_URL, DEFAULT_CAMPAIGN_STATE } from "utilities/constants"; // @ts-ignore
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Row } from "react-table";
|
||||
import { forEach, isEmpty, remove, unionBy } from "lodash";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { formatSelectedTargetsForApi } from "fleet/helpers";
|
||||
import useQueryTargets, { ITargetsQueryResponse } from "hooks/useQueryTargets";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useContext, useEffect, KeyboardEvent } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { size } from "lodash";
|
||||
import classnames from "classnames";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
@@ -3,9 +3,10 @@ import SockJS from "sockjs-client";
|
||||
|
||||
import { QueryContext } from "context/query";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { formatSelectedTargetsForApi } from "fleet/helpers"; // @ts-ignore
|
||||
import campaignHelpers from "redux/nodes/entities/campaigns/helpers";
|
||||
import queryAPI from "services/entities/queries"; // @ts-ignore
|
||||
import { formatSelectedTargetsForApi } from "fleet/helpers";
|
||||
|
||||
import queryAPI from "services/entities/queries";
|
||||
import campaignHelpers from "utilities/campaign_helpers"; // @ts-ignore
|
||||
import debounce from "utilities/debounce"; // @ts-ignore
|
||||
import { BASE_URL, DEFAULT_CAMPAIGN_STATE } from "utilities/constants"; // @ts-ignore
|
||||
import local from "utilities/local"; // @ts-ignore
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Row } from "react-table";
|
||||
import { forEach, isEmpty, remove, unionWith } from "lodash";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { formatSelectedTargetsForApi } from "fleet/helpers";
|
||||
import useQueryTargets, { ITargetsQueryResponse } from "hooks/useQueryTargets";
|
||||
|
||||
+3
-5
@@ -10,10 +10,8 @@ import { IEditScheduledQuery } from "interfaces/scheduled_query";
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner"; // @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown"; // @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import {
|
||||
FREQUENCY_DROPDOWN_OPTIONS,
|
||||
@@ -95,7 +93,7 @@ const ScheduleEditorModal = ({
|
||||
}: IScheduleEditorModalProps): JSX.Element => {
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
const loggingConfig = config?.result.plugin || "unknown";
|
||||
const loggingConfig = config?.logging.result.plugin || "unknown";
|
||||
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState<boolean>(
|
||||
false
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { InjectedRouter } from "react-router/lib/Router";
|
||||
import { useDebouncedCallback } from "use-debounce/lib";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { IConfig, IConfigNested } from "interfaces/config";
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
|
||||
// @ts-ignore
|
||||
import { getConfig } from "redux/nodes/app/actions";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; // @ts-ignore
|
||||
import configAPI from "services/entities/config";
|
||||
import softwareAPI, {
|
||||
ISoftwareResponse,
|
||||
@@ -58,7 +55,6 @@ const ManageSoftwarePage = ({
|
||||
router,
|
||||
location,
|
||||
}: IManageSoftwarePageProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
availableTeams,
|
||||
currentTeam,
|
||||
@@ -69,6 +65,10 @@ const ManageSoftwarePage = ({
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [isSoftwareEnabled, setIsSoftwareEnabled] = useState<boolean>();
|
||||
const [
|
||||
softwareVulnerabilitiesWebhook,
|
||||
setSoftwareVulnerabilitiesWebhook,
|
||||
] = useState<IWebhookSoftwareVulnerabilities>();
|
||||
const [filterVuln, setFilterVuln] = useState(
|
||||
location?.query?.vulnerable || false
|
||||
);
|
||||
@@ -168,19 +168,18 @@ const ManageSoftwarePage = ({
|
||||
|
||||
const canAddOrRemoveSoftwareWebhook = isGlobalAdmin || isGlobalMaintainer;
|
||||
|
||||
const {
|
||||
data: softwareVulnerabilitiesWebhook,
|
||||
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
|
||||
refetch: refetchSoftwareVulnerabilitiesWebhook,
|
||||
} = useQuery<IConfigNested, Error, IWebhookSoftwareVulnerabilities>(
|
||||
["config"],
|
||||
() => configAPI.loadAll(),
|
||||
{
|
||||
enabled: canAddOrRemoveSoftwareWebhook,
|
||||
select: (data: IConfigNested) =>
|
||||
data.webhook_settings.vulnerabilities_webhook,
|
||||
}
|
||||
);
|
||||
const { isLoading: isLoadingConfig, refetch: refetchConfig } = useQuery<
|
||||
IConfig,
|
||||
Error
|
||||
>(["config"], () => configAPI.loadAll(), {
|
||||
enabled: canAddOrRemoveSoftwareWebhook,
|
||||
onSuccess: (data) => {
|
||||
setSoftwareVulnerabilitiesWebhook(
|
||||
data.webhook_settings.vulnerabilities_webhook
|
||||
);
|
||||
setConfig(data);
|
||||
},
|
||||
});
|
||||
|
||||
const onQueryChange = useDebouncedCallback(
|
||||
async ({
|
||||
@@ -239,13 +238,7 @@ const ManageSoftwarePage = ({
|
||||
);
|
||||
} finally {
|
||||
toggleManageAutomationsModal();
|
||||
refetchSoftwareVulnerabilitiesWebhook();
|
||||
// Config must be updated in both Redux and AppContext
|
||||
dispatch(getConfig())
|
||||
.then((configState: IConfig) => {
|
||||
setConfig(configState);
|
||||
})
|
||||
.catch(() => false);
|
||||
refetchConfig();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -304,12 +297,12 @@ const ManageSoftwarePage = ({
|
||||
buttons={(state) =>
|
||||
renderHeaderButtons({
|
||||
...state,
|
||||
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
|
||||
isLoading: isLoadingConfig,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, [router, location, isLoadingSoftwareVulnerabilitiesWebhook]);
|
||||
}, [router, location, isLoadingConfig]);
|
||||
|
||||
const renderSoftwareCount = useCallback(() => {
|
||||
const count = softwareCount;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import config from "./config";
|
||||
|
||||
export default config.actions;
|
||||
@@ -1,19 +0,0 @@
|
||||
import {
|
||||
destroyFunc,
|
||||
updateCampaignState,
|
||||
} from "redux/nodes/entities/campaigns/helpers";
|
||||
import Fleet from "fleet";
|
||||
import Config from "redux/nodes/entities/base/config";
|
||||
import schemas from "redux/nodes/entities/base/schemas";
|
||||
|
||||
const { CAMPAIGNS: schema } = schemas;
|
||||
|
||||
export default new Config({
|
||||
createFunc: Fleet.queries.run,
|
||||
destroyFunc,
|
||||
updateFunc: updateCampaignState,
|
||||
entityName: "campaigns",
|
||||
schema,
|
||||
});
|
||||
|
||||
export const initialState = Object.assign({}, Config.initialState);
|
||||
@@ -1,143 +0,0 @@
|
||||
import helpers from "./helpers";
|
||||
|
||||
const host = {
|
||||
hostname: "jmeller-mbp.local",
|
||||
id: 1,
|
||||
};
|
||||
const campaign = {
|
||||
id: 4,
|
||||
query_id: 12,
|
||||
status: 0,
|
||||
user_id: 1,
|
||||
hosts_count: {
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
const campaignWithResults = {
|
||||
...campaign,
|
||||
hosts: [{ id: 2, hostname: "some-machine" }],
|
||||
query_results: [
|
||||
{ host: "some-machine", feature: "vendor", value: "GenuineIntel" },
|
||||
],
|
||||
totals: {
|
||||
count: 3,
|
||||
online: 2,
|
||||
},
|
||||
};
|
||||
const { destroyFunc, updateCampaignState } = helpers;
|
||||
const resultSocketData = {
|
||||
type: "result",
|
||||
data: {
|
||||
distributed_query_execution_id: 5,
|
||||
host,
|
||||
rows: [
|
||||
{ feature: "product_name", value: "Intel Core" },
|
||||
{ feature: "family", value: "0600" },
|
||||
],
|
||||
},
|
||||
};
|
||||
const statusSocketData = {
|
||||
type: "status",
|
||||
data: "finished",
|
||||
};
|
||||
const totalsSocketData = {
|
||||
type: "totals",
|
||||
data: {
|
||||
count: 5,
|
||||
online: 1,
|
||||
},
|
||||
};
|
||||
|
||||
describe("campaign entity - helpers", () => {
|
||||
describe("#destroyFunc", () => {
|
||||
it("returns the campaign", (done) => {
|
||||
destroyFunc(campaign)
|
||||
.then((response) => {
|
||||
expect(response).toEqual(campaign);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#updateCampaignState", () => {
|
||||
it("appends query results to the campaign when the campaign has query results", () => {
|
||||
const state = { campaign: campaignWithResults };
|
||||
const updatedState = updateCampaignState(resultSocketData)(state, {});
|
||||
|
||||
expect(updatedState.campaign.query_results).toEqual([
|
||||
...campaignWithResults.query_results,
|
||||
{ feature: "product_name", value: "Intel Core" },
|
||||
{ feature: "family", value: "0600" },
|
||||
]);
|
||||
expect(updatedState.campaign.hosts).toContainEqual(host);
|
||||
});
|
||||
|
||||
it("adds query results to the campaign when the campaign does not have query results", () => {
|
||||
const state = { campaign };
|
||||
const updatedState = updateCampaignState(resultSocketData)(state, {});
|
||||
|
||||
expect(updatedState.campaign.query_results).toEqual([
|
||||
{ feature: "product_name", value: "Intel Core" },
|
||||
{ feature: "family", value: "0600" },
|
||||
]);
|
||||
expect(updatedState.campaign.hosts).toContainEqual(host);
|
||||
});
|
||||
|
||||
it("updates totals on the campaign when the campaign has totals", () => {
|
||||
const state = { campaign: campaignWithResults };
|
||||
const updatedState = updateCampaignState(totalsSocketData)(state, {});
|
||||
|
||||
expect(updatedState.campaign.totals).toEqual(totalsSocketData.data);
|
||||
});
|
||||
|
||||
it("adds totals to the campaign when the campaign does not have totals", () => {
|
||||
const state = { campaign };
|
||||
const updatedState = updateCampaignState(totalsSocketData)(state, {});
|
||||
|
||||
expect(updatedState.campaign.totals).toEqual(totalsSocketData.data);
|
||||
});
|
||||
|
||||
it("increases the successful hosts count and total when the result has no error", () => {
|
||||
const state = { campaign };
|
||||
const updatedState = updateCampaignState(resultSocketData)(state, {});
|
||||
|
||||
expect(updatedState.campaign.hosts_count).toEqual({
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("increases the failed hosts count and total when the result has an error", () => {
|
||||
const resultErrorSocketData = {
|
||||
type: "result",
|
||||
data: {
|
||||
...resultSocketData.data,
|
||||
error: "failed",
|
||||
},
|
||||
};
|
||||
|
||||
const state = { campaign };
|
||||
const updatedState = updateCampaignState(resultErrorSocketData)(
|
||||
state,
|
||||
{}
|
||||
);
|
||||
|
||||
expect(updatedState.campaign.hosts_count).toEqual({
|
||||
successful: 0,
|
||||
failed: 1,
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets the queryIsRunning attribute for status socket data", () => {
|
||||
const state = { campaign };
|
||||
const updatedState = updateCampaignState(statusSocketData)(state, {});
|
||||
|
||||
expect(updatedState.queryIsRunning).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import config from "./config";
|
||||
|
||||
export default config.reducer;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { combineReducers } from "redux";
|
||||
|
||||
import campaigns from "./campaigns/reducer";
|
||||
import hosts from "./hosts/reducer";
|
||||
import invites from "./invites/reducer";
|
||||
import labels from "./labels/reducer";
|
||||
@@ -13,7 +12,6 @@ import users from "./users/reducer";
|
||||
import teams from "./teams/reducer";
|
||||
|
||||
export default combineReducers({
|
||||
campaigns,
|
||||
hosts,
|
||||
invites,
|
||||
labels,
|
||||
|
||||
+95
-98
@@ -5,6 +5,7 @@ import {
|
||||
browserHistory,
|
||||
IndexRedirect,
|
||||
IndexRoute,
|
||||
InjectedRouter,
|
||||
Route,
|
||||
Router,
|
||||
} from "react-router";
|
||||
@@ -16,22 +17,21 @@ import AdminUserManagementPage from "pages/admin/UserManagementPage";
|
||||
import AdminTeamManagementPage from "pages/admin/TeamManagementPage";
|
||||
import TeamDetailsWrapper from "pages/admin/TeamManagementPage/TeamDetailsWrapper";
|
||||
import App from "components/App";
|
||||
import AccessRoutes from "components/AccessRoutes";
|
||||
import AuthenticatedAdminRoutes from "components/AuthenticatedAdminRoutes";
|
||||
import AuthAnyAdminRoutes from "components/AuthAnyAdminRoutes";
|
||||
import AuthenticatedRoutes from "components/AuthenticatedRoutes";
|
||||
import AuthGlobalAdminMaintainerRoutes from "components/AuthGlobalAdminMaintainerRoutes";
|
||||
import AuthAnyMaintainerAnyAdminRoutes from "components/AuthAnyMaintainerAnyAdminRoutes";
|
||||
import PremiumTierRoutes from "components/PremiumTierRoutes";
|
||||
import ConfirmInvitePage from "pages/ConfirmInvitePage";
|
||||
import ConfirmSSOInvitePage from "pages/ConfirmSSOInvitePage";
|
||||
import CoreLayout from "layouts/CoreLayout";
|
||||
import DeviceUserPage from "pages/hosts/details/DeviceUserPage";
|
||||
import EditPackPage from "pages/packs/EditPackPage";
|
||||
import EmailTokenRedirect from "components/EmailTokenRedirect";
|
||||
import ForgotPasswordPage from "pages/ForgotPasswordPage";
|
||||
import HostDetailsPage from "pages/hosts/details/HostDetailsPage";
|
||||
import Homepage from "pages/Homepage";
|
||||
import LoginRoutes from "components/LoginRoutes";
|
||||
import LoginPage, { LoginPreviewPage } from "pages/LoginPage";
|
||||
import LogoutPage from "pages/LogoutPage";
|
||||
import ManageHostsPage from "pages/hosts/ManageHostsPage";
|
||||
import ManageSoftwarePage from "pages/software/ManageSoftwarePage";
|
||||
@@ -45,6 +45,7 @@ import PoliciesPageWrapper from "components/policies/PoliciesPageWrapper";
|
||||
import PolicyPage from "pages/policies/PolicyPage";
|
||||
import QueryPage from "pages/queries/QueryPage";
|
||||
import RegistrationPage from "pages/RegistrationPage";
|
||||
import ResetPasswordPage from "pages/ResetPasswordPage";
|
||||
import SchedulePageWrapper from "components/schedule/SchedulePageWrapper";
|
||||
import SoftwarePageWrapper from "components/software/SoftwarePageWrapper";
|
||||
import ApiOnlyUser from "pages/ApiOnlyUser";
|
||||
@@ -57,17 +58,21 @@ import AgentOptionsPage from "pages/admin/TeamManagementPage/TeamDetailsWrapper/
|
||||
import PATHS from "router/paths";
|
||||
import store from "redux/store";
|
||||
import AppProvider from "context/app";
|
||||
import RoutingProvider from "context/routing";
|
||||
|
||||
interface IAppWrapperProps {
|
||||
children: JSX.Element;
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const history = syncHistoryWithStore(browserHistory, store);
|
||||
|
||||
// App.tsx needs the context for user and config
|
||||
const AppWrapper = ({ children }: IAppWrapperProps) => (
|
||||
const AppWrapper = ({ children, router }: IAppWrapperProps) => (
|
||||
<AppProvider>
|
||||
<App>{children}</App>
|
||||
<RoutingProvider>
|
||||
<App router={router}>{children}</App>
|
||||
</RoutingProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
@@ -76,106 +81,98 @@ const routes = (
|
||||
<Router history={history}>
|
||||
<Route path={PATHS.ROOT} component={AppWrapper}>
|
||||
<Route path="setup" component={RegistrationPage} />
|
||||
<Route path="previewlogin" component={LoginRoutes} />
|
||||
<Route path="login" component={LoginRoutes}>
|
||||
<Route path="invites/:invite_token" component={ConfirmInvitePage} />
|
||||
<Route
|
||||
path="ssoinvites/:invite_token"
|
||||
component={ConfirmSSOInvitePage}
|
||||
/>
|
||||
<Route path="forgot" />
|
||||
<Route path="reset" />
|
||||
</Route>
|
||||
<Route path="previewlogin" component={LoginPreviewPage} />
|
||||
<Route path="login" component={LoginPage} />
|
||||
<Route
|
||||
path="login/invites/:invite_token"
|
||||
component={ConfirmInvitePage}
|
||||
/>
|
||||
<Route
|
||||
path="login/ssoinvites/:invite_token"
|
||||
component={ConfirmSSOInvitePage}
|
||||
/>
|
||||
<Route path="login/forgot" component={ForgotPasswordPage} />
|
||||
<Route path="login/reset" component={ResetPasswordPage} />
|
||||
<Route component={AuthenticatedRoutes}>
|
||||
<Route path="email/change/:token" component={EmailTokenRedirect} />
|
||||
<Route path="logout" component={LogoutPage} />
|
||||
<Route component={AccessRoutes}>
|
||||
<Route component={CoreLayout}>
|
||||
<IndexRedirect to={"dashboard"} />
|
||||
<Route path="dashboard" component={Homepage} />
|
||||
<Route path="settings" component={AuthAnyAdminRoutes}>
|
||||
<IndexRedirect to={"/dashboard"} />
|
||||
<Route component={SettingsWrapper}>
|
||||
<Route component={AuthenticatedAdminRoutes}>
|
||||
<Route
|
||||
path="organization"
|
||||
component={AdminAppSettingsPage}
|
||||
/>
|
||||
<Route path="users" component={AdminUserManagementPage} />
|
||||
<Route component={PremiumTierRoutes}>
|
||||
<Route path="teams" component={AdminTeamManagementPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="teams/:team_id" component={TeamDetailsWrapper}>
|
||||
<Route path="members" component={MembersPage} />
|
||||
<Route path="options" component={AgentOptionsPage} />
|
||||
<Route component={CoreLayout}>
|
||||
<IndexRedirect to={"dashboard"} />
|
||||
<Route path="dashboard" component={Homepage} />
|
||||
<Route path="settings" component={AuthAnyAdminRoutes}>
|
||||
<IndexRedirect to={"/dashboard"} />
|
||||
<Route component={SettingsWrapper}>
|
||||
<Route component={AuthenticatedAdminRoutes}>
|
||||
<Route path="organization" component={AdminAppSettingsPage} />
|
||||
<Route path="users" component={AdminUserManagementPage} />
|
||||
<Route path="teams" component={AdminTeamManagementPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="hosts">
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManageHostsPage} />
|
||||
<Route
|
||||
path="manage/labels/:label_id"
|
||||
component={ManageHostsPage}
|
||||
/>
|
||||
<Route
|
||||
path="manage/:active_label"
|
||||
component={ManageHostsPage}
|
||||
/>
|
||||
<Route
|
||||
path="manage/labels/:label_id/:active_label"
|
||||
component={ManageHostsPage}
|
||||
/>
|
||||
<Route
|
||||
path="manage/:active_label/labels/:label_id"
|
||||
component={ManageHostsPage}
|
||||
/>
|
||||
<Route path=":host_id" component={HostDetailsPage} />
|
||||
<Route path="teams/:team_id" component={TeamDetailsWrapper}>
|
||||
<Route path="members" component={MembersPage} />
|
||||
<Route path="options" component={AgentOptionsPage} />
|
||||
</Route>
|
||||
<Route path="software" component={SoftwarePageWrapper}>
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManageSoftwarePage} />
|
||||
</Route>
|
||||
<Route component={AuthGlobalAdminMaintainerRoutes}>
|
||||
<Route path="packs" component={PackPageWrapper}>
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManagePacksPage} />
|
||||
<Route path="new" component={PackComposerPage} />
|
||||
<Route path=":id">
|
||||
<IndexRoute component={EditPackPage} />
|
||||
<Route path="edit" component={EditPackPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="schedule" component={SchedulePageWrapper}>
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManageSchedulePage} />
|
||||
<Route
|
||||
path="manage/teams/:team_id"
|
||||
component={ManageSchedulePage}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="queries">
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManageQueriesPage} />
|
||||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="new" component={QueryPage} />
|
||||
</Route>
|
||||
<Route path=":id" component={QueryPage} />
|
||||
</Route>
|
||||
<Route path="policies" component={PoliciesPageWrapper}>
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManagePoliciesPage} />
|
||||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="new" component={PolicyPage} />
|
||||
</Route>
|
||||
<Route path=":id" component={PolicyPage} />
|
||||
</Route>
|
||||
<Route path="profile" component={UserSettingsPage} />
|
||||
</Route>
|
||||
<Route path="hosts">
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManageHostsPage} />
|
||||
<Route
|
||||
path="manage/labels/:label_id"
|
||||
component={ManageHostsPage}
|
||||
/>
|
||||
<Route path="manage/:active_label" component={ManageHostsPage} />
|
||||
<Route
|
||||
path="manage/labels/:label_id/:active_label"
|
||||
component={ManageHostsPage}
|
||||
/>
|
||||
<Route
|
||||
path="manage/:active_label/labels/:label_id"
|
||||
component={ManageHostsPage}
|
||||
/>
|
||||
<Route path=":host_id" component={HostDetailsPage} />
|
||||
</Route>
|
||||
<Route path="software" component={SoftwarePageWrapper}>
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManageSoftwarePage} />
|
||||
</Route>
|
||||
<Route component={AuthGlobalAdminMaintainerRoutes}>
|
||||
<Route path="packs" component={PackPageWrapper}>
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManagePacksPage} />
|
||||
<Route path="new" component={PackComposerPage} />
|
||||
<Route path=":id">
|
||||
<IndexRoute component={EditPackPage} />
|
||||
<Route path="edit" component={EditPackPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="schedule" component={SchedulePageWrapper}>
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManageSchedulePage} />
|
||||
<Route
|
||||
path="manage/teams/:team_id"
|
||||
component={ManageSchedulePage}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="queries">
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManageQueriesPage} />
|
||||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="new" component={QueryPage} />
|
||||
</Route>
|
||||
<Route path=":id" component={QueryPage} />
|
||||
</Route>
|
||||
<Route path="policies" component={PoliciesPageWrapper}>
|
||||
<IndexRedirect to={"manage"} />
|
||||
<Route path="manage" component={ManagePoliciesPage} />
|
||||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="new" component={PolicyPage} />
|
||||
</Route>
|
||||
<Route path=":id" component={PolicyPage} />
|
||||
</Route>
|
||||
<Route path="profile" component={UserSettingsPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="/device/:device_auth_token" component={DeviceUserPage} />
|
||||
|
||||
@@ -27,7 +27,6 @@ export default {
|
||||
FORGOT_PASSWORD: `${URL_PREFIX}/login/forgot`,
|
||||
API_ONLY_USER: `${URL_PREFIX}/apionlyuser`,
|
||||
FLEET_403: `${URL_PREFIX}/403`,
|
||||
// FLEET_500: `${URL_PREFIX}/500`,
|
||||
LOGIN: `${URL_PREFIX}/login`,
|
||||
LOGOUT: `${URL_PREFIX}/logout`,
|
||||
MANAGE_HOSTS: `${URL_PREFIX}/hosts/manage`,
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
import sendRequest from "services";
|
||||
import endpoints from "fleet/endpoints";
|
||||
import { IConfigNested } from "interfaces/config";
|
||||
|
||||
// TODO: add other methods from "fleet/entities/config"
|
||||
import { IConfig } from "interfaces/config";
|
||||
|
||||
export default {
|
||||
loadAll: (): Promise<IConfigNested> => {
|
||||
loadAll: (): Promise<IConfig> => {
|
||||
const { CONFIG } = endpoints;
|
||||
const path = `${CONFIG}`;
|
||||
|
||||
@@ -31,6 +29,11 @@ export default {
|
||||
return Promise.resolve(decodedCertificate);
|
||||
});
|
||||
},
|
||||
loadEnrollSecret: () => {
|
||||
const { GLOBAL_ENROLL_SECRETS } = endpoints;
|
||||
|
||||
return sendRequest("GET", GLOBAL_ENROLL_SECRETS);
|
||||
},
|
||||
update: (formData: any) => {
|
||||
const { CONFIG } = endpoints;
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest from "services";
|
||||
import endpoints from "fleet/endpoints";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
export default {
|
||||
// Unneeded for teams, but might need this for global
|
||||
loadAll: () => {
|
||||
const { OSQUERY_OPTIONS } = endpoints;
|
||||
|
||||
return sendRequest("GET", OSQUERY_OPTIONS);
|
||||
},
|
||||
update: (osqueryOptionsData: any, endpoint: string) => {
|
||||
const yamlOptions = yaml.load(osqueryOptionsData.osquery_options);
|
||||
|
||||
return sendRequest("POST", endpoint, yamlOptions);
|
||||
},
|
||||
};
|
||||
@@ -27,6 +27,11 @@ interface IForgotPassword {
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface IUpdatePassword {
|
||||
new_password: string;
|
||||
old_password: string;
|
||||
}
|
||||
|
||||
interface IRequirePasswordReset {
|
||||
require: boolean;
|
||||
}
|
||||
@@ -37,6 +42,11 @@ export interface IGetMeResponse {
|
||||
}
|
||||
|
||||
export default {
|
||||
changePassword: (passwordParams: IUpdatePassword) => {
|
||||
const { CHANGE_PASSWORD } = endpoints;
|
||||
|
||||
return sendRequest("POST", CHANGE_PASSWORD, passwordParams);
|
||||
},
|
||||
confirmEmailChange: (currentUser: IUser, token: string) => {
|
||||
const { CONFIRM_EMAIL_CHANGE } = endpoints;
|
||||
|
||||
@@ -70,12 +80,18 @@ export default {
|
||||
|
||||
return sendRequest("DELETE", path);
|
||||
},
|
||||
enable: (user: IUser, enabled: boolean) => {
|
||||
const { ENABLE_USER } = endpoints;
|
||||
|
||||
return sendRequest("POST", ENABLE_USER(user.id), {
|
||||
enabled,
|
||||
}).then((response) => helpers.addGravatarUrlToResource(response.user));
|
||||
},
|
||||
forgotPassword: ({ email }: IForgotPassword) => {
|
||||
const { FORGOT_PASSWORD } = endpoints;
|
||||
|
||||
return sendRequest("POST", FORGOT_PASSWORD, { email });
|
||||
},
|
||||
// TODO: changePassword (UserSettingsPage.jsx refactor)
|
||||
loadAll: ({
|
||||
page = 0,
|
||||
perPage = 100,
|
||||
@@ -126,6 +142,13 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
performRequiredPasswordReset: (new_password: string) => {
|
||||
const { PERFORM_REQUIRED_PASSWORD_RESET } = endpoints;
|
||||
|
||||
return sendRequest("POST", PERFORM_REQUIRED_PASSWORD_RESET, {
|
||||
new_password,
|
||||
}).then((response) => helpers.addGravatarUrlToResource(response.user));
|
||||
},
|
||||
requirePasswordReset: (
|
||||
userId: number,
|
||||
{ require }: IRequirePasswordReset
|
||||
@@ -137,6 +160,17 @@ export default {
|
||||
helpers.addGravatarUrlToResource(response.user)
|
||||
);
|
||||
},
|
||||
resetPassword: (formData: any) => {
|
||||
const { RESET_PASSWORD } = endpoints;
|
||||
|
||||
return sendRequest("POST", RESET_PASSWORD, formData);
|
||||
},
|
||||
setup: (formData: any) => {
|
||||
const { SETUP } = endpoints;
|
||||
const setupData = helpers.setupData(formData);
|
||||
|
||||
return sendRequest("POST", SETUP, setupData);
|
||||
},
|
||||
update: (userId: number, formData: IUpdateUserFormData) => {
|
||||
const { USERS } = endpoints;
|
||||
const path = `${USERS}/${userId}`;
|
||||
@@ -145,4 +179,13 @@ export default {
|
||||
helpers.addGravatarUrlToResource(response.user)
|
||||
);
|
||||
},
|
||||
updateAdmin: (user: IUser, admin: boolean) => {
|
||||
const { UPDATE_USER_ADMIN } = endpoints;
|
||||
|
||||
return sendRequest(
|
||||
"POST",
|
||||
UPDATE_USER_ADMIN(user.id),
|
||||
admin
|
||||
).then((response) => helpers.addGravatarUrlToResource(response.user));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest from "services";
|
||||
import endpoints from "fleet/endpoints";
|
||||
|
||||
export default {
|
||||
load: () => {
|
||||
const { VERSION } = endpoints;
|
||||
|
||||
return sendRequest("GET", VERSION);
|
||||
},
|
||||
};
|
||||
+6
-10
@@ -1,14 +1,10 @@
|
||||
export const destroyFunc = (campaign) => {
|
||||
return Promise.resolve(campaign);
|
||||
};
|
||||
|
||||
const updateCampaignStateFromTotals = (campaign, { data }) => {
|
||||
const updateCampaignStateFromTotals = (campaign: any, { data }: any) => {
|
||||
return {
|
||||
campaign: { ...campaign, totals: data },
|
||||
};
|
||||
};
|
||||
|
||||
const updateCampaignStateFromResults = (campaign, { data }) => {
|
||||
const updateCampaignStateFromResults = (campaign: any, { data }: any) => {
|
||||
const queryResults = campaign.query_results || [];
|
||||
const errors = campaign.errors || [];
|
||||
const hosts = campaign.hosts || [];
|
||||
@@ -82,7 +78,7 @@ const updateCampaignStateFromResults = (campaign, { data }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const updateCampaignStateFromStatus = (campaign, { data }) => {
|
||||
const updateCampaignStateFromStatus = (campaign: any, { data }: any) => {
|
||||
const { status } = data;
|
||||
const updatedCampaign = { ...campaign, status };
|
||||
|
||||
@@ -92,8 +88,8 @@ const updateCampaignStateFromStatus = (campaign, { data }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const updateCampaignState = (socketData) => {
|
||||
return (prevState) => {
|
||||
export const updateCampaignState = (socketData: any) => {
|
||||
return (prevState: any) => {
|
||||
const { campaign } = prevState;
|
||||
|
||||
switch (socketData.type) {
|
||||
@@ -109,4 +105,4 @@ export const updateCampaignState = (socketData) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default { destroyFunc, updateCampaignState };
|
||||
export default { updateCampaignState };
|
||||
@@ -0,0 +1,36 @@
|
||||
import { get, join } from "lodash";
|
||||
import { IError } from "interfaces/errors";
|
||||
|
||||
const formatServerErrors = (errors: IError[]) => {
|
||||
if (!errors || !errors.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const result: { [key: string]: string } = {};
|
||||
|
||||
errors.forEach((error) => {
|
||||
const { name, reason } = error;
|
||||
|
||||
if (result[name]) {
|
||||
result[name] = join([result[name], reason], ", ");
|
||||
} else {
|
||||
result[name] = reason;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const formatErrorResponse = (errorResponse: any) => {
|
||||
const errors =
|
||||
get(errorResponse, "message.errors") ||
|
||||
get(errorResponse, "data.errors") ||
|
||||
[];
|
||||
|
||||
return {
|
||||
...formatServerErrors(errors),
|
||||
http_status: errorResponse.status,
|
||||
} as any;
|
||||
};
|
||||
|
||||
export default formatErrorResponse;
|
||||
@@ -2,11 +2,11 @@ import { IUser } from "interfaces/user";
|
||||
import { IConfig } from "interfaces/config";
|
||||
|
||||
export const isFreeTier = (config: IConfig): boolean => {
|
||||
return config.tier === "free";
|
||||
return config.license.tier === "free";
|
||||
};
|
||||
|
||||
export const isPremiumTier = (config: IConfig): boolean => {
|
||||
return config.tier === "premium";
|
||||
return config.license.tier === "premium";
|
||||
};
|
||||
|
||||
export const isGlobalAdmin = (user: IUser): boolean => {
|
||||
|
||||
Reference in New Issue
Block a user