From 8257cba96f941e0feffd80e0d81038f74075d1c4 Mon Sep 17 00:00:00 2001 From: peterschmidt85 Date: Fri, 27 Jun 2025 22:23:20 +0200 Subject: [PATCH] [UI] Added `LoginByGoogle` --- .../src/App/Login/EnterpriseLogin/index.tsx | 6 +- .../src/App/Login/LoginByGoogle/index.tsx | 37 +++++++++++ .../Login/LoginByGoogle/styles.module.scss | 20 ++++++ .../App/Login/LoginByGoogleCallback/index.tsx | 63 +++++++++++++++++++ frontend/src/App/index.tsx | 1 + frontend/src/api.ts | 6 ++ frontend/src/assets/icons/google.svg | 1 + frontend/src/locale/en.json | 1 + frontend/src/router.tsx | 5 ++ frontend/src/routes.ts | 1 + frontend/src/services/auth.ts | 27 ++++++++ 11 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 frontend/src/App/Login/LoginByGoogle/index.tsx create mode 100644 frontend/src/App/Login/LoginByGoogle/styles.module.scss create mode 100644 frontend/src/App/Login/LoginByGoogleCallback/index.tsx create mode 100644 frontend/src/assets/icons/google.svg diff --git a/frontend/src/App/Login/EnterpriseLogin/index.tsx b/frontend/src/App/Login/EnterpriseLogin/index.tsx index a48db31df0..07b76e1b3f 100644 --- a/frontend/src/App/Login/EnterpriseLogin/index.tsx +++ b/frontend/src/App/Login/EnterpriseLogin/index.tsx @@ -6,10 +6,11 @@ import { Box, NavigateLink, SpaceBetween } from 'components'; import { UnauthorizedLayout } from 'layouts/UnauthorizedLayout'; import { ROUTES } from 'routes'; -import { useGetEntraInfoQuery, useGetOktaInfoQuery } from 'services/auth'; +import { useGetEntraInfoQuery, useGetOktaInfoQuery, useGetGoogleInfoQuery } from 'services/auth'; import { LoginByEntraID } from '../EntraID/LoginByEntraID'; import { LoginByOkta } from '../LoginByOkta'; +import { LoginByGoogle } from '../LoginByGoogle'; import { LoginByTokenForm } from '../LoginByTokenForm'; import styles from './styles.module.scss'; @@ -18,9 +19,11 @@ export const EnterpriseLogin: React.FC = () => { const { t } = useTranslation(); const { data: oktaData, isLoading: isLoadingOkta } = useGetOktaInfoQuery(); const { data: entraData, isLoading: isLoadingEntra } = useGetEntraInfoQuery(); + const { data: googleData, isLoading: isLoadingGoogle } = useGetGoogleInfoQuery(); const oktaEnabled = oktaData?.enabled; const entraEnabled = entraData?.enabled; + const googleEnabled = googleData?.enabled; const isLoading = isLoadingOkta || isLoadingEntra; const isShowTokenForm = !oktaEnabled && !entraEnabled; @@ -36,6 +39,7 @@ export const EnterpriseLogin: React.FC = () => { {!isLoading && isShowTokenForm && } {!isLoadingOkta && oktaEnabled && } {!isLoadingEntra && entraEnabled && } + {!isLoadingGoogle && googleEnabled && } {!isLoading && !isShowTokenForm && ( diff --git a/frontend/src/App/Login/LoginByGoogle/index.tsx b/frontend/src/App/Login/LoginByGoogle/index.tsx new file mode 100644 index 0000000000..b83a93f187 --- /dev/null +++ b/frontend/src/App/Login/LoginByGoogle/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import cn from 'classnames'; + +import { Button } from 'components'; + +import { goToUrl } from 'libs'; +import { useGoogleAuthorizeMutation } from 'services/auth'; + +import { ReactComponent as GoogleIcon } from 'assets/icons/google.svg'; +import styles from './styles.module.scss'; + +export const LoginByGoogle: React.FC<{ className?: string }> = ({ className }) => { + const { t } = useTranslation(); + + const [googleAuthorize, { isLoading }] = useGoogleAuthorizeMutation(); + + const signInClick = () => { + googleAuthorize() + .unwrap() + .then((data) => { + goToUrl(data.authorization_url); + }) + .catch(console.log); + }; + + return ( +
+ +
+ ); +}; diff --git a/frontend/src/App/Login/LoginByGoogle/styles.module.scss b/frontend/src/App/Login/LoginByGoogle/styles.module.scss new file mode 100644 index 0000000000..93407bda8d --- /dev/null +++ b/frontend/src/App/Login/LoginByGoogle/styles.module.scss @@ -0,0 +1,20 @@ +@use '@cloudscape-design/design-tokens/index' as awsui; + +.signIn { + display: flex; + justify-content: center; + + button { + .loginButtonInner { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + } + + .loginButtonLabel { + height: 20px; + line-height: 21px; + } + } +} diff --git a/frontend/src/App/Login/LoginByGoogleCallback/index.tsx b/frontend/src/App/Login/LoginByGoogleCallback/index.tsx new file mode 100644 index 0000000000..465d0be3ee --- /dev/null +++ b/frontend/src/App/Login/LoginByGoogleCallback/index.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +import { NavigateLink } from 'components'; +import { UnauthorizedLayout } from 'layouts/UnauthorizedLayout'; + +import { useAppDispatch } from 'hooks'; +import { ROUTES } from 'routes'; +import { useGoogleCallbackMutation } from 'services/auth'; + +import { AuthErrorMessage } from 'App/AuthErrorMessage'; +import { Loading } from 'App/Loading'; +import { setAuthData } from 'App/slice'; + +export const LoginByGoogleCallback: React.FC = () => { + const { t } = useTranslation(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const code = searchParams.get('code'); + const state = searchParams.get('state'); + const [isInvalidCode, setIsInvalidCode] = useState(false); + const dispatch = useAppDispatch(); + + const [googleCallback] = useGoogleCallbackMutation(); + + const checkCode = () => { + if (code && state) { + googleCallback({ code, state }) + .unwrap() + .then(({ creds: { token } }) => { + dispatch(setAuthData({ token })); + navigate('/'); + }) + .catch(() => { + setIsInvalidCode(true); + }); + } + }; + + useEffect(() => { + if (code && state) { + checkCode(); + } else { + setIsInvalidCode(true); + } + }, []); + + if (isInvalidCode) + return ( + + + {t('auth.try_again')} + + + ); + + return ( + + ; + + ); +}; diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx index 67006b8168..de0f919079 100644 --- a/frontend/src/App/index.tsx +++ b/frontend/src/App/index.tsx @@ -19,6 +19,7 @@ const IGNORED_AUTH_PATHS = [ ROUTES.AUTH.GITHUB_CALLBACK, ROUTES.AUTH.OKTA_CALLBACK, ROUTES.AUTH.ENTRA_CALLBACK, + ROUTES.AUTH.GOOGLE_CALLBACK, ROUTES.AUTH.TOKEN, ]; diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 226d5edde3..46b3730f65 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -22,6 +22,12 @@ export const API = { AUTHORIZE: () => `${API.AUTH.ENTRA.BASE()}/authorize`, CALLBACK: () => `${API.AUTH.ENTRA.BASE()}/callback`, }, + GOOGLE: { + BASE: () => `${API.AUTH.BASE()}/google`, + INFO: () => `${API.AUTH.GOOGLE.BASE()}/info`, + AUTHORIZE: () => `${API.AUTH.GOOGLE.BASE()}/authorize`, + CALLBACK: () => `${API.AUTH.GOOGLE.BASE()}/callback`, + }, }, USERS: { diff --git a/frontend/src/assets/icons/google.svg b/frontend/src/assets/icons/google.svg new file mode 100644 index 0000000000..b352dde5d2 --- /dev/null +++ b/frontend/src/assets/icons/google.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 7aedc2d8ca..e1e0c90f25 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -32,6 +32,7 @@ "login_github": "Sign in with GitHub", "login_okta": "Sign in with Okta", "login_entra": "Sign in with EntraID", + "login_google": "Sign in with Google", "general": "General", "test": "Test", "local_storage_unavailable": "Local Storage is unavailable", diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index a953cbc6b3..59f215d28d 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -7,6 +7,7 @@ import App from 'App'; import { LoginByEntraIDCallback } from 'App/Login/EntraID/LoginByEntraIDCallback'; import { LoginByGithubCallback } from 'App/Login/LoginByGithubCallback'; import { LoginByOktaCallback } from 'App/Login/LoginByOktaCallback'; +import { LoginByGoogleCallback } from 'App/Login/LoginByGoogleCallback'; import { TokenLogin } from 'App/Login/TokenLogin'; import { Logout } from 'App/Logout'; import { FleetDetails, FleetList } from 'pages/Fleets'; @@ -45,6 +46,10 @@ export const router = createBrowserRouter([ path: ROUTES.AUTH.ENTRA_CALLBACK, element: , }, + { + path: ROUTES.AUTH.GOOGLE_CALLBACK, + element: , + }, { path: ROUTES.AUTH.TOKEN, element: , diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 335d6378be..3df4544656 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -8,6 +8,7 @@ export const ROUTES = { GITHUB_CALLBACK: `/auth/github/callback`, OKTA_CALLBACK: `/auth/okta/callback`, ENTRA_CALLBACK: `/auth/entra/callback`, + GOOGLE_CALLBACK: `/auth/google/callback`, TOKEN: `/auth/token`, }, diff --git a/frontend/src/services/auth.ts b/frontend/src/services/auth.ts index f566bf3328..5b1f99d406 100644 --- a/frontend/src/services/auth.ts +++ b/frontend/src/services/auth.ts @@ -75,6 +75,30 @@ export const authApi = createApi({ body, }), }), + + getGoogleInfo: builder.query<{ enabled: boolean }, void>({ + query: () => { + return { + url: API.AUTH.GOOGLE.INFO(), + method: 'POST', + }; + }, + }), + + googleAuthorize: builder.mutation<{ authorization_url: string }, void>({ + query: () => ({ + url: API.AUTH.GOOGLE.AUTHORIZE(), + method: 'POST', + }), + }), + + googleCallback: builder.mutation({ + query: (body) => ({ + url: API.AUTH.GOOGLE.CALLBACK(), + method: 'POST', + body, + }), + }), }), }); @@ -87,4 +111,7 @@ export const { useGetEntraInfoQuery, useEntraAuthorizeMutation, useEntraCallbackMutation, + useGetGoogleInfoQuery, + useGoogleAuthorizeMutation, + useGoogleCallbackMutation, } = authApi;