d-g-town 2 سال پیش
والد
کامیت
dbd715a372

+ 0 - 1
api/server/router/addons.go

@@ -54,6 +54,5 @@ func getAddonRoutes(
 
 	var routes []*router.Route
 
-
 	return routes, newPath
 }

+ 1 - 6
dashboard/.eslintrc.json

@@ -33,12 +33,7 @@
     ],
     "@typesecript-eslint/consistent-type-imports": "off",
     "@typescript-eslint/strict-boolean-expressions": "off",
-    "@typescript-eslint/no-misused-promises": [
-      "error",
-      {
-        "checksVoidReturn": false
-      }
-    ],
+    "@typescript-eslint/no-misused-promises": "error",
     "@typescript-eslint/no-floating-promises": [
       "error",
       {

+ 36 - 215
dashboard/src/main/Main.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import { Redirect, Route, Switch } from "react-router-dom";
 
 import Loading from "components/Loading";
@@ -7,249 +7,72 @@ import api from "shared/api";
 import { Context } from "shared/Context";
 import { PorterUrls, type PorterUrl } from "shared/routing";
 
-import Login from "./auth/Login";
-import Register from "./auth/Register";
-import ResetPasswordFinalize from "./auth/ResetPasswordFinalize";
-import ResetPasswordInit from "./auth/ResetPasswordInit";
-import SetInfo from "./auth/SetInfo";
-import VerifyEmail from "./auth/VerifyEmail";
+import { useAuthn } from "../shared/auth/AuthnContext";
 import CurrentError from "./CurrentError";
 import Home from "./home/Home";
 
 type PropsType = {};
 
-type StateType = {
-  loading: boolean;
-  isLoggedIn: boolean;
-  isEmailVerified: boolean;
-  hasInfo: boolean;
-  initialized: boolean;
-  local: boolean;
-  userId: number;
-  version: string;
-};
-
-export default class Main extends Component<PropsType, StateType> {
-  state = {
-    loading: true,
-    isLoggedIn: false,
-    isEmailVerified: false,
-    hasInfo: false,
-    initialized: localStorage.getItem("init") === "true",
-    local: false,
-    userId: null as number,
-    version: null as string,
-  };
-
-  componentDidMount() {
+const Main: React.FC<PropsType> = () => {
+  const {
+    currentError,
+    setCurrentError,
+    setEdition,
+    setEnableGitlab,
+    currentProject,
+    currentCluster,
+  } = useContext(Context);
+  const { handleLogOut } = useAuthn();
+  const [version, setVersion] = useState("");
+
+  useEffect(() => {
     // Get capabilities to case on user info requirements
     api
       .getMetadata("", {}, {})
       .then((res) => {
-        this.setState({
-          version: res.data?.version,
-        });
+        setVersion(res.data?.version);
       })
-      .catch((err) => {
-        console.log(err);
-      });
+      .catch(() => {});
 
-    const { setUser, setCurrentError } = this.context;
     const urlParams = new URLSearchParams(window.location.search);
     const error = urlParams.get("error");
     error && setCurrentError(error);
-    api
-      .checkAuth("", {}, {})
-      .then((res) => {
-        if (res?.data) {
-          setUser(res.data.id, res.data.email);
-          this.setState({
-            isLoggedIn: true,
-            isEmailVerified: res.data.email_verified,
-            initialized: true,
-            hasInfo: res.data.company_name && true,
-            loading: false,
-            userId: res.data.id,
-          });
-        } else {
-          this.setState({ isLoggedIn: false, loading: false });
-        }
-      })
-      .catch((err) => {
-        this.setState({ isLoggedIn: false, loading: false });
-      });
 
     api
       .getMetadata("", {}, {})
       .then((res) => {
-        this.context.setEdition(res.data?.version);
-        this.setState({ local: !res.data?.provisioner });
-        this.context.setEnableGitlab(!!res.data?.gitlab);
+        setEdition(res.data?.version);
+        setEnableGitlab(!!res.data?.gitlab);
       })
-      .catch((err) => {
-        console.log(err);
-      });
-  }
+      .catch(() => {});
+  }, []);
 
-  initialize = () => {
-    this.setState({ isLoggedIn: true, initialized: true });
-    localStorage.setItem("init", "true");
-  };
-
-  authenticate = () => {
-    api
-      .checkAuth("", {}, {})
-      .then((res) => {
-        if (res?.data) {
-          this.context.setUser(res?.data?.id, res?.data?.email);
-          this.setState({
-            isLoggedIn: true,
-            isEmailVerified: res?.data?.email_verified,
-            initialized: true,
-            hasInfo: res.data.company_name && true,
-            loading: false,
-            userId: res.data.id,
-          });
-        } else {
-          this.setState({ isLoggedIn: false, loading: false });
-        }
-      })
-      .catch((err) => {
-        this.setState({ isLoggedIn: false, loading: false });
-      });
-  };
-
-  handleLogOut = () => {
-    // Clears local storage for proper rendering of clusters
-    // Attempt user logout
-    api
-      .logOutUser("<token>", {}, {})
-      .then(() => {
-        this.context.clearContext();
-        this.setState({ isLoggedIn: false, initialized: true });
-        localStorage.clear();
-      })
-      .catch((err) =>
-        this.context.setCurrentError(err.response?.data.errors[0])
-      );
-  };
-
-  renderMain = () => {
-    if (this.state.loading || !this.state.version) {
+  const renderMain = (): JSX.Element => {
+    if (!version) {
       return <Loading />;
     }
 
-    // if logged in but not verified, block until email verification
-    if (
-      !this.state.local &&
-      this.state.isLoggedIn &&
-      !this.state.isEmailVerified
-    ) {
-      return (
-        <Switch>
-          <Route
-            path="/"
-            render={() => {
-              return <VerifyEmail handleLogOut={this.handleLogOut} />;
-            }}
-          />
-        </Switch>
-      );
-    }
-
-    // Handle case where new user signs up via OAuth and has not set name and company
-    if (
-      this.state.version === "production" &&
-      !this.state.hasInfo &&
-      this.state.userId > 9312 &&
-      this.state.isLoggedIn
-    ) {
-      return (
-        <Switch>
-          <Route
-            path="/"
-            render={() => {
-              return (
-                <SetInfo
-                  handleLogOut={this.handleLogOut}
-                  authenticate={this.authenticate}
-                />
-              );
-            }}
-          />
-        </Switch>
-      );
-    }
-
     return (
       <Switch>
-        <Route
-          path="/login"
-          render={() => {
-            if (!this.state.isLoggedIn) {
-              return <Login authenticate={this.authenticate} />;
-            } else {
-              return <Redirect to="/" />;
-            }
-          }}
-        />
-        <Route
-          path="/register"
-          render={() => {
-            if (!this.state.isLoggedIn) {
-              return <Register authenticate={this.authenticate} />;
-            } else {
-              return <Redirect to="/" />;
-            }
-          }}
-        />
-        <Route
-          path="/password/reset/finalize"
-          render={() => {
-            if (!this.state.isLoggedIn) {
-              return <ResetPasswordFinalize />;
-            } else {
-              return <Redirect to="/" />;
-            }
-          }}
-        />
-        <Route
-          path="/password/reset"
-          render={() => {
-            if (!this.state.isLoggedIn) {
-              return <ResetPasswordInit />;
-            } else {
-              return <Redirect to="/" />;
-            }
-          }}
-        />
         <Route
           exact
           path="/"
           render={() => {
-            if (this.state.isLoggedIn) {
-              return <Redirect to="/dashboard" />;
-            } else {
-              return <Redirect to="/login" />;
-            }
+            return <Redirect to="/dashboard" />;
           }}
         />
         <Route
           path={`/:baseRoute/:cluster?/:namespace?`}
           render={(routeProps) => {
             const baseRoute = routeProps.match.params.baseRoute;
-            if (
-              this.state.isLoggedIn &&
-              this.state.initialized &&
-              PorterUrls.includes(baseRoute)
-            ) {
+            if (PorterUrls.includes(baseRoute)) {
               return (
                 <Home
                   key="home"
-                  currentProject={this.context.currentProject}
-                  currentCluster={this.context.currentCluster}
+                  currentProject={currentProject}
+                  currentCluster={currentCluster}
                   currentRoute={baseRoute as PorterUrl}
-                  logOut={this.handleLogOut}
+                  logOut={handleLogOut}
                 />
               );
             } else {
@@ -261,14 +84,12 @@ export default class Main extends Component<PropsType, StateType> {
     );
   };
 
-  render() {
-    return (
-      <>
-        {this.renderMain()}
-        <CurrentError currentError={this.context.currentError} />
-      </>
-    );
-  }
-}
+  return (
+    <>
+      {renderMain()}
+      <CurrentError currentError={currentError} />
+    </>
+  );
+};
 
-Main.contextType = Context;
+export default Main;

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

@@ -1,28 +1,27 @@
-import React, { Component } from "react";
+import React from "react";
+import { withRouter, type RouteComponentProps } from "react-router";
 
+import AuthzProvider from "shared/auth/AuthzContext";
+import MainWrapperErrorBoundary from "shared/error_handling/MainWrapperErrorBoundary";
+
+import AuthnProvider from "../shared/auth/AuthnContext";
 import { ContextProvider } from "../shared/Context";
 import Main from "./Main";
-import { RouteComponentProps, withRouter } from "react-router";
-import AuthProvider from "shared/auth/AuthContext";
-import MainWrapperErrorBoundary from "shared/error_handling/MainWrapperErrorBoundary";
 
 type PropsType = RouteComponentProps & {};
 
-type StateType = {};
-
-class MainWrapper extends Component<PropsType, StateType> {
-  render() {
-    let { history, location } = this.props;
-    return (
-      <ContextProvider history={history} location={location}>
-        <AuthProvider>
+const MainWrapper: React.FC<PropsType> = ({ history, location }) => {
+  return (
+    <ContextProvider history={history} location={location}>
+      <AuthzProvider>
+        <AuthnProvider>
           <MainWrapperErrorBoundary>
             <Main />
           </MainWrapperErrorBoundary>
-        </AuthProvider>
-      </ContextProvider>
-    );
-  }
-}
+        </AuthnProvider>
+      </AuthzProvider>
+    </ContextProvider>
+  );
+};
 
 export default withRouter(MainWrapper);

+ 2 - 2
dashboard/src/main/auth/Login.tsx

@@ -21,7 +21,7 @@ import GoogleIcon from "assets/GoogleIcon";
 import logo from "assets/logo.png";
 
 type Props = {
-  authenticate: () => void;
+  authenticate: () => Promise<void>;
 };
 
 const getWindowDimensions = () => {
@@ -56,7 +56,7 @@ const Login: React.FC<Props> = ({ authenticate }) => {
             window.location.href = res.data.redirect;
           } else {
             setUser(res?.data?.id, res?.data?.email);
-            authenticate();
+            authenticate().catch(() => {});
           }
         })
         .catch((err) => {

+ 2 - 2
dashboard/src/main/auth/Register.tsx

@@ -21,7 +21,7 @@ import logo from "assets/logo.png";
 import InfoPanel from "./InfoPanel";
 
 type Props = {
-  authenticate: () => void;
+  authenticate: () => Promise<void>;
 };
 
 const getWindowDimensions = () => {
@@ -141,7 +141,7 @@ const Register: React.FC<Props> = ({ authenticate }) => {
             window.location.href = res.data.redirect;
           } else {
             setUser(res?.data?.id, res?.data?.email);
-            authenticate();
+            authenticate().catch(() => {});
 
             try {
               window.dataLayer?.push({

+ 2 - 2
dashboard/src/main/auth/SetInfo.tsx

@@ -19,7 +19,7 @@ import logo from "assets/logo.png";
 import InfoPanel from "./InfoPanel";
 
 type Props = {
-  authenticate: () => void;
+  authenticate: () => Promise<void>;
   handleLogOut: () => void;
 };
 
@@ -69,7 +69,7 @@ const SetInfo: React.FC<Props> = ({ authenticate, handleLogOut }) => {
           { id: user.id }
         )
         .then((res: any) => {
-          authenticate();
+          authenticate().catch(() => {});
 
           try {
             window.dataLayer?.push({

+ 186 - 0
dashboard/src/shared/auth/AuthnContext.tsx

@@ -0,0 +1,186 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Redirect, Route, Switch } from "react-router-dom";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+import Loading from "../../components/Loading";
+import Login from "../../main/auth/Login";
+import Register from "../../main/auth/Register";
+import ResetPasswordFinalize from "../../main/auth/ResetPasswordFinalize";
+import ResetPasswordInit from "../../main/auth/ResetPasswordInit";
+import SetInfo from "../../main/auth/SetInfo";
+import VerifyEmail from "../../main/auth/VerifyEmail";
+
+type AuthnState = {
+  userId: number;
+  handleLogOut: () => void;
+};
+
+export const AuthnContext = React.createContext<AuthnState | null>(null);
+
+export const useAuthn = (): AuthnState => {
+  const context = useContext(AuthnContext);
+  if (context == null) {
+    throw new Error("useAuthn must be used within an AuthnContext");
+  }
+  return context;
+};
+
+const AuthnProvider = ({
+  children,
+}: {
+  children: JSX.Element;
+}): JSX.Element => {
+  const { setUser, clearContext, setCurrentError } = useContext(Context);
+  const [isLoggedIn, setIsLoggedIn] = useState(false);
+  const [isEmailVerified, setIsEmailVerified] = useState(false);
+  const [isLoading, setIsLoading] = useState(true);
+  const [userId, setUserId] = useState(-1);
+  const [hasInfo, setHasInfo] = useState(false);
+  const [local, setLocal] = useState(false);
+
+  const authenticate = async (): Promise<void> => {
+    api
+      .checkAuth("", {}, {})
+      .then((res) => {
+        if (res?.data) {
+          setUser?.(res.data.id, res.data.email);
+          setIsLoggedIn(true);
+          setIsEmailVerified(res.data.email_verified);
+          setHasInfo(res.data.company_name && true);
+          setIsLoading(false);
+          setUserId(res.data.id);
+        } else {
+          setIsLoggedIn(false);
+          setIsEmailVerified(false);
+          setHasInfo(false);
+          setIsLoading(false);
+          setUserId(-1);
+        }
+      })
+      .catch(() => {
+        setIsLoggedIn(false);
+        setIsEmailVerified(false);
+        setHasInfo(false);
+        setIsLoading(false);
+        setUserId(-1);
+      });
+  };
+
+  const handleLogOut = (): void => {
+    // Clears local storage for proper rendering of clusters
+    // Attempt user logout
+    api
+      .logOutUser("<token>", {}, {})
+      .then(() => {
+        setIsLoggedIn(false);
+        setIsEmailVerified(false);
+        clearContext?.();
+        localStorage.clear();
+      })
+      .catch((err) => {
+        setCurrentError?.(err.response?.data.errors[0]);
+      });
+  };
+
+  useEffect(() => {
+    authenticate().catch(() => {});
+
+    // check if porter server is local (does not require email verification)
+    api
+      .getMetadata("", {}, {})
+      .then((res) => {
+        setLocal(!res.data?.provisioner);
+      })
+      .catch(() => {});
+  }, []);
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
+  // return unauthenticated routes
+  if (!isLoggedIn) {
+    return (
+      <Switch>
+        <Route
+          path="/login"
+          render={() => {
+            return <Login authenticate={authenticate} />;
+          }}
+        />
+        <Route
+          path="/register"
+          render={() => {
+            return <Register authenticate={authenticate} />;
+          }}
+        />
+        <Route
+          path="/password/reset/finalize"
+          render={() => {
+            return <ResetPasswordFinalize />;
+          }}
+        />
+        <Route
+          path="/password/reset"
+          render={() => {
+            return <ResetPasswordInit />;
+          }}
+        />
+        <Route
+          path="*"
+          render={() => {
+            return <Redirect to="/login" />;
+          }}
+        />
+      </Switch>
+    );
+  }
+
+  // if logged in but not verified, block until email verification
+  if (!local && !isEmailVerified) {
+    return (
+      <Switch>
+        <Route
+          path="/"
+          render={() => {
+            return <VerifyEmail handleLogOut={handleLogOut} />;
+          }}
+        />
+      </Switch>
+    );
+  }
+
+  // Handle case where new user signs up via OAuth and has not set name and company
+  if (!hasInfo && userId > 9312) {
+    return (
+      <Switch>
+        <Route
+          path="/"
+          render={() => {
+            return (
+              <SetInfo
+                handleLogOut={handleLogOut}
+                authenticate={authenticate}
+              />
+            );
+          }}
+        />
+      </Switch>
+    );
+  }
+
+  return (
+    <AuthnContext.Provider
+      value={{
+        userId,
+        handleLogOut,
+      }}
+    >
+      {children}
+    </AuthnContext.Provider>
+  );
+};
+
+export default AuthnProvider;

+ 3 - 3
dashboard/src/shared/auth/AuthorizationHoc.tsx

@@ -1,5 +1,5 @@
 import React, { useCallback, useContext } from "react";
-import { AuthContext } from "./AuthContext";
+import { AuthzContext } from "./AuthzContext";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";
 
@@ -8,7 +8,7 @@ export const GuardedComponent = <ComponentProps extends object>(
   resource: string,
   verb: Verbs | Array<Verbs>
 ) => (Component: any) => (props: ComponentProps) => {
-  const authContext = useContext(AuthContext);
+  const authContext = useContext(AuthzContext);
 
   if (isAuthorized(authContext.currentPolicy, scope, resource, verb)) {
     return <Component {...props} />;
@@ -36,7 +36,7 @@ export function withAuth<P>(
   })`;
 
   const C = (props: P) => {
-    const authContext = useContext(AuthContext);
+    const authContext = useContext(AuthzContext);
 
     const isAuth = useCallback(
       (scope: ScopeType, resource: string, verb: Verbs | Array<Verbs>) =>

+ 15 - 7
dashboard/src/shared/auth/AuthContext.tsx → dashboard/src/shared/auth/AuthzContext.tsx

@@ -1,16 +1,24 @@
 import React, { useContext, useEffect, useState } from "react";
+
 import api from "shared/api";
 import { Context } from "shared/Context";
+
 import { POLICY_HIERARCHY_TREE, populatePolicy } from "./authorization-helpers";
-import { PolicyDocType } from "./types";
+import { type PolicyDocType } from "./types";
 
-type AuthContext = {
+type AuthzContext = {
   currentPolicy: PolicyDocType;
 };
 
-export const AuthContext = React.createContext<AuthContext>({} as AuthContext);
+export const AuthzContext = React.createContext<AuthzContext>(
+  {} as AuthzContext
+);
 
-const AuthProvider: React.FC = ({ children }) => {
+const AuthzProvider = ({
+  children,
+}: {
+  children: JSX.Element;
+}): JSX.Element => {
   const { user, currentProject } = useContext(Context);
   const [currentPolicy, setCurrentPolicy] = useState(null);
 
@@ -42,10 +50,10 @@ const AuthProvider: React.FC = ({ children }) => {
   }, [user, currentProject?.id]);
 
   return (
-    <AuthContext.Provider value={{ currentPolicy }}>
+    <AuthzContext.Provider value={{ currentPolicy }}>
       {children}
-    </AuthContext.Provider>
+    </AuthzContext.Provider>
   );
 };
 
-export default AuthProvider;
+export default AuthzProvider;

+ 28 - 24
dashboard/src/shared/auth/RouteGuard.tsx

@@ -1,16 +1,17 @@
-import UnauthorizedPage from "components/UnauthorizedPage";
 import React, { useContext, useMemo } from "react";
-import { Route, RouteProps } from "react-router";
-import { AuthContext } from "./AuthContext";
-import { isAuthorized } from "./authorization-helpers";
-import { ScopeType, Verbs } from "./types";
+import { Route, type RouteProps } from "react-router";
 
 import Loading from "components/Loading";
+import UnauthorizedPage from "components/UnauthorizedPage";
+
+import { isAuthorized } from "./authorization-helpers";
+import { AuthzContext } from "./AuthzContext";
+import { type ScopeType, type Verbs } from "./types";
 
 type GuardedRouteProps = {
   scope: ScopeType;
   resource: string;
-  verb: Verbs | Array<Verbs>;
+  verb: Verbs | Verbs[];
 };
 
 const GuardedRoute: React.FC<RouteProps & GuardedRouteProps> = ({
@@ -21,7 +22,7 @@ const GuardedRoute: React.FC<RouteProps & GuardedRouteProps> = ({
   children,
   ...rest
 }) => {
-  const { currentPolicy } = useContext(AuthContext);
+  const { currentPolicy } = useContext(AuthzContext);
   const auth = useMemo(() => {
     return isAuthorized(currentPolicy, scope, resource, verb);
   }, [currentPolicy, scope, resource, verb]);
@@ -39,24 +40,27 @@ const GuardedRoute: React.FC<RouteProps & GuardedRouteProps> = ({
   return <Route {...rest} render={render} />;
 };
 
-export const fakeGuardedRoute = <ComponentProps extends object>(
-  scope: string,
-  resource: string,
-  verb: Verbs | Array<Verbs>
-) => (Component: any) => (props: ComponentProps) => {
-  const { currentPolicy } = useContext(AuthContext);
-  const auth = useMemo(() => {
-    return isAuthorized(currentPolicy, scope, resource, verb);
-  }, [currentPolicy, scope, resource, verb]);
+export const fakeGuardedRoute =
+  <ComponentProps extends object>(
+    scope: string,
+    resource: string,
+    verb: Verbs | Verbs[]
+  ) =>
+  (Component: any) =>
+  (props: ComponentProps) => {
+    const { currentPolicy } = useContext(AuthzContext);
+    const auth = useMemo(() => {
+      return isAuthorized(currentPolicy, scope, resource, verb);
+    }, [currentPolicy, scope, resource, verb]);
 
-  if (!currentPolicy) {
-    return <Loading />;
-  }
-  if (auth) {
-    return <Component {...props} />;
-  }
+    if (!currentPolicy) {
+      return <Loading />;
+    }
+    if (auth) {
+      return <Component {...props} />;
+    }
 
-  return <UnauthorizedPage />;
-};
+    return <UnauthorizedPage />;
+  };
 
 export default GuardedRoute;

+ 6 - 8
dashboard/src/shared/auth/useAuth.ts

@@ -1,17 +1,15 @@
 import { useCallback, useContext } from "react";
-import { AuthContext } from "./AuthContext";
+
 import { isAuthorized } from "./authorization-helpers";
-import { ScopeType, Verbs } from "./types";
+import { AuthzContext } from "./AuthzContext";
+import { type ScopeType, type Verbs } from "./types";
 
 const useAuth = () => {
-  const authContext = useContext(AuthContext);
+  const authContext = useContext(AuthzContext);
 
   const isAuth = useCallback(
-    (
-      scope: ScopeType,
-      resource: string | string[],
-      verb: Verbs | Array<Verbs>
-    ) => isAuthorized(authContext.currentPolicy, scope, resource, verb),
+    (scope: ScopeType, resource: string | string[], verb: Verbs | Verbs[]) =>
+      isAuthorized(authContext.currentPolicy, scope, resource, verb),
     [authContext.currentPolicy]
   );
 

+ 2 - 2
dashboard/src/test-utils.tsx

@@ -8,7 +8,7 @@ import { useHistory, useLocation } from "react-router";
 import { BrowserRouter } from "react-router-dom";
 import { createGlobalStyle, ThemeProvider } from "styled-components";
 
-import AuthProvider from "shared/auth/AuthContext";
+import AuthzProvider from "shared/auth/AuthzContext";
 import { ContextProvider } from "shared/Context";
 import standard from "shared/themes/standard";
 
@@ -19,7 +19,7 @@ const AllTheProviders: React.FC<{ children: React.ReactNode }> = ({
     <ThemeProvider theme={standard}>
       <BrowserRouter>
         <ContextProvider history={null} location={null}>
-          <AuthProvider>{children}</AuthProvider>
+          <AuthzProvider>{children}</AuthzProvider>
         </ContextProvider>
       </BrowserRouter>
     </ThemeProvider>