From 7297ddf944d717949ed0ae1102bf6aad376efaf2 Mon Sep 17 00:00:00 2001 From: AlinaGoaga <35202557+AlinaGoaga@users.noreply.github.com> Date: Thu, 24 Feb 2022 10:23:52 +0000 Subject: [PATCH] Auth logout (#1492) * WG309 Auth Context and Welcome screen - WIP * WG309 Auth Context and Welcome screen - WIP2 * WG309 Auth Context and Welcome screen - WIP * Add sign_in handler * WG309 Update welcome screen form * WG309 Update sign in formData submit * WG309 Update sign in req and payload * Fix format in helm testdata * Enable CORS for dev * Remove unnecessary package * WG309 Userinfo draft and cleanup * Callback working - use REACT_API_URL=http://0.0.0.0:9001 * WG309 User info returns 200 * WG309 Remove CORS related code * WG309 Remove CORS related code - update * Add tests for Signin handler * Update tests * Remove username from sign in form * Add token signer/verifier * Fix test * Add new middleware * Add tests for Signin handler * User from stash * WG309 AuthContext update on user info check * WG309 AuthContext update on user info check - 2 * WG309 Refactor loading in AuthContext * WG309 AuthContext reruns on history change * Fix conflict * WG309 AuthContext refactor * WG309 On refresh page doesnt go to 404 anymore * Wrap loading page * WG309 Fix oidc return url * Issued cookies should have the Secure attribute to true * WG309 Display alert error * Remove secret yaml example * WG309 Improve loading transition * Add OIDC flow test for user info endpoint * WG309 Improve loading transition - 2 * Fix eslint errors * Split out authchecking from the authcontext, single router * WG309 Add switch for password visibility * WG309 Add switch for password visibility - updated * Add BE logout code * Update package.json with main version * Lint it * WG407 Add user settings section - WIP * Rm security risk printing user-supplied value * WG407 Add user settings section - WIP2 * WG309 Hide UI behind flag - WIP * Push first pass at GET /v1/config * https in tests * Revert "https in tests" This reverts commit 286211b2eac8fd133dde61257136476475ffc1cf. * get feature flags innit * WG309 Hide UI behind feature flag - updated * Linting and testing * Update exports * untagglin * fix fix fix * OIDC is optional now * Update package.lock * WG407 Hide userSettings when authFlag is null * WG309 Hide UI behind feature flag - updated2 * WG407 Hide userSettings when authFlag is null - 2 * Fix issues in package-lock.json * Fix issues in package-lock.json - 2 * WG407 Add FeatureFlags context * WG407 Add FeatureFlags context - updated * WG407 Add FeatureFlags context - updated2 * Fix linting error * Update ui/contexts/AuthContext.tsx Co-authored-by: Simon * Implement PR feedback * Implement PR feedback - 2 Co-authored-by: Yiannis Co-authored-by: Simon Howe --- ui/App.tsx | 27 ++------------ ui/components/Icon.tsx | 5 +++ ui/components/Layout.tsx | 10 ++--- ui/components/UserSettings.tsx | 68 ++++++++++++++++++++++++++++++++++ ui/contexts/AuthContext.tsx | 65 +++++++++++++++++++++++++------- ui/contexts/FeatureFlags.tsx | 40 ++++++++++++++++++++ ui/index.ts | 7 ++++ 7 files changed, 178 insertions(+), 44 deletions(-) create mode 100644 ui/components/UserSettings.tsx create mode 100644 ui/contexts/FeatureFlags.tsx diff --git a/ui/App.tsx b/ui/App.tsx index a0950fcf4b..e8a7632513 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -14,6 +14,7 @@ import ErrorBoundary from "./components/ErrorBoundary"; import Layout from "./components/Layout"; import AppContextProvider from "./contexts/AppContext"; import AuthContextProvider, { AuthCheck } from "./contexts/AuthContext"; +import FeatureFlagsContextProvider from "./contexts/FeatureFlags"; import { Applications as appsClient, GitProvider, @@ -86,33 +87,13 @@ const App = () => ( ); export default function AppContainer() { - const [authFlag, setAuthFlag] = React.useState(null); - - const getAuthFlag = React.useCallback(() => { - fetch("/v1/featureflags") - .then((response) => response.json()) - .then((data) => - setAuthFlag(data.flags.WEAVE_GITOPS_AUTH_ENABLED === "true") - ) - .catch((err) => console.log(err)); - }, []); - - React.useEffect(() => { - getAuthFlag(); - }, [getAuthFlag]); - - // Loading... - if (authFlag === null) { - return null; - } - return ( - {authFlag ? ( + {/* does not use the base page so pull it up here */} @@ -125,9 +106,7 @@ export default function AppContainer() { - ) : ( - - )} + diff --git a/ui/components/Icon.tsx b/ui/components/Icon.tsx index 03b9a6677f..5f279f3cf2 100644 --- a/ui/components/Icon.tsx +++ b/ui/components/Icon.tsx @@ -12,6 +12,7 @@ import RemoveCircleIcon from "@material-ui/icons/RemoveCircle"; import SaveAltIcon from "@material-ui/icons/SaveAlt"; import SkipNextIcon from "@material-ui/icons/SkipNext"; import SkipPreviousIcon from "@material-ui/icons/SkipPrevious"; +import LogoutIcon from "@material-ui/icons/ExitToApp"; import * as React from "react"; import styled from "styled-components"; import { colors, spacing } from "../typedefs/styled"; @@ -34,6 +35,7 @@ export enum IconType { SkipNextIcon, SkipPreviousIcon, RemoveCircleIcon, + LogoutIcon, } type Props = { @@ -91,6 +93,9 @@ function getIcon(i: IconType) { case IconType.RemoveCircleIcon: return RemoveCircleIcon; + case IconType.LogoutIcon: + return LogoutIcon; + default: break; } diff --git a/ui/components/Layout.tsx b/ui/components/Layout.tsx index 5a1f07c824..344b332d92 100644 --- a/ui/components/Layout.tsx +++ b/ui/components/Layout.tsx @@ -5,9 +5,11 @@ import styled from "styled-components"; import useNavigation from "../hooks/navigation"; import { PageRoute } from "../lib/types"; import { formatURL, getNavValue } from "../lib/utils"; +import { FeatureFlags } from "../contexts/FeatureFlags"; import Flex from "./Flex"; import Link from "./Link"; import Logo from "./Logo"; +import UserSettings from "./UserSettings"; type Props = { className?: string; @@ -94,12 +96,8 @@ const TopToolBar = styled(Flex)` } `; -//style for account icon - disabled while no account functionality exists -// const UserAvatar = styled(Icon)` -// padding-right: ${(props) => props.theme.spacing.medium}; -// `; - function Layout({ className, children }: Props) { + const { authFlag } = React.useContext(FeatureFlags); const { currentPage } = useNavigation(); return ( @@ -107,7 +105,7 @@ function Layout({ className, children }: Props) { - {/* code for account icon - disabled while no account functionality exists */} + {authFlag ? : null}
diff --git a/ui/components/UserSettings.tsx b/ui/components/UserSettings.tsx new file mode 100644 index 0000000000..13f1f6bcd1 --- /dev/null +++ b/ui/components/UserSettings.tsx @@ -0,0 +1,68 @@ +import { + IconButton, + ListItemIcon, + Menu, + MenuItem, + Tooltip, +} from "@material-ui/core"; +import * as React from "react"; +import styled from "styled-components"; +import { Auth } from "../contexts/AuthContext"; +import Icon, { IconType } from "./Icon"; + +const UserAvatar = styled(Icon)` + padding-right: ${(props) => props.theme.spacing.medium}; +`; + +const SettingsMenu = styled(Menu)` + .MuiListItemIcon-root { + min-width: 25px; + color: ${(props) => props.theme.colors.black}; + } +`; + +function UserSettings() { + const [anchorEl, setAnchorEl] = React.useState(null); + const { userInfo, logOut } = React.useContext(Auth); + + const open = Boolean(anchorEl); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + + + + + Hello, {userInfo?.email} + logOut()}> + + + + Logout + + + + ); +} + +export default styled(UserSettings)``; diff --git a/ui/contexts/AuthContext.tsx b/ui/contexts/AuthContext.tsx index f19f37403e..e06619df1c 100644 --- a/ui/contexts/AuthContext.tsx +++ b/ui/contexts/AuthContext.tsx @@ -2,12 +2,21 @@ import * as React from "react"; import { useHistory, Redirect } from "react-router-dom"; import Layout from "../components/Layout"; import LoadingPage from "../components/LoadingPage"; +import { FeatureFlags } from "../contexts/FeatureFlags"; const USER_INFO = "/oauth2/userinfo"; const SIGN_IN = "/oauth2/sign_in"; +const LOG_OUT = "/oauth2/logout"; const AUTH_PATH_SIGNIN = "/sign_in"; export const AuthCheck = ({ children }) => { + // If the auth flag is null go straight to rendering the children + const { authFlag } = React.useContext(FeatureFlags); + + if (!authFlag) { + return children; + } + const { loading, userInfo } = React.useContext(Auth); // Wait until userInfo is loaded before showing signin or app content @@ -36,15 +45,18 @@ export type AuthContext = { }; error: { status: number; statusText: string }; loading: boolean; + logOut: () => void; }; export const Auth = React.createContext(null); export default function AuthContextProvider({ children }) { - const [userInfo, setUserInfo] = React.useState<{ - email: string; - groups: string[]; - }>(null); + const { authFlag } = React.useContext(FeatureFlags); + const [userInfo, setUserInfo] = + React.useState<{ + email: string; + groups: string[]; + }>(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const history = useHistory(); @@ -80,21 +92,46 @@ export default function AuthContextProvider({ children }) { .finally(() => setLoading(false)); }, []); + const logOut = React.useCallback(() => { + setLoading(true); + fetch(LOG_OUT, { + method: "POST", + }) + .then((response) => { + if (response.status !== 200) { + setError(response); + return; + } + history.push("/sign_in"); + }) + .finally(() => setLoading(false)); + }, []); + React.useEffect(() => { + if (!authFlag) { + return null; + } getUserInfo(); return history.listen(getUserInfo); }, [getUserInfo, history]); return ( - - {children} - + <> + {authFlag ? ( + + {children} + + ) : ( + children + )} + ); } diff --git a/ui/contexts/FeatureFlags.tsx b/ui/contexts/FeatureFlags.tsx new file mode 100644 index 0000000000..c90506835e --- /dev/null +++ b/ui/contexts/FeatureFlags.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; + +export type FeatureFlagsContext = { + authFlag: boolean | null; +}; + +export const FeatureFlags = + React.createContext(null); + +export default function FeatureFlagsContextProvider({ children }) { + const [authFlag, setAuthFlag] = React.useState(null); + + const getAuthFlag = React.useCallback(() => { + fetch("/v1/featureflags") + .then((response) => response.json()) + .then((data) => + setAuthFlag(data.flags.WEAVE_GITOPS_AUTH_ENABLED === "true") + ) + .catch((err) => console.log(err)); + }, []); + + React.useEffect(() => { + getAuthFlag(); + }, [getAuthFlag]); + + // Loading... + if (authFlag === null) { + return null; + } + + return ( + + {children} + + ); +} diff --git a/ui/index.ts b/ui/index.ts index f74da8a626..c06407697b 100644 --- a/ui/index.ts +++ b/ui/index.ts @@ -3,9 +3,13 @@ import Footer from "./components/Footer"; import GithubDeviceAuthModal from "./components/GithubDeviceAuthModal"; import LoadingPage from "./components/LoadingPage"; import RepoInputWithAuth from "./components/RepoInputWithAuth"; +import UserSettings from "./components/UserSettings"; import Icon, { IconType } from "./components/Icon"; import AppContextProvider from "./contexts/AppContext"; import AuthContextProvider from "./contexts/AuthContext"; +import FeatureFlagsContextProvider, { + FeatureFlags, +} from "./contexts/FeatureFlags"; import CallbackStateContextProvider from "./contexts/CallbackStateContext"; import useApplications from "./hooks/applications"; import { Applications as applicationsClient } from "./lib/api/applications/applications.pb"; @@ -21,6 +25,8 @@ import Applications from "./pages/Applications"; import OAuthCallback from "./pages/OAuthCallback"; export { + FeatureFlagsContextProvider, + FeatureFlags, AuthContextProvider, AppContextProvider, ApplicationAdd, @@ -42,4 +48,5 @@ export { Button, Icon, IconType, + UserSettings, };