Refactor login (#748)
* wip refactor login * wip refactor login * Fix lint conflicts * Complete Sign In only * Feature complete * Fix test * Fix test
This commit is contained in:
@ -4,7 +4,7 @@ import { ThemeProvider } from '@emotion/react';
|
|||||||
import { withThemeFromJSXProvider } from '@storybook/addon-styling';
|
import { withThemeFromJSXProvider } from '@storybook/addon-styling';
|
||||||
import { lightTheme, darkTheme } from '../src/modules/ui/themes/themes';
|
import { lightTheme, darkTheme } from '../src/modules/ui/themes/themes';
|
||||||
import { RootDecorator } from '../src/testing/decorators';
|
import { RootDecorator } from '../src/testing/decorators';
|
||||||
|
import { mockedUserJWT } from '../src/testing/mock-data/jwt';
|
||||||
initialize();
|
initialize();
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
@ -28,6 +28,9 @@ const preview: Preview = {
|
|||||||
date: /Date$/,
|
date: /Date$/,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
cookie: {
|
||||||
|
tokenPair: `{%22accessToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-07-18T15:06:40.704Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22refreshToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-10-15T15:06:41.558Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22__typename%22:%22AuthTokenPair%22}`,
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
storySort: {
|
storySort: {
|
||||||
order: ['UI', 'Modules', 'Pages'],
|
order: ['UI', 'Modules', 'Pages'],
|
||||||
|
|||||||
@ -8,6 +8,9 @@ module.exports = {
|
|||||||
if (error.message === "ResizeObserver loop limit exceeded") {
|
if (error.message === "ResizeObserver loop limit exceeded") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (error.message === "Unauthorized") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,18 +1,10 @@
|
|||||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
|
||||||
|
|
||||||
import { RequireOnboarded } from '@/auth/components/RequireOnboarded';
|
|
||||||
import { RequireOnboarding } from '@/auth/components/RequireOnboarding';
|
|
||||||
import { AuthModal } from '@/auth/components/ui/Modal';
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { AuthPath } from '@/types/AuthPath';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { AuthLayout } from '@/ui/layout/components/AuthLayout';
|
|
||||||
import { DefaultLayout } from '@/ui/layout/components/DefaultLayout';
|
import { DefaultLayout } from '@/ui/layout/components/DefaultLayout';
|
||||||
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
||||||
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
|
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
|
||||||
import { Index } from '~/pages/auth/Index';
|
|
||||||
import { PasswordLogin } from '~/pages/auth/PasswordLogin';
|
|
||||||
import { Verify } from '~/pages/auth/Verify';
|
import { Verify } from '~/pages/auth/Verify';
|
||||||
import { Companies } from '~/pages/companies/Companies';
|
import { Companies } from '~/pages/companies/Companies';
|
||||||
import { CompanyShow } from '~/pages/companies/CompanyShow';
|
import { CompanyShow } from '~/pages/companies/CompanyShow';
|
||||||
@ -25,32 +17,7 @@ import { SettingsWorksapce } from '~/pages/settings/SettingsWorkspace';
|
|||||||
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
||||||
import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';
|
import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';
|
||||||
|
|
||||||
/**
|
import { SignInUp } from './pages/auth/SignInUp';
|
||||||
* AuthRoutes is used to allow transitions between auth pages with framer-motion.
|
|
||||||
*/
|
|
||||||
function AuthRoutes() {
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LayoutGroup>
|
|
||||||
<AuthModal>
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<Routes location={location} key={location.pathname}>
|
|
||||||
<Route path={AuthPath.Index} element={<Index />} />
|
|
||||||
<Route path={AuthPath.Callback} element={<Verify />} />
|
|
||||||
<Route path={AuthPath.PasswordLogin} element={<PasswordLogin />} />
|
|
||||||
<Route path={AuthPath.InviteLink} element={<PasswordLogin />} />
|
|
||||||
<Route
|
|
||||||
path={AuthPath.CreateWorkspace}
|
|
||||||
element={<CreateWorkspace />}
|
|
||||||
/>
|
|
||||||
<Route path={AuthPath.CreateProfile} element={<CreateProfile />} />
|
|
||||||
</Routes>
|
|
||||||
</AnimatePresence>
|
|
||||||
</AuthModal>
|
|
||||||
</LayoutGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
@ -58,65 +25,40 @@ export function App() {
|
|||||||
<AppInternalHooks />
|
<AppInternalHooks />
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route path={AppPath.Verify} element={<Verify />} />
|
||||||
path={AppPath.AuthCatchAll}
|
<Route path={AppPath.SignIn} element={<SignInUp />} />
|
||||||
element={
|
<Route path={AppPath.SignUp} element={<SignInUp />} />
|
||||||
<RequireOnboarding>
|
<Route path={AppPath.Invite} element={<SignInUp />} />
|
||||||
<AuthLayout>
|
<Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} />
|
||||||
<AuthRoutes />
|
<Route path={AppPath.CreateProfile} element={<CreateProfile />} />
|
||||||
</AuthLayout>
|
<Route path="/" element={<Navigate to={AppPath.CompaniesPage} />} />
|
||||||
</RequireOnboarding>
|
<Route path={AppPath.PeoplePage} element={<People />} />
|
||||||
}
|
<Route path={AppPath.PersonShowPage} element={<PersonShow />} />
|
||||||
/>
|
<Route path={AppPath.CompaniesPage} element={<Companies />} />
|
||||||
<Route
|
<Route path={AppPath.CompanyShowPage} element={<CompanyShow />} />
|
||||||
path="*"
|
|
||||||
element={
|
|
||||||
<RequireOnboarded>
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path=""
|
|
||||||
element={<Navigate to={AppPath.CompaniesPage} replace />}
|
|
||||||
/>
|
|
||||||
<Route path={AppPath.PeoplePage} element={<People />} />
|
|
||||||
<Route
|
|
||||||
path={AppPath.PersonShowPage}
|
|
||||||
element={<PersonShow />}
|
|
||||||
/>
|
|
||||||
<Route path={AppPath.CompaniesPage} element={<Companies />} />
|
|
||||||
<Route
|
|
||||||
path={AppPath.CompanyShowPage}
|
|
||||||
element={<CompanyShow />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
<Route path={AppPath.OpportunitiesPage} element={<Opportunities />} />
|
||||||
path={AppPath.OpportunitiesPage}
|
<Route
|
||||||
element={<Opportunities />}
|
path={AppPath.SettingsCatchAll}
|
||||||
/>
|
element={
|
||||||
<Route
|
<Routes>
|
||||||
path={AppPath.SettingsCatchAll}
|
<Route
|
||||||
element={
|
path={SettingsPath.ProfilePage}
|
||||||
<Routes>
|
element={<SettingsProfile />}
|
||||||
<Route
|
/>
|
||||||
path={SettingsPath.ProfilePage}
|
<Route
|
||||||
element={<SettingsProfile />}
|
path={SettingsPath.Experience}
|
||||||
/>
|
element={<SettingsExperience />}
|
||||||
<Route
|
/>
|
||||||
path={SettingsPath.Experience}
|
<Route
|
||||||
element={<SettingsExperience />}
|
path={SettingsPath.WorkspaceMembersPage}
|
||||||
/>
|
element={<SettingsWorkspaceMembers />}
|
||||||
<Route
|
/>
|
||||||
path={SettingsPath.WorkspaceMembersPage}
|
<Route
|
||||||
element={<SettingsWorkspaceMembers />}
|
path={SettingsPath.Workspace}
|
||||||
/>
|
element={<SettingsWorksapce />}
|
||||||
<Route
|
/>
|
||||||
path={SettingsPath.Workspace}
|
</Routes>
|
||||||
element={<SettingsWorksapce />}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</RequireOnboarded>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -2024,7 +2024,7 @@ export type VerifyMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null } } | null }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type RenewTokenMutationVariables = Exact<{
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
refreshToken: Scalars['String'];
|
refreshToken: Scalars['String'];
|
||||||
@ -2924,6 +2924,11 @@ export const VerifyDocument = gql`
|
|||||||
logo
|
logo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
settings {
|
||||||
|
id
|
||||||
|
colorScheme
|
||||||
|
locale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tokens {
|
tokens {
|
||||||
accessToken {
|
accessToken {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { AppBasePath } from '@/types/AppBasePath';
|
|||||||
export function useIsMatchingLocation() {
|
export function useIsMatchingLocation() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return function isMatchingLocation(basePath: AppBasePath, path: string) {
|
return function isMatchingLocation(path: string, basePath?: AppBasePath) {
|
||||||
const constructedPath = basePath
|
const constructedPath = basePath
|
||||||
? parse(`${basePath}/${path}`).pathname ?? ''
|
? parse(`${basePath}/${path}`).pathname ?? ''
|
||||||
: path;
|
: path;
|
||||||
|
|||||||
@ -4,6 +4,8 @@ type Props = React.ComponentProps<'div'>;
|
|||||||
|
|
||||||
const StyledLogo = styled.div`
|
const StyledLogo = styled.div`
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -11,9 +11,6 @@ const StyledContainer = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: ${({ theme }) => theme.spacing(10)};
|
padding: ${({ theme }) => theme.spacing(10)};
|
||||||
width: calc(400px - ${({ theme }) => theme.spacing(10 * 2)});
|
width: calc(400px - ${({ theme }) => theme.spacing(10 * 2)});
|
||||||
> * + * {
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function AuthModal({ children, ...restProps }: Props) {
|
export function AuthModal({ children, ...restProps }: Props) {
|
||||||
@ -1,71 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { keyframes } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { useOnboardingStatus } from '../hooks/useOnboardingStatus';
|
|
||||||
import { OnboardingStatus } from '../utils/getOnboardingStatus';
|
|
||||||
|
|
||||||
const EmptyContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const fadeIn = keyframes`
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FadeInStyle = styled.div`
|
|
||||||
animation: ${fadeIn} 1s forwards;
|
|
||||||
opacity: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function RequireOnboarded({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: JSX.Element;
|
|
||||||
}): JSX.Element {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onboardingStatus = useOnboardingStatus();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onboardingStatus === OnboardingStatus.OngoingUserCreation) {
|
|
||||||
navigate('/auth');
|
|
||||||
} else if (onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation) {
|
|
||||||
navigate('/auth/create/workspace');
|
|
||||||
} else if (onboardingStatus === OnboardingStatus.OngoingProfileCreation) {
|
|
||||||
navigate('/auth/create/profile');
|
|
||||||
}
|
|
||||||
}, [onboardingStatus, navigate]);
|
|
||||||
|
|
||||||
if (onboardingStatus && onboardingStatus !== OnboardingStatus.Completed) {
|
|
||||||
return (
|
|
||||||
<EmptyContainer>
|
|
||||||
<FadeInStyle>
|
|
||||||
{onboardingStatus === OnboardingStatus.OngoingUserCreation && (
|
|
||||||
<div>
|
|
||||||
Please hold on a moment, we're directing you to our login page...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{onboardingStatus !== OnboardingStatus.OngoingUserCreation && (
|
|
||||||
<div>
|
|
||||||
Please hold on a moment, we're directing you to our onboarding
|
|
||||||
flow...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FadeInStyle>
|
|
||||||
</EmptyContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { keyframes } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { useOnboardingStatus } from '../hooks/useOnboardingStatus';
|
|
||||||
import { OnboardingStatus } from '../utils/getOnboardingStatus';
|
|
||||||
|
|
||||||
const EmptyContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const fadeIn = keyframes`
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FadeInStyle = styled.div`
|
|
||||||
animation: ${fadeIn} 1s forwards;
|
|
||||||
opacity: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function RequireOnboarding({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: JSX.Element;
|
|
||||||
}): JSX.Element {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onboardingStatus = useOnboardingStatus();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onboardingStatus === OnboardingStatus.Completed) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, [navigate, onboardingStatus]);
|
|
||||||
|
|
||||||
if (onboardingStatus === OnboardingStatus.Completed) {
|
|
||||||
return (
|
|
||||||
<EmptyContainer>
|
|
||||||
<FadeInStyle>
|
|
||||||
Please hold on a moment, we're directing you to the app...
|
|
||||||
</FadeInStyle>
|
|
||||||
</EmptyContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
@ -7,7 +7,6 @@ type OwnProps = {
|
|||||||
|
|
||||||
const StyledSubTitle = styled.div`
|
const StyledSubTitle = styled.div`
|
||||||
color: ${({ theme }) => theme.font.color.secondary};
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function SubTitle({ children }: OwnProps): JSX.Element {
|
export function SubTitle({ children }: OwnProps): JSX.Element {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { AnimatedTextWord } from '@/ui/animation/components/AnimatedTextWord';
|
import { AnimatedEaseIn } from '../../ui/animation/components/AnimatedEaseIn';
|
||||||
|
|
||||||
type Props = React.PropsWithChildren & {
|
type Props = React.PropsWithChildren & {
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
@ -11,17 +11,17 @@ const StyledTitle = styled.div`
|
|||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
font-size: ${({ theme }) => theme.font.size.xl};
|
font-size: ${({ theme }) => theme.font.size.xl};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
`;
|
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||||
const StyledAnimatedTextWord = styled(AnimatedTextWord)`
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.xl};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function Title({ children, animate = false }: Props) {
|
export function Title({ children, animate = false }: Props) {
|
||||||
if (animate && typeof children === 'string') {
|
if (animate) {
|
||||||
return <StyledAnimatedTextWord text={children} />;
|
return (
|
||||||
|
<StyledTitle>
|
||||||
|
<AnimatedEaseIn>{children}</AnimatedEaseIn>
|
||||||
|
</StyledTitle>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <StyledTitle>{children}</StyledTitle>;
|
return <StyledTitle>{children}</StyledTitle>;
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useChallengeMutation,
|
useChallengeMutation,
|
||||||
|
useCheckUserExistsLazyQuery,
|
||||||
useSignUpMutation,
|
useSignUpMutation,
|
||||||
useVerifyMutation,
|
useVerifyMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
@ -13,12 +15,16 @@ import { tokenPairState } from '../states/tokenPairState';
|
|||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const [, setTokenPair] = useRecoilState(tokenPairState);
|
const [, setTokenPair] = useRecoilState(tokenPairState);
|
||||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
|
||||||
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
||||||
|
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||||
|
|
||||||
const [challenge] = useChallengeMutation();
|
const [challenge] = useChallengeMutation();
|
||||||
const [signUp] = useSignUpMutation();
|
const [signUp] = useSignUpMutation();
|
||||||
const [verify] = useVerifyMutation();
|
const [verify] = useVerifyMutation();
|
||||||
|
const [checkUserExistsQuery, { data: checkUserExistsData }] =
|
||||||
|
useCheckUserExistsLazyQuery();
|
||||||
|
|
||||||
|
const client = useApolloClient();
|
||||||
|
|
||||||
const handleChallenge = useCallback(
|
const handleChallenge = useCallback(
|
||||||
async (email: string, password: string) => {
|
async (email: string, password: string) => {
|
||||||
@ -65,21 +71,25 @@ export function useAuth() {
|
|||||||
[setIsAuthenticating, setTokenPair, verify],
|
[setIsAuthenticating, setTokenPair, verify],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLogin = useCallback(
|
const handleCrendentialsSignIn = useCallback(
|
||||||
async (email: string, password: string) => {
|
async (email: string, password: string) => {
|
||||||
const { loginToken } = await handleChallenge(email, password);
|
const { loginToken } = await handleChallenge(email, password);
|
||||||
|
|
||||||
await handleVerify(loginToken.token);
|
const { user } = await handleVerify(loginToken.token);
|
||||||
|
return { user };
|
||||||
},
|
},
|
||||||
[handleChallenge, handleVerify],
|
[handleChallenge, handleVerify],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLogout = useCallback(() => {
|
const handleSignOut = useCallback(() => {
|
||||||
setTokenPair(null);
|
setTokenPair(null);
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
}, [setTokenPair, setCurrentUser]);
|
client.clearStore().then(() => {
|
||||||
|
sessionStorage.clear();
|
||||||
|
});
|
||||||
|
}, [setTokenPair, client, setCurrentUser]);
|
||||||
|
|
||||||
const handleSignUp = useCallback(
|
const handleCredentialsSignUp = useCallback(
|
||||||
async (email: string, password: string, workspaceInviteHash?: string) => {
|
async (email: string, password: string, workspaceInviteHash?: string) => {
|
||||||
const signUpResult = await signUp({
|
const signUpResult = await signUp({
|
||||||
variables: {
|
variables: {
|
||||||
@ -97,16 +107,33 @@ export function useAuth() {
|
|||||||
throw new Error('No login token');
|
throw new Error('No login token');
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleVerify(signUpResult.data?.signUp.loginToken.token);
|
const { user } = await handleVerify(
|
||||||
|
signUpResult.data?.signUp.loginToken.token,
|
||||||
|
);
|
||||||
|
|
||||||
|
setCurrentUser(user);
|
||||||
|
|
||||||
|
return { user };
|
||||||
},
|
},
|
||||||
[signUp, handleVerify],
|
[signUp, handleVerify, setCurrentUser],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleGoogleLogin = useCallback((workspaceInviteHash?: string) => {
|
||||||
|
window.location.href =
|
||||||
|
`${process.env.REACT_APP_AUTH_URL}/google/${
|
||||||
|
workspaceInviteHash ? '?inviteHash=' + workspaceInviteHash : ''
|
||||||
|
}` || '';
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
challenge: handleChallenge,
|
challenge: handleChallenge,
|
||||||
verify: handleVerify,
|
verify: handleVerify,
|
||||||
login: handleLogin,
|
|
||||||
signUp: handleSignUp,
|
checkUserExists: { checkUserExistsData, checkUserExistsQuery },
|
||||||
logout: handleLogout,
|
|
||||||
|
signOut: handleSignOut,
|
||||||
|
signUpWithCredentials: handleCredentialsSignUp,
|
||||||
|
signInWithCredentials: handleCrendentialsSignIn,
|
||||||
|
signInWithGoogle: handleGoogleLogin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useIsLogged } from '../hooks/useIsLogged';
|
import { useIsLogged } from '../hooks/useIsLogged';
|
||||||
@ -12,10 +11,5 @@ export function useOnboardingStatus(): OnboardingStatus | undefined {
|
|||||||
const [currentUser] = useRecoilState(currentUserState);
|
const [currentUser] = useRecoilState(currentUserState);
|
||||||
const isLoggedIn = useIsLogged();
|
const isLoggedIn = useIsLogged();
|
||||||
|
|
||||||
const onboardingStatus = useMemo(
|
return getOnboardingStatus(isLoggedIn, currentUser);
|
||||||
() => getOnboardingStatus(isLoggedIn, currentUser),
|
|
||||||
[currentUser, isLoggedIn],
|
|
||||||
);
|
|
||||||
|
|
||||||
return onboardingStatus;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,6 +48,11 @@ export const VERIFY = gql`
|
|||||||
logo
|
logo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
settings {
|
||||||
|
id
|
||||||
|
colorScheme
|
||||||
|
locale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tokens {
|
tokens {
|
||||||
accessToken {
|
accessToken {
|
||||||
|
|||||||
210
front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
Normal file
210
front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
import { AnimatedEaseIn } from '@/ui/animation/components/AnimatedEaseIn';
|
||||||
|
import { MainButton } from '@/ui/button/components/MainButton';
|
||||||
|
import { IconBrandGoogle } from '@/ui/icon';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
|
||||||
|
import { Logo } from '../../components/Logo';
|
||||||
|
import { Title } from '../../components/Title';
|
||||||
|
import { SignInUpMode, SignInUpStep, useSignInUp } from '../hooks/useSignInUp';
|
||||||
|
|
||||||
|
import { FooterNote } from './FooterNote';
|
||||||
|
import { HorizontalSeparator } from './HorizontalSeparator';
|
||||||
|
|
||||||
|
const StyledContentContainer = styled.div`
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||||
|
width: 200px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFooterNote = styled(FooterNote)`
|
||||||
|
max-width: 280px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledForm = styled.form`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFullWidthMotionDiv = styled(motion.div)`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInputContainer = styled.div`
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function SignInUpForm() {
|
||||||
|
const {
|
||||||
|
authProviders,
|
||||||
|
signInWithGoogle,
|
||||||
|
signInUpStep,
|
||||||
|
signInUpMode,
|
||||||
|
showErrors,
|
||||||
|
setShowErrors,
|
||||||
|
continueWithCredentials,
|
||||||
|
continueWithEmail,
|
||||||
|
submitCredentials,
|
||||||
|
form: {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
},
|
||||||
|
} = useSignInUp();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const buttonTitle = useMemo(() => {
|
||||||
|
if (signInUpStep === SignInUpStep.Init) {
|
||||||
|
return 'Continue With Email';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signInUpStep === SignInUpStep.Email) {
|
||||||
|
return 'Continue';
|
||||||
|
}
|
||||||
|
|
||||||
|
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
|
||||||
|
}, [signInUpMode, signInUpStep]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatedEaseIn>
|
||||||
|
<Logo />
|
||||||
|
</AnimatedEaseIn>
|
||||||
|
<Title animate>
|
||||||
|
{signInUpMode === SignInUpMode.SignIn
|
||||||
|
? 'Sign in to Twenty'
|
||||||
|
: 'Sign up to Twenty'}
|
||||||
|
</Title>
|
||||||
|
<StyledContentContainer>
|
||||||
|
{authProviders.google && (
|
||||||
|
<>
|
||||||
|
<MainButton
|
||||||
|
icon={<IconBrandGoogle size={theme.icon.size.sm} stroke={4} />}
|
||||||
|
title="Continue with Google"
|
||||||
|
onClick={signInWithGoogle}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<HorizontalSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<StyledForm
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{signInUpStep !== SignInUpStep.Init && (
|
||||||
|
<StyledFullWidthMotionDiv
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 800,
|
||||||
|
damping: 35,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, onBlur, value },
|
||||||
|
fieldState: { error },
|
||||||
|
}) => (
|
||||||
|
<StyledInputContainer>
|
||||||
|
<TextInput
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
placeholder="Email"
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={(value: string) => {
|
||||||
|
onChange(value);
|
||||||
|
if (signInUpStep === SignInUpStep.Password) {
|
||||||
|
continueWithEmail();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
error={showErrors ? error?.message : undefined}
|
||||||
|
fullWidth
|
||||||
|
disableHotkeys
|
||||||
|
/>
|
||||||
|
</StyledInputContainer>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFullWidthMotionDiv>
|
||||||
|
)}
|
||||||
|
{signInUpStep === SignInUpStep.Password && (
|
||||||
|
<StyledFullWidthMotionDiv
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 800,
|
||||||
|
damping: 35,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="password"
|
||||||
|
control={control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, onBlur, value },
|
||||||
|
fieldState: { error },
|
||||||
|
}) => (
|
||||||
|
<StyledInputContainer>
|
||||||
|
<TextInput
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={onChange}
|
||||||
|
error={showErrors ? error?.message : undefined}
|
||||||
|
fullWidth
|
||||||
|
disableHotkeys
|
||||||
|
/>
|
||||||
|
</StyledInputContainer>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFullWidthMotionDiv>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MainButton
|
||||||
|
variant="secondary"
|
||||||
|
title={buttonTitle}
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
if (signInUpStep === SignInUpStep.Init) {
|
||||||
|
continueWithEmail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (signInUpStep === SignInUpStep.Email) {
|
||||||
|
continueWithCredentials();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowErrors(true);
|
||||||
|
handleSubmit(submitCredentials)();
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
SignInUpStep.Init
|
||||||
|
? false
|
||||||
|
: signInUpStep === SignInUpStep.Email
|
||||||
|
? !watch('email')
|
||||||
|
: !watch('email') || !watch('password') || isSubmitting
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</StyledForm>
|
||||||
|
</StyledContentContainer>
|
||||||
|
<StyledFooterNote>
|
||||||
|
By using Twenty, you agree to the Terms of Service and Data Processing
|
||||||
|
Agreement.
|
||||||
|
</StyledFooterNote>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
178
front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
Normal file
178
front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
|
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
|
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||||
|
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||||
|
|
||||||
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
import { currentUserState } from '../../states/currentUserState';
|
||||||
|
import { PASSWORD_REGEX } from '../../utils/passwordRegex';
|
||||||
|
|
||||||
|
export enum SignInUpMode {
|
||||||
|
SignIn = 'sign-in',
|
||||||
|
SignUp = 'sign-up',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SignInUpStep {
|
||||||
|
Init = 'init',
|
||||||
|
Email = 'email',
|
||||||
|
Password = 'password',
|
||||||
|
}
|
||||||
|
const validationSchema = Yup.object()
|
||||||
|
.shape({
|
||||||
|
exist: Yup.boolean().required(),
|
||||||
|
email: Yup.string()
|
||||||
|
.email('Email must be a valid email')
|
||||||
|
.required('Email must be a valid email'),
|
||||||
|
password: Yup.string()
|
||||||
|
.matches(PASSWORD_REGEX, 'Password must contain at least 8 characters')
|
||||||
|
.required(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
type Form = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
|
export function useSignInUp() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const isMatchingLocation = useIsMatchingLocation();
|
||||||
|
const [authProviders] = useRecoilState(authProvidersState);
|
||||||
|
const isDemoMode = useRecoilValue(isDemoModeState);
|
||||||
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||||
|
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
||||||
|
SignInUpStep.Init,
|
||||||
|
);
|
||||||
|
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(
|
||||||
|
isMatchingLocation(AppPath.SignIn)
|
||||||
|
? SignInUpMode.SignIn
|
||||||
|
: SignInUpMode.SignUp,
|
||||||
|
);
|
||||||
|
const [showErrors, setShowErrors] = useState(false);
|
||||||
|
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||||
|
|
||||||
|
const form = useForm<Form>({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues: {
|
||||||
|
exist: false,
|
||||||
|
email: isDemoMode ? 'tim@apple.dev' : '',
|
||||||
|
password: isDemoMode ? 'Applecar2025' : '',
|
||||||
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
signInWithCredentials,
|
||||||
|
signUpWithCredentials,
|
||||||
|
signInWithGoogle,
|
||||||
|
checkUserExists: { checkUserExistsQuery },
|
||||||
|
} = useAuth();
|
||||||
|
|
||||||
|
const continueWithEmail = useCallback(() => {
|
||||||
|
setSignInUpStep(SignInUpStep.Email);
|
||||||
|
setSignInUpMode(
|
||||||
|
isMatchingLocation(AppPath.SignIn)
|
||||||
|
? SignInUpMode.SignIn
|
||||||
|
: SignInUpMode.SignUp,
|
||||||
|
);
|
||||||
|
}, [setSignInUpStep, setSignInUpMode, isMatchingLocation]);
|
||||||
|
|
||||||
|
const continueWithCredentials = useCallback(() => {
|
||||||
|
checkUserExistsQuery({
|
||||||
|
variables: {
|
||||||
|
email: form.getValues('email'),
|
||||||
|
},
|
||||||
|
onCompleted: (data) => {
|
||||||
|
if (data?.checkUserExists.exists) {
|
||||||
|
setSignInUpMode(SignInUpMode.SignIn);
|
||||||
|
} else {
|
||||||
|
setSignInUpMode(SignInUpMode.SignUp);
|
||||||
|
}
|
||||||
|
setSignInUpStep(SignInUpStep.Password);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [setSignInUpStep, checkUserExistsQuery, form, setSignInUpMode]);
|
||||||
|
|
||||||
|
const submitCredentials: SubmitHandler<Form> = useCallback(
|
||||||
|
async (data) => {
|
||||||
|
try {
|
||||||
|
if (!data.email || !data.password) {
|
||||||
|
throw new Error('Email and password are required');
|
||||||
|
}
|
||||||
|
if (signInUpMode === SignInUpMode.SignIn) {
|
||||||
|
const { user } = await signInWithCredentials(
|
||||||
|
data.email,
|
||||||
|
data.password,
|
||||||
|
);
|
||||||
|
setCurrentUser(user);
|
||||||
|
} else {
|
||||||
|
const { user } = await signUpWithCredentials(
|
||||||
|
data.email,
|
||||||
|
data.password,
|
||||||
|
workspaceInviteHash,
|
||||||
|
);
|
||||||
|
setCurrentUser(user);
|
||||||
|
}
|
||||||
|
navigate('/create/workspace');
|
||||||
|
} catch (err: any) {
|
||||||
|
enqueueSnackBar(err?.message, {
|
||||||
|
variant: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
navigate,
|
||||||
|
signInWithCredentials,
|
||||||
|
signUpWithCredentials,
|
||||||
|
workspaceInviteHash,
|
||||||
|
enqueueSnackBar,
|
||||||
|
signInUpMode,
|
||||||
|
setCurrentUser,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const goBackToEmailStep = useCallback(() => {
|
||||||
|
setSignInUpStep(SignInUpStep.Email);
|
||||||
|
}, [setSignInUpStep]);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
'enter',
|
||||||
|
() => {
|
||||||
|
if (signInUpStep === SignInUpStep.Init) {
|
||||||
|
continueWithEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signInUpStep === SignInUpStep.Email) {
|
||||||
|
continueWithCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signInUpStep === SignInUpStep.Password) {
|
||||||
|
form.handleSubmit(submitCredentials)();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PageHotkeyScope.SignInUp,
|
||||||
|
[continueWithEmail],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
authProviders,
|
||||||
|
signInWithGoogle: () => signInWithGoogle(workspaceInviteHash),
|
||||||
|
signInUpStep,
|
||||||
|
signInUpMode,
|
||||||
|
showErrors,
|
||||||
|
setShowErrors,
|
||||||
|
continueWithCredentials,
|
||||||
|
continueWithEmail,
|
||||||
|
goBackToEmailStep,
|
||||||
|
submitCredentials,
|
||||||
|
form,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -17,11 +17,11 @@ import SubNavbar from '@/ui/navbar/components/SubNavbar';
|
|||||||
export function SettingsNavbar() {
|
export function SettingsNavbar() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { logout } = useAuth();
|
const { signOut } = useAuth();
|
||||||
|
|
||||||
const handleLogout = useCallback(() => {
|
const handleLogout = useCallback(() => {
|
||||||
logout();
|
signOut();
|
||||||
}, [logout]);
|
}, [signOut]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubNavbar backButtonTitle="Settings">
|
<SubNavbar backButtonTitle="Settings">
|
||||||
|
|||||||
@ -1,5 +1,16 @@
|
|||||||
export enum AppPath {
|
export enum AppPath {
|
||||||
AuthCatchAll = `/auth/*`,
|
// Not logged-in
|
||||||
|
Verify = 'verify',
|
||||||
|
SignIn = 'sign-in',
|
||||||
|
SignUp = 'sign-up',
|
||||||
|
Invite = 'invite/:workspaceInviteHash',
|
||||||
|
|
||||||
|
// Onboarding
|
||||||
|
CreateWorkspace = 'create/workspace',
|
||||||
|
CreateProfile = 'create/profile',
|
||||||
|
|
||||||
|
// Onboarded
|
||||||
|
Index = '',
|
||||||
PeoplePage = '/people',
|
PeoplePage = '/people',
|
||||||
CompaniesPage = '/companies',
|
CompaniesPage = '/companies',
|
||||||
CompanyShowPage = '/companies/:companyId',
|
CompanyShowPage = '/companies/:companyId',
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
export enum AuthPath {
|
|
||||||
Index = '',
|
|
||||||
Callback = 'callback',
|
|
||||||
PasswordLogin = 'password-login',
|
|
||||||
CreateWorkspace = 'create/workspace',
|
|
||||||
CreateProfile = 'create/profile',
|
|
||||||
InviteLink = 'invite/:workspaceInviteHash',
|
|
||||||
}
|
|
||||||
@ -1,8 +1,7 @@
|
|||||||
export enum PageHotkeyScope {
|
export enum PageHotkeyScope {
|
||||||
Settings = 'settings',
|
Settings = 'settings',
|
||||||
CreateWokspace = 'create-workspace',
|
CreateWokspace = 'create-workspace',
|
||||||
PasswordLogin = 'password-login',
|
SignInUp = 'sign-in-up',
|
||||||
AuthIndex = 'auth-index',
|
|
||||||
CreateProfile = 'create-profile',
|
CreateProfile = 'create-profile',
|
||||||
ShowPage = 'show-page',
|
ShowPage = 'show-page',
|
||||||
PersonShowPage = 'person-show-page',
|
PersonShowPage = 'person-show-page',
|
||||||
|
|||||||
@ -9,7 +9,7 @@ type Props = Omit<
|
|||||||
|
|
||||||
export function AnimatedEaseIn({
|
export function AnimatedEaseIn({
|
||||||
children,
|
children,
|
||||||
duration = 0.8,
|
duration = 0.3,
|
||||||
...restProps
|
...restProps
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const initial = { opacity: 0 };
|
const initial = { opacity: 0 };
|
||||||
|
|||||||
@ -55,9 +55,9 @@ const StyledButton = styled.button<Pick<Props, 'fullWidth' | 'variant'>>`
|
|||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
outline: none;
|
||||||
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
||||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
||||||
|
|
||||||
${({ theme, variant }) => {
|
${({ theme, variant }) => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'secondary':
|
case 'secondary':
|
||||||
|
|||||||
@ -20,6 +20,7 @@ type OwnProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
|
|||||||
label?: string;
|
label?: string;
|
||||||
onChange?: (text: string) => void;
|
onChange?: (text: string) => void;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
disableHotkeys?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,6 +105,7 @@ export function TextInput({
|
|||||||
error,
|
error,
|
||||||
required,
|
required,
|
||||||
type,
|
type,
|
||||||
|
disableHotkeys = false,
|
||||||
...props
|
...props
|
||||||
}: OwnProps): JSX.Element {
|
}: OwnProps): JSX.Element {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -117,16 +119,20 @@ export function TextInput({
|
|||||||
|
|
||||||
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||||
onFocus?.(e);
|
onFocus?.(e);
|
||||||
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
if (!disableHotkeys) {
|
||||||
|
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||||
onBlur?.(e);
|
onBlur?.(e);
|
||||||
goBackToPreviousHotkeyScope();
|
if (!disableHotkeys) {
|
||||||
|
goBackToPreviousHotkeyScope();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.Enter, Key.Escape],
|
[Key.Escape, Key.Enter],
|
||||||
() => {
|
() => {
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,12 +1,20 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { AuthModal } from '@/auth/components/Modal';
|
||||||
|
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||||
|
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||||
import { NavbarAnimatedContainer } from '@/ui/navbar/components/NavbarAnimatedContainer';
|
import { NavbarAnimatedContainer } from '@/ui/navbar/components/NavbarAnimatedContainer';
|
||||||
import { MOBILE_VIEWPORT } from '@/ui/themes/themes';
|
import { MOBILE_VIEWPORT } from '@/ui/themes/themes';
|
||||||
import { AppNavbar } from '~/AppNavbar';
|
import { AppNavbar } from '~/AppNavbar';
|
||||||
|
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||||
|
import { CompaniesMockMode } from '~/pages/companies/CompaniesMockMode';
|
||||||
|
|
||||||
|
import { AppPath } from '../../../types/AppPath';
|
||||||
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
|
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
|
||||||
|
|
||||||
const StyledLayout = styled.div`
|
const StyledLayout = styled.div`
|
||||||
@ -38,22 +46,71 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function DefaultLayout({ children }: OwnProps) {
|
export function DefaultLayout({ children }: OwnProps) {
|
||||||
const currentUser = useRecoilState(currentUserState);
|
const navigate = useNavigate();
|
||||||
const userIsAuthenticated = !!currentUser;
|
const isMatchingLocation = useIsMatchingLocation();
|
||||||
|
|
||||||
|
const onboardingStatus = useOnboardingStatus();
|
||||||
|
useEffect(() => {
|
||||||
|
const isMachinOngoingUserCreationRoute =
|
||||||
|
isMatchingLocation(AppPath.SignUp) ||
|
||||||
|
isMatchingLocation(AppPath.SignIn) ||
|
||||||
|
isMatchingLocation(AppPath.Invite) ||
|
||||||
|
isMatchingLocation(AppPath.Verify);
|
||||||
|
|
||||||
|
const isMatchingOnboardingRoute =
|
||||||
|
isMatchingLocation(AppPath.SignUp) ||
|
||||||
|
isMatchingLocation(AppPath.SignIn) ||
|
||||||
|
isMatchingLocation(AppPath.Invite) ||
|
||||||
|
isMatchingLocation(AppPath.Verify) ||
|
||||||
|
isMatchingLocation(AppPath.CreateWorkspace) ||
|
||||||
|
isMatchingLocation(AppPath.CreateProfile);
|
||||||
|
|
||||||
|
if (
|
||||||
|
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
|
||||||
|
!isMachinOngoingUserCreationRoute
|
||||||
|
) {
|
||||||
|
navigate(AppPath.SignIn);
|
||||||
|
} else if (
|
||||||
|
onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation &&
|
||||||
|
!isMatchingLocation(AppPath.CreateWorkspace)
|
||||||
|
) {
|
||||||
|
navigate(AppPath.CreateWorkspace);
|
||||||
|
} else if (
|
||||||
|
onboardingStatus === OnboardingStatus.OngoingProfileCreation &&
|
||||||
|
!isMatchingLocation(AppPath.CreateProfile)
|
||||||
|
) {
|
||||||
|
navigate(AppPath.CreateProfile);
|
||||||
|
} else if (
|
||||||
|
onboardingStatus === OnboardingStatus.Completed &&
|
||||||
|
isMatchingOnboardingRoute
|
||||||
|
) {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}, [onboardingStatus, navigate, isMatchingLocation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledLayout>
|
<StyledLayout>
|
||||||
{userIsAuthenticated ? (
|
<>
|
||||||
<>
|
<CommandMenu />
|
||||||
<CommandMenu />
|
<NavbarAnimatedContainer>
|
||||||
<NavbarAnimatedContainer>
|
<AppNavbar />
|
||||||
<AppNavbar />
|
</NavbarAnimatedContainer>
|
||||||
</NavbarAnimatedContainer>
|
<MainContainer>
|
||||||
<MainContainer>{children}</MainContainer>
|
{onboardingStatus &&
|
||||||
</>
|
onboardingStatus !== OnboardingStatus.Completed ? (
|
||||||
) : (
|
<>
|
||||||
children
|
<CompaniesMockMode />
|
||||||
)}
|
<AnimatePresence mode="wait">
|
||||||
|
<LayoutGroup>
|
||||||
|
<AuthModal>{children}</AuthModal>
|
||||||
|
</LayoutGroup>
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>{children}</>
|
||||||
|
)}
|
||||||
|
</MainContainer>
|
||||||
|
</>
|
||||||
</StyledLayout>
|
</StyledLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ type Props = {
|
|||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTitle = styled.h2`
|
const StyledTitle = styled.h2`
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
@ -9,11 +8,7 @@ export function UserProvider({ children }: React.PropsWithChildren) {
|
|||||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const isLogged = useIsLogged();
|
const { data, loading } = useGetCurrentUserQuery();
|
||||||
|
|
||||||
const { data, loading } = useGetCurrentUserQuery({
|
|
||||||
skip: !isLogged,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
@ -8,11 +8,9 @@ import { useRecoilState } from 'recoil';
|
|||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
import { SubTitle } from '@/auth/components/SubTitle';
|
||||||
import { Title } from '@/auth/components/ui/Title';
|
import { Title } from '@/auth/components/Title';
|
||||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
|
||||||
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { MainButton } from '@/ui/button/components/MainButton';
|
import { MainButton } from '@/ui/button/components/MainButton';
|
||||||
@ -25,18 +23,14 @@ import { useUpdateUserMutation } from '~/generated/graphql';
|
|||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
const StyledContentContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
> * + * {
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(6)};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSectionContainer = styled.div`
|
const StyledSectionContainer = styled.div`
|
||||||
> * + * {
|
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
const StyledButtonContainer = styled.div`
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||||
width: 200px;
|
width: 200px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -59,7 +53,6 @@ type Form = Yup.InferType<typeof validationSchema>;
|
|||||||
|
|
||||||
export function CreateProfile() {
|
export function CreateProfile() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const onboardingStatus = useOnboardingStatus();
|
|
||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
@ -129,12 +122,6 @@ export function CreateProfile() {
|
|||||||
[onSubmit],
|
[onSubmit],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, [onboardingStatus, navigate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Create profile</Title>
|
<Title>Create profile</Title>
|
||||||
@ -159,6 +146,7 @@ export function CreateProfile() {
|
|||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
autoFocus
|
||||||
label="First Name"
|
label="First Name"
|
||||||
value={value}
|
value={value}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
@ -166,6 +154,7 @@ export function CreateProfile() {
|
|||||||
placeholder="Tim"
|
placeholder="Tim"
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disableHotkeys
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -184,6 +173,7 @@ export function CreateProfile() {
|
|||||||
placeholder="Cook"
|
placeholder="Cook"
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disableHotkeys
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
@ -6,10 +6,8 @@ import styled from '@emotion/styled';
|
|||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
import { SubTitle } from '@/auth/components/SubTitle';
|
||||||
import { Title } from '@/auth/components/ui/Title';
|
import { Title } from '@/auth/components/Title';
|
||||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
|
||||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
|
||||||
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { MainButton } from '@/ui/button/components/MainButton';
|
import { MainButton } from '@/ui/button/components/MainButton';
|
||||||
@ -18,22 +16,21 @@ import { TextInput } from '@/ui/input/components/TextInput';
|
|||||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||||
import { SubSectionTitle } from '@/ui/title/components/SubSectionTitle';
|
import { SubSectionTitle } from '@/ui/title/components/SubSectionTitle';
|
||||||
import { GET_CURRENT_USER } from '@/users/queries';
|
import { GET_CURRENT_USER } from '@/users/queries';
|
||||||
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
import {
|
||||||
|
useGetCurrentUserLazyQuery,
|
||||||
|
useUpdateWorkspaceMutation,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
const StyledContentContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
> * + * {
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(6)};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSectionContainer = styled.div`
|
const StyledSectionContainer = styled.div`
|
||||||
> * + * {
|
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
const StyledButtonContainer = styled.div`
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||||
width: 200px;
|
width: 200px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -47,11 +44,11 @@ type Form = Yup.InferType<typeof validationSchema>;
|
|||||||
|
|
||||||
export function CreateWorkspace() {
|
export function CreateWorkspace() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const onboardingStatus = useOnboardingStatus();
|
|
||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||||
|
useGetCurrentUserLazyQuery();
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
const {
|
const {
|
||||||
@ -84,7 +81,9 @@ export function CreateWorkspace() {
|
|||||||
throw result.errors ?? new Error('Unknown error');
|
throw result.errors ?? new Error('Unknown error');
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate('/auth/create/profile');
|
setTimeout(() => {
|
||||||
|
navigate('/create/profile');
|
||||||
|
}, 20);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
enqueueSnackBar(error?.message, {
|
enqueueSnackBar(error?.message, {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
@ -103,12 +102,6 @@ export function CreateWorkspace() {
|
|||||||
[onSubmit],
|
[onSubmit],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceCreation) {
|
|
||||||
navigate('/auth/create/profile');
|
|
||||||
}
|
|
||||||
}, [onboardingStatus, navigate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Create your workspace</Title>
|
<Title>Create your workspace</Title>
|
||||||
@ -119,7 +112,6 @@ export function CreateWorkspace() {
|
|||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
<SubSectionTitle title="Workspace logo" />
|
<SubSectionTitle title="Workspace logo" />
|
||||||
{/* Picture is actually uploaded on the fly */}
|
|
||||||
<WorkspaceLogoUploader />
|
<WorkspaceLogoUploader />
|
||||||
</StyledSectionContainer>
|
</StyledSectionContainer>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
@ -135,12 +127,14 @@ export function CreateWorkspace() {
|
|||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
autoFocus
|
||||||
value={value}
|
value={value}
|
||||||
placeholder="Apple"
|
placeholder="Apple"
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disableHotkeys
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,119 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { FooterNote } from '@/auth/components/ui/FooterNote';
|
|
||||||
import { HorizontalSeparator } from '@/auth/components/ui/HorizontalSeparator';
|
|
||||||
import { Logo } from '@/auth/components/ui/Logo';
|
|
||||||
import { Title } from '@/auth/components/ui/Title';
|
|
||||||
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
|
||||||
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
|
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
|
||||||
import { AnimatedEaseIn } from '@/ui/animation/components/AnimatedEaseIn';
|
|
||||||
import { MainButton } from '@/ui/button/components/MainButton';
|
|
||||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
|
||||||
import { IconBrandGoogle } from '@/ui/icon';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
|
||||||
width: 200px;
|
|
||||||
> * + * {
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledFooterNote = styled(FooterNote)`
|
|
||||||
max-width: 283px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function Index() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const theme = useTheme();
|
|
||||||
const [authProviders] = useRecoilState(authProvidersState);
|
|
||||||
const [demoMode] = useRecoilState(isDemoModeState);
|
|
||||||
|
|
||||||
const [authFlowUserEmail, setAuthFlowUserEmail] = useRecoilState(
|
|
||||||
authFlowUserEmailState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
|
|
||||||
const onGoogleLoginClick = useCallback(() => {
|
|
||||||
window.location.href = process.env.REACT_APP_AUTH_URL + '/google' || '';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onPasswordLoginClick = useCallback(() => {
|
|
||||||
if (!visible) {
|
|
||||||
setVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate('/auth/password-login');
|
|
||||||
}, [navigate, visible]);
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
|
||||||
'enter',
|
|
||||||
() => {
|
|
||||||
onPasswordLoginClick();
|
|
||||||
},
|
|
||||||
PageHotkeyScope.AuthIndex,
|
|
||||||
[onPasswordLoginClick],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAuthFlowUserEmail(demoMode ? 'tim@apple.dev' : '');
|
|
||||||
}, [navigate, setAuthFlowUserEmail, demoMode]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AnimatedEaseIn>
|
|
||||||
<Logo />
|
|
||||||
</AnimatedEaseIn>
|
|
||||||
<Title animate>Welcome to Twenty</Title>
|
|
||||||
<StyledContentContainer>
|
|
||||||
{authProviders.google && (
|
|
||||||
<MainButton
|
|
||||||
icon={<IconBrandGoogle size={theme.icon.size.sm} stroke={4} />}
|
|
||||||
title="Continue with Google"
|
|
||||||
onClick={onGoogleLoginClick}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{visible && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
stiffness: 800,
|
|
||||||
damping: 35,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HorizontalSeparator />
|
|
||||||
<TextInput
|
|
||||||
value={authFlowUserEmail}
|
|
||||||
placeholder="Email"
|
|
||||||
onChange={(value) => setAuthFlowUserEmail(value)}
|
|
||||||
fullWidth={true}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
<MainButton
|
|
||||||
title="Continue with Email"
|
|
||||||
onClick={onPasswordLoginClick}
|
|
||||||
disabled={!authFlowUserEmail && visible}
|
|
||||||
variant="secondary"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</StyledContentContainer>
|
|
||||||
<StyledFooterNote>
|
|
||||||
By using Twenty, you agree to the Terms of Service and Data Processing
|
|
||||||
Agreement.
|
|
||||||
</StyledFooterNote>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
import { useCallback, useState } from 'react';
|
|
||||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
import { Logo } from '@/auth/components/ui/Logo';
|
|
||||||
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
|
||||||
import { Title } from '@/auth/components/ui/Title';
|
|
||||||
import { useAuth } from '@/auth/hooks/useAuth';
|
|
||||||
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
|
||||||
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
|
|
||||||
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
|
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
|
||||||
import { MainButton } from '@/ui/button/components/MainButton';
|
|
||||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
|
||||||
import { SubSectionTitle } from '@/ui/title/components/SubSectionTitle';
|
|
||||||
import { useCheckUserExistsQuery } from '~/generated/graphql';
|
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
> * + * {
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(6)};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledForm = styled.form`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
> * + * {
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledSectionContainer = styled.div`
|
|
||||||
> * + * {
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
|
||||||
width: 200px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const validationSchema = Yup.object()
|
|
||||||
.shape({
|
|
||||||
exist: Yup.boolean().required(),
|
|
||||||
email: Yup.string().email('Email must be a valid email').required(),
|
|
||||||
password: Yup.string()
|
|
||||||
.matches(PASSWORD_REGEX, 'Password must contain at least 8 characters')
|
|
||||||
.required(),
|
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
type Form = Yup.InferType<typeof validationSchema>;
|
|
||||||
|
|
||||||
export function PasswordLogin() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
|
||||||
|
|
||||||
const [isDemoMode] = useRecoilState(isDemoModeState);
|
|
||||||
const [authFlowUserEmail] = useRecoilState(authFlowUserEmailState);
|
|
||||||
const [showErrors, setShowErrors] = useState(false);
|
|
||||||
|
|
||||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
|
||||||
|
|
||||||
const { data: checkUserExistsData } = useCheckUserExistsQuery({
|
|
||||||
variables: {
|
|
||||||
email: authFlowUserEmail,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { login, signUp } = useAuth();
|
|
||||||
|
|
||||||
// Form
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
watch,
|
|
||||||
getValues,
|
|
||||||
} = useForm<Form>({
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
exist: false,
|
|
||||||
email: authFlowUserEmail,
|
|
||||||
password: isDemoMode ? 'Applecar2025' : '',
|
|
||||||
},
|
|
||||||
resolver: yupResolver(validationSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<Form> = useCallback(
|
|
||||||
async (data) => {
|
|
||||||
try {
|
|
||||||
if (!data.email || !data.password) {
|
|
||||||
throw new Error('Email and password are required');
|
|
||||||
}
|
|
||||||
if (checkUserExistsData?.checkUserExists.exists) {
|
|
||||||
await login(data.email, data.password);
|
|
||||||
} else {
|
|
||||||
await signUp(data.email, data.password, workspaceInviteHash);
|
|
||||||
}
|
|
||||||
navigate('/auth/create/workspace');
|
|
||||||
} catch (err: any) {
|
|
||||||
enqueueSnackBar(err?.message, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
checkUserExistsData?.checkUserExists.exists,
|
|
||||||
navigate,
|
|
||||||
login,
|
|
||||||
signUp,
|
|
||||||
workspaceInviteHash,
|
|
||||||
enqueueSnackBar,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
useScopedHotkeys(
|
|
||||||
'enter',
|
|
||||||
() => {
|
|
||||||
onSubmit(getValues());
|
|
||||||
},
|
|
||||||
PageHotkeyScope.PasswordLogin,
|
|
||||||
[onSubmit],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Logo />
|
|
||||||
<Title>Welcome to Twenty</Title>
|
|
||||||
<SubTitle>
|
|
||||||
Enter your credentials to sign{' '}
|
|
||||||
{checkUserExistsData?.checkUserExists.exists ? 'in' : 'up'}
|
|
||||||
</SubTitle>
|
|
||||||
<StyledForm
|
|
||||||
onSubmit={(event) => {
|
|
||||||
setShowErrors(true);
|
|
||||||
return handleSubmit(onSubmit)(event);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledContentContainer>
|
|
||||||
<StyledSectionContainer>
|
|
||||||
<SubSectionTitle title="Email" />
|
|
||||||
<Controller
|
|
||||||
name="email"
|
|
||||||
control={control}
|
|
||||||
render={({
|
|
||||||
field: { onChange, onBlur, value },
|
|
||||||
fieldState: { error },
|
|
||||||
}) => (
|
|
||||||
<TextInput
|
|
||||||
value={value}
|
|
||||||
placeholder="Email"
|
|
||||||
onBlur={onBlur}
|
|
||||||
onChange={onChange}
|
|
||||||
error={showErrors ? error?.message : undefined}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</StyledSectionContainer>
|
|
||||||
<StyledSectionContainer>
|
|
||||||
<SubSectionTitle title="Password" />
|
|
||||||
<Controller
|
|
||||||
name="password"
|
|
||||||
control={control}
|
|
||||||
render={({
|
|
||||||
field: { onChange, onBlur, value },
|
|
||||||
fieldState: { error },
|
|
||||||
}) => (
|
|
||||||
<TextInput
|
|
||||||
value={value}
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
onBlur={onBlur}
|
|
||||||
onChange={onChange}
|
|
||||||
error={showErrors ? error?.message : undefined}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</StyledSectionContainer>
|
|
||||||
</StyledContentContainer>
|
|
||||||
<StyledButtonContainer>
|
|
||||||
<MainButton
|
|
||||||
title="Continue"
|
|
||||||
type="submit"
|
|
||||||
disabled={!watch('email') || !watch('password') || isSubmitting}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</StyledButtonContainer>
|
|
||||||
</StyledForm>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
5
front/src/pages/auth/SignInUp.tsx
Normal file
5
front/src/pages/auth/SignInUp.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { SignInUpForm } from '../../modules/auth/sign-in-up/components/SignInUpForm';
|
||||||
|
|
||||||
|
export function SignInUp() {
|
||||||
|
return <SignInUpForm />;
|
||||||
|
}
|
||||||
@ -4,6 +4,8 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
|||||||
import { useAuth } from '@/auth/hooks/useAuth';
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||||
|
|
||||||
|
import { AppPath } from '../../modules/types/AppPath';
|
||||||
|
|
||||||
export function Verify() {
|
export function Verify() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const loginToken = searchParams.get('loginToken');
|
const loginToken = searchParams.get('loginToken');
|
||||||
@ -16,10 +18,11 @@ export function Verify() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getTokens() {
|
async function getTokens() {
|
||||||
if (!loginToken) {
|
if (!loginToken) {
|
||||||
return;
|
navigate(AppPath.SignIn);
|
||||||
|
} else {
|
||||||
|
await verify(loginToken);
|
||||||
|
navigate('/');
|
||||||
}
|
}
|
||||||
await verify(loginToken);
|
|
||||||
navigate('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLogged) {
|
if (!isLogged) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { AuthModal } from '@/auth/components/ui/Modal';
|
import { AuthModal } from '@/auth/components/Modal';
|
||||||
import { AuthLayout } from '@/ui/layout/components/AuthLayout';
|
import { AuthLayout } from '@/ui/layout/components/AuthLayout';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { AuthModal } from '@/auth/components/ui/Modal';
|
import { AuthModal } from '@/auth/components/Modal';
|
||||||
import { AuthLayout } from '@/ui/layout/components/AuthLayout';
|
import { AuthLayout } from '@/ui/layout/components/AuthLayout';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||||
@ -23,7 +23,7 @@ export const Default: Story = {
|
|||||||
<CreateWorkspace />
|
<CreateWorkspace />
|
||||||
</AuthModal>
|
</AuthModal>
|
||||||
</AuthLayout>,
|
</AuthLayout>,
|
||||||
'/auth/create-workspace',
|
'/create-workspace',
|
||||||
),
|
),
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: graphqlMocks,
|
msw: graphqlMocks,
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { AuthModal } from '@/auth/components/ui/Modal';
|
import { AuthModal } from '@/auth/components/Modal';
|
||||||
import { AuthLayout } from '@/ui/layout/components/AuthLayout';
|
import { AuthLayout } from '@/ui/layout/components/AuthLayout';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||||
|
|
||||||
import { Index } from '../Index';
|
import { SignInUp } from '../SignInUp';
|
||||||
|
|
||||||
const meta: Meta<typeof Index> = {
|
const meta: Meta<typeof SignInUp> = {
|
||||||
title: 'Pages/Auth/Index',
|
title: 'Pages/Auth/SignInUp',
|
||||||
component: Index,
|
component: SignInUp,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
export type Story = StoryObj<typeof Index>;
|
export type Story = StoryObj<typeof SignInUp>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
render: getRenderWrapperForPage(
|
render: getRenderWrapperForPage(
|
||||||
<AuthLayout>
|
<AuthLayout>
|
||||||
<AuthModal>
|
<AuthModal>
|
||||||
<Index />
|
<SignInUp />
|
||||||
</AuthModal>
|
</AuthModal>
|
||||||
</AuthLayout>,
|
</AuthLayout>,
|
||||||
'/auth',
|
'/',
|
||||||
),
|
),
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: graphqlMocks,
|
msw: graphqlMocks,
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
|
|
||||||
import { AuthModal } from '@/auth/components/ui/Modal';
|
|
||||||
import { AuthLayout } from '@/ui/layout/components/AuthLayout';
|
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
|
||||||
|
|
||||||
import { PasswordLogin } from '../PasswordLogin';
|
|
||||||
|
|
||||||
const meta: Meta<typeof PasswordLogin> = {
|
|
||||||
title: 'Pages/Auth/PasswordLogin',
|
|
||||||
component: PasswordLogin,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
export type Story = StoryObj<typeof PasswordLogin>;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
render: getRenderWrapperForPage(
|
|
||||||
<AuthLayout>
|
|
||||||
<AuthModal>
|
|
||||||
<PasswordLogin />
|
|
||||||
</AuthModal>
|
|
||||||
</AuthLayout>,
|
|
||||||
'/auth/password-login',
|
|
||||||
),
|
|
||||||
parameters: {
|
|
||||||
msw: graphqlMocks,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -85,7 +85,7 @@ export function SettingsWorkspaceMembers() {
|
|||||||
description="Send an invitation to use Twenty"
|
description="Send an invitation to use Twenty"
|
||||||
/>
|
/>
|
||||||
<WorkspaceInviteLink
|
<WorkspaceInviteLink
|
||||||
inviteLink={`${window.location.origin}/auth/invite/${workspace?.inviteHash}`}
|
inviteLink={`${window.location.origin}/invite/${workspace?.inviteHash}`}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|||||||
import { within } from '@storybook/testing-library';
|
import { within } from '@storybook/testing-library';
|
||||||
|
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { mockedUserJWT } from '~/testing/mock-data/jwt';
|
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||||
|
|
||||||
import { SettingsProfile } from '../SettingsProfile';
|
import { SettingsProfile } from '../SettingsProfile';
|
||||||
@ -20,9 +19,6 @@ export const Default: Story = {
|
|||||||
render: getRenderWrapperForPage(<SettingsProfile />, '/settings/profile'),
|
render: getRenderWrapperForPage(<SettingsProfile />, '/settings/profile'),
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: graphqlMocks,
|
msw: graphqlMocks,
|
||||||
cookie: {
|
|
||||||
tokenPair: `{%22accessToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-07-18T15:06:40.704Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22refreshToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-10-15T15:06:41.558Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22__typename%22:%22AuthTokenPair%22}`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { mockedUserJWT } from '~/testing/mock-data/jwt';
|
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||||
|
|
||||||
import { SettingsWorkspaceMembers } from '../SettingsWorkspaceMembers';
|
import { SettingsWorkspaceMembers } from '../SettingsWorkspaceMembers';
|
||||||
@ -22,8 +21,5 @@ export const Default: Story = {
|
|||||||
),
|
),
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: graphqlMocks,
|
msw: graphqlMocks,
|
||||||
cookie: {
|
|
||||||
tokenPair: `{%22accessToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-07-18T15:06:40.704Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22refreshToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-10-15T15:06:41.558Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22__typename%22:%22AuthTokenPair%22}`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useEffect } from 'react';
|
|||||||
|
|
||||||
import { AppBasePath } from '@/types/AppBasePath';
|
import { AppBasePath } from '@/types/AppBasePath';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { AuthPath } from '@/types/AuthPath';
|
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
||||||
@ -16,49 +15,53 @@ export function HotkeyScopeBrowserRouterSync() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case isMatchingLocation(AppBasePath.Root, AppPath.CompaniesPage): {
|
case isMatchingLocation(AppPath.CompaniesPage): {
|
||||||
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
|
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppBasePath.Root, AppPath.PeoplePage): {
|
case isMatchingLocation(AppPath.PeoplePage): {
|
||||||
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
|
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppBasePath.Root, AppPath.CompanyShowPage): {
|
case isMatchingLocation(AppPath.CompanyShowPage): {
|
||||||
setHotkeyScope(PageHotkeyScope.CompanyShowPage, { goto: true });
|
setHotkeyScope(PageHotkeyScope.CompanyShowPage, { goto: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppBasePath.Root, AppPath.PersonShowPage): {
|
case isMatchingLocation(AppPath.PersonShowPage): {
|
||||||
setHotkeyScope(PageHotkeyScope.PersonShowPage, { goto: true });
|
setHotkeyScope(PageHotkeyScope.PersonShowPage, { goto: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppBasePath.Root, AppPath.OpportunitiesPage): {
|
case isMatchingLocation(AppPath.OpportunitiesPage): {
|
||||||
setHotkeyScope(PageHotkeyScope.OpportunitiesPage, { goto: true });
|
setHotkeyScope(PageHotkeyScope.OpportunitiesPage, { goto: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppBasePath.Auth, AuthPath.Index): {
|
case isMatchingLocation(AppPath.SignIn): {
|
||||||
setHotkeyScope(PageHotkeyScope.AuthIndex);
|
setHotkeyScope(PageHotkeyScope.SignInUp);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppBasePath.Auth, AuthPath.CreateProfile): {
|
case isMatchingLocation(AppPath.SignUp): {
|
||||||
|
setHotkeyScope(PageHotkeyScope.SignInUp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case isMatchingLocation(AppPath.Invite): {
|
||||||
|
setHotkeyScope(PageHotkeyScope.SignInUp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case isMatchingLocation(AppPath.CreateProfile): {
|
||||||
setHotkeyScope(PageHotkeyScope.CreateProfile);
|
setHotkeyScope(PageHotkeyScope.CreateProfile);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppBasePath.Auth, AuthPath.CreateWorkspace): {
|
case isMatchingLocation(AppPath.CreateWorkspace): {
|
||||||
setHotkeyScope(PageHotkeyScope.CreateWokspace);
|
setHotkeyScope(PageHotkeyScope.CreateWokspace);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppBasePath.Auth, AuthPath.PasswordLogin): {
|
case isMatchingLocation(SettingsPath.ProfilePage, AppBasePath.Settings): {
|
||||||
setHotkeyScope(PageHotkeyScope.PasswordLogin);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case isMatchingLocation(AppBasePath.Settings, SettingsPath.ProfilePage): {
|
|
||||||
setHotkeyScope(PageHotkeyScope.ProfilePage, { goto: true });
|
setHotkeyScope(PageHotkeyScope.ProfilePage, { goto: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(
|
case isMatchingLocation(
|
||||||
AppBasePath.Settings,
|
|
||||||
SettingsPath.WorkspaceMembersPage,
|
SettingsPath.WorkspaceMembersPage,
|
||||||
|
AppBasePath.Settings,
|
||||||
): {
|
): {
|
||||||
setHotkeyScope(PageHotkeyScope.WorkspaceMemberPage, { goto: true });
|
setHotkeyScope(PageHotkeyScope.WorkspaceMemberPage, { goto: true });
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -111,7 +111,6 @@ export const graphqlMocks = [
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
graphql.query(getOperationName(GET_PERSON) ?? '', (req, res, ctx) => {
|
graphql.query(getOperationName(GET_PERSON) ?? '', (req, res, ctx) => {
|
||||||
console.log({ req });
|
|
||||||
const returnedMockedData = fetchOneFromData<
|
const returnedMockedData = fetchOneFromData<
|
||||||
GetPersonQuery['findUniquePerson']
|
GetPersonQuery['findUniquePerson']
|
||||||
>(mockedPeopleData, req.variables.id);
|
>(mockedPeopleData, req.variables.id);
|
||||||
|
|||||||
@ -9,6 +9,7 @@ type MockedCompany = Pick<
|
|||||||
| 'createdAt'
|
| 'createdAt'
|
||||||
| 'address'
|
| 'address'
|
||||||
| 'employees'
|
| 'employees'
|
||||||
|
| 'linkedinUrl'
|
||||||
| '_commentThreadCount'
|
| '_commentThreadCount'
|
||||||
> & {
|
> & {
|
||||||
accountOwner: Pick<
|
accountOwner: Pick<
|
||||||
@ -31,6 +32,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
|
|||||||
createdAt: '2023-04-26T10:08:54.724515+00:00',
|
createdAt: '2023-04-26T10:08:54.724515+00:00',
|
||||||
address: '17 rue de clignancourt',
|
address: '17 rue de clignancourt',
|
||||||
employees: 12,
|
employees: 12,
|
||||||
|
linkedinUrl: 'https://www.linkedin.com/company/airbnb/',
|
||||||
_commentThreadCount: 1,
|
_commentThreadCount: 1,
|
||||||
accountOwner: {
|
accountOwner: {
|
||||||
email: 'charles@test.com',
|
email: 'charles@test.com',
|
||||||
@ -50,6 +52,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
|
|||||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
address: '',
|
address: '',
|
||||||
employees: 1,
|
employees: 1,
|
||||||
|
linkedinUrl: 'https://www.linkedin.com/company/aircall/',
|
||||||
_commentThreadCount: 1,
|
_commentThreadCount: 1,
|
||||||
accountOwner: null,
|
accountOwner: null,
|
||||||
__typename: 'Company',
|
__typename: 'Company',
|
||||||
@ -61,6 +64,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
|
|||||||
createdAt: '2023-04-26T10:10:32.530184+00:00',
|
createdAt: '2023-04-26T10:10:32.530184+00:00',
|
||||||
address: '',
|
address: '',
|
||||||
employees: 1,
|
employees: 1,
|
||||||
|
linkedinUrl: 'https://www.linkedin.com/company/algolia/',
|
||||||
_commentThreadCount: 1,
|
_commentThreadCount: 1,
|
||||||
accountOwner: null,
|
accountOwner: null,
|
||||||
__typename: 'Company',
|
__typename: 'Company',
|
||||||
@ -72,6 +76,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
|
|||||||
createdAt: '2023-03-21T06:30:25.39474+00:00',
|
createdAt: '2023-03-21T06:30:25.39474+00:00',
|
||||||
address: '',
|
address: '',
|
||||||
employees: 10,
|
employees: 10,
|
||||||
|
linkedinUrl: 'https://www.linkedin.com/company/apple/',
|
||||||
_commentThreadCount: 0,
|
_commentThreadCount: 0,
|
||||||
accountOwner: null,
|
accountOwner: null,
|
||||||
__typename: 'Company',
|
__typename: 'Company',
|
||||||
@ -83,6 +88,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
|
|||||||
createdAt: '2023-04-26T10:13:29.712485+00:00',
|
createdAt: '2023-04-26T10:13:29.712485+00:00',
|
||||||
address: '10 rue de la Paix',
|
address: '10 rue de la Paix',
|
||||||
employees: 1,
|
employees: 1,
|
||||||
|
linkedinUrl: 'https://www.linkedin.com/company/qonto/',
|
||||||
_commentThreadCount: 2,
|
_commentThreadCount: 2,
|
||||||
accountOwner: null,
|
accountOwner: null,
|
||||||
__typename: 'Company',
|
__typename: 'Company',
|
||||||
@ -94,6 +100,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
|
|||||||
createdAt: '2023-04-26T10:09:25.656555+00:00',
|
createdAt: '2023-04-26T10:09:25.656555+00:00',
|
||||||
address: '',
|
address: '',
|
||||||
employees: 1,
|
employees: 1,
|
||||||
|
linkedinUrl: 'https://www.linkedin.com/company/facebook/',
|
||||||
_commentThreadCount: 13,
|
_commentThreadCount: 13,
|
||||||
accountOwner: null,
|
accountOwner: null,
|
||||||
__typename: 'Company',
|
__typename: 'Company',
|
||||||
@ -105,6 +112,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
|
|||||||
createdAt: '2023-04-26T10:09:25.656555+00:00',
|
createdAt: '2023-04-26T10:09:25.656555+00:00',
|
||||||
address: '',
|
address: '',
|
||||||
employees: 1,
|
employees: 1,
|
||||||
|
linkedinUrl: 'https://www.linkedin.com/company/sequoia/',
|
||||||
_commentThreadCount: 1,
|
_commentThreadCount: 1,
|
||||||
accountOwner: null,
|
accountOwner: null,
|
||||||
__typename: 'Company',
|
__typename: 'Company',
|
||||||
|
|||||||
@ -7,7 +7,7 @@ LOGIN_TOKEN_EXPIRES_IN=15m
|
|||||||
REFRESH_TOKEN_SECRET=secret_refresh_token
|
REFRESH_TOKEN_SECRET=secret_refresh_token
|
||||||
REFRESH_TOKEN_EXPIRES_IN=90d
|
REFRESH_TOKEN_EXPIRES_IN=90d
|
||||||
PG_DATABASE_URL=postgres://postgres:postgrespassword@localhost:5432/default?connection_limit=1
|
PG_DATABASE_URL=postgres://postgres:postgrespassword@localhost:5432/default?connection_limit=1
|
||||||
FRONT_AUTH_CALLBACK_URL=http://localhost:3001/auth/callback
|
FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
|
||||||
STORAGE_TYPE=local
|
STORAGE_TYPE=local
|
||||||
STORAGE_LOCAL_PATH=.local-storage
|
STORAGE_LOCAL_PATH=.local-storage
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
InternalServerErrorException,
|
|
||||||
Req,
|
|
||||||
Res,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
@ -14,45 +6,64 @@ import { GoogleRequest } from 'src/core/auth/strategies/google.auth.strategy';
|
|||||||
import { UserService } from 'src/core/user/user.service';
|
import { UserService } from 'src/core/user/user.service';
|
||||||
import { TokenService } from 'src/core/auth/services/token.service';
|
import { TokenService } from 'src/core/auth/services/token.service';
|
||||||
import { GoogleProviderEnabledGuard } from 'src/core/auth/guards/google-provider-enabled.guard';
|
import { GoogleProviderEnabledGuard } from 'src/core/auth/guards/google-provider-enabled.guard';
|
||||||
|
import { GoogleOauthGuard } from 'src/core/auth/guards/google-oauth.guard';
|
||||||
|
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
|
||||||
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
|
|
||||||
@Controller('auth/google')
|
@Controller('auth/google')
|
||||||
export class GoogleAuthController {
|
export class GoogleAuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseGuards(GoogleProviderEnabledGuard, AuthGuard('google'))
|
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
||||||
async googleAuth() {
|
async googleAuth() {
|
||||||
// As this method is protected by Google Auth guard, it will trigger Google SSO flow
|
// As this method is protected by Google Auth guard, it will trigger Google SSO flow
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('redirect')
|
@Get('redirect')
|
||||||
@UseGuards(GoogleProviderEnabledGuard, AuthGuard('google'))
|
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
||||||
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
||||||
const { firstName, lastName, email } = req.user;
|
const { firstName, lastName, email, workspaceInviteHash } = req.user;
|
||||||
|
|
||||||
const user = await this.userService.createUser({
|
let workspaceId: string | undefined = undefined;
|
||||||
data: {
|
if (workspaceInviteHash) {
|
||||||
email,
|
const workspace = await this.workspaceService.findFirst({
|
||||||
firstName: firstName ?? '',
|
where: {
|
||||||
lastName: lastName ?? '',
|
inviteHash: workspaceInviteHash,
|
||||||
locale: 'en',
|
},
|
||||||
settings: {
|
});
|
||||||
create: {
|
|
||||||
locale: 'en',
|
if (!workspace) {
|
||||||
|
return res.redirect(
|
||||||
|
`${this.environmentService.getFrontAuthCallbackUrl()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceId = workspace.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userService.createUser(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
firstName: firstName ?? '',
|
||||||
|
lastName: lastName ?? '',
|
||||||
|
locale: 'en',
|
||||||
|
settings: {
|
||||||
|
create: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
workspaceId,
|
||||||
|
);
|
||||||
if (!user) {
|
|
||||||
throw new InternalServerErrorException(
|
|
||||||
'User email domain does not match an existing workspace',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginToken = await this.tokenService.generateLoginToken(user.email);
|
const loginToken = await this.tokenService.generateLoginToken(user.email);
|
||||||
|
|
||||||
|
|||||||
27
server/src/core/auth/guards/google-oauth.guard.ts
Normal file
27
server/src/core/auth/guards/google-oauth.guard.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GoogleOauthGuard extends AuthGuard('google') {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
prompt: 'select_account',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext) {
|
||||||
|
try {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const workspaceInviteHash = request.query.inviteHash;
|
||||||
|
|
||||||
|
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
|
||||||
|
request.params.workspaceInviteHash = workspaceInviteHash;
|
||||||
|
}
|
||||||
|
const activate = (await super.canActivate(context)) as boolean;
|
||||||
|
|
||||||
|
return activate;
|
||||||
|
} catch (ex) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,9 +8,10 @@ import { EnvironmentService } from 'src/integrations/environment/environment.ser
|
|||||||
|
|
||||||
export type GoogleRequest = Request & {
|
export type GoogleRequest = Request & {
|
||||||
user: {
|
user: {
|
||||||
firstName: string | undefined | null;
|
firstName?: string | null;
|
||||||
lastName: string | undefined | null;
|
lastName?: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
|
workspaceInviteHash?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -22,23 +23,39 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
|||||||
clientSecret: environmentService.getAuthGoogleClientSecret(),
|
clientSecret: environmentService.getAuthGoogleClientSecret(),
|
||||||
callbackURL: environmentService.getAuthGoogleCallbackUrl(),
|
callbackURL: environmentService.getAuthGoogleCallbackUrl(),
|
||||||
scope: ['email', 'profile'],
|
scope: ['email', 'profile'],
|
||||||
|
passReqToCallback: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticate(req: any, options: any) {
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
state: JSON.stringify({
|
||||||
|
workspaceInviteHash: req.params.workspaceInviteHash,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return super.authenticate(req, options);
|
||||||
|
}
|
||||||
|
|
||||||
async validate(
|
async validate(
|
||||||
|
request: GoogleRequest,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
profile: any,
|
profile: any,
|
||||||
done: VerifyCallback,
|
done: VerifyCallback,
|
||||||
): Promise<any> {
|
): Promise<void> {
|
||||||
const { name, emails, photos } = profile;
|
const { name, emails } = profile;
|
||||||
|
const state =
|
||||||
|
typeof request.query.state === 'string'
|
||||||
|
? JSON.parse(request.query.state)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
email: emails[0].value,
|
email: emails[0].value,
|
||||||
firstName: name.givenName,
|
firstName: name.givenName,
|
||||||
lastName: name.familyName,
|
lastName: name.familyName,
|
||||||
picture: photos[0].value,
|
workspaceInviteHash: state.workspaceInviteHash,
|
||||||
refreshToken,
|
|
||||||
accessToken,
|
|
||||||
};
|
};
|
||||||
done(null, user);
|
done(null, user);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,6 @@ export class CompanyService {
|
|||||||
data: companies,
|
data: companies,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.findMany({ where: { workspaceId }});
|
return this.findMany({ where: { workspaceId } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user