From 7f5b3ae9a921690ea79b7caa084ec9963130e922 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 17 Jul 2024 17:26:32 -0700 Subject: [PATCH] Add Login and LoginContinue support (nextrequest, choice/create still TBD) --- src/app/Routes.tsx | 2 + src/common/api/authService.ts | 86 ++++++++++- src/features/login/LogIn.tsx | 205 +++++++++++++++++---------- src/features/login/LogInContinue.tsx | 79 +++++++++++ src/features/params/hooks.ts | 4 +- 5 files changed, 298 insertions(+), 78 deletions(-) create mode 100644 src/features/login/LogInContinue.tsx diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index b53d339d..4ec9ee7b 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -27,6 +27,7 @@ import { } from '../common/hooks'; import ORCIDLinkFeature from '../features/orcidlink'; import { LogIn } from '../features/login/LogIn'; +import { LogInContinue } from '../features/login/LogInContinue'; import ORCIDLinkCreateLink from '../features/orcidlink/CreateLink'; export const LOGIN_ROUTE = '/legacy/login'; @@ -54,6 +55,7 @@ const Routes: FC = () => { {/* Log In */} } /> + } /> {/* Navigator */} ; searchUsers: Record; + getLoginChoice: { + // cancelurl: string; + create: { + availablename: string; + id: string; + provemail: string; + provfullname: string; + provusername: string; + }[]; + // createurl: string; + creationallowed: true; + expires: number; + login: { + adminonly: boolean; + disabled: boolean; + id: string; + loginallowed: true; + policyids: { + agreedon: number; + id: string; + }[]; + provusernames: string[]; + user: string; + }[]; + // pickurl: string; + provider: string; + // redirecturl: string | null; + // suggestnameurl: string; + }; + postLoginPick: { + redirecturl: null | string; + token: { + agent: string; + agentver: string; + created: number; + custom: unknown; + device: unknown; + expires: number; + id: string; + ip: string; + name: unknown; + os: unknown; + osver: unknown; + token: string; + type: string; + user: string; + }; + }; } // Auth does not use JSONRpc, so we use queryFn to make custom queries @@ -102,8 +152,40 @@ export const authApi = baseApi.injectEndpoints({ method: 'DELETE', }), }), + getLoginChoice: builder.query< + AuthResults['getLoginChoice'], + AuthParams['getLoginChoice'] + >({ + query: () => + // MUST have an in-process-login-token cookie + authService({ + headers: { + accept: 'application/json', + }, + method: 'GET', + url: '/login/choice', + }), + }), + postLoginPick: builder.mutation< + AuthResults['postLoginPick'], + AuthParams['postLoginPick'] + >({ + query: (pickedChoice) => + authService({ + url: encode`/login/pick`, + body: pickedChoice, + method: 'POST', + }), + }), }), }); -export const { authFromToken, getMe, getUsers, searchUsers, revokeToken } = - authApi.endpoints; +export const { + authFromToken, + getMe, + getUsers, + searchUsers, + revokeToken, + getLoginChoice, + postLoginPick, +} = authApi.endpoints; diff --git a/src/features/login/LogIn.tsx b/src/features/login/LogIn.tsx index 5bcab436..b0cf11f2 100644 --- a/src/features/login/LogIn.tsx +++ b/src/features/login/LogIn.tsx @@ -1,4 +1,5 @@ import { + Alert, Box, Button, Container, @@ -13,99 +14,153 @@ import orcidLogo from '../../common/assets/orcid.png'; import globusLogo from '../../common/assets/globus.png'; import googleLogo from '../../common/assets/google.webp'; import classes from './LogIn.module.scss'; +import { useAppSelector } from '../../common/hooks'; +import { useAppParam } from '../params/hooks'; +import { useNavigate } from 'react-router-dom'; + +export const useCheckLoggedIn = () => { + const { initialized, token } = useAppSelector((state) => state.auth); + + const navigate = useNavigate(); + if (token && initialized) { + // TODO: handle nextrequest + navigate('/narratives'); + } +}; export const LogIn: FC = () => { + useCheckLoggedIn(); + const nextRequest = useAppParam('nextrequest'); + + // OAuth Login wont work in dev mode, but send dev users to CI so they can grab their token + const loginOrigin = + process.env.NODE_ENV === 'development' + ? 'https://ci.kbase.us' + : document.location.origin; + + // Triggering login requires a form POST submission + const loginActionUrl = new URL( + '/services/auth/login/start/', + loginOrigin + ).toString(); + const loginState = encodeURIComponent( + JSON.stringify({ + nextrequest: nextRequest, + origin: loginOrigin, + }) + ); + const loginRedirectURL = `${loginOrigin}/login/redirect?state=${loginState}`; + return ( - - - KBase circles logo - - - A collaborative, open environment for systems biology of plants, - microbes and their communities. - - - - - Log in - +
+ + + + KBase circles logo + + + A collaborative, open environment for systems biology of plants, + microbes and their communities. + + - - - + + Log in + + {process.env.NODE_ENV === 'development' ? ( + + DEV MODE: Login will occur on {loginOrigin} + + ) : ( + <> + )} + - + + + + + + + + New to KBase? Sign up + + + + Need help logging in? + + - - - New to KBase? Sign up - - - - Need help logging in? - - - - - + + +
); }; diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx new file mode 100644 index 00000000..53d3e8d8 --- /dev/null +++ b/src/features/login/LogInContinue.tsx @@ -0,0 +1,79 @@ +import { Container, Paper, Stack, Typography } from '@mui/material'; +import { FC, useEffect } from 'react'; +import logoRectangle from '../../common/assets/logo/rectangle.png'; +import classes from './LogIn.module.scss'; +import { Loader } from '../../common/components'; +import { useCookie } from '../../common/cookie'; +import { getLoginChoice, postLoginPick } from '../../common/api/authService'; +import { useTryAuthFromToken } from '../auth/hooks'; +import { useCheckLoggedIn } from './LogIn'; + +export const LogInContinue: FC = () => { + // redirect if/when login is completed + useCheckLoggedIn(); + + const [loginProcessToken] = useCookie('in-process-login-token'); + + const { data: choiceData } = getLoginChoice.useQuery(undefined, { + skip: !loginProcessToken, + }); + + const [trigger, result] = postLoginPick.useMutation(); + + // if/when postLoginPick has a result, update app auth state using that token + useTryAuthFromToken(result.data?.token.token); + + // wrap choiceData handling in an effect so we only trigger the pick call once + useEffect(() => { + if (choiceData) { + const accountExists = choiceData.login.length > 0; + // TODO: support choiceData.create cases + if (accountExists) { + if (choiceData.login.length > 1) { + // needs to be implemented if we have multiple KBase accounts linked to one provider account + } else { + trigger({ + id: choiceData.login[0].id, + policyids: choiceData.login[0].policyids.map(({ id }) => id), + }); + } + } + } + }, [choiceData, trigger]); + + return ( + + + + KBase circles logo + + + A collaborative, open environment for systems biology of plants, + microbes and their communities. + + + + + Logging in + +
{JSON.stringify(result.data || choiceData, null, 4)}
+
+
+
+
+ ); +}; diff --git a/src/features/params/hooks.ts b/src/features/params/hooks.ts index 1457c995..934b5556 100644 --- a/src/features/params/hooks.ts +++ b/src/features/params/hooks.ts @@ -26,7 +26,9 @@ export const useUpdateAppParams = () => { ); }; -export const useAppParam = (key: Key) => { +export const useAppParam = ( + key: Key +): NonNullable | undefined => { const val = useAppSelector((state) => state.params[key]); if (val === undefined || val === null) return undefined; return val as NonNullable;