From 877ddebfcaef8afbb387cbcfb51dfff85f8596a2 Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Tue, 11 Jun 2024 16:31:05 -0700 Subject: [PATCH 01/25] Add routes and initial page for sign up flow --- src/app/App.module.scss | 2 + src/app/App.tsx | 4 +- src/app/Routes.tsx | 4 + src/features/login/LogIn.tsx | 4 +- src/features/signup/SignUp.module.scss | 18 +++++ src/features/signup/SignUp.tsx | 106 +++++++++++++++++++++++++ src/theme.tsx | 22 +++++ 7 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 src/features/signup/SignUp.module.scss create mode 100644 src/features/signup/SignUp.tsx diff --git a/src/app/App.module.scss b/src/app/App.module.scss index 1fc115d8..7f1ad56b 100644 --- a/src/app/App.module.scss +++ b/src/app/App.module.scss @@ -53,6 +53,8 @@ flex-shrink: 1; max-height: 100%; overflow-y: auto; + padding-bottom: 1rem; + padding-top: 1rem; position: relative; } } diff --git a/src/app/App.tsx b/src/app/App.tsx index c2679a36..279fa40a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -60,7 +60,7 @@ const App: FC = () => {
-
+
@@ -68,7 +68,7 @@ const App: FC = () => { -
+ ); diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index 4174b794..a9af0570 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 { SignUp } from '../features/signup/SignUp'; export const LOGIN_ROUTE = '/legacy/login'; export const ROOT_REDIRECT_ROUTE = '/narratives'; @@ -54,6 +55,9 @@ const Routes: FC = () => { {/* Log In */} } /> + {/* Sign Up */} + } /> + {/* Navigator */} { }} > - - Log in - + Log in + + + + + + + + + Already have an account? Log in + + + + Need help signing up? + + + + + + + + + ); +}; diff --git a/src/theme.tsx b/src/theme.tsx index b8797303..ac76a629 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -64,6 +64,28 @@ export const theme = createTheme({ }, }, }, + MuiTypography: { + styleOverrides: { + h1: { + fontSize: '2.5rem', + }, + h2: { + fontSize: '2rem', + }, + h3: { + fontSize: '1.75rem', + }, + h4: { + fontSize: '1.5rem', + }, + h5: { + fontSize: '1.25rem', + }, + h6: { + fontSize: '1rem', + }, + }, + }, }, }); From fef70b22d1a786532e33fefdc68a1f993128e7af Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Fri, 14 Jun 2024 15:04:18 -0700 Subject: [PATCH 02/25] Add account and policies steps to sign up flow --- src/features/signup/AccountInformation.tsx | 147 +++++++++++++ src/features/signup/ProviderSelect.tsx | 90 ++++++++ src/features/signup/SignUp.module.scss | 62 ++++++ src/features/signup/SignUp.tsx | 118 +++-------- src/features/signup/UsePolicies.tsx | 231 +++++++++++++++++++++ 5 files changed, 561 insertions(+), 87 deletions(-) create mode 100644 src/features/signup/AccountInformation.tsx create mode 100644 src/features/signup/ProviderSelect.tsx create mode 100644 src/features/signup/UsePolicies.tsx diff --git a/src/features/signup/AccountInformation.tsx b/src/features/signup/AccountInformation.tsx new file mode 100644 index 00000000..b8f8f42e --- /dev/null +++ b/src/features/signup/AccountInformation.tsx @@ -0,0 +1,147 @@ +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Alert, + Button, + Checkbox, + FormControl, + FormControlLabel, + FormGroup, + FormLabel, + Paper, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { FC } from 'react'; +import classes from './SignUp.module.scss'; + +/** + * Account information form for sign up flow + */ +export const AccountInformation: FC<{ + setActiveStep: React.Dispatch>; +}> = ({ setActiveStep }) => { + return ( + + + + + You have signed in with your Google account ctodonnell@lbl.gov. This + will be the account linked to your KBase account. + + + } + aria-controls="panel1-content" + id="panel1-header" + > + Not the account you were expecting? + + + If this browser is already signed in to Google, a sign-in attempt + from KBase will route you to Google and back again without any + warning. If this just happened to you, and the account you see + above is not the one you want, you should use the link below to + log out of Google, and then try again. If you have signed in with + a Google account already linked to a KBase account, you will be + unable to create a new KBase account using that Google account. + Logout from Google After signing out from Google you will need to + Sign in to KBaseagain. + + + + + + + Create a new KBase Account + + Some field values have been pre-populated from your{' '} + Google account. + All fields are required. + + + Full Name + + + + Email + + + + KBase Username + + + + Organization + + + + Department + + + + + How did you hear about us? (Select all that apply) + + + } + label="Journal Publication" + /> + } + label="Conference Presentation" + /> + } + label="Workshop/Webinar" + /> + } label="Colleague" /> + } + label="Course/Instructor" + /> + } + label="Newsletter/Email" + /> + } label="YouTube" /> + } label="Twitter" /> + } label="Search Engine" /> + } + label="Online Advertisement" + /> + } label="Other" /> + + + + + + + + + + ); +}; diff --git a/src/features/signup/ProviderSelect.tsx b/src/features/signup/ProviderSelect.tsx new file mode 100644 index 00000000..4f7a2821 --- /dev/null +++ b/src/features/signup/ProviderSelect.tsx @@ -0,0 +1,90 @@ +import { + Box, + Button, + Container, + Link, + Paper, + Stack, + Typography, +} from '@mui/material'; +import { FC } from 'react'; +import globusLogo from '../../common/assets/globus.png'; +import googleLogo from '../../common/assets/google.webp'; +import orcidLogo from '../../common/assets/orcid.png'; +import classes from './SignUp.module.scss'; + +/** + * Provider selection screen for sign up flow + */ +export const ProviderSelect: FC = () => { + return ( + + + + + Choose a provider + + + + + + + + + + + Already have an account? Log in + + + + Need help signing up? + + + + + + + ); +}; diff --git a/src/features/signup/SignUp.module.scss b/src/features/signup/SignUp.module.scss index 0ad57806..f7038417 100644 --- a/src/features/signup/SignUp.module.scss +++ b/src/features/signup/SignUp.module.scss @@ -1,3 +1,5 @@ +/* stylelint-disable selector-class-pattern */ + @import "../../common/colors"; .signup-panel { @@ -16,3 +18,63 @@ height: 1px; width: 80%; } + +.collapsible-message { + &:global(.MuiAccordion-root) { + background: none; + box-shadow: none; + color: inherit; + min-height: 0; + } + + :global(.MuiAccordionSummary-root) { + flex-direction: row-reverse; + min-height: 0; + padding-left: 0.25rem; + padding-right: 0; + } + + :global(.MuiAccordionSummary-content) { + margin-bottom: 0.25rem; + margin-left: 0.5rem; + margin-top: 0.25rem; + } + + :global(.MuiAccordionSummary-expandIconWrapper.Mui-expanded) { + transform: rotate(90deg); + } + + :global(.MuiAccordionDetails-root) { + padding-bottom: 0; + } +} + +.account-information-panel, +.use-policies-panel { + padding: 1rem; + + :global(.MuiFormLabel-root) { + color: #000; + } +} + +.policy-panel { + background-color: use-color("base-lightest") !important; + max-height: 500px; + overflow: auto; + padding: 1rem; + + blockquote { + border-left: 4px solid use-color("base"); + padding-left: 1rem; + } +} + +.agreement-box { + border: 1px solid use-color("base"); + border-radius: 4px; + display: inline-block; + margin-top: 1rem; + padding: 1rem; + +} diff --git a/src/features/signup/SignUp.tsx b/src/features/signup/SignUp.tsx index 8c72a243..9c674e9e 100644 --- a/src/features/signup/SignUp.tsx +++ b/src/features/signup/SignUp.tsx @@ -1,105 +1,49 @@ import { - Box, - Button, Container, - Link, - Paper, Stack, Step, StepLabel, Stepper, Typography, } from '@mui/material'; -import { FC } from 'react'; -import orcidLogo from '../../common/assets/orcid.png'; -import globusLogo from '../../common/assets/globus.png'; -import googleLogo from '../../common/assets/google.webp'; -import classes from './SignUp.module.scss'; +import { FC, useEffect, useState } from 'react'; +import { AccountInformation } from './AccountInformation'; +import { ProviderSelect } from './ProviderSelect'; +import { UsePolicies } from './UsePolicies'; +const signUpSteps = [ + 'Sign up with a supported provider', + 'Account information', + 'KBase use policies', +]; + +/** + * Sign up flow that handles choosing a provider, populating account information, + * and accepting the KBase use policies. + */ export const SignUp: FC = () => { + const [activeStep, setActiveStep] = useState(0); + + useEffect(() => { + document.querySelector('main')?.scrollTo(0, 0); + }, [activeStep]); + return ( Sign up for KBase - - - Sign up with a supported provider - - - Account information - - - KBase use policies - + + {signUpSteps.map((step, i) => ( + setActiveStep(i)}> + {step} + + ))} - - - - - Choose a provider - - - - - - - - - - - Already have an account? Log in - - - - Need help signing up? - - - - - - + {activeStep === 0 && } + {activeStep === 1 && ( + + )} + {activeStep === 2 && } ); diff --git a/src/features/signup/UsePolicies.tsx b/src/features/signup/UsePolicies.tsx new file mode 100644 index 00000000..e5c807cb --- /dev/null +++ b/src/features/signup/UsePolicies.tsx @@ -0,0 +1,231 @@ +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Box, + Button, + Checkbox, + FormControl, + FormControlLabel, + Link, + Paper, + Stack, + Typography, +} from '@mui/material'; +import { FC } from 'react'; +import classes from './SignUp.module.scss'; + +/** + * Use policy agreements for sign up flow. + */ +export const UsePolicies: FC<{ + setActiveStep: React.Dispatch>; +}> = ({ setActiveStep }) => { + return ( + + + + KBase Use Policies + + To finish signing up and create your account, you must agree to the + following KBase use policies. + + + KBase Use Agreement + + + Terms and Conditions + Prohibited Behavior + + As a condition of your use of KBase (the DOE Systems Biology + Knowledgebase) you agree not to upload any type of human data + or personally identifiable information. +
+ Improper use of KBase, including uploading human data, may + result in the termination of KBase access privileges. +
+
+ Data Sharing + + KBase conforms to the{' '} + + Information and Data Sharing Policy + {' '} + of the Genomic Science Program of the Office of Biological and + Environmental Research within the Office of Science. Please + see the{' '} + + KBase Data Policy + {' '} + page for more information. + + Responsibility for Data + + As a condition of your use of KBase you accept sole + responsibility for all files you upload or transfer through + use of KBase. You recognize and accept that{' '} + + KBase does not guarantee long-term retention of user + uploaded data + + , and will not be responsible for any failure to store or + transfer, or deletion, corruption or loss for any data, + information or content contained in your files. It is strongly + recommended that you back up all files prior to using KBase. + + Use Agreement + + By using KBase, including its websites and services or via + published APIs, you are agreeing to the terms stated in our{' '} + + Use Agreement + + . Please read them carefully. They include limitations on what + is acceptable user behavior, accountability, availability, + data retention, and conditions for account termination. If you + do not agree, do not access or use KBase. + + Privacy Policy + + KBase is provided as a public service. KBase reserves the + right to monitor any and all use of kbase.us. KBase never + collects information for commercial marketing or any purpose + unrelated to KBase functions. The{' '} + + Privacy Policy + {' '} + describes the ways KBase collects, stores, uses, discloses and + protects the personal information about users and how they use + KBase. + + Open Source License + + All software developed by the KBase project team, and any + contributed by you to KBase, is stored and maintained in the + public{' '} + + KBase GitHub code repository + {' '} + under the{' '} + + MIT Open Source License + {' '} + (“License”). By contributing to or using KBase, you + acknowledge having read and understood the License and agree + to abide by it. + +
+
+
+ + } + label="I have read and agree to this policy" + /> + +
+
+ + KBase Data Policy + + + Data Policy + Data Policies + + KBase conforms to the{' '} + + Information and Data Sharing Policy + {' '} + of the Genomic Science Program of the Office of Biological and + Environmental Research within the Office of Science. This + requires that all publishable data, metadata, and software + resulting from research funded by the Genomic Science program + must conform to community-recognized standard formats when + they exist; be clearly attributable; and be deposited within a + community-recognized public database(s) appropriate for the + research. + + + + Data publicly available in KBase + {' '} + comes from the sources listed on this page. Additionally, + users can upload their own data to KBase to analyze it, and + can choose how widely their data should be shared. (All data + uploaded by users is private to them unless they choose to + share it.){' '} + + +
+ NOTICE: KBase does not guarantee long-term retention of + user-uploaded data. Please take appropriate precautions in + storing and backing up your data locally. +
+
+ +
+ WARNING: Improper use of KBase, including uploading human + data, may result in the termination of KBase access + privileges. Please see the{' '} + + Terms and Conditions + {' '} + page for more information. +
+
+
+
+
+ + } + label="I have read and agree to this policy" + /> + +
+
+
+
+ + + + + + +
+ ); +}; From 71a5a834f14356fd0608a12b3442abd52bf1bb0e Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Fri, 14 Jun 2024 15:34:46 -0700 Subject: [PATCH 03/25] Add colors used in sign up to mui theme --- src/theme.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/theme.tsx b/src/theme.tsx index ac76a629..5b19cf81 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -18,6 +18,14 @@ const baseColor = 'rgb(62, 56, 50)'; export const theme = createTheme({ palette: { + primary: { + // TODO: import from single source of truth + main: 'rgb(2, 109, 170)', + }, + warning: { + // TODO: import from single source of truth + main: 'rgb(255, 210, 0)', + }, base: { main: baseColor, contrastText: getContrastRatio(baseColor, '#fff') > 4.5 ? '#fff' : '#111', From b34e3c72e92830bf5d50d905b89e79bf31cdee4f Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Fri, 14 Jun 2024 15:54:39 -0700 Subject: [PATCH 04/25] Fix layout in sign up alert box --- src/features/signup/AccountInformation.tsx | 36 +++++++++++++++------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/features/signup/AccountInformation.tsx b/src/features/signup/AccountInformation.tsx index b8f8f42e..324c7170 100644 --- a/src/features/signup/AccountInformation.tsx +++ b/src/features/signup/AccountInformation.tsx @@ -5,6 +5,7 @@ import { AccordionDetails, AccordionSummary, Alert, + Box, Button, Checkbox, FormControl, @@ -30,8 +31,9 @@ export const AccountInformation: FC<{ - You have signed in with your Google account ctodonnell@lbl.gov. This - will be the account linked to your KBase account. + You have signed in with your Google account{' '} + ctodonnell@lbl.gov. This will be the account linked + to your KBase account. - If this browser is already signed in to Google, a sign-in attempt - from KBase will route you to Google and back again without any - warning. If this just happened to you, and the account you see - above is not the one you want, you should use the link below to - log out of Google, and then try again. If you have signed in with - a Google account already linked to a KBase account, you will be - unable to create a new KBase account using that Google account. - Logout from Google After signing out from Google you will need to - Sign in to KBaseagain. + + + If the account you see above is not the one you want, use the + link below to log out of Google, and then try again. + + + + + + If you are trying to sign up with a Google account that is + already linked to a KBase account, you will be unable to + create a new KBase account using that Google account. + + + After signing out from Google you will need to restart the + sign up process. + + + + + From 7b17975a33b71de60da8e24614884c263eefb57b Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Tue, 9 Jul 2024 10:32:48 -0700 Subject: [PATCH 05/25] Fix container padding and nav border --- src/app/App.module.scss | 1 + src/features/collections/Collections.module.scss | 4 +--- src/features/navigator/Navigator.module.scss | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app/App.module.scss b/src/app/App.module.scss index 7f1ad56b..4d5a5181 100644 --- a/src/app/App.module.scss +++ b/src/app/App.module.scss @@ -39,6 +39,7 @@ .left_navbar { background-color: use-color("white"); + border-right: 1px solid use-color("silver"); flex-grow: 0; flex-shrink: 0; max-height: 100%; diff --git a/src/features/collections/Collections.module.scss b/src/features/collections/Collections.module.scss index 3fa010b7..00ac28d7 100644 --- a/src/features/collections/Collections.module.scss +++ b/src/features/collections/Collections.module.scss @@ -3,8 +3,6 @@ $border: 1px solid use-color("base-lighter"); .collections-main { - background-color: use-color("base-lightest"); - border-left: 1px solid use-color("silver"); min-height: 100%; } @@ -22,7 +20,7 @@ $border: 1px solid use-color("base-lighter"); font-size: 1.25rem; font-weight: 500; margin: 0; - padding: 2rem 1rem 1rem; + padding: 1rem; } .collection-card { diff --git a/src/features/navigator/Navigator.module.scss b/src/features/navigator/Navigator.module.scss index 3d07e8b5..e7f1fb8d 100644 --- a/src/features/navigator/Navigator.module.scss +++ b/src/features/navigator/Navigator.module.scss @@ -5,10 +5,8 @@ pre { } .navigator { - background-color: use-color("base-lightest"); - border-left: 1px solid use-color("silver"); min-height: 100%; - padding: 1rem; + padding: 0 1rem; } .navigator a { From 7f5b3ae9a921690ea79b7caa084ec9963130e922 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 17 Jul 2024 17:26:32 -0700 Subject: [PATCH 06/25] 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; From e9967cd6da050f8fdcf1d593b947431e9aac8d26 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 18 Jul 2024 10:22:00 -0700 Subject: [PATCH 07/25] fix login provider values --- src/features/login/LogIn.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/login/LogIn.tsx b/src/features/login/LogIn.tsx index b0cf11f2..90ad9498 100644 --- a/src/features/login/LogIn.tsx +++ b/src/features/login/LogIn.tsx @@ -92,7 +92,7 @@ export const LogIn: FC = () => { From 81b6d261766b9704b1dfd79a833a42f0f6a7fff5 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 1 Aug 2024 10:54:35 -0700 Subject: [PATCH 11/25] Add loggout page, implement legacy login/out redirect --- src/app/Routes.tsx | 2 + src/features/layout/TopBar.tsx | 33 +--------- src/features/legacy/Legacy.tsx | 28 +++++---- src/features/login/LogIn.tsx | 31 +++++++++- src/features/login/LogInContinue.tsx | 2 +- src/features/login/LoggedOut.tsx | 93 ++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+), 44 deletions(-) create mode 100644 src/features/login/LoggedOut.tsx diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index 79f11c56..56a9176b 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -30,6 +30,7 @@ import ORCIDLinkFeature from '../features/orcidlink'; import { LogIn } from '../features/login/LogIn'; import { LogInContinue } from '../features/login/LogInContinue'; import ORCIDLinkCreateLink from '../features/orcidlink/CreateLink'; +import { LoggedOut } from '../features/login/LoggedOut'; export const LOGIN_ROUTE = '/login'; export const ROOT_REDIRECT_ROUTE = '/narratives'; @@ -57,6 +58,7 @@ const Routes: FC = () => { {/* Log In */} } /> } /> + } /> {/* Navigator */} { ); }; -const useLogout = () => { - const tokenId = useAppSelector(({ auth }) => auth.tokenInfo?.id); - const dispatch = useAppDispatch(); - const [revoke] = revokeToken.useMutation(); - const navigate = useNavigate(); - - if (!tokenId) return noOp; - - return () => { - revoke(tokenId) - .unwrap() - .then(() => { - dispatch(resetStateAction()); - // setAuth(null) follow the state reset to initialize the page as un-Authed - dispatch(setAuth(null)); - toast('You have been signed out'); - navigate('/legacy/auth2/signedout'); - }) - .catch(() => { - toast('Error, could not log out.'); - }); - }; -}; - const HamburgerMenu: FC = () => { const navigate = useNavigate(); return ( diff --git a/src/features/legacy/Legacy.tsx b/src/features/legacy/Legacy.tsx index aae94569..030cc3cf 100644 --- a/src/features/legacy/Legacy.tsx +++ b/src/features/legacy/Legacy.tsx @@ -1,11 +1,9 @@ import { RefObject, useEffect, useRef, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { createSearchParams, useLocation, useNavigate } from 'react-router-dom'; import { usePageTitle } from '../layout/layoutSlice'; import { useTryAuthFromToken } from '../auth/hooks'; -import { useAppDispatch } from '../../common/hooks'; -import { resetStateAction } from '../../app/store'; -import { setAuth } from '../auth/authSlice'; -import { toast } from 'react-hot-toast'; +import { LOGIN_ROUTE } from '../../app/Routes'; +import { useLogout } from '../login/LogIn'; export const LEGACY_BASE_ROUTE = '/legacy'; @@ -17,7 +15,7 @@ export default function Legacy() { const location = useLocation(); const navigate = useNavigate(); - const dispatch = useAppDispatch(); + const logout = useLogout(); const legacyContentRef = useRef(null); const [legacyTitle, setLegacyTitle] = useState(''); @@ -43,8 +41,17 @@ export default function Legacy() { let path = d.payload.request.original; if (path[0] === '/') path = path.slice(1); if (legacyPath !== path) { - setLegacyPath(path); - navigate(`./${path}`); + if (path === 'login') { + navigate({ + pathname: LOGIN_ROUTE, + search: createSearchParams({ + nextRequest: JSON.stringify(location), + }).toString(), + }); + } else { + setLegacyPath(path); + navigate(`./${path}`); + } } } else if (isTitleMessage(d)) { setLegacyTitle(d.payload); @@ -53,10 +60,7 @@ export default function Legacy() { setReceivedToken(d.payload.token); } } else if (isLogoutMessage(d)) { - dispatch(resetStateAction()); - dispatch(setAuth(null)); - toast('You have been signed out'); - navigate('/legacy/auth2/signedout'); + logout(); } }); diff --git a/src/features/login/LogIn.tsx b/src/features/login/LogIn.tsx index 0533becc..51266973 100644 --- a/src/features/login/LogIn.tsx +++ b/src/features/login/LogIn.tsx @@ -14,9 +14,14 @@ 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 { useAppDispatch, useAppSelector } from '../../common/hooks'; import { useAppParam } from '../params/hooks'; import { To, useNavigate } from 'react-router-dom'; +import { resetStateAction } from '../../app/store'; +import { setAuth } from '../auth/authSlice'; +import { toast } from 'react-hot-toast'; +import { revokeToken } from '../../common/api/authService'; +import { noOp } from '../common'; export const useCheckLoggedIn = (nextRequest: string | undefined) => { const { initialized, token } = useAppSelector((state) => state.auth); @@ -38,6 +43,30 @@ export const useCheckLoggedIn = (nextRequest: string | undefined) => { }, [initialized, navigate, nextRequest, token]); }; +export const useLogout = () => { + const tokenId = useAppSelector(({ auth }) => auth.tokenInfo?.id); + const dispatch = useAppDispatch(); + const [revoke] = revokeToken.useMutation(); + const navigate = useNavigate(); + + if (!tokenId) return noOp; + + return () => { + revoke(tokenId) + .unwrap() + .then(() => { + dispatch(resetStateAction()); + // setAuth(null) follow the state reset to initialize the page as un-Authed + dispatch(setAuth(null)); + toast('You have been signed out'); + navigate('/loggedout'); + }) + .catch(() => { + toast('Error, could not log out.'); + }); + }; +}; + export const LogIn: FC = () => { const nextRequest = useAppParam('nextRequest'); useCheckLoggedIn(nextRequest); diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index 28a2e136..bee8e548 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -47,6 +47,7 @@ export const LogInContinue: FC = () => { if (choiceData) { const accountExists = choiceData.login.length > 0; // TODO: support choiceData.create cases + // TODO: support policy enforcement if (accountExists) { if (choiceData.login.length > 1) { // needs to be implemented if we have multiple KBase accounts linked to one provider account @@ -114,7 +115,6 @@ export const LogInContinue: FC = () => { Logging in -
{JSON.stringify(pickResult.data || choiceData, null, 4)}
diff --git a/src/features/login/LoggedOut.tsx b/src/features/login/LoggedOut.tsx new file mode 100644 index 00000000..203f21ba --- /dev/null +++ b/src/features/login/LoggedOut.tsx @@ -0,0 +1,93 @@ +import { + Box, + Button, + Container, + Paper, + Stack, + Typography, +} from '@mui/material'; +import { useCheckLoggedIn } from './LogIn'; +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'; + +export const LoggedOut = () => { + useCheckLoggedIn(undefined); + + return ( + + + + + + You are signed out of KBase + + + You may still be logged in to your identity provider. If you wish + to ensure that your KBase account is inaccessible from this + browser, you should sign out of any provider accounts you have + used to access KBase. + + + + + + + + + + + + ); +}; From ab62bb0914fa01438c977eec5d15552d89e32c1e Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 1 Aug 2024 12:57:02 -0700 Subject: [PATCH 12/25] clear narrative_session on logout --- src/features/login/LogIn.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/features/login/LogIn.tsx b/src/features/login/LogIn.tsx index 51266973..e40b9c25 100644 --- a/src/features/login/LogIn.tsx +++ b/src/features/login/LogIn.tsx @@ -22,6 +22,7 @@ import { setAuth } from '../auth/authSlice'; import { toast } from 'react-hot-toast'; import { revokeToken } from '../../common/api/authService'; import { noOp } from '../common'; +import { useCookie } from '../../common/cookie'; export const useCheckLoggedIn = (nextRequest: string | undefined) => { const { initialized, token } = useAppSelector((state) => state.auth); @@ -49,6 +50,8 @@ export const useLogout = () => { const [revoke] = revokeToken.useMutation(); const navigate = useNavigate(); + const clearNarrativeSession = useCookie('narrative_session')[2]; + if (!tokenId) return noOp; return () => { @@ -58,6 +61,7 @@ export const useLogout = () => { dispatch(resetStateAction()); // setAuth(null) follow the state reset to initialize the page as un-Authed dispatch(setAuth(null)); + clearNarrativeSession(); toast('You have been signed out'); navigate('/loggedout'); }) From 5c2e7db70880c4e7a8bf05d428f4191773e3e7f6 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Mon, 5 Aug 2024 17:36:07 -0700 Subject: [PATCH 13/25] Add LoginContinue tests --- src/features/login/LogIn.test.tsx | 11 +- src/features/login/LogInContinue.test.tsx | 255 ++++++++++++++++++++++ src/features/login/LogInContinue.tsx | 7 +- 3 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 src/features/login/LogInContinue.test.tsx diff --git a/src/features/login/LogIn.test.tsx b/src/features/login/LogIn.test.tsx index 17bd2d7c..9f9dcd29 100644 --- a/src/features/login/LogIn.test.tsx +++ b/src/features/login/LogIn.test.tsx @@ -112,12 +112,7 @@ describe('Login', () => { }); it('redirect if logged in', () => { - const redirectSpy = jest.fn(); - const Redirect = () => { - redirectSpy(); - return <>; - }; - + const Narratives = jest.fn(() => <>); render( { } /> - } /> + ); - expect(redirectSpy).toBeCalled(); + expect(Narratives).toBeCalled(); }); it('redirect if logged in with nextRequest', () => { diff --git a/src/features/login/LogInContinue.test.tsx b/src/features/login/LogInContinue.test.tsx new file mode 100644 index 00000000..e2315a30 --- /dev/null +++ b/src/features/login/LogInContinue.test.tsx @@ -0,0 +1,255 @@ +import { ThemeProvider } from '@mui/material'; +import { render, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { createTestStore } from '../../app/store'; +import { theme } from '../../theme'; +import { LogInContinue } from './LogInContinue'; +import fetchMock from 'jest-fetch-mock'; +import { toast } from 'react-hot-toast'; +import { noOp } from '../common'; + +jest.mock('react-hot-toast', () => ({ + toast: jest.fn(), +})); + +describe('Login Continue', () => { + beforeEach(() => { + fetchMock.enableMocks(); + fetchMock.resetMocks(); + }); + + it('renders and logs in', async () => { + // getLoginChoice + fetchMock.mockResponseOnce( + JSON.stringify({ + login: [ + { + id: 'foouserid', + policyids: [ + { + agreedon: 0, + id: 'foopolicy', + }, + ], + }, + ], + }) + ); + // postLoginPick + fetchMock.mockResponseOnce( + JSON.stringify({ + token: { + token: 'foobartoken', + }, + }) + ); + // authFromToken + fetchMock.mockResponseOnce( + JSON.stringify({ + user: 'someusername', + }) + ); + + const store = createTestStore(); + const Narratives = jest.fn(() => <>); + const { container } = render( + + + + + } /> + + + + + + ); + await waitFor(() => expect(container).toHaveTextContent('Logging in')); + await waitFor(() => { + expect(store.getState().auth.initialized).toBe(true); + expect(store.getState().auth.token).toBe('FOOBARTOKEN'); + expect(store.getState().auth.username).toBe('someusername'); + expect(Narratives).toHaveBeenCalled(); + }); + }); + + it('renders and logs in with redirect (nextRequest)', async () => { + // getLoginChoice + fetchMock.mockResponseOnce( + JSON.stringify({ + login: [ + { + id: 'foouserid', + policyids: [ + { + agreedon: 0, + id: 'foopolicy', + }, + ], + }, + ], + }) + ); + // postLoginPick + fetchMock.mockResponseOnce( + JSON.stringify({ + redirecturl: + 'http://localhost/login/continue?state=%7B%22nextRequest%22%3A%22%7B%5C%22pathname%5C%22%3A%5C%22%2FsomeRedirect%5C%22%7D%22%2C%22origin%22%3A%22http%3A%2F%2Flocalhost%22%7D', + token: { + token: 'foobartoken', + }, + }) + ); + // authFromToken + fetchMock.mockResponseOnce( + JSON.stringify({ + user: 'someusername', + }) + ); + + const store = createTestStore(); + const SomeRedirect = jest.fn(() => <>); + const { container } = render( + + + + + } /> + + + + + + ); + await waitFor(() => expect(container).toHaveTextContent('Logging in')); + await waitFor(() => { + expect(store.getState().auth.initialized).toBe(true); + expect(store.getState().auth.token).toBe('FOOBARTOKEN'); + expect(store.getState().auth.username).toBe('someusername'); + expect(SomeRedirect).toHaveBeenCalled(); + }); + }); + + it('getLoginChoice fails gracefully', async () => { + const consoleError = jest.spyOn(console, 'error'); + consoleError.mockImplementation(noOp); + // getLoginChoice + fetchMock.mockResponseOnce('', { status: 500 }); + const Login = jest.fn(() => <>); + const store = createTestStore(); + render( + + + + + } /> + + + + + + ); + await waitFor(() => { + expect(store.getState().auth.initialized).toBe(false); + expect(toast).toHaveBeenCalled(); + expect(consoleError).toHaveBeenCalled(); + expect(Login).toHaveBeenCalled(); + }); + }); + + it('postLoginPick fails gracefully', async () => { + const consoleError = jest.spyOn(console, 'error'); + consoleError.mockImplementation(noOp); + // getLoginChoice + fetchMock.mockResponseOnce( + JSON.stringify({ + login: [ + { + id: 'foouserid', + policyids: [ + { + agreedon: 0, + id: 'foopolicy', + }, + ], + }, + ], + }) + ); + // postLoginPick + fetchMock.mockResponseOnce('', { status: 500 }); + + const Login = jest.fn(() => <>); + const store = createTestStore(); + render( + + + + + } /> + + + + + + ); + await waitFor(() => { + expect(store.getState().auth.initialized).toBe(false); + expect(toast).toHaveBeenCalled(); + expect(consoleError).toHaveBeenCalled(); + expect(Login).toHaveBeenCalled(); + }); + }); + + it('authFromToken fails gracefully', async () => { + const consoleError = jest.spyOn(console, 'error'); + consoleError.mockImplementation(noOp); + // getLoginChoice + fetchMock.mockResponseOnce( + JSON.stringify({ + login: [ + { + id: 'foouserid', + policyids: [ + { + agreedon: 0, + id: 'foopolicy', + }, + ], + }, + ], + }) + ); + // postLoginPick + fetchMock.mockResponseOnce( + JSON.stringify({ + token: { + token: 'foobartoken', + }, + }) + ); + // authFromToken + fetchMock.mockResponseOnce('', { status: 500 }); + const Login = jest.fn(() => <>); + const store = createTestStore(); + render( + + + + + } /> + + + + + + ); + await waitFor(() => { + expect(store.getState().auth.initialized).toBe(false); + expect(toast).toHaveBeenCalled(); + expect(consoleError).toHaveBeenCalled(); + expect(Login).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index bee8e548..d02f84cf 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -40,7 +40,7 @@ export const LogInContinue: FC = () => { const choiceData = choiceResult.data; // if/when postLoginPick has a result, update app auth state using that token - useTryAuthFromToken(pickResult.data?.token.token); + const tokenResult = useTryAuthFromToken(pickResult.data?.token.token); // wrap choiceData handling in an effect so we only trigger the pick call once useEffect(() => { @@ -65,7 +65,7 @@ export const LogInContinue: FC = () => { useEffect(() => { // Monitor error state, return to login - if (!pickResult.isError && !choiceResult.isError) { + if (!pickResult.isError && !choiceResult.isError && !tokenResult.isError) { return; } else { // eslint-disable-next-line no-console @@ -73,6 +73,7 @@ export const LogInContinue: FC = () => { 'login error(s)': { pick: pickResult.error, choice: choiceResult.error, + token: tokenResult.error, }, }); toast('An error occured during login, please try again.'); @@ -84,6 +85,8 @@ export const LogInContinue: FC = () => { navigate, pickResult.error, pickResult.isError, + tokenResult.error, + tokenResult.isError, ]); return ( From bd1634736cfdcfb9ae11fd09aaa6850a8e4bb0e7 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Tue, 3 Dec 2024 15:07:32 -0800 Subject: [PATCH 14/25] steps 1-2 (squashed), initial T&C commits --- package-lock.json | 486 ++++++++++++++++++++- package.json | 2 + src/app/Routes.tsx | 15 +- src/app/store.ts | 2 + src/common/api/authService.ts | 18 + src/features/auth/policies.module.scss | 8 + src/features/auth/policies.tsx | 87 ++++ src/features/login/LogInContinue.tsx | 12 +- src/features/signup/AccountInformation.tsx | 342 +++++++++++---- src/features/signup/KBasePolicies.tsx | 74 ++++ src/features/signup/ReferralSources.json | 47 ++ src/features/signup/SignUp.module.scss | 11 +- src/features/signup/SignUp.tsx | 16 +- src/features/signup/SignupSlice.tsx | 65 +++ src/features/signup/UsePolicies.tsx | 231 ---------- 15 files changed, 1053 insertions(+), 363 deletions(-) create mode 100644 src/features/auth/policies.module.scss create mode 100644 src/features/auth/policies.tsx create mode 100644 src/features/signup/KBasePolicies.tsx create mode 100644 src/features/signup/ReferralSources.json create mode 100644 src/features/signup/SignupSlice.tsx delete mode 100644 src/features/signup/UsePolicies.tsx diff --git a/package-lock.json b/package-lock.json index c545faec..47c98058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,9 @@ "d3-zoom": "^3.0.0", "dompurify": "^3.0.1", "downsample-lttb-ts": "^0.0.6", + "front-matter": "^4.0.2", "jest-fetch-mock": "^3.0.3", + "kbase-policies": "github:kbase/policies", "leaflet": "^1.9.4", "marked": "^4.2.12", "node-sass": "^9.0.0", @@ -4886,6 +4888,161 @@ "rollup": "^1.20.0 || ^2.0.0" } }, + "node_modules/@rollup/plugin-typescript": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", + "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@rollup/plugin-typescript/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/@rollup/plugin-typescript/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-url": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-url/-/plugin-url-8.0.2.tgz", + "integrity": "sha512-5yW2LP5NBEgkvIRSSEdJkmxe5cUNZKG3eenKtfJvSkxVm/xTTu7w+ayBtNwhozl1ZnTUCU0xFaRQR+cBl2H7TQ==", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "make-dir": "^3.1.0", + "mime": "^3.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-url/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-url/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@rollup/plugin-url/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/@rollup/plugin-url/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@rollup/plugin-url/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@rollup/plugin-url/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/pluginutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", @@ -4907,6 +5064,222 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz", + "integrity": "sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz", + "integrity": "sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz", + "integrity": "sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz", + "integrity": "sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz", + "integrity": "sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz", + "integrity": "sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz", + "integrity": "sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz", + "integrity": "sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz", + "integrity": "sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz", + "integrity": "sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz", + "integrity": "sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz", + "integrity": "sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz", + "integrity": "sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz", + "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz", + "integrity": "sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz", + "integrity": "sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz", + "integrity": "sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz", + "integrity": "sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -13988,17 +14361,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "license": "MIT" @@ -14009,12 +14371,6 @@ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "dev": true }, - "node_modules/buffer/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -19807,6 +20163,14 @@ "readable-stream": "^2.0.0" } }, + "node_modules/front-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", + "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", + "dependencies": { + "js-yaml": "^3.13.1" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "license": "MIT", @@ -25617,6 +25981,71 @@ "node": ">=8" } }, + "node_modules/kbase-policies": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/kbase/policies.git#3a645cd5f26b7d08f09e81ad36d17c893574b4c6", + "hasInstallScript": true, + "dependencies": { + "@rollup/plugin-typescript": "^12.1.1", + "@rollup/plugin-url": "^8.0.2", + "rollup": "^4.27.4", + "tslib": "^2.8.1", + "typescript": "^5.7.2" + } + }, + "node_modules/kbase-policies/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/kbase-policies/node_modules/rollup": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.0.tgz", + "integrity": "sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.28.0", + "@rollup/rollup-android-arm64": "4.28.0", + "@rollup/rollup-darwin-arm64": "4.28.0", + "@rollup/rollup-darwin-x64": "4.28.0", + "@rollup/rollup-freebsd-arm64": "4.28.0", + "@rollup/rollup-freebsd-x64": "4.28.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.0", + "@rollup/rollup-linux-arm-musleabihf": "4.28.0", + "@rollup/rollup-linux-arm64-gnu": "4.28.0", + "@rollup/rollup-linux-arm64-musl": "4.28.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.0", + "@rollup/rollup-linux-riscv64-gnu": "4.28.0", + "@rollup/rollup-linux-s390x-gnu": "4.28.0", + "@rollup/rollup-linux-x64-gnu": "4.28.0", + "@rollup/rollup-linux-x64-musl": "4.28.0", + "@rollup/rollup-win32-arm64-msvc": "4.28.0", + "@rollup/rollup-win32-ia32-msvc": "4.28.0", + "@rollup/rollup-win32-x64-msvc": "4.28.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/kbase-policies/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/kdbush": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", @@ -28013,6 +28442,23 @@ "vm-browserify": "^1.0.1" } }, + "node_modules/node-libs-browser/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/node-libs-browser/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/node-libs-browser/node_modules/path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -38389,9 +38835,9 @@ } }, "node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 2d3cc508..f06dcd81 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "d3-zoom": "^3.0.0", "dompurify": "^3.0.1", "downsample-lttb-ts": "^0.0.6", + "front-matter": "^4.0.2", "jest-fetch-mock": "^3.0.3", + "kbase-policies": "github:kbase/policies", "leaflet": "^1.9.4", "marked": "^4.2.12", "node-sass": "^9.0.0", diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index dd53491e..6e58a5d4 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -62,7 +62,7 @@ const Routes: FC = () => { } /> {/* Sign Up */} - } /> + } /> {/* Navigator */} { export const Authed: FC<{ element: ReactElement }> = ({ element }) => { const token = useAppSelector((state) => state.auth.token); const location = useLocation(); - if (!token) + if (!token) { return ( = ({ element }) => { replace /> ); + } //else if (!true) { + // ; + // } return <>{element}; }; diff --git a/src/app/store.ts b/src/app/store.ts index 5c028d8f..82835d55 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -8,6 +8,7 @@ import navigator from '../features/navigator/navigatorSlice'; import orcidlink from '../features/orcidlink/orcidlinkSlice'; import params from '../features/params/paramsSlice'; import profile from '../features/profile/profileSlice'; +import signup from '../features/signup/SignupSlice'; const everyReducer = combineReducers({ auth, @@ -18,6 +19,7 @@ const everyReducer = combineReducers({ params, profile, orcidlink, + signup, [baseApi.reducerPath]: baseApi.reducer, }); diff --git a/src/common/api/authService.ts b/src/common/api/authService.ts index c56b0bce..b55fa010 100644 --- a/src/common/api/authService.ts +++ b/src/common/api/authService.ts @@ -32,6 +32,7 @@ interface AuthParams { }; getLoginChoice: void; postLoginPick: { id: string; policyids: string[] }; + loginUsernameSuggest: string; } interface AuthResults { @@ -86,6 +87,7 @@ interface AuthResults { user: string; }; }; + loginUsernameSuggest: { availablename: string }; } // Auth does not use JSONRpc, so we use queryFn to make custom queries @@ -177,6 +179,20 @@ export const authApi = baseApi.injectEndpoints({ method: 'POST', }), }), + loginUsernameSuggest: builder.query< + AuthResults['loginUsernameSuggest'], + AuthParams['loginUsernameSuggest'] + >({ + query: (username) => + // MUST have an in-process-login-token cookie + authService({ + headers: { + accept: 'application/json', + }, + method: 'GET', + url: `/login/suggestname/${encodeURIComponent(username)}`, + }), + }), }), }); @@ -188,4 +204,6 @@ export const { revokeToken, getLoginChoice, postLoginPick, + loginUsernameSuggest, } = authApi.endpoints; +export type GetLoginChoiceResult = AuthResults['getLoginChoice']; diff --git a/src/features/auth/policies.module.scss b/src/features/auth/policies.module.scss new file mode 100644 index 00000000..c55bbe92 --- /dev/null +++ b/src/features/auth/policies.module.scss @@ -0,0 +1,8 @@ +.agreement-box { + border: 1px solid use-color("base"); + border-radius: 4px; + display: inline-block; + margin-top: 1rem; + padding: 1rem; + +} diff --git a/src/features/auth/policies.tsx b/src/features/auth/policies.tsx new file mode 100644 index 00000000..22d7ee0f --- /dev/null +++ b/src/features/auth/policies.tsx @@ -0,0 +1,87 @@ +import policyStrings from 'kbase-policies'; +import frontmatter from 'front-matter'; +import { + Box, + Checkbox, + FormControl, + FormControlLabel, + Paper, + Typography, +} from '@mui/material'; +import classes from './policies.module.scss'; +import createDOMPurify from 'dompurify'; +import { marked } from 'marked'; + +const purify = createDOMPurify(window); + +interface PolicyMeta { + title: string; + id: string; + version: string; + equivalentVersions: string[]; +} + +export const kbasePolicies = policyStrings.reduce( + (policies, str) => { + const parsed = frontmatter(str); + const attr = parsed.attributes as PolicyMeta; + const policy = { + raw: str, + markdown: parsed.body, + title: String(attr.title) ?? '', + id: String(attr.id) ?? '', + version: String(attr.version) ?? '', + equivalentVersions: (attr.equivalentVersions ?? []) as string[], + }; + policies[policy.id] = policy; + return policies; + }, + {} as Record< + string, + PolicyMeta & { + raw: string; + markdown: string; + } + > +); + +export const PolicyViewer = ({ + policyId, + setAccept, + accepted = undefined, +}: { + policyId: string; + setAccept: (accepted: boolean) => void; + accepted?: boolean; +}) => { + const policy = kbasePolicies[policyId]; + if (!policy) + throw new Error(`Required policy "${policyId}" cannot be loaded`); + return ( + + {policy.title} + +
+ +
+ + { + setAccept(e.currentTarget.checked); + }} + /> + } + label="I have read and agree to this policy" + /> + +
+ + ); +}; diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index d02f84cf..8d489c63 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -9,9 +9,13 @@ import { useCheckLoggedIn } from './LogIn'; import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import { LOGIN_ROUTE } from '../../app/Routes'; +import { useAppDispatch } from '../../common/hooks'; +import { setLoginData } from '../signup/SignupSlice'; export const LogInContinue: FC = () => { const [trigger, pickResult] = postLoginPick.useMutation(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); // Redirect logic is somewhat odd due to how state must be passed with the Auth service. // Instead of redirecting to redirecturl, we extract the state param and from that, which @@ -46,7 +50,6 @@ export const LogInContinue: FC = () => { useEffect(() => { if (choiceData) { const accountExists = choiceData.login.length > 0; - // TODO: support choiceData.create cases // TODO: support policy enforcement if (accountExists) { if (choiceData.login.length > 1) { @@ -57,11 +60,12 @@ export const LogInContinue: FC = () => { policyids: choiceData.login[0].policyids.map(({ id }) => id), }); } + } else if (choiceData.create.length > 0) { + dispatch(setLoginData(choiceData)); + navigate('/signup/2'); } } - }, [choiceData, trigger]); - - const navigate = useNavigate(); + }, [choiceData, trigger, dispatch, navigate]); useEffect(() => { // Monitor error state, return to login diff --git a/src/features/signup/AccountInformation.tsx b/src/features/signup/AccountInformation.tsx index 324c7170..50b766a3 100644 --- a/src/features/signup/AccountInformation.tsx +++ b/src/features/signup/AccountInformation.tsx @@ -17,23 +17,115 @@ import { TextField, Typography, } from '@mui/material'; -import { FC } from 'react'; +import { FC, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import { useNavigate } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../common/hooks'; import classes from './SignUp.module.scss'; +import ReferalSources from './ReferralSources.json'; +import { loginUsernameSuggest } from '../../common/api/authService'; +import { useForm } from 'react-hook-form'; +import { setAccount, setProfile } from './SignupSlice'; /** * Account information form for sign up flow */ export const AccountInformation: FC<{ - setActiveStep: React.Dispatch>; + setActiveStep: (step: number) => void; }> = ({ setActiveStep }) => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + // Login Data + const loginData = useAppSelector( + (state) => + state.signup.loginData || { + provider: 'ORCiD', + create: [ + { + availablename: 'dlyon', + id: 'someuserid', + provemail: 'dlyon@lbl.gov', + provfullname: 'David Lyon', + provusername: 'dlyon', + }, + ], + } + ); + + // Account data + const account = useAppSelector((state) => state.signup.account); + + if (!loginData) { + toast('You must login using a provider first to sign up!'); + navigate('/signup/1'); + } + + //username availibility + const [username, setUsername] = useState(account.username ?? ''); + const userAvail = loginUsernameSuggest.useQuery(username); + const nameShort = username.length < 3; + const nameAvail = + userAvail.currentData?.availablename === username.toLowerCase(); + + const surveyQuestion = 'How did you hear about us? (select all that apply)'; + const [optionalText, setOptionalText] = useState>({}); + + // Form + const { register, handleSubmit } = useForm({ + defaultValues: { + account: account, + profile: { + userdata: { + organization: '', + department: '', + }, + surveydata: { + referralSources: { + question: surveyQuestion, + response: {} as Record, + }, + }, + }, + }, + }); + + const onSubmit = handleSubmit(async (fieldValues, event) => { + event?.preventDefault(); + // Add in the optional survey text content + ReferalSources.forEach((src) => { + if ( + src.customText && + fieldValues.profile.surveydata.referralSources.response[src.value] === + true + ) + fieldValues.profile.surveydata.referralSources.response[src.value] = + optionalText[src.value]; + }); + // dispatch form data to state + dispatch(setAccount(fieldValues.account)); + dispatch( + setProfile({ + userdata: { + ...fieldValues.profile.userdata, + avatarOption: 'gravatar', + gravatarDefault: 'identicon', + }, + surveydata: fieldValues.profile.surveydata, + }) + ); + // next step! + navigate('/signup/3'); + }); + return ( - You have signed in with your Google account{' '} - ctodonnell@lbl.gov. This will be the account linked - to your KBase account. + You have signed in with your {loginData.provider}{' '} + account {loginData.create[0].provemail}. This will + be the account linked to your KBase account. If the account you see above is not the one you want, use the - link below to log out of Google, and then try again. + link below to log out of {loginData.provider}, and then try + again. - + - If you are trying to sign up with a Google account that is - already linked to a KBase account, you will be unable to - create a new KBase account using that Google account. + If you are trying to sign up with a {loginData.provider}{' '} + account that is already linked to a KBase account, you will be + unable to create a new KBase account using that{' '} + {loginData.provider} account. - After signing out from Google you will need to restart the - sign up process. + After signing out from {loginData.provider} you will need to + restart the sign up process. @@ -69,93 +165,155 @@ export const AccountInformation: FC<{ - - - Create a new KBase Account - - Some field values have been pre-populated from your{' '} - Google account. - All fields are required. - - - Full Name - - - - Email - - - - KBase Username - - - - Organization - - - - Department - - - - - How did you hear about us? (Select all that apply) - - - } - label="Journal Publication" + + + + Create a new KBase Account + + Some field values have been pre-populated from your{' '} + {loginData.provider} account. + All fields are required. + + + Full Name + - } - label="Conference Presentation" + + + Email + ()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + message: 'Invalid email address', + }, + })} + defaultValue={account.email} + helperText="KBase may occasionally use this email address to communicate important information about KBase or your account. KBase will not share your email address with anyone, and other KBase users will not be able to see it." /> - } - label="Workshop/Webinar" + + + KBase Username + setUsername(e.currentTarget.value), + validate: () => + !nameShort && !userAvail.isFetching && nameAvail, + })} + defaultValue={account.username} + helperText={ + <> + {nameShort ? ( + + Username is too short. +
+
+ ) : !nameAvail && !userAvail.isFetching ? ( + + Username is not available. Suggested: " + {userAvail.currentData?.availablename}". +
+
+ ) : undefined} + + Your KBase username is the primary identifier associated + with all of your work and assets within KBase.Your + username is permanent and may not be changed later, so + please choose wisely. + + + } + error={nameShort || (!userAvail.isFetching && !nameAvail)} /> - } label="Colleague" /> - } - label="Course/Instructor" +
+ + Organization + - } - label="Newsletter/Email" + + + Department + - } label="YouTube" /> - } label="Twitter" /> - } label="Search Engine" /> - } - label="Online Advertisement" + + + {surveyQuestion} + - } label="Other" /> -
-
+ + {ReferalSources.map((source) => { + if (source.customText) { + return ( + <> + + } + label={source.label} + /> + + { + setOptionalText((s) => ({ + ...s, + [source.value]: e.target.value, + })); + }} + /> + + + ); + } else { + return ( + + } + label={source.label} + /> + ); + } + })} + + +
+
+ + + - - - - - +
); }; diff --git a/src/features/signup/KBasePolicies.tsx b/src/features/signup/KBasePolicies.tsx new file mode 100644 index 00000000..3b18a179 --- /dev/null +++ b/src/features/signup/KBasePolicies.tsx @@ -0,0 +1,74 @@ +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button, Paper, Stack, Typography } from '@mui/material'; +import { FC, useState } from 'react'; +import { PolicyViewer } from '../auth/policies'; +import classes from './SignUp.module.scss'; + +const signupPolicies = ['kbase-user'] as const; + +/** + * Use policy agreements for sign up flow. + */ +export const KBasePolicies: FC<{ + setActiveStep: (step: number) => void; +}> = ({ setActiveStep }) => { + const [accepted, setAccepted] = useState<{ + [k in typeof signupPolicies[number]]?: boolean; + }>({}); + + const createOk = signupPolicies.every( + (policyId) => accepted[policyId] === true + ); + + return ( + + + + KBase Use Policies + + To finish signing up and create your account, you must agree to the + following KBase use policies. + + {signupPolicies.map((policyId) => { + return ( + + setAccepted((current) => { + return { ...current, policyId: val }; + }) + } + /> + ); + })} + + + + + + + + + + ); +}; diff --git a/src/features/signup/ReferralSources.json b/src/features/signup/ReferralSources.json new file mode 100644 index 00000000..e4a4213a --- /dev/null +++ b/src/features/signup/ReferralSources.json @@ -0,0 +1,47 @@ +[ + { + "label": "Journal Publication", + "value": "journal-publication" + }, + { + "label": "Conference Presentation", + "value": "conference-presentation" + }, + { + "label": "Workshop/Webinar", + "value": "workshop-webinar" + }, + { + "label": "Colleague", + "value": "colleague" + }, + { + "label": "Course/Instructor", + "value": "course" + }, + { + "label": "Newsletter/Email", + "value": "newsletter-email" + }, + { + "label": "YouTube", + "value": "youtube" + }, + { + "label": "Twitter", + "value": "twitter" + }, + { + "label": "Search Engine", + "value": "search-engine" + }, + { + "label": "Online Advertisement", + "value": "online-ad" + }, + { + "label": "Other...", + "value": "other", + "customText": true + } +] diff --git a/src/features/signup/SignUp.module.scss b/src/features/signup/SignUp.module.scss index f7038417..9314d91d 100644 --- a/src/features/signup/SignUp.module.scss +++ b/src/features/signup/SignUp.module.scss @@ -33,7 +33,7 @@ padding-left: 0.25rem; padding-right: 0; } - + :global(.MuiAccordionSummary-content) { margin-bottom: 0.25rem; margin-left: 0.5rem; @@ -69,12 +69,3 @@ padding-left: 1rem; } } - -.agreement-box { - border: 1px solid use-color("base"); - border-radius: 4px; - display: inline-block; - margin-top: 1rem; - padding: 1rem; - -} diff --git a/src/features/signup/SignUp.tsx b/src/features/signup/SignUp.tsx index 9c674e9e..0504481f 100644 --- a/src/features/signup/SignUp.tsx +++ b/src/features/signup/SignUp.tsx @@ -6,10 +6,11 @@ import { Stepper, Typography, } from '@mui/material'; -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import { AccountInformation } from './AccountInformation'; import { ProviderSelect } from './ProviderSelect'; -import { UsePolicies } from './UsePolicies'; +import { KBasePolicies } from './KBasePolicies'; const signUpSteps = [ 'Sign up with a supported provider', @@ -22,7 +23,14 @@ const signUpSteps = [ * and accepting the KBase use policies. */ export const SignUp: FC = () => { - const [activeStep, setActiveStep] = useState(0); + const navigate = useNavigate(); + + const { step = '1' } = useParams(); + const activeStep = Number.parseInt(step) - 1; + + const setActiveStep = (step: number) => { + navigate(`/signup/${step + 1}`); + }; useEffect(() => { document.querySelector('main')?.scrollTo(0, 0); @@ -43,7 +51,7 @@ export const SignUp: FC = () => { {activeStep === 1 && ( )} - {activeStep === 2 && } + {activeStep === 2 && } ); diff --git a/src/features/signup/SignupSlice.tsx b/src/features/signup/SignupSlice.tsx new file mode 100644 index 00000000..b6c61738 --- /dev/null +++ b/src/features/signup/SignupSlice.tsx @@ -0,0 +1,65 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { GetLoginChoiceResult } from '../../common/api/authService'; + +// Define a type for the slice state +export interface SignupState { + loginData?: GetLoginChoiceResult; + account: { + display?: string; + email?: string; + username?: string; + policyids: string[]; + }; + profile?: { + userdata: { + organization: string; + department: string; + avatarOption: 'gravatar'; + gravatarDefault: 'identicon'; + }; + surveydata: { + referralSources: { + question: string; + response: Record; + }; + }; + }; +} + +const initialState: SignupState = { + loginData: undefined, + account: { + display: 'someName', + email: 'someEmail@email.co', + username: 'someUser', + policyids: [], + }, + profile: undefined, +}; + +export const signupSlice = createSlice({ + name: 'signup', + initialState, + reducers: { + setLoginData: (state, action: PayloadAction) => { + // Set provider creeation data + state.loginData = action.payload; + // Set account defaults from provider + state.account.display = action.payload?.create[0].provfullname; + state.account.email = action.payload?.create[0].provemail; + state.account.username = action.payload?.create[0].availablename; + }, + setAccount: ( + state, + action: PayloadAction> + ) => { + state.account = { ...state.account, ...action.payload }; + }, + setProfile: (state, action: PayloadAction) => { + state.profile = action.payload; + }, + }, +}); + +export default signupSlice.reducer; +export const { setLoginData, setAccount, setProfile } = signupSlice.actions; diff --git a/src/features/signup/UsePolicies.tsx b/src/features/signup/UsePolicies.tsx deleted file mode 100644 index e5c807cb..00000000 --- a/src/features/signup/UsePolicies.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - Box, - Button, - Checkbox, - FormControl, - FormControlLabel, - Link, - Paper, - Stack, - Typography, -} from '@mui/material'; -import { FC } from 'react'; -import classes from './SignUp.module.scss'; - -/** - * Use policy agreements for sign up flow. - */ -export const UsePolicies: FC<{ - setActiveStep: React.Dispatch>; -}> = ({ setActiveStep }) => { - return ( - - - - KBase Use Policies - - To finish signing up and create your account, you must agree to the - following KBase use policies. - - - KBase Use Agreement - - - Terms and Conditions - Prohibited Behavior - - As a condition of your use of KBase (the DOE Systems Biology - Knowledgebase) you agree not to upload any type of human data - or personally identifiable information. -
- Improper use of KBase, including uploading human data, may - result in the termination of KBase access privileges. -
-
- Data Sharing - - KBase conforms to the{' '} - - Information and Data Sharing Policy - {' '} - of the Genomic Science Program of the Office of Biological and - Environmental Research within the Office of Science. Please - see the{' '} - - KBase Data Policy - {' '} - page for more information. - - Responsibility for Data - - As a condition of your use of KBase you accept sole - responsibility for all files you upload or transfer through - use of KBase. You recognize and accept that{' '} - - KBase does not guarantee long-term retention of user - uploaded data - - , and will not be responsible for any failure to store or - transfer, or deletion, corruption or loss for any data, - information or content contained in your files. It is strongly - recommended that you back up all files prior to using KBase. - - Use Agreement - - By using KBase, including its websites and services or via - published APIs, you are agreeing to the terms stated in our{' '} - - Use Agreement - - . Please read them carefully. They include limitations on what - is acceptable user behavior, accountability, availability, - data retention, and conditions for account termination. If you - do not agree, do not access or use KBase. - - Privacy Policy - - KBase is provided as a public service. KBase reserves the - right to monitor any and all use of kbase.us. KBase never - collects information for commercial marketing or any purpose - unrelated to KBase functions. The{' '} - - Privacy Policy - {' '} - describes the ways KBase collects, stores, uses, discloses and - protects the personal information about users and how they use - KBase. - - Open Source License - - All software developed by the KBase project team, and any - contributed by you to KBase, is stored and maintained in the - public{' '} - - KBase GitHub code repository - {' '} - under the{' '} - - MIT Open Source License - {' '} - (“License”). By contributing to or using KBase, you - acknowledge having read and understood the License and agree - to abide by it. - -
-
-
- - } - label="I have read and agree to this policy" - /> - -
-
- - KBase Data Policy - - - Data Policy - Data Policies - - KBase conforms to the{' '} - - Information and Data Sharing Policy - {' '} - of the Genomic Science Program of the Office of Biological and - Environmental Research within the Office of Science. This - requires that all publishable data, metadata, and software - resulting from research funded by the Genomic Science program - must conform to community-recognized standard formats when - they exist; be clearly attributable; and be deposited within a - community-recognized public database(s) appropriate for the - research. - - - - Data publicly available in KBase - {' '} - comes from the sources listed on this page. Additionally, - users can upload their own data to KBase to analyze it, and - can choose how widely their data should be shared. (All data - uploaded by users is private to them unless they choose to - share it.){' '} - - -
- NOTICE: KBase does not guarantee long-term retention of - user-uploaded data. Please take appropriate precautions in - storing and backing up your data locally. -
-
- -
- WARNING: Improper use of KBase, including uploading human - data, may result in the termination of KBase access - privileges. Please see the{' '} - - Terms and Conditions - {' '} - page for more information. -
-
-
-
-
- - } - label="I have read and agree to this policy" - /> - -
-
-
-
- - - - - - -
- ); -}; From 284fd3c381a6aec5dd93f72068c866bb95208795 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 4 Dec 2024 13:11:54 -0800 Subject: [PATCH 15/25] policy enforcment and signup API commits (squashed) --- package-lock.json | 6 ++ package.json | 1 + src/app/Routes.tsx | 15 +--- src/common/api/authService.ts | 25 ++++++ src/common/api/userProfileApi.ts | 22 +++-- src/common/api/utils/kbaseBaseQuery.ts | 2 +- src/common/types/auth.ts | 2 +- ...s.module.scss => PolicyViewer.module.scss} | 0 src/features/auth/policies.tsx | 8 +- src/features/login/EnforcePolicies.tsx | 69 +++++++++++++++ src/features/login/LogInContinue.tsx | 61 ++++++++++++-- src/features/signup/SignUp.tsx | 83 ++++++++++++++++++- .../{KBasePolicies.tsx => SignupPolicies.tsx} | 61 ++++++++++++-- 13 files changed, 312 insertions(+), 43 deletions(-) rename src/features/auth/{policies.module.scss => PolicyViewer.module.scss} (100%) create mode 100644 src/features/login/EnforcePolicies.tsx rename src/features/signup/{KBasePolicies.tsx => SignupPolicies.tsx} (53%) diff --git a/package-lock.json b/package-lock.json index 47c98058..9b64d2f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "downsample-lttb-ts": "^0.0.6", "front-matter": "^4.0.2", "jest-fetch-mock": "^3.0.3", + "js-md5": "^0.8.3", "kbase-policies": "github:kbase/policies", "leaflet": "^1.9.4", "marked": "^4.2.12", @@ -25739,6 +25740,11 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==" + }, "node_modules/js-sdsl": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", diff --git a/package.json b/package.json index f06dcd81..ab84b34e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "downsample-lttb-ts": "^0.0.6", "front-matter": "^4.0.2", "jest-fetch-mock": "^3.0.3", + "js-md5": "^0.8.3", "kbase-policies": "github:kbase/policies", "leaflet": "^1.9.4", "marked": "^4.2.12", diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index 6e58a5d4..a7ee9d47 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -34,6 +34,7 @@ import { SignUp } from '../features/signup/SignUp'; import ORCIDLinkCreateLink from '../features/orcidlink/CreateLink'; export const LOGIN_ROUTE = '/login'; +export const SIGNUP_ROUTE = '/signup'; export const ROOT_REDIRECT_ROUTE = '/narratives'; const Routes: FC = () => { @@ -62,7 +63,7 @@ const Routes: FC = () => { } /> {/* Sign Up */} - } /> + } /> {/* Navigator */} = ({ element }) => { replace /> ); - } //else if (!true) { - // ; - // } + } return <>{element}; }; diff --git a/src/common/api/authService.ts b/src/common/api/authService.ts index b55fa010..d41a204e 100644 --- a/src/common/api/authService.ts +++ b/src/common/api/authService.ts @@ -33,6 +33,14 @@ interface AuthParams { getLoginChoice: void; postLoginPick: { id: string; policyids: string[] }; loginUsernameSuggest: string; + loginCreate: { + id: string; + user: string; + display: string; + email: string; + linkall: false; + policyids: string[]; + }; } interface AuthResults { @@ -88,6 +96,7 @@ interface AuthResults { }; }; loginUsernameSuggest: { availablename: string }; + loginCreate: AuthResults['postLoginPick']; } // Auth does not use JSONRpc, so we use queryFn to make custom queries @@ -193,6 +202,21 @@ export const authApi = baseApi.injectEndpoints({ url: `/login/suggestname/${encodeURIComponent(username)}`, }), }), + loginCreate: builder.mutation< + AuthResults['loginCreate'], + AuthParams['loginCreate'] + >({ + query: (params) => + // MUST have an in-process-login-token cookie + authService({ + headers: { + accept: 'application/json', + }, + method: 'POST', + body: params, + url: `/login/create/`, + }), + }), }), }); @@ -205,5 +229,6 @@ export const { getLoginChoice, postLoginPick, loginUsernameSuggest, + loginCreate, } = authApi.endpoints; export type GetLoginChoiceResult = AuthResults['getLoginChoice']; diff --git a/src/common/api/userProfileApi.ts b/src/common/api/userProfileApi.ts index a2f8b2cb..4b62bd23 100644 --- a/src/common/api/userProfileApi.ts +++ b/src/common/api/userProfileApi.ts @@ -8,12 +8,15 @@ const userProfile = jsonRpcService({ interface UserProfileParams { status: void; get_user_profile: { usernames: string[] }; - set_user_profile: { + set_user_profile: [ profile: { - user: { username: string; realname: string }; - profile: unknown; - }; - }; + profile: { + user: { username: string; realname: string }; + profile: unknown; + }; + }, + optionalToken: string | undefined + ]; } interface UserProfileResults { @@ -64,13 +67,16 @@ export const userProfileApi = baseApi UserProfileResults['set_user_profile'], UserProfileParams['set_user_profile'] >({ - query: ({ profile }) => + query: ([profile, optionalToken]) => userProfile({ + fetchArgs: optionalToken + ? { headers: { Authorization: optionalToken } } + : {}, method: 'UserProfile.set_user_profile', - params: [{ profile }], + params: [profile], }), // Invalidates the cache for any queries with a matching tag - invalidatesTags: (result, error, { profile }) => [ + invalidatesTags: (result, error, [{ profile }]) => [ { type: 'Profile', id: profile.user.username }, ], }), diff --git a/src/common/api/utils/kbaseBaseQuery.ts b/src/common/api/utils/kbaseBaseQuery.ts index eff973d4..f77fce14 100644 --- a/src/common/api/utils/kbaseBaseQuery.ts +++ b/src/common/api/utils/kbaseBaseQuery.ts @@ -28,7 +28,7 @@ export interface JsonRpcQueryArgs { service: StaticService | DynamicService; method: string; params?: unknown; - fetchArgs?: FetchArgs; + fetchArgs?: Partial; } export interface JSONRPC11Body { diff --git a/src/common/types/auth.ts b/src/common/types/auth.ts index bb25fc85..508377b3 100644 --- a/src/common/types/auth.ts +++ b/src/common/types/auth.ts @@ -8,7 +8,7 @@ export interface Me { idents: Record[]; lastlogin: number; local: boolean; - policyids: Record[]; + policyids: { id: string; agreedon: number }[]; roles: Record[]; user: string; } diff --git a/src/features/auth/policies.module.scss b/src/features/auth/PolicyViewer.module.scss similarity index 100% rename from src/features/auth/policies.module.scss rename to src/features/auth/PolicyViewer.module.scss diff --git a/src/features/auth/policies.tsx b/src/features/auth/policies.tsx index 22d7ee0f..8d6bbeee 100644 --- a/src/features/auth/policies.tsx +++ b/src/features/auth/policies.tsx @@ -8,10 +8,12 @@ import { Paper, Typography, } from '@mui/material'; -import classes from './policies.module.scss'; +import classes from './PolicyViewer.module.scss'; import createDOMPurify from 'dompurify'; import { marked } from 'marked'; +export const ENFORCED_POLICIES = ['kbase-user']; + const purify = createDOMPurify(window); interface PolicyMeta { @@ -33,7 +35,7 @@ export const kbasePolicies = policyStrings.reduce( version: String(attr.version) ?? '', equivalentVersions: (attr.equivalentVersions ?? []) as string[], }; - policies[policy.id] = policy; + if (ENFORCED_POLICIES.includes(policy.id)) policies[policy.id] = policy; return policies; }, {} as Record< @@ -48,7 +50,7 @@ export const kbasePolicies = policyStrings.reduce( export const PolicyViewer = ({ policyId, setAccept, - accepted = undefined, + accepted = false, }: { policyId: string; setAccept: (accepted: boolean) => void; diff --git a/src/features/login/EnforcePolicies.tsx b/src/features/login/EnforcePolicies.tsx new file mode 100644 index 00000000..76bd5286 --- /dev/null +++ b/src/features/login/EnforcePolicies.tsx @@ -0,0 +1,69 @@ +import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Alert, Button, Container, Paper } from '@mui/material'; +import { Stack } from '@mui/system'; +import { useState } from 'react'; +import classes from '../signup/SignUp.module.scss'; +import { kbasePolicies, PolicyViewer } from '../auth/Policies'; + +export const EnforcePolicies = ({ + policyIds, + onAccept, +}: { + policyIds: string[]; + onAccept: (versionedPolicyIds: string[]) => void; +}) => { + const targetPolicies = policyIds.map((id) => kbasePolicies[id]); + + const [accepted, setAccepted] = useState<{ + [k in typeof targetPolicies[number]['id']]?: boolean; + }>({}); + + const allAccepted = targetPolicies.every( + (policy) => accepted[policy.id] === true + ); + + return ( + + + + + To continue using your account, you must agree to the following + KBase use policies. + + {targetPolicies.map((policy) => { + return ( + + setAccepted((current) => { + return { ...current, [policy.id]: val }; + }) + } + /> + ); + })} + + + + + + + ); +}; diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index 8d489c63..11ab8dfd 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -1,5 +1,5 @@ import { Container, Paper, Stack, Typography } from '@mui/material'; -import { FC, useEffect } from 'react'; +import { FC, useEffect, useMemo, useState } from 'react'; import logoRectangle from '../../common/assets/logo/rectangle.png'; import classes from './LogIn.module.scss'; import { Loader } from '../../common/components'; @@ -11,9 +11,11 @@ import { useNavigate } from 'react-router-dom'; import { LOGIN_ROUTE } from '../../app/Routes'; import { useAppDispatch } from '../../common/hooks'; import { setLoginData } from '../signup/SignupSlice'; +import { kbasePolicies } from '../auth/Policies'; +import { EnforcePolicies } from './EnforcePolicies'; export const LogInContinue: FC = () => { - const [trigger, pickResult] = postLoginPick.useMutation(); + const [triggerPick, pickResult] = postLoginPick.useMutation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -43,10 +45,30 @@ export const LogInContinue: FC = () => { const choiceResult = getLoginChoice.useQuery(); const choiceData = choiceResult.data; + const policyids = choiceData?.login[0].policyids; + // Check for missing policies + const missingPolicies = useMemo( + () => + Object.values(kbasePolicies).filter((policy) => { + const policyVersionsOk = [ + policy.version, + ...policy.equivalentVersions, + ].map((version) => `${policy.id}.${version}`); + return !policyids?.find((policy) => + policyVersionsOk.find((policyVersion) => policyVersion === policy.id) + ); + }), + [policyids] + ); + const [agreedPolicyIds, setAgreedPolicyIds] = useState([]); + const allNewPolicyAgreed = missingPolicies.every((p) => + agreedPolicyIds.includes([p.id, p.version].join('.')) + ); + // if/when postLoginPick has a result, update app auth state using that token const tokenResult = useTryAuthFromToken(pickResult.data?.token.token); - // wrap choiceData handling in an effect so we only trigger the pick call once + // wrap choiceData handling in an effect so we only triggerPick the pick call once useEffect(() => { if (choiceData) { const accountExists = choiceData.login.length > 0; @@ -55,17 +77,29 @@ export const LogInContinue: FC = () => { 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), - }); + if (allNewPolicyAgreed) { + const existingPolicyIds = choiceData.login[0].policyids.map( + ({ id }) => id + ); + triggerPick({ + id: choiceData.login[0].id, + policyids: [...agreedPolicyIds, ...existingPolicyIds], + }); + } } } else if (choiceData.create.length > 0) { dispatch(setLoginData(choiceData)); navigate('/signup/2'); } } - }, [choiceData, trigger, dispatch, navigate]); + }, [ + choiceData, + triggerPick, + dispatch, + navigate, + allNewPolicyAgreed, + agreedPolicyIds, + ]); useEffect(() => { // Monitor error state, return to login @@ -93,6 +127,17 @@ export const LogInContinue: FC = () => { tokenResult.isError, ]); + if (!allNewPolicyAgreed) { + return ( + p.id)} + onAccept={(accepted) => { + setAgreedPolicyIds([...accepted]); + }} + /> + ); + } + return ( diff --git a/src/features/signup/SignUp.tsx b/src/features/signup/SignUp.tsx index 0504481f..a0a1b871 100644 --- a/src/features/signup/SignUp.tsx +++ b/src/features/signup/SignUp.tsx @@ -8,9 +8,14 @@ import { } from '@mui/material'; import { FC, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { loginCreate } from '../../common/api/authService'; +import { setUserProfile } from '../../common/api/userProfileApi'; +import { useAppSelector } from '../../common/hooks'; +import { useTryAuthFromToken } from '../auth/hooks'; import { AccountInformation } from './AccountInformation'; import { ProviderSelect } from './ProviderSelect'; -import { KBasePolicies } from './KBasePolicies'; +import { KBasePolicies } from './SignupPolicies'; +import { md5 } from 'js-md5'; const signUpSteps = [ 'Sign up with a supported provider', @@ -56,3 +61,79 @@ export const SignUp: FC = () => { ); }; + +export const useDoSignup = () => { + const data = useAppSelector((state) => state.signup); + + const signupOk = !!data.loginData; + const [triggerAccount, accountResult] = loginCreate.useMutation(); + const [triggerProfile, profileResult] = setUserProfile.useMutation(); + + const loading = + !accountResult.isUninitialized && + (accountResult.isLoading || profileResult.isLoading); + + const complete = + !loading && + !accountResult.isUninitialized && + !profileResult.isUninitialized && + accountResult.isSuccess && + profileResult.isSuccess; + + const error = accountResult.error || profileResult.error; + + const doSignup = async () => { + if (!signupOk) return; + triggerAccount({ + id: String(data.loginData?.create[0].id), + user: String(data.account.username), + display: String(data.account.display), + email: String(data.account.email), + policyids: data.account.policyids, + linkall: false, + }); + }; + + useEffect(() => { + triggerProfile([ + { + profile: { + user: { + realname: String(data.account.display), + username: String(data.account.username), + }, + profile: { + metadata: { + createdBy: 'ui_europa', + created: new Date().toISOString(), + }, + // was globus info, no longer used + preferences: {}, + synced: { + gravatarHash: gravatarHash(data.account.email || ''), + }, + ...data.profile, + }, + }, + }, + accountResult.data?.token.token ?? '', + ]); + }, [ + accountResult, + data.account.display, + data.account.email, + data.account.username, + data.profile, + triggerProfile, + ]); + + // Once everything completes, try auth from token. + const tryToken = complete ? accountResult.data.token.token : undefined; + useTryAuthFromToken(tryToken); + + return [signupOk, doSignup, loading, complete, error] as const; +}; + +const gravatarHash = (email: string) => { + return md5.create().update(email.trim().toLowerCase()).hex(); +}; diff --git a/src/features/signup/KBasePolicies.tsx b/src/features/signup/SignupPolicies.tsx similarity index 53% rename from src/features/signup/KBasePolicies.tsx rename to src/features/signup/SignupPolicies.tsx index 3b18a179..28dd4782 100644 --- a/src/features/signup/KBasePolicies.tsx +++ b/src/features/signup/SignupPolicies.tsx @@ -2,10 +2,14 @@ import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, Paper, Stack, Typography } from '@mui/material'; import { FC, useState } from 'react'; -import { PolicyViewer } from '../auth/policies'; +import { Navigate } from 'react-router-dom'; +import { ROOT_REDIRECT_ROUTE } from '../../app/Routes'; +import { Loader } from '../../common/components'; +import { useAppDispatch } from '../../common/hooks'; +import { kbasePolicies, PolicyViewer } from '../auth/Policies'; +import { useDoSignup } from './SignUp'; import classes from './SignUp.module.scss'; - -const signupPolicies = ['kbase-user'] as const; +import { setAccount } from './SignupSlice'; /** * Use policy agreements for sign up flow. @@ -13,14 +17,47 @@ const signupPolicies = ['kbase-user'] as const; export const KBasePolicies: FC<{ setActiveStep: (step: number) => void; }> = ({ setActiveStep }) => { + const dispatch = useAppDispatch(); + + const signupPolicies = Object.values(kbasePolicies).map((p) => p.id); + const [accepted, setAccepted] = useState<{ [k in typeof signupPolicies[number]]?: boolean; }>({}); - const createOk = signupPolicies.every( + const allAccepted = signupPolicies.every( (policyId) => accepted[policyId] === true ); + const [signupOk, doSignup, loading, complete, errors] = useDoSignup(); + // eslint-disable-next-line no-console + console.error(errors); + + const onSubmit = async () => { + await dispatch( + setAccount({ + policyids: signupPolicies.map((policyId) => { + return [ + kbasePolicies[policyId].id, + kbasePolicies[policyId].version, + ].join('.'); + }), + }) + ); + doSignup(); + }; + + if (complete) { + return ( + + ); + } + return ( @@ -30,14 +67,14 @@ export const KBasePolicies: FC<{ To finish signing up and create your account, you must agree to the following KBase use policies. - {signupPolicies.map((policyId) => { + {Object.values(kbasePolicies).map((policy) => { return ( setAccepted((current) => { - return { ...current, policyId: val }; + return { ...current, [policy.id]: val }; }) } /> @@ -46,7 +83,13 @@ export const KBasePolicies: FC<{ - From 16b2caaa48ffa6f3304e43ddddad8bc082eaf8b8 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 4 Dec 2024 13:19:18 -0800 Subject: [PATCH 16/25] fix filename casing issue --- src/features/auth/{policies.tsx => Policies.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/features/auth/{policies.tsx => Policies.tsx} (100%) diff --git a/src/features/auth/policies.tsx b/src/features/auth/Policies.tsx similarity index 100% rename from src/features/auth/policies.tsx rename to src/features/auth/Policies.tsx From 0a690923bd0fecf5818986a3ea373d61aec50266 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 4 Dec 2024 13:45:47 -0800 Subject: [PATCH 17/25] fix existing tests --- src/features/login/EnforcePolicies.tsx | 4 +-- src/features/login/LogInContinue.test.tsx | 41 ++++++++++------------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/features/login/EnforcePolicies.tsx b/src/features/login/EnforcePolicies.tsx index 76bd5286..ad224a3d 100644 --- a/src/features/login/EnforcePolicies.tsx +++ b/src/features/login/EnforcePolicies.tsx @@ -28,8 +28,8 @@ export const EnforcePolicies = ({ - To continue using your account, you must agree to the following - KBase use policies. + To continue to your account, you must agree to the following KBase + use policies. {targetPolicies.map((policy) => { return ( diff --git a/src/features/login/LogInContinue.test.tsx b/src/features/login/LogInContinue.test.tsx index e2315a30..2b5279fe 100644 --- a/src/features/login/LogInContinue.test.tsx +++ b/src/features/login/LogInContinue.test.tsx @@ -8,6 +8,7 @@ import { LogInContinue } from './LogInContinue'; import fetchMock from 'jest-fetch-mock'; import { toast } from 'react-hot-toast'; import { noOp } from '../common'; +import { kbasePolicies } from '../auth/Policies'; jest.mock('react-hot-toast', () => ({ toast: jest.fn(), @@ -26,12 +27,10 @@ describe('Login Continue', () => { login: [ { id: 'foouserid', - policyids: [ - { - agreedon: 0, - id: 'foopolicy', - }, - ], + policyids: Object.values(kbasePolicies).map((p) => ({ + agreedon: 0, + id: [p.id, p.version].join('.'), + })), }, ], }) @@ -81,12 +80,10 @@ describe('Login Continue', () => { login: [ { id: 'foouserid', - policyids: [ - { - agreedon: 0, - id: 'foopolicy', - }, - ], + policyids: Object.values(kbasePolicies).map((p) => ({ + agreedon: 0, + id: [p.id, p.version].join('.'), + })), }, ], }) @@ -167,12 +164,10 @@ describe('Login Continue', () => { login: [ { id: 'foouserid', - policyids: [ - { - agreedon: 0, - id: 'foopolicy', - }, - ], + policyids: Object.values(kbasePolicies).map((p) => ({ + agreedon: 0, + id: [p.id, p.version].join('.'), + })), }, ], }) @@ -211,12 +206,10 @@ describe('Login Continue', () => { login: [ { id: 'foouserid', - policyids: [ - { - agreedon: 0, - id: 'foopolicy', - }, - ], + policyids: Object.values(kbasePolicies).map((p) => ({ + agreedon: 0, + id: [p.id, p.version].join('.'), + })), }, ], }) From c003bf0155da5fad1745c6ae151ea8be375cdef3 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 4 Dec 2024 14:52:29 -0800 Subject: [PATCH 18/25] add policy renewal message, fix bugs --- src/app/Routes.tsx | 2 + src/features/login/EnforcePolicies.tsx | 14 ++- src/features/login/LogIn.tsx | 166 +++++++++++++------------ src/features/login/LogInContinue.tsx | 6 +- src/features/signup/ProviderSelect.tsx | 21 ++++ 5 files changed, 127 insertions(+), 82 deletions(-) diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index a7ee9d47..8bde81e3 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -42,7 +42,9 @@ const Routes: FC = () => { usePageTracking(); return ( + {/* Legacy */} } /> + } /> accepted[policy.id] === true ); + let message = + 'To continue to your account, you must agree to the following KBase use policies.'; // Default message + if ( + targetPolicies.find( + (p) => p.id === 'kbase-user' && String(p.version) === '2' + ) + ) { + message = + "KBase's recent renewal (Oct '2024) has prompted an update and version 2 release to our Terms and Conditions. Please review and agree to these policies changes to continue using this free resource."; + } + return ( - To continue to your account, you must agree to the following KBase - use policies. + {message} {targetPolicies.map((policy) => { return ( diff --git a/src/features/login/LogIn.tsx b/src/features/login/LogIn.tsx index e40b9c25..ff113fbb 100644 --- a/src/features/login/LogIn.tsx +++ b/src/features/login/LogIn.tsx @@ -74,25 +74,8 @@ export const useLogout = () => { export const LogIn: FC = () => { const nextRequest = useAppParam('nextRequest'); useCheckLoggedIn(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); - - // Redirect URL is used to pass state to login/continue - const loginRedirectUrl = new URL(`${loginOrigin}/login/continue`); - loginRedirectUrl.searchParams.set( - 'state', - JSON.stringify({ - nextRequest: nextRequest, - origin: loginOrigin, - }) - ); + const { loginActionUrl, loginRedirectUrl, loginOrigin } = + makelLoginURLs(nextRequest); return ( @@ -142,66 +125,10 @@ export const LogIn: FC = () => { ) : ( <> )} - - - - - - - - + - New to KBase? Sign up + New to KBase? Sign up { ); }; + +export const makelLoginURLs = (nextRequest?: string) => { + // 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); + + // Redirect URL is used to pass state to login/continue + const loginRedirectUrl = new URL(`${loginOrigin}/login/continue`); + loginRedirectUrl.searchParams.set( + 'state', + JSON.stringify({ + nextRequest: nextRequest, + origin: loginOrigin, + }) + ); + + return { loginOrigin, loginActionUrl, loginRedirectUrl }; +}; + +export const LoginButtons = () => { + return ( + + + + + + + + + ); +}; diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index 11ab8dfd..2080016e 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -45,7 +45,7 @@ export const LogInContinue: FC = () => { const choiceResult = getLoginChoice.useQuery(); const choiceData = choiceResult.data; - const policyids = choiceData?.login[0].policyids; + const policyids = choiceData?.login[0]?.policyids; // Check for missing policies const missingPolicies = useMemo( () => @@ -78,11 +78,11 @@ export const LogInContinue: FC = () => { // needs to be implemented if we have multiple KBase accounts linked to one provider account } else { if (allNewPolicyAgreed) { - const existingPolicyIds = choiceData.login[0].policyids.map( + const existingPolicyIds = choiceData.login[0]?.policyids.map( ({ id }) => id ); triggerPick({ - id: choiceData.login[0].id, + id: choiceData.login[0]?.id, policyids: [...agreedPolicyIds, ...existingPolicyIds], }); } diff --git a/src/features/signup/ProviderSelect.tsx b/src/features/signup/ProviderSelect.tsx index 4f7a2821..21dd0541 100644 --- a/src/features/signup/ProviderSelect.tsx +++ b/src/features/signup/ProviderSelect.tsx @@ -1,4 +1,5 @@ import { + Alert, Box, Button, Container, @@ -11,18 +12,38 @@ import { FC } from 'react'; import globusLogo from '../../common/assets/globus.png'; import googleLogo from '../../common/assets/google.webp'; import orcidLogo from '../../common/assets/orcid.png'; +import { LoginButtons, makelLoginURLs } from '../login/LogIn'; import classes from './SignUp.module.scss'; /** * Provider selection screen for sign up flow */ export const ProviderSelect: FC = () => { + const { loginActionUrl, loginRedirectUrl, loginOrigin } = makelLoginURLs(); + return ( Choose a provider + {process.env.NODE_ENV === 'development' ? ( + + DEV MODE: Login will occur on {loginOrigin} + + ) : ( + <> + )} +
+ + + @@ -207,7 +211,7 @@ export const LoginButtons = () => { /> } > - Continue with Google + {text('Google')} diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index 2080016e..a736801c 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -68,11 +68,11 @@ export const LogInContinue: FC = () => { // if/when postLoginPick has a result, update app auth state using that token const tokenResult = useTryAuthFromToken(pickResult.data?.token.token); + const accountExists = (choiceData?.login?.length || 0) > 0; + // wrap choiceData handling in an effect so we only triggerPick the pick call once useEffect(() => { if (choiceData) { - const accountExists = choiceData.login.length > 0; - // TODO: support policy enforcement if (accountExists) { if (choiceData.login.length > 1) { // needs to be implemented if we have multiple KBase accounts linked to one provider account @@ -99,6 +99,7 @@ export const LogInContinue: FC = () => { navigate, allNewPolicyAgreed, agreedPolicyIds, + accountExists, ]); useEffect(() => { @@ -127,7 +128,7 @@ export const LogInContinue: FC = () => { tokenResult.isError, ]); - if (!allNewPolicyAgreed) { + if (!allNewPolicyAgreed && accountExists) { return ( p.id)} diff --git a/src/features/signup/ProviderSelect.tsx b/src/features/signup/ProviderSelect.tsx index 21dd0541..f6a02b09 100644 --- a/src/features/signup/ProviderSelect.tsx +++ b/src/features/signup/ProviderSelect.tsx @@ -1,7 +1,6 @@ import { Alert, Box, - Button, Container, Link, Paper, @@ -9,9 +8,6 @@ import { Typography, } from '@mui/material'; import { FC } from 'react'; -import globusLogo from '../../common/assets/globus.png'; -import googleLogo from '../../common/assets/google.webp'; -import orcidLogo from '../../common/assets/orcid.png'; import { LoginButtons, makelLoginURLs } from '../login/LogIn'; import classes from './SignUp.module.scss'; @@ -35,7 +31,7 @@ export const ProviderSelect: FC = () => { <> )}
- + `Sign up with ${provider}`} /> { data-testid="redirecturl" /> - - - - - - - - Already have an account? Log in From 19d433c03fbf984a412990021eafa8c6bce568b7 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 5 Dec 2024 10:27:11 -0800 Subject: [PATCH 20/25] update T&C --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f17eac26..86230408 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25989,7 +25989,7 @@ }, "node_modules/kbase-policies": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/kbase/policies.git#9482aa721ffc45f13fe626a21567688160bf5ac6", + "resolved": "git+ssh://git@github.com/kbase/policies.git#e53d734ca832543f32738c3cab40e809a6847270", "hasInstallScript": true, "dependencies": { "@rollup/plugin-typescript": "^12.1.1", From 23858e82c118dc074c51ea589bbe58d7350eab1e Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 5 Dec 2024 15:45:37 -0800 Subject: [PATCH 21/25] bugfix for signup policy dispatch/query race condition --- src/features/login/LogInContinue.tsx | 2 +- src/features/signup/SignUp.tsx | 5 +++-- src/features/signup/SignupPolicies.tsx | 18 +++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index a736801c..a69e0219 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -43,7 +43,7 @@ export const LogInContinue: FC = () => { useCheckLoggedIn(nextRequest); const choiceResult = getLoginChoice.useQuery(); - const choiceData = choiceResult.data; + const choiceData = choiceResult.currentData; const policyids = choiceData?.login[0]?.policyids; // Check for missing policies diff --git a/src/features/signup/SignUp.tsx b/src/features/signup/SignUp.tsx index a0a1b871..021b8bab 100644 --- a/src/features/signup/SignUp.tsx +++ b/src/features/signup/SignUp.tsx @@ -82,19 +82,20 @@ export const useDoSignup = () => { const error = accountResult.error || profileResult.error; - const doSignup = async () => { + const doSignup = async (policyIds: string[]) => { if (!signupOk) return; triggerAccount({ id: String(data.loginData?.create[0].id), user: String(data.account.username), display: String(data.account.display), email: String(data.account.email), - policyids: data.account.policyids, + policyids: policyIds, linkall: false, }); }; useEffect(() => { + if (!accountResult.data?.token.token) return; triggerProfile([ { profile: { diff --git a/src/features/signup/SignupPolicies.tsx b/src/features/signup/SignupPolicies.tsx index 28dd4782..5dad9c35 100644 --- a/src/features/signup/SignupPolicies.tsx +++ b/src/features/signup/SignupPolicies.tsx @@ -33,18 +33,18 @@ export const KBasePolicies: FC<{ // eslint-disable-next-line no-console console.error(errors); - const onSubmit = async () => { - await dispatch( + const onSubmit = () => { + const policyIds = signupPolicies.map((policyId) => { + return [kbasePolicies[policyId].id, kbasePolicies[policyId].version].join( + '.' + ); + }); + dispatch( setAccount({ - policyids: signupPolicies.map((policyId) => { - return [ - kbasePolicies[policyId].id, - kbasePolicies[policyId].version, - ].join('.'); - }), + policyids: policyIds, }) ); - doSignup(); + doSignup(policyIds); }; if (complete) { From fbb48e8dd2287c61e3e4b94f0f646c1c8b2147b0 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 5 Dec 2024 15:45:37 -0800 Subject: [PATCH 22/25] bugfix for signup policy dispatch/query race condition --- src/features/login/LogInContinue.tsx | 2 +- src/features/signup/SignUp.tsx | 5 +++-- src/features/signup/SignupPolicies.tsx | 19 ++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index a736801c..a69e0219 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -43,7 +43,7 @@ export const LogInContinue: FC = () => { useCheckLoggedIn(nextRequest); const choiceResult = getLoginChoice.useQuery(); - const choiceData = choiceResult.data; + const choiceData = choiceResult.currentData; const policyids = choiceData?.login[0]?.policyids; // Check for missing policies diff --git a/src/features/signup/SignUp.tsx b/src/features/signup/SignUp.tsx index a0a1b871..c48dff2a 100644 --- a/src/features/signup/SignUp.tsx +++ b/src/features/signup/SignUp.tsx @@ -82,19 +82,20 @@ export const useDoSignup = () => { const error = accountResult.error || profileResult.error; - const doSignup = async () => { + const doSignup = (policyIds: string[]) => { if (!signupOk) return; triggerAccount({ id: String(data.loginData?.create[0].id), user: String(data.account.username), display: String(data.account.display), email: String(data.account.email), - policyids: data.account.policyids, + policyids: policyIds, linkall: false, }); }; useEffect(() => { + if (!accountResult.data?.token.token) return; triggerProfile([ { profile: { diff --git a/src/features/signup/SignupPolicies.tsx b/src/features/signup/SignupPolicies.tsx index 28dd4782..19ef8740 100644 --- a/src/features/signup/SignupPolicies.tsx +++ b/src/features/signup/SignupPolicies.tsx @@ -20,6 +20,11 @@ export const KBasePolicies: FC<{ const dispatch = useAppDispatch(); const signupPolicies = Object.values(kbasePolicies).map((p) => p.id); + const versionedPolicyIds = signupPolicies.map((policyId) => { + return [kbasePolicies[policyId].id, kbasePolicies[policyId].version].join( + '.' + ); + }); const [accepted, setAccepted] = useState<{ [k in typeof signupPolicies[number]]?: boolean; @@ -33,18 +38,14 @@ export const KBasePolicies: FC<{ // eslint-disable-next-line no-console console.error(errors); - const onSubmit = async () => { - await dispatch( + const onSubmit = () => { + if (!allAccepted) return; + dispatch( setAccount({ - policyids: signupPolicies.map((policyId) => { - return [ - kbasePolicies[policyId].id, - kbasePolicies[policyId].version, - ].join('.'); - }), + policyids: versionedPolicyIds, }) ); - doSignup(); + doSignup(versionedPolicyIds); }; if (complete) { From 6a2bcfec85a964ea3a03aed71f3b8a09727e946c Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 5 Dec 2024 19:18:28 -0800 Subject: [PATCH 23/25] code cleanup before merging tests --- src/features/login/EnforcePolicies.tsx | 6 +- src/features/login/LogIn.tsx | 4 +- src/features/login/LogInContinue.test.tsx | 2 +- src/features/login/LogInContinue.tsx | 2 +- src/features/{auth => login}/Policies.tsx | 0 .../{auth => login}/PolicyViewer.module.scss | 0 src/features/signup/AccountInformation.tsx | 63 ++++++------ src/features/signup/ProviderSelect.tsx | 6 +- src/features/signup/SignUp.tsx | 95 +++++++++++-------- src/features/signup/SignupPolicies.tsx | 54 +++++------ src/features/signup/SignupSlice.tsx | 1 - 11 files changed, 117 insertions(+), 116 deletions(-) rename src/features/{auth => login}/Policies.tsx (100%) rename src/features/{auth => login}/PolicyViewer.module.scss (100%) diff --git a/src/features/login/EnforcePolicies.tsx b/src/features/login/EnforcePolicies.tsx index caa35720..de196255 100644 --- a/src/features/login/EnforcePolicies.tsx +++ b/src/features/login/EnforcePolicies.tsx @@ -4,7 +4,7 @@ import { Alert, Button, Container, Paper } from '@mui/material'; import { Stack } from '@mui/system'; import { useState } from 'react'; import classes from '../signup/SignUp.module.scss'; -import { kbasePolicies, PolicyViewer } from '../auth/Policies'; +import { kbasePolicies, PolicyViewer } from './Policies'; export const EnforcePolicies = ({ policyIds, @@ -13,16 +13,16 @@ export const EnforcePolicies = ({ policyIds: string[]; onAccept: (versionedPolicyIds: string[]) => void; }) => { + // Get policy information const targetPolicies = policyIds.map((id) => kbasePolicies[id]); - const [accepted, setAccepted] = useState<{ [k in typeof targetPolicies[number]['id']]?: boolean; }>({}); - const allAccepted = targetPolicies.every( (policy) => accepted[policy.id] === true ); + // Message to user, uses a special message when agreeing to kbase-user.2 let message = 'To continue to your account, you must agree to the following KBase use policies.'; // Default message if ( diff --git a/src/features/login/LogIn.tsx b/src/features/login/LogIn.tsx index c551850c..a01eba07 100644 --- a/src/features/login/LogIn.tsx +++ b/src/features/login/LogIn.tsx @@ -75,7 +75,7 @@ export const LogIn: FC = () => { const nextRequest = useAppParam('nextRequest'); useCheckLoggedIn(nextRequest); const { loginActionUrl, loginRedirectUrl, loginOrigin } = - makelLoginURLs(nextRequest); + makeLoginURLs(nextRequest); return ( @@ -146,7 +146,7 @@ export const LogIn: FC = () => { ); }; -export const makelLoginURLs = (nextRequest?: string) => { +export const makeLoginURLs = (nextRequest?: string) => { // 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' diff --git a/src/features/login/LogInContinue.test.tsx b/src/features/login/LogInContinue.test.tsx index 2b5279fe..e0a8b2a6 100644 --- a/src/features/login/LogInContinue.test.tsx +++ b/src/features/login/LogInContinue.test.tsx @@ -8,7 +8,7 @@ import { LogInContinue } from './LogInContinue'; import fetchMock from 'jest-fetch-mock'; import { toast } from 'react-hot-toast'; import { noOp } from '../common'; -import { kbasePolicies } from '../auth/Policies'; +import { kbasePolicies } from './Policies'; jest.mock('react-hot-toast', () => ({ toast: jest.fn(), diff --git a/src/features/login/LogInContinue.tsx b/src/features/login/LogInContinue.tsx index a69e0219..2b58d3e6 100644 --- a/src/features/login/LogInContinue.tsx +++ b/src/features/login/LogInContinue.tsx @@ -11,7 +11,7 @@ import { useNavigate } from 'react-router-dom'; import { LOGIN_ROUTE } from '../../app/Routes'; import { useAppDispatch } from '../../common/hooks'; import { setLoginData } from '../signup/SignupSlice'; -import { kbasePolicies } from '../auth/Policies'; +import { kbasePolicies } from './Policies'; import { EnforcePolicies } from './EnforcePolicies'; export const LogInContinue: FC = () => { diff --git a/src/features/auth/Policies.tsx b/src/features/login/Policies.tsx similarity index 100% rename from src/features/auth/Policies.tsx rename to src/features/login/Policies.tsx diff --git a/src/features/auth/PolicyViewer.module.scss b/src/features/login/PolicyViewer.module.scss similarity index 100% rename from src/features/auth/PolicyViewer.module.scss rename to src/features/login/PolicyViewer.module.scss diff --git a/src/features/signup/AccountInformation.tsx b/src/features/signup/AccountInformation.tsx index 50b766a3..28895be7 100644 --- a/src/features/signup/AccountInformation.tsx +++ b/src/features/signup/AccountInformation.tsx @@ -17,7 +17,7 @@ import { TextField, Typography, } from '@mui/material'; -import { FC, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from '../../common/hooks'; @@ -27,40 +27,32 @@ import { loginUsernameSuggest } from '../../common/api/authService'; import { useForm } from 'react-hook-form'; import { setAccount, setProfile } from './SignupSlice'; +export const useCheckLoginDataOk = () => { + const navigate = useNavigate(); + const loginData = useAppSelector((state) => state.signup.loginData); + useEffect(() => { + if (!loginData) { + toast('You must login using a provider first to sign up!'); + navigate('/signup/1'); + } + }, [loginData, navigate]); +}; + /** * Account information form for sign up flow */ -export const AccountInformation: FC<{ - setActiveStep: (step: number) => void; -}> = ({ setActiveStep }) => { +export const AccountInformation: FC<{}> = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); + useCheckLoginDataOk(); + // Login Data - const loginData = useAppSelector( - (state) => - state.signup.loginData || { - provider: 'ORCiD', - create: [ - { - availablename: 'dlyon', - id: 'someuserid', - provemail: 'dlyon@lbl.gov', - provfullname: 'David Lyon', - provusername: 'dlyon', - }, - ], - } - ); + const loginData = useAppSelector((state) => state.signup.loginData); // Account data const account = useAppSelector((state) => state.signup.account); - if (!loginData) { - toast('You must login using a provider first to sign up!'); - navigate('/signup/1'); - } - //username availibility const [username, setUsername] = useState(account.username ?? ''); const userAvail = loginUsernameSuggest.useQuery(username); @@ -71,7 +63,7 @@ export const AccountInformation: FC<{ const surveyQuestion = 'How did you hear about us? (select all that apply)'; const [optionalText, setOptionalText] = useState>({}); - // Form + // Form state const { register, handleSubmit } = useForm({ defaultValues: { account: account, @@ -90,9 +82,10 @@ export const AccountInformation: FC<{ }, }); + // Form submission const onSubmit = handleSubmit(async (fieldValues, event) => { event?.preventDefault(); - // Add in the optional survey text content + // Add in survey text content from form ReferalSources.forEach((src) => { if ( src.customText && @@ -102,7 +95,7 @@ export const AccountInformation: FC<{ fieldValues.profile.surveydata.referralSources.response[src.value] = optionalText[src.value]; }); - // dispatch form data to state + // dispatch form data to signup state dispatch(setAccount(fieldValues.account)); dispatch( setProfile({ @@ -123,8 +116,8 @@ export const AccountInformation: FC<{ - You have signed in with your {loginData.provider}{' '} - account {loginData.create[0].provemail}. This will + You have signed in with your {loginData?.provider}{' '} + account {loginData?.create[0].provemail}. This will be the account linked to your KBase account. @@ -139,22 +132,22 @@ export const AccountInformation: FC<{ If the account you see above is not the one you want, use the - link below to log out of {loginData.provider}, and then try + link below to log out of {loginData?.provider}, and then try again. - If you are trying to sign up with a {loginData.provider}{' '} + If you are trying to sign up with a {loginData?.provider}{' '} account that is already linked to a KBase account, you will be unable to create a new KBase account using that{' '} - {loginData.provider} account. + {loginData?.provider} account. - After signing out from {loginData.provider} you will need to + After signing out from {loginData?.provider} you will need to restart the sign up process. @@ -171,7 +164,7 @@ export const AccountInformation: FC<{ Create a new KBase Account Some field values have been pre-populated from your{' '} - {loginData.provider} account. + {loginData?.provider} account. All fields are required. diff --git a/src/features/signup/ProviderSelect.tsx b/src/features/signup/ProviderSelect.tsx index f6a02b09..f6c30c8e 100644 --- a/src/features/signup/ProviderSelect.tsx +++ b/src/features/signup/ProviderSelect.tsx @@ -8,14 +8,14 @@ import { Typography, } from '@mui/material'; import { FC } from 'react'; -import { LoginButtons, makelLoginURLs } from '../login/LogIn'; +import { LoginButtons, makeLoginURLs } from '../login/LogIn'; import classes from './SignUp.module.scss'; /** * Provider selection screen for sign up flow */ export const ProviderSelect: FC = () => { - const { loginActionUrl, loginRedirectUrl, loginOrigin } = makelLoginURLs(); + const { loginActionUrl, loginRedirectUrl, loginOrigin } = makeLoginURLs(); return ( @@ -25,7 +25,7 @@ export const ProviderSelect: FC = () => { Choose a provider {process.env.NODE_ENV === 'development' ? ( - DEV MODE: Login will occur on {loginOrigin} + DEV MODE: Signup will occur on {loginOrigin} ) : ( <> diff --git a/src/features/signup/SignUp.tsx b/src/features/signup/SignUp.tsx index c48dff2a..fe62cf90 100644 --- a/src/features/signup/SignUp.tsx +++ b/src/features/signup/SignUp.tsx @@ -16,6 +16,7 @@ import { AccountInformation } from './AccountInformation'; import { ProviderSelect } from './ProviderSelect'; import { KBasePolicies } from './SignupPolicies'; import { md5 } from 'js-md5'; +import { ROOT_REDIRECT_ROUTE } from '../../app/Routes'; const signUpSteps = [ 'Sign up with a supported provider', @@ -47,61 +48,54 @@ export const SignUp: FC = () => { Sign up for KBase {signUpSteps.map((step, i) => ( - setActiveStep(i)}> + { + if (i < activeStep) setActiveStep(i); + }} + > {step} ))} {activeStep === 0 && } - {activeStep === 1 && ( - - )} - {activeStep === 2 && } + {activeStep === 1 && } + {activeStep === 2 && } ); }; export const useDoSignup = () => { - const data = useAppSelector((state) => state.signup); - - const signupOk = !!data.loginData; - const [triggerAccount, accountResult] = loginCreate.useMutation(); - const [triggerProfile, profileResult] = setUserProfile.useMutation(); - - const loading = - !accountResult.isUninitialized && - (accountResult.isLoading || profileResult.isLoading); - - const complete = - !loading && - !accountResult.isUninitialized && - !profileResult.isUninitialized && - accountResult.isSuccess && - profileResult.isSuccess; + const signupData = useAppSelector((state) => state.signup); + const navigate = useNavigate(); + // Queries for creating an account and a profile for the user. + const [triggerCreateAccount, accountResult] = loginCreate.useMutation(); + const [triggerCreateProfile, profileResult] = setUserProfile.useMutation(); const error = accountResult.error || profileResult.error; + // Callback to trigger the first call. Consumer should check signup data is present before calling! const doSignup = (policyIds: string[]) => { - if (!signupOk) return; - triggerAccount({ - id: String(data.loginData?.create[0].id), - user: String(data.account.username), - display: String(data.account.display), - email: String(data.account.email), + triggerCreateAccount({ + id: String(signupData.loginData?.create[0].id), + user: String(signupData.account.username), + display: String(signupData.account.display), + email: String(signupData.account.email), policyids: policyIds, linkall: false, }); }; + // Once the account is created, use the account token to set the account profile. useEffect(() => { if (!accountResult.data?.token.token) return; - triggerProfile([ + triggerCreateProfile([ { profile: { user: { - realname: String(data.account.display), - username: String(data.account.username), + realname: String(signupData.account.display), + username: String(signupData.account.username), }, profile: { metadata: { @@ -111,9 +105,9 @@ export const useDoSignup = () => { // was globus info, no longer used preferences: {}, synced: { - gravatarHash: gravatarHash(data.account.email || ''), + gravatarHash: gravatarHash(signupData.account.email || ''), }, - ...data.profile, + ...signupData.profile, }, }, }, @@ -121,18 +115,37 @@ export const useDoSignup = () => { ]); }, [ accountResult, - data.account.display, - data.account.email, - data.account.username, - data.profile, - triggerProfile, + signupData.account.display, + signupData.account.email, + signupData.account.username, + signupData.profile, + triggerCreateProfile, ]); - // Once everything completes, try auth from token. - const tryToken = complete ? accountResult.data.token.token : undefined; - useTryAuthFromToken(tryToken); + const createLoading = + !accountResult.isUninitialized && + (accountResult.isLoading || profileResult.isLoading); + + const createComplete = + !createLoading && + !accountResult.isUninitialized && + !profileResult.isUninitialized && + accountResult.isSuccess && + profileResult.isSuccess; + + // Once create completes, try auth from token. + const tryToken = createComplete ? accountResult.data.token.token : undefined; + const tokenQuery = useTryAuthFromToken(tryToken); + + const complete = createComplete && tokenQuery.isSuccess; + const loading = createLoading || !complete; + + // once everything completes and we're authed from the token, redirect to root. + useEffect(() => { + if (complete) navigate(ROOT_REDIRECT_ROUTE); + }, [complete, navigate]); - return [signupOk, doSignup, loading, complete, error] as const; + return [doSignup, loading, complete, error] as const; }; const gravatarHash = (email: string) => { diff --git a/src/features/signup/SignupPolicies.tsx b/src/features/signup/SignupPolicies.tsx index 19ef8740..0666bd0a 100644 --- a/src/features/signup/SignupPolicies.tsx +++ b/src/features/signup/SignupPolicies.tsx @@ -1,43 +1,50 @@ import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, Paper, Stack, Typography } from '@mui/material'; -import { FC, useState } from 'react'; -import { Navigate } from 'react-router-dom'; -import { ROOT_REDIRECT_ROUTE } from '../../app/Routes'; +import { FC, useEffect, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import { useNavigate } from 'react-router-dom'; import { Loader } from '../../common/components'; -import { useAppDispatch } from '../../common/hooks'; -import { kbasePolicies, PolicyViewer } from '../auth/Policies'; +import { useAppDispatch, useAppSelector } from '../../common/hooks'; +import { kbasePolicies, PolicyViewer } from '../login/Policies'; +import { useCheckLoginDataOk } from './AccountInformation'; import { useDoSignup } from './SignUp'; import classes from './SignUp.module.scss'; import { setAccount } from './SignupSlice'; /** - * Use policy agreements for sign up flow. + * KBase policy agreements step for sign up flow. */ -export const KBasePolicies: FC<{ - setActiveStep: (step: number) => void; -}> = ({ setActiveStep }) => { +export const KBasePolicies: FC<{}> = () => { const dispatch = useAppDispatch(); + const navigate = useNavigate(); + // Check prev steps data is filled out. + useCheckLoginDataOk(); + const account = useAppSelector((state) => state.signup.account); + useEffect(() => { + if (Object.values(account).some((v) => v === undefined)) { + toast('You must fill out your account information to sign up!'); + navigate('/signup/2'); + } + }, [account, navigate]); + + // The policies the user needs to accept. const signupPolicies = Object.values(kbasePolicies).map((p) => p.id); const versionedPolicyIds = signupPolicies.map((policyId) => { return [kbasePolicies[policyId].id, kbasePolicies[policyId].version].join( '.' ); }); - const [accepted, setAccepted] = useState<{ [k in typeof signupPolicies[number]]?: boolean; }>({}); - const allAccepted = signupPolicies.every( (policyId) => accepted[policyId] === true ); - const [signupOk, doSignup, loading, complete, errors] = useDoSignup(); - // eslint-disable-next-line no-console - console.error(errors); - + // Performs signup (if all policies have been accepted) + const [doSignup, loading] = useDoSignup(); const onSubmit = () => { if (!allAccepted) return; dispatch( @@ -48,17 +55,6 @@ export const KBasePolicies: FC<{ doSignup(versionedPolicyIds); }; - if (complete) { - return ( - - ); - } - return ( @@ -88,7 +84,7 @@ export const KBasePolicies: FC<{ variant="contained" endIcon={} size="large" - disabled={!(allAccepted && signupOk) || loading} + disabled={!allAccepted || loading} onClick={onSubmit} > Create KBase account @@ -99,7 +95,7 @@ export const KBasePolicies: FC<{ color="warning" size="large" onClick={() => { - setActiveStep(0); + navigate('/signup/1'); }} > Cancel sign up @@ -108,7 +104,7 @@ export const KBasePolicies: FC<{ variant="outlined" size="large" startIcon={} - onClick={() => setActiveStep(1)} + onClick={() => navigate('/signup/2')} > Back to account information diff --git a/src/features/signup/SignupSlice.tsx b/src/features/signup/SignupSlice.tsx index b6c61738..242a4fba 100644 --- a/src/features/signup/SignupSlice.tsx +++ b/src/features/signup/SignupSlice.tsx @@ -1,7 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { GetLoginChoiceResult } from '../../common/api/authService'; -// Define a type for the slice state export interface SignupState { loginData?: GetLoginChoiceResult; account: { From 0668a0e0e82c344a60c5b33cc7d6ae235213e69c Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 6 Dec 2024 13:45:49 -0800 Subject: [PATCH 24/25] add back in refactored tests --- src/features/login/EnforcePolicies.test.tsx | 111 +++++++++++ src/features/login/EnforcePolicies.tsx | 72 ++++++- src/features/login/LogInContinue.test.tsx | 48 +++++ src/features/login/Policies.tsx | 54 ------ src/features/login/PolicyViewer.module.scss | 8 - .../signup/AccountInformation.test.tsx | 138 +++++++++++++ src/features/signup/AccountInformation.tsx | 21 +- src/features/signup/ProviderSelect.tsx | 3 +- src/features/signup/SignUp.test.tsx | 183 ++++++++++++++++++ src/features/signup/SignUp.tsx | 4 +- src/features/signup/SignupPolicies.test.tsx | 154 +++++++++++++++ src/features/signup/SignupPolicies.tsx | 7 +- src/features/signup/SignupSlice.tsx | 8 +- 13 files changed, 731 insertions(+), 80 deletions(-) create mode 100644 src/features/login/EnforcePolicies.test.tsx delete mode 100644 src/features/login/PolicyViewer.module.scss create mode 100644 src/features/signup/AccountInformation.test.tsx create mode 100644 src/features/signup/SignUp.test.tsx create mode 100644 src/features/signup/SignupPolicies.test.tsx diff --git a/src/features/login/EnforcePolicies.test.tsx b/src/features/login/EnforcePolicies.test.tsx new file mode 100644 index 00000000..e4d2c059 --- /dev/null +++ b/src/features/login/EnforcePolicies.test.tsx @@ -0,0 +1,111 @@ +import { ThemeProvider } from '@emotion/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { createTestStore } from '../../app/store'; +import { theme } from '../../theme'; +import { noOp } from '../common'; +import { EnforcePolicies } from './EnforcePolicies'; + +jest.mock('./Policies', () => ({ + ...jest.requireActual('./Policies'), + kbasePolicies: { + 'kbase-user': { + raw: '---\ntitle: KBase Terms and Conditions\nid: kbase-user\nversion: 1\nequivalentVersions: []\n---\nsome content', + markdown: 'some content', + title: 'KBase Terms and Conditions', + id: 'kbase-user', + version: '2', + equivalentVersions: [], + }, + 'test-policy': { + raw: '---\ntitle: Test Policy\nid: test-policy\nversion: 1\nequivalentVersions: []\n---\ntest content', + markdown: 'test content', + title: 'Test Policy', + id: 'test-policy', + version: '1', + equivalentVersions: [], + }, + }, +})); + +const renderWithProviders = ( + ui: React.ReactElement, + { store = createTestStore() } = {} +) => { + return render( + + + +
{ui}
+
+
+
+ ); +}; + +describe('EnforcePolicies', () => { + it('renders default message', () => { + renderWithProviders( + + ); + expect( + screen.getByText( + 'To continue to your account, you must agree to the following KBase use policies.' + ) + ).toBeInTheDocument(); + }); + + it('renders special v2 policy message', () => { + renderWithProviders( + + ); + expect( + screen.getByText( + "KBase's recent renewal (Oct '2024) has prompted an update and version 2 release to our Terms and Conditions. Please review and agree to these policies changes to continue using this free resource." + ) + ).toBeInTheDocument(); + }); + + it('disables accept button until all policies are accepted', async () => { + const mockAccept = jest.fn(); + renderWithProviders( + + ); + + const acceptButton = screen.getByRole('button', { + name: /agree and continue/i, + }); + expect(acceptButton).toBeDisabled(); + + const checkbox = screen.getByTestId('policy-checkbox'); + await userEvent.click(checkbox); + + expect(acceptButton).toBeEnabled(); + }); + + it('calls onAccept when accept button clicked', async () => { + const mockAccept = jest.fn(); + renderWithProviders( + + ); + + const checkbox = screen.getByTestId('policy-checkbox'); + await userEvent.click(checkbox); + + const acceptButton = screen.getByRole('button', { + name: /agree and continue/i, + }); + await userEvent.click(acceptButton); + + expect(mockAccept).toHaveBeenCalledWith(['kbase-user.2']); + }); + it('throws error when policy does not exist', () => { + expect(() => + renderWithProviders( + + ) + ).toThrow('Required policy "non-existent-policy" cannot be loaded'); + }); +}); diff --git a/src/features/login/EnforcePolicies.tsx b/src/features/login/EnforcePolicies.tsx index de196255..2c779b88 100644 --- a/src/features/login/EnforcePolicies.tsx +++ b/src/features/login/EnforcePolicies.tsx @@ -1,10 +1,22 @@ import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Alert, Button, Container, Paper } from '@mui/material'; +import { + Alert, + Button, + Container, + Paper, + Box, + Checkbox, + FormControl, + FormControlLabel, + Typography, +} from '@mui/material'; import { Stack } from '@mui/system'; import { useState } from 'react'; import classes from '../signup/SignUp.module.scss'; -import { kbasePolicies, PolicyViewer } from './Policies'; +import { kbasePolicies } from './Policies'; +import createDOMPurify from 'dompurify'; +import { marked } from 'marked'; export const EnforcePolicies = ({ policyIds, @@ -14,13 +26,17 @@ export const EnforcePolicies = ({ onAccept: (versionedPolicyIds: string[]) => void; }) => { // Get policy information - const targetPolicies = policyIds.map((id) => kbasePolicies[id]); + const targetPolicies = policyIds.map((id) => { + if (!kbasePolicies[id]) + throw new Error(`Required policy "${id}" cannot be loaded`); + return kbasePolicies[id]; + }); const [accepted, setAccepted] = useState<{ [k in typeof targetPolicies[number]['id']]?: boolean; }>({}); - const allAccepted = targetPolicies.every( - (policy) => accepted[policy.id] === true - ); + const allAccepted = targetPolicies.every((policy) => { + return accepted[policy.id] === true; + }); // Message to user, uses a special message when agreeing to kbase-user.2 let message = @@ -77,3 +93,47 @@ export const EnforcePolicies = ({
); }; + +const purify = createDOMPurify(window); + +export const PolicyViewer = ({ + policyId, + setAccept, + accepted = false, +}: { + policyId: string; + setAccept: (accepted: boolean) => void; + accepted?: boolean; +}) => { + const policy = kbasePolicies[policyId]; + if (!policy) + throw new Error(`Required policy "${policyId}" cannot be loaded`); + return ( + + {policy.title} + +
+ +
+ + { + setAccept(e.currentTarget.checked); + }} + /> + } + label="I have read and agree to this policy" + /> + +
+ + ); +}; diff --git a/src/features/login/LogInContinue.test.tsx b/src/features/login/LogInContinue.test.tsx index e0a8b2a6..7a09ae38 100644 --- a/src/features/login/LogInContinue.test.tsx +++ b/src/features/login/LogInContinue.test.tsx @@ -245,4 +245,52 @@ describe('Login Continue', () => { expect(Login).toHaveBeenCalled(); }); }); + + it('handles new user signup flow', async () => { + // getLoginChoice - return create data instead of login data + fetchMock.mockResponseOnce( + JSON.stringify({ + login: [], + create: [ + { + id: 'newuserid', + provider: 'google', + username: 'newuser@google.com', + }, + ], + }) + ); + + const Signup = jest.fn(() => <>); + const store = createTestStore(); + render( + + + + + } /> + + + + + + ); + + await waitFor(() => { + // Check that login data was set in store + expect(store.getState().signup.loginData).toEqual({ + login: [], + create: [ + { + id: 'newuserid', + provider: 'google', + username: 'newuser@google.com', + }, + ], + }); + }); + await waitFor(() => { + expect(window.location.pathname === '/signup/2'); + }); + }); }); diff --git a/src/features/login/Policies.tsx b/src/features/login/Policies.tsx index 8d6bbeee..07196036 100644 --- a/src/features/login/Policies.tsx +++ b/src/features/login/Policies.tsx @@ -1,21 +1,8 @@ import policyStrings from 'kbase-policies'; import frontmatter from 'front-matter'; -import { - Box, - Checkbox, - FormControl, - FormControlLabel, - Paper, - Typography, -} from '@mui/material'; -import classes from './PolicyViewer.module.scss'; -import createDOMPurify from 'dompurify'; -import { marked } from 'marked'; export const ENFORCED_POLICIES = ['kbase-user']; -const purify = createDOMPurify(window); - interface PolicyMeta { title: string; id: string; @@ -46,44 +33,3 @@ export const kbasePolicies = policyStrings.reduce( } > ); - -export const PolicyViewer = ({ - policyId, - setAccept, - accepted = false, -}: { - policyId: string; - setAccept: (accepted: boolean) => void; - accepted?: boolean; -}) => { - const policy = kbasePolicies[policyId]; - if (!policy) - throw new Error(`Required policy "${policyId}" cannot be loaded`); - return ( - - {policy.title} - -
- -
- - { - setAccept(e.currentTarget.checked); - }} - /> - } - label="I have read and agree to this policy" - /> - -
- - ); -}; diff --git a/src/features/login/PolicyViewer.module.scss b/src/features/login/PolicyViewer.module.scss deleted file mode 100644 index c55bbe92..00000000 --- a/src/features/login/PolicyViewer.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -.agreement-box { - border: 1px solid use-color("base"); - border-radius: 4px; - display: inline-block; - margin-top: 1rem; - padding: 1rem; - -} diff --git a/src/features/signup/AccountInformation.test.tsx b/src/features/signup/AccountInformation.test.tsx new file mode 100644 index 00000000..d491669b --- /dev/null +++ b/src/features/signup/AccountInformation.test.tsx @@ -0,0 +1,138 @@ +import { fireEvent, screen } from '@testing-library/react'; +import { createTestStore } from '../../app/store'; +import { Provider } from 'react-redux'; +import { ThemeProvider } from '@mui/material'; +import { BrowserRouter, useNavigate } from 'react-router-dom'; +import { render } from '@testing-library/react'; +import { AccountInformation } from './AccountInformation'; +import { setLoginData } from './SignupSlice'; +import { theme } from '../../theme'; +import { act } from 'react-dom/test-utils'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), +})); +jest.mock('../../common/api/authService', () => ({ + ...jest.requireActual('../../common/api/authService'), + loginUsernameSuggest: { + useQuery: jest.fn().mockReturnValue({ + currentData: { availablename: 'testuser' }, + isFetching: false, + }), + }, +})); + +const renderWithProviders = ( + ui: React.ReactElement, + { store = createTestStore() } = {} +) => { + return render( + + + +
{ui}
+
+
+
+ ); +}; + +describe('AccountInformation', () => { + const mockNavigate = jest.fn(); + + beforeEach(() => { + (useNavigate as jest.Mock).mockImplementation(() => mockNavigate); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('redirects to step 1 if no login data', () => { + const store = createTestStore(); + renderWithProviders(, { store }); + expect(mockNavigate).toHaveBeenCalledWith('/signup/1'); + }); + + test('displays login data from provider', () => { + const store = createTestStore(); + store.dispatch( + setLoginData({ + creationallowed: true, + expires: 0, + login: [], + provider: 'Google', + create: [ + { + provemail: 'test@test.com', + provfullname: 'Test User', + availablename: 'testuser', + id: '123', + provusername: 'testuser', + }, + ], + }) + ); + renderWithProviders(, { store }); + expect(screen.getAllByText(/Google/)[0]).toBeInTheDocument(); + expect(screen.getAllByText(/test@test.com/)[0]).toBeInTheDocument(); + }); + + test('form submission with valid data', async () => { + const store = createTestStore(); + store.dispatch( + setLoginData({ + creationallowed: true, + expires: 0, + login: [], + provider: 'Google', + create: [ + { + provemail: 'test@test.com', + provfullname: 'Test User', + availablename: 'testuser', + id: '123', + provusername: 'testuser', + }, + ], + }) + ); + renderWithProviders(, { store }); + + await act(() => { + fireEvent.change(screen.getByRole('textbox', { name: /Full Name/i }), { + target: { value: 'Test User' }, + }); + }); + await act(() => { + fireEvent.change(screen.getByRole('textbox', { name: /Email/i }), { + target: { value: 'test@test.com' }, + }); + }); + await act(() => { + fireEvent.change( + screen.getByRole('textbox', { name: /KBase Username/i }), + { + target: { value: 'testuser' }, + } + ); + }); + await act(() => { + fireEvent.change(screen.getByRole('textbox', { name: /Organization/i }), { + target: { value: 'Test Org' }, + }); + }); + await act(() => { + fireEvent.change(screen.getByRole('textbox', { name: /Department/i }), { + target: { value: 'Test Dept' }, + }); + }); + + await act(() => { + fireEvent.submit(screen.getByTestId('accountinfoform')); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/signup/3'); + }); +}); diff --git a/src/features/signup/AccountInformation.tsx b/src/features/signup/AccountInformation.tsx index 28895be7..955ba960 100644 --- a/src/features/signup/AccountInformation.tsx +++ b/src/features/signup/AccountInformation.tsx @@ -17,7 +17,7 @@ import { TextField, Typography, } from '@mui/material'; -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useState, Fragment } from 'react'; import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from '../../common/hooks'; @@ -25,7 +25,7 @@ import classes from './SignUp.module.scss'; import ReferalSources from './ReferralSources.json'; import { loginUsernameSuggest } from '../../common/api/authService'; import { useForm } from 'react-hook-form'; -import { setAccount, setProfile } from './SignupSlice'; +import { resetSignup, setAccount, setProfile } from './SignupSlice'; export const useCheckLoginDataOk = () => { const navigate = useNavigate(); @@ -158,7 +158,7 @@ export const AccountInformation: FC<{}> = () => { -
+ Create a new KBase Account @@ -254,7 +254,7 @@ export const AccountInformation: FC<{}> = () => { {ReferalSources.map((source) => { if (source.customText) { return ( - <> + = () => { }} /> - + ); } else { return ( = () => { - diff --git a/src/features/signup/ProviderSelect.tsx b/src/features/signup/ProviderSelect.tsx index f6c30c8e..6c7f1409 100644 --- a/src/features/signup/ProviderSelect.tsx +++ b/src/features/signup/ProviderSelect.tsx @@ -8,6 +8,7 @@ import { Typography, } from '@mui/material'; import { FC } from 'react'; +import { LOGIN_ROUTE } from '../../app/Routes'; import { LoginButtons, makeLoginURLs } from '../login/LogIn'; import classes from './SignUp.module.scss'; @@ -42,7 +43,7 @@ export const ProviderSelect: FC = () => { - Already have an account? Log in + Already have an account? Log in ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), + useParams: jest.fn(), +})); + +const mockNavigate = jest.fn(); +const mockScrollTo = jest.fn(); + +const renderWithProviders = ( + ui: React.ReactElement, + { store = createTestStore() } = {} +) => { + return render( + + + +
{ui}
+
+
+
+ ); +}; + +describe('SignUp', () => { + beforeEach(() => { + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + (useParams as jest.Mock).mockReturnValue({ step: '1' }); + Element.prototype.scrollTo = mockScrollTo; + }); + + it('renders signup steps', () => { + renderWithProviders(); + expect(screen.getByText('Sign up for KBase')).toBeInTheDocument(); + expect( + screen.getByText('Sign up with a supported provider') + ).toBeInTheDocument(); + expect(screen.getByText('Account information')).toBeInTheDocument(); + expect(screen.getByText('KBase use policies')).toBeInTheDocument(); + }); + + it('navigates between steps when clicking previous steps', async () => { + (useParams as jest.Mock).mockReturnValue({ step: '3' }); + renderWithProviders(); + + const step1 = screen.getByText('Sign up with a supported provider'); + await userEvent.click(step1); + expect(mockNavigate).toHaveBeenCalledWith('/signup/1'); + expect(mockScrollTo).toHaveBeenCalledWith(0, 0); + }); +}); + +describe('useDoSignup', () => { + const mockLoginCreateMutation = jest.fn(); + const mockSetUserProfileMutation = jest.fn(); + + beforeEach(() => { + jest.spyOn(loginCreate, 'useMutation').mockReturnValue([ + mockLoginCreateMutation, + { + isUninitialized: false, + isSuccess: true, + data: { token: { token: 'someToken' } }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ]); + jest.spyOn(setUserProfile, 'useMutation').mockReturnValue([ + mockSetUserProfileMutation, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { isUninitialized: true } as any, + ]); + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + }); + + it('calls login create and set user profile mutations', async () => { + const mockStore = createTestStore({ + signup: { + loginData: { + create: [ + { + id: '123', + availablename: '', + provemail: '', + provfullname: '', + provusername: '', + }, + ], + creationallowed: true, + expires: 0, + login: [], + provider: 'Google', + }, + account: { + username: 'testuser', + display: 'Test User', + email: 'test@test.com', + policyids: [], + }, + profile: { + userdata: { + avatarOption: 'gravatar', + department: '', + gravatarDefault: 'identicon', + organization: '', + }, + surveydata: { + referralSources: { + question: '', + response: {}, + }, + }, + }, + }, + }); + + let doSignup: (policyIds: string[]) => void; + act(() => { + const TestComponent = () => { + [doSignup] = useDoSignup(); + return null; + }; + renderWithProviders(, { + store: mockStore, + }); + }); + + await act(async () => { + doSignup(['policy1']); + }); + + expect(mockLoginCreateMutation).toHaveBeenCalledWith({ + id: '123', + user: 'testuser', + display: 'Test User', + email: 'test@test.com', + policyids: ['policy1'], + linkall: false, + }); + + expect(mockSetUserProfileMutation).toHaveBeenCalledWith([ + { + profile: { + user: { + username: 'testuser', + realname: 'Test User', + }, + profile: { + metadata: expect.any(Object), + preferences: {}, + synced: { + gravatarHash: gravatarHash('test@test.com'), + }, + userdata: { + avatarOption: 'gravatar', + department: '', + gravatarDefault: 'identicon', + organization: '', + }, + surveydata: { + referralSources: { + question: '', + response: {}, + }, + }, + }, + }, + }, + 'someToken', + ]); + }); +}); diff --git a/src/features/signup/SignUp.tsx b/src/features/signup/SignUp.tsx index fe62cf90..1fdc9e2e 100644 --- a/src/features/signup/SignUp.tsx +++ b/src/features/signup/SignUp.tsx @@ -39,7 +39,7 @@ export const SignUp: FC = () => { }; useEffect(() => { - document.querySelector('main')?.scrollTo(0, 0); + document.querySelector('main')?.scrollTo?.(0, 0); }, [activeStep]); return ( @@ -148,6 +148,6 @@ export const useDoSignup = () => { return [doSignup, loading, complete, error] as const; }; -const gravatarHash = (email: string) => { +export const gravatarHash = (email: string) => { return md5.create().update(email.trim().toLowerCase()).hex(); }; diff --git a/src/features/signup/SignupPolicies.test.tsx b/src/features/signup/SignupPolicies.test.tsx new file mode 100644 index 00000000..34801157 --- /dev/null +++ b/src/features/signup/SignupPolicies.test.tsx @@ -0,0 +1,154 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { configureStore } from '@reduxjs/toolkit'; +import { KBasePolicies } from './SignupPolicies'; +import { toast } from 'react-hot-toast'; +import * as SignUp from './SignUp'; + +jest.mock('react-hot-toast'); +jest.mock('./AccountInformation', () => ({ + useCheckLoginDataOk: jest.fn(), +})); +jest.mock('./SignUp', () => ({ + useDoSignup: jest.fn(), +})); + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +jest.mock('../login/EnforcePolicies', () => ({ + PolicyViewer: ({ + policyId, + accepted, + setAccept, + }: { + policyId: string; + accepted: boolean; + setAccept: (checked: boolean) => void; + }) => ( +
+ setAccept(e.target.checked)} + data-testid={`checkbox-${policyId}`} + /> +
+ ), +})); + +jest.mock('../login/Policies', () => ({ + kbasePolicies: { + termsOfService: { + id: 'termsOfService', + version: '1', + title: 'Terms of Service', + markdown: 'Terms of Service content', + }, + privacyPolicy: { + id: 'privacyPolicy', + version: '1', + title: 'Privacy Policy', + markdown: 'Privacy Policy content', + }, + }, +})); + +const mockScrollTo = jest.fn(); +Element.prototype.scrollTo = mockScrollTo; + +const createMockStore = (initialState = {}) => { + return configureStore({ + reducer: { + signup: (state = { account: {} }, action) => state, + }, + preloadedState: { + signup: { + account: { + username: 'testuser', + email: 'test@test.com', + ...initialState, + }, + }, + }, + }); +}; + +describe('Signup Policies', () => { + const mockDoSignup = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (SignUp.useDoSignup as jest.Mock).mockReturnValue([mockDoSignup, false]); + }); + + const renderComponent = (store = createMockStore()) => { + return render( + + + + + + ); + }; + + it('should render all policies', () => { + renderComponent(); + expect(screen.getByTestId('policy-termsOfService')).toBeInTheDocument(); + expect(screen.getByTestId('policy-privacyPolicy')).toBeInTheDocument(); + }); + + it('should not call doSignup when policies are not accepted', () => { + renderComponent(); + const submitButton = screen.getByText('Create KBase account'); + Object.defineProperty(submitButton, 'disabled', { value: false }); + fireEvent.click(submitButton); + expect(mockDoSignup).not.toHaveBeenCalled(); + }); + + it('should handle policy acceptance', () => { + renderComponent(); + const tosCheckbox = screen.getByTestId('checkbox-termsOfService'); + const privacyCheckbox = screen.getByTestId('checkbox-privacyPolicy'); + const submitButton = screen.getByText('Create KBase account'); + expect(submitButton).toBeDisabled(); + fireEvent.click(tosCheckbox); + fireEvent.click(privacyCheckbox); + expect(submitButton).not.toBeDisabled(); + }); + + it('should call doSignup when all policies are accepted and form is submitted', () => { + renderComponent(); + fireEvent.click(screen.getByTestId('checkbox-termsOfService')); + fireEvent.click(screen.getByTestId('checkbox-privacyPolicy')); + fireEvent.click(screen.getByText('Create KBase account')); + expect(mockDoSignup).toHaveBeenCalledWith([ + 'termsOfService.1', + 'privacyPolicy.1', + ]); + }); + + it('should show warning toast if account information is missing', () => { + const store = createMockStore({ username: undefined }); + renderComponent(store); + expect(toast).toHaveBeenCalledWith( + 'You must fill out your account information to sign up!' + ); + }); + + it('should navigate when cancel button is clicked', () => { + renderComponent(); + fireEvent.click(screen.getByText('Cancel sign up')); + expect(mockNavigate).toHaveBeenCalledWith('/signup/1'); + }); + + it('should navigate when back button is clicked', () => { + renderComponent(); + fireEvent.click(screen.getByText('Back to account information')); + expect(mockNavigate).toHaveBeenCalledWith('/signup/2'); + }); +}); diff --git a/src/features/signup/SignupPolicies.tsx b/src/features/signup/SignupPolicies.tsx index 0666bd0a..5a5496fc 100644 --- a/src/features/signup/SignupPolicies.tsx +++ b/src/features/signup/SignupPolicies.tsx @@ -6,11 +6,12 @@ import { toast } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; import { Loader } from '../../common/components'; import { useAppDispatch, useAppSelector } from '../../common/hooks'; -import { kbasePolicies, PolicyViewer } from '../login/Policies'; +import { PolicyViewer } from '../login/EnforcePolicies'; +import { kbasePolicies } from '../login/Policies'; import { useCheckLoginDataOk } from './AccountInformation'; import { useDoSignup } from './SignUp'; import classes from './SignUp.module.scss'; -import { setAccount } from './SignupSlice'; +import { resetSignup, setAccount } from './SignupSlice'; /** * KBase policy agreements step for sign up flow. @@ -67,6 +68,7 @@ export const KBasePolicies: FC<{}> = () => { {Object.values(kbasePolicies).map((policy) => { return ( @@ -96,6 +98,7 @@ export const KBasePolicies: FC<{}> = () => { size="large" onClick={() => { navigate('/signup/1'); + dispatch(resetSignup()); }} > Cancel sign up diff --git a/src/features/signup/SignupSlice.tsx b/src/features/signup/SignupSlice.tsx index 242a4fba..7c398b45 100644 --- a/src/features/signup/SignupSlice.tsx +++ b/src/features/signup/SignupSlice.tsx @@ -57,8 +57,14 @@ export const signupSlice = createSlice({ setProfile: (state, action: PayloadAction) => { state.profile = action.payload; }, + resetSignup: (state) => { + state.account = initialState.account; + state.loginData = initialState.loginData; + state.profile = initialState.profile; + }, }, }); export default signupSlice.reducer; -export const { setLoginData, setAccount, setProfile } = signupSlice.actions; +export const { setLoginData, setAccount, setProfile, resetSignup } = + signupSlice.actions; From 904306defad15b7927b8f6fb4e9c35a76de8be07 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 6 Dec 2024 17:15:47 -0800 Subject: [PATCH 25/25] fix loader error --- src/features/signup/SignUp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/signup/SignUp.tsx b/src/features/signup/SignUp.tsx index 1fdc9e2e..c3b5ec3d 100644 --- a/src/features/signup/SignUp.tsx +++ b/src/features/signup/SignUp.tsx @@ -138,7 +138,7 @@ export const useDoSignup = () => { const tokenQuery = useTryAuthFromToken(tryToken); const complete = createComplete && tokenQuery.isSuccess; - const loading = createLoading || !complete; + const loading = createLoading || tokenQuery.isLoading; // once everything completes and we're authed from the token, redirect to root. useEffect(() => {