Răsfoiți Sursa

implemented first version of unauthorized popup

jnfrati 3 ani în urmă
părinte
comite
0f7f63acdd

+ 14 - 14
dashboard/src/App.tsx

@@ -5,20 +5,20 @@ import styled, { createGlobalStyle } from "styled-components";
 
 
 import MainWrapper from "./main/MainWrapper";
 import MainWrapper from "./main/MainWrapper";
 
 
-export default class App extends Component {
-  render() {
-    return (
-      <StyledMain>
-        <GlobalStyle />
-        <PorterErrorBoundary errorBoundaryLocation="globalErrorBoundary">
-          <BrowserRouter>
-            <MainWrapper />
-          </BrowserRouter>
-        </PorterErrorBoundary>
-      </StyledMain>
-    );
-  }
-}
+const App = () => {
+  return (
+    <StyledMain>
+      <GlobalStyle />
+      <PorterErrorBoundary errorBoundaryLocation="globalErrorBoundary">
+        <BrowserRouter>
+          <MainWrapper />
+        </BrowserRouter>
+      </PorterErrorBoundary>
+    </StyledMain>
+  );
+};
+
+export default App;
 
 
 const GlobalStyle = createGlobalStyle`
 const GlobalStyle = createGlobalStyle`
   * {
   * {

+ 16 - 18
dashboard/src/main/MainWrapper.tsx

@@ -2,27 +2,25 @@ import React, { Component } from "react";
 
 
 import { ContextProvider } from "../shared/Context";
 import { ContextProvider } from "../shared/Context";
 import Main from "./Main";
 import Main from "./Main";
-import { RouteComponentProps, withRouter } from "react-router";
+import { useHistory, useLocation, withRouter } from "react-router";
 import AuthProvider from "shared/auth/AuthContext";
 import AuthProvider from "shared/auth/AuthContext";
 import MainWrapperErrorBoundary from "shared/error_handling/MainWrapperErrorBoundary";
 import MainWrapperErrorBoundary from "shared/error_handling/MainWrapperErrorBoundary";
+import { UnauthorizedPopup } from "shared/auth/UnauthorizedPopup";
 
 
-type PropsType = RouteComponentProps & {};
+const MainWrapper = () => {
+  const location = useLocation();
+  const history = useHistory();
 
 
-type StateType = {};
-
-class MainWrapper extends Component<PropsType, StateType> {
-  render() {
-    let { history, location } = this.props;
-    return (
-      <ContextProvider history={history} location={location}>
-        <AuthProvider>
-          <MainWrapperErrorBoundary>
-            <Main />
-          </MainWrapperErrorBoundary>
-        </AuthProvider>
-      </ContextProvider>
-    );
-  }
-}
+  return (
+    <ContextProvider history={history} location={location}>
+      <AuthProvider>
+        <MainWrapperErrorBoundary>
+          <Main />
+        </MainWrapperErrorBoundary>
+      </AuthProvider>
+      <UnauthorizedPopup />
+    </ContextProvider>
+  );
+};
 
 
 export default withRouter(MainWrapper);
 export default withRouter(MainWrapper);

+ 31 - 16
dashboard/src/shared/api.tsx

@@ -20,7 +20,9 @@ import {
  * @param {(err: Object, res: Object) => void} callback - Callback function.
  * @param {(err: Object, res: Object) => void} callback - Callback function.
  */
  */
 
 
-const checkAuth = baseApi("GET", "/api/users/current");
+const checkAuth = baseApi("GET", "/api/users/current", {
+  disableUnauthorizedPopup: true,
+});
 
 
 const connectECRRegistry = baseApi<
 const connectECRRegistry = baseApi<
   {
   {
@@ -542,9 +544,11 @@ const detectBuildpack = baseApi<
     branch: string;
     branch: string;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
 });
 });
 
 
 const detectGitlabBuildpack = baseApi<
 const detectGitlabBuildpack = baseApi<
@@ -575,9 +579,11 @@ const getBranchContents = baseApi<
     branch: string;
     branch: string;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/contents`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/contents`;
 });
 });
 
 
 const getProcfileContents = baseApi<
 const getProcfileContents = baseApi<
@@ -593,9 +599,11 @@ const getProcfileContents = baseApi<
     branch: string;
     branch: string;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/procfile`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/procfile`;
 });
 });
 
 
 const getGitlabProcfileContents = baseApi<
 const getGitlabProcfileContents = baseApi<
@@ -1348,9 +1356,11 @@ const getEnvGroup = baseApi<
     version?: number;
     version?: number;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id
-    }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : ""
-    }`;
+  return `/api/projects/${pathParams.id}/clusters/${
+    pathParams.cluster_id
+  }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${
+    pathParams.version ? "&version=" + pathParams.version : ""
+  }`;
 });
 });
 
 
 const getConfigMap = baseApi<
 const getConfigMap = baseApi<
@@ -1567,7 +1577,11 @@ const updateCollaborator = baseApi<
     project_id: number;
     project_id: number;
     user_id: number;
     user_id: number;
   }
   }
->("PATCH", ({ project_id, user_id }) => `/api/projects/${project_id}/collaborators/${user_id}`);
+>(
+  "PATCH",
+  ({ project_id, user_id }) =>
+    `/api/projects/${project_id}/collaborators/${user_id}`
+);
 
 
 const removeCollaborator = baseApi<
 const removeCollaborator = baseApi<
   {},
   {},
@@ -1577,7 +1591,8 @@ const removeCollaborator = baseApi<
   }
   }
 >(
 >(
   "DELETE",
   "DELETE",
-  ({ project_id, user_id }) => `/api/projects/${project_id}/collaborators/${user_id}`
+  ({ project_id, user_id }) =>
+    `/api/projects/${project_id}/collaborators/${user_id}`
 );
 );
 
 
 const getPolicyDocument = baseApi<{}, { project_id: number }>(
 const getPolicyDocument = baseApi<{}, { project_id: number }>(
@@ -2167,7 +2182,7 @@ const removeStackEnvGroup = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
 );
 );
 
 
-const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`);
+const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
 
 
 // ROLES
 // ROLES
 
 

+ 84 - 0
dashboard/src/shared/auth/UnauthorizedPopup.tsx

@@ -0,0 +1,84 @@
+import { Collapse, Snackbar } from "@material-ui/core";
+import { Alert } from "@material-ui/lab";
+import React from "react";
+import styled from "styled-components";
+import { proxy, useSnapshot } from "valtio";
+
+const state = proxy({
+  showUnauthorizedPopup: false,
+  message: "",
+  expanded: false,
+});
+
+const actions = {
+  showUnauthorizedPopup: (message: string) => {
+    state.showUnauthorizedPopup = true;
+    state.message = message;
+  },
+  hideUnauthorizedPopup: () => {
+    state.showUnauthorizedPopup = false;
+    state.message = "";
+  },
+  toggleExpanded: () => {
+    state.expanded = !state.expanded;
+  },
+};
+
+export const UnauthorizedPopupActions = actions;
+
+export const UnauthorizedPopup = () => {
+  const { showUnauthorizedPopup, message, expanded } = useSnapshot(state);
+
+  if (expanded) {
+    // TODO: Implement expanded view. Should be a modal with the full message and contact information.
+    return null;
+  }
+
+  if (!showUnauthorizedPopup) {
+    return null;
+  }
+
+  return (
+    <>
+      <Collapse in={showUnauthorizedPopup}>
+        <Snackbar open={true} onClose={actions.hideUnauthorizedPopup}>
+          <Alert
+            onClose={actions.hideUnauthorizedPopup}
+            severity="warning"
+            action={
+              <>
+                <ActionButton onClick={actions.toggleExpanded}>
+                  <i className="material-icons">open_in_new</i>
+                </ActionButton>
+                <ActionButton onClick={actions.hideUnauthorizedPopup}>
+                  <i className="material-icons">close</i>
+                </ActionButton>
+              </>
+            }
+          >
+            {message}
+          </Alert>
+        </Snackbar>
+      </Collapse>
+    </>
+  );
+};
+
+const ActionButton = styled.button`
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  padding: 0;
+  margin: 0;
+  margin-left: 10px;
+  :hover {
+    background: #ffffff22;
+  }
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 18px;
+  }
+`;

+ 48 - 7
dashboard/src/shared/baseApi.ts

@@ -1,5 +1,11 @@
-import axios, { AxiosPromise, AxiosRequestConfig, Method } from "axios";
+import axios, {
+  AxiosError,
+  AxiosPromise,
+  AxiosRequestConfig,
+  Method,
+} from "axios";
 import qs from "qs";
 import qs from "qs";
+import { UnauthorizedPopupActions } from "./auth/UnauthorizedPopup";
 
 
 type EndpointParam<PathParamsType> =
 type EndpointParam<PathParamsType> =
   | string
   | string
@@ -68,17 +74,52 @@ const buildAxiosConfig: BuildAxiosConfigFunction = (
   return config;
   return config;
 };
 };
 
 
+type Options = {
+  disableUnauthorizedPopup?: boolean;
+};
+
 const apiQueryBuilder = <ParamsType extends {}, PathParamsType = {}>(
 const apiQueryBuilder = <ParamsType extends {}, PathParamsType = {}>(
   method: Method = "GET",
   method: Method = "GET",
-  endpoint: EndpointParam<PathParamsType>
-) => <ResponseType = any>(
+  endpoint: EndpointParam<PathParamsType>,
+  options?: Options
+) => async <ResponseType = any>(
   token: string,
   token: string,
   params: ParamsType,
   params: ParamsType,
   pathParams: PathParamsType
   pathParams: PathParamsType
-) =>
-  axios(
-    buildAxiosConfig(method, endpoint, token, params, pathParams)
-  ) as AxiosPromise<ResponseType>;
+) => {
+  try {
+    return axios(
+      buildAxiosConfig(method, endpoint, token, params, pathParams)
+    ) as AxiosPromise<ResponseType>;
+  } catch (error) {
+    const axiosError = error as AxiosError;
+
+    if (options?.disableUnauthorizedPopup) {
+      throw axiosError;
+    }
+
+    /**
+     * Made concatenated if/else-if to avoid throwing the error if its 401 or 403
+     *
+     * Base idea here is to have a single place where we handle all the unauthorized errors.
+     * If the error corresponds to 401 or 403, we show the unauthorized popup. Otherwise, we throw the error.
+     *
+     * This way, we avoid having to handle the error in every single place where we use the apiQueryBuilder
+     * and the components can just handle the error they want.
+     */
+    if (axiosError.response?.status === 401) {
+      UnauthorizedPopupActions.showUnauthorizedPopup(
+        "Your session has expired. Please log in again."
+      );
+    } else if (axiosError.response?.status === 403) {
+      UnauthorizedPopupActions.showUnauthorizedPopup(
+        "You are not authorized to perform this action."
+      );
+    } else {
+      throw error;
+    }
+  }
+};
 
 
 export { apiQueryBuilder as baseApi };
 export { apiQueryBuilder as baseApi };
 export default apiQueryBuilder;
 export default apiQueryBuilder;