Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(web): More auth refactoring #5636

Merged
merged 2 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { colors, Text, Title, Container } from '@novu/design-system';
import PageMeta from './PageMeta';

export default function AuthContainer({
export default function AuthLayout({
title,
description = '',
children,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { addOrganization, switchOrganization } from '../../../api/organization';
import { useSpotlightContext } from '../../providers/SpotlightProvider';

export const useOrganizationSelect = () => {
const [value, setValue] = useState<string>('');
const [search, setSearch] = useState<string>('');
const [loadingSwitch, setLoadingSwitch] = useState<boolean>(false);
const { addItem, removeItems } = useSpotlightContext();
Expand Down Expand Up @@ -60,6 +59,8 @@ export const useOrganizationSelect = () => {
});
}

const value = currentOrganization?._id;

const organizationItems = useMemo(() => {
return (organizations || [])
.filter((item) => item._id !== value)
Expand All @@ -72,13 +73,8 @@ export const useOrganizationSelect = () => {
}));
}, [organizations, value, switchOrgCallback]);

useEffect(() => {
setValue(currentOrganization?._id || '');
}, [currentOrganization]);

useEffect(() => {
removeItems(['change-org-' + value]);

addItem(organizationItems);
}, [addItem, removeItems, organizationItems, value]);

Expand Down
14 changes: 2 additions & 12 deletions apps/web/src/components/utils/Spotlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { UTM_CAMPAIGN_QUERY_PARAM } from '@novu/shared';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

import { useAuth } from '@novu/shared-web';
import { ROUTES } from '../../constants/routes.enum';
import useThemeChange from '../../hooks/useThemeChange';
import { useSpotlightContext } from '../providers/SpotlightProvider';
Expand All @@ -13,9 +12,8 @@ import useStyles from './Spotlight.styles';
export const SpotLight = ({ children }) => {
const navigate = useNavigate();
const { items, addItem } = useSpotlightContext();
const { logout } = useAuth();
const { themeIcon, toggleColorScheme } = useThemeChange();
const { classes, theme } = useStyles();
const { classes } = useStyles();

useEffect(() => {
addItem([
Expand Down Expand Up @@ -91,16 +89,8 @@ export const SpotLight = ({ children }) => {
toggleColorScheme();
},
},
{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes logout from ctrl+K to prevent large number of rerenders.

id: 'sign-out',
title: 'Sign out',
icon: <IconLogout />,
onTrigger: () => {
logout();
},
},
]);
}, [navigate, addItem, themeIcon, toggleColorScheme, logout]);
}, [navigate, addItem, themeIcon, toggleColorScheme]);

return (
<SpotlightProvider limit={7} shortcut={['mod + K']} actions={items} classNames={classes}>
Expand Down
7 changes: 2 additions & 5 deletions apps/web/src/hooks/useBlueprint.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useEffect } from 'react';

import { useAuth } from '@novu/shared-web';
import { useSegment } from '../components/providers/SegmentProvider';
import { ROUTES } from '../constants/routes.enum';

export const useBlueprint = () => {
const [params] = useSearchParams();
const blueprintId = params.get('blueprintId');
const navigate = useNavigate();
const { pathname } = useLocation();
const segment = useSegment();
const id = localStorage.getItem('blueprintId');
const { token } = useAuth();

useEffect(() => {
if (id) {
navigate(ROUTES.WORKFLOWS_CREATE, {
replace: true,
});
}
}, [navigate, id, token, pathname]);
}, [navigate, id]);

useEffect(() => {
if (blueprintId) {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/hooks/useVercelParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export function useVercelParams() {
const code = params.get('code');
const next = params.get('next');
const configurationId = params.get('configurationId');

const isFromVercel = !!(code && next);

return {
params,
code,
next,
configurationId,
Expand Down
52 changes: 20 additions & 32 deletions apps/web/src/pages/auth/InvitationPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useLocation, useNavigate, useParams, Link } from 'react-router-dom';
import { useParams, Link } from 'react-router-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { Center, LoadingOverlay } from '@mantine/core';
import { IGetInviteResponseDto } from '@novu/shared';

import { getInviteTokenData } from '../../api/invitation';
import AuthContainer from '../../components/layout/components/AuthContainer';
import AuthLayout from '../../components/layout/components/AuthLayout';
import { SignUpForm } from './components/SignUpForm';
import { colors, Text, Button } from '@novu/design-system';
import { useAuth } from '@novu/shared-web';
Expand All @@ -14,16 +14,10 @@ import { LoginForm } from './components/LoginForm';

export default function InvitationPage() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { token, logout, currentUser } = useAuth();
const location = useLocation();
// TODO: Replace token check with currentUser check
const isLoggedIn = !!token;
const { currentUser, logout } = useAuth();
const { token: invitationToken } = useParams<{ token: string }>();
const tokensRef = useRef({ token, invitationToken });
tokensRef.current = { token, invitationToken };
const { isLoading: isAcceptingInvite, submitToken } = useAcceptInvite();
const { data, isInitialLoading } = useQuery<IGetInviteResponseDto, IGetInviteResponseDto>(
const { isLoading: isAcceptingInvite, acceptInvite } = useAcceptInvite();
const { data, isLoading: isInviteTokenDataLoading } = useQuery<IGetInviteResponseDto, IGetInviteResponseDto>(
['getInviteTokenData'],
() => getInviteTokenData(invitationToken || ''),
{
Expand All @@ -33,30 +27,24 @@ export default function InvitationPage() {
);
const inviterFirstName = data?.inviter?.firstName || '';
const organizationName = data?.organization.name || '';
const existingUser = !!(invitationToken && data?._userId);
const isLoggedInAsInvitedUser = !!(isLoggedIn && existingUser && currentUser && currentUser._id === data?._userId);
const Form = existingUser ? LoginForm : SignUpForm;

const logoutWhenActiveSession = () => {
logout();
navigate(location.pathname);
};
const existingUserId = data?._userId;
const isLoggedInAsInvitedUser = !!(existingUserId && currentUser && currentUser._id === existingUserId);
const Form = existingUserId ? LoginForm : SignUpForm;

useEffect(() => {
// auto accept invitation when logged in as invited user
if (isLoggedInAsInvitedUser) {
submitToken(tokensRef.current.token as string, tokensRef.current.invitationToken as string, true);
if (invitationToken && isLoggedInAsInvitedUser) {
acceptInvite(invitationToken);
}
}, [isLoggedInAsInvitedUser, submitToken]);
}, [isLoggedInAsInvitedUser, acceptInvite, invitationToken]);

useEffect(() => {
return () => {
queryClient.removeQueries(['getInviteTokenData']);
};
}, [queryClient]);

return isLoggedIn ? (
<AuthContainer
return currentUser ? (
<AuthLayout
title="Active Session!"
customDescription={
<Center inline mb={40} mt={20}>
Expand All @@ -70,7 +58,7 @@ export default function InvitationPage() {
</Center>
}
>
<Button data-test-id="success-screen-reset" onClick={logoutWhenActiveSession} inherit>
<Button data-test-id="success-screen-reset" onClick={() => logout()} inherit>
Log out
</Button>
<Center mt={20}>
Expand All @@ -81,10 +69,10 @@ export default function InvitationPage() {
<Text>Dashboard</Text>
</Link>
</Center>
</AuthContainer>
</AuthLayout>
) : (
<AuthContainer
title={existingUser ? 'Sign In & Accept Invite' : 'Get Started'}
<AuthLayout
title={existingUserId ? 'Sign In & Accept Invite' : 'Get Started'}
customDescription={
inviterFirstName && organizationName ? (
<Center inline mb={60} mt={20} data-test-id="invitation-description">
Expand All @@ -107,7 +95,7 @@ export default function InvitationPage() {
) : undefined
}
>
{isInitialLoading ? (
{isInviteTokenDataLoading ? (
<LoadingOverlay
visible
overlayColor={colors.B30}
Expand All @@ -118,6 +106,6 @@ export default function InvitationPage() {
) : (
<Form email={data?.email} invitationToken={invitationToken} />
)}
</AuthContainer>
</AuthLayout>
);
}
76 changes: 4 additions & 72 deletions apps/web/src/pages/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,10 @@
import { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

import { useAuth } from '@novu/shared-web';
import { LoginForm } from './components/LoginForm';
import AuthContainer from '../../components/layout/components/AuthContainer';
import { useVercelIntegration, useBlueprint, useVercelParams } from '../../hooks';
import SetupLoader from './components/SetupLoader';
import { useSegment } from '../../components/providers/SegmentProvider';
import { useAcceptInvite } from './components/useAcceptInvite';
import { ROUTES } from '../../constants/routes.enum';
import AuthLayout from '../../components/layout/components/AuthLayout';

export default function LoginPage() {
useBlueprint();
const { login, token: oldToken, currentUser, claims } = useAuth();
const segment = useSegment();
const navigate = useNavigate();
const [params] = useSearchParams();
const queryToken = params.get('token');
const invitationToken = params.get('invitationToken');
const source = params.get('source');
const sourceWidget = params.get('source_widget');
const token = queryToken ?? oldToken;

const { startVercelSetup, isLoading } = useVercelIntegration();
const { code, isFromVercel, next } = useVercelParams();
const { isLoading: isLoadingAcceptInvite, submitToken } = useAcceptInvite();

useEffect(() => {
if (token) {
if (!invitationToken && currentUser?._id && (!claims?.organizationId || !claims?.environmentId)) {
const authApplicationLink = isFromVercel
? `${ROUTES.AUTH_APPLICATION}?code=${code}&next=${next}`
: ROUTES.AUTH_APPLICATION;
login(token);
navigate(authApplicationLink);

return;
}

if (isFromVercel) {
login(token);
startVercelSetup();

return;
}

if (source === 'cli') {
segment.track('Dashboard Visit', {
widget: sourceWidget || 'unknown',
source: 'cli',
});
login(token);
navigate(ROUTES.GET_STARTED);

return;
}

if (invitationToken) {
submitToken(token, invitationToken);

return;
}

login(token);
navigate('/');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);

return isLoading || isLoadingAcceptInvite ? (
<SetupLoader title="Loading..." />
) : (
<AuthContainer title="Sign In" description="Welcome back! Sign in with the data you entered in your registration">
return (
<AuthLayout title="Sign In" description="Welcome back!">
<LoginForm />
</AuthContainer>
</AuthLayout>
);
}
30 changes: 13 additions & 17 deletions apps/web/src/pages/auth/PasswordResetPage.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,35 @@
import { useParams, useNavigate } from 'react-router-dom';
import { useState } from 'react';
import AuthContainer from '../../components/layout/components/AuthContainer';
import AuthLayout from '../../components/layout/components/AuthLayout';
import { PasswordResetRequestForm } from './components/PasswordResetRequestForm';
import { PasswordResetForm } from './components/PasswordResetForm';
import { Button } from '@novu/design-system';
import { ROUTES } from '../../constants/routes.enum';
import { useVercelParams } from '../../hooks';

type Props = {};

export function PasswordResetPage({}: Props) {
export function PasswordResetPage() {
const navigate = useNavigate();
const { token } = useParams<{ token: string }>();
const [showSentSuccess, setShowSentSuccess] = useState<boolean>();
const { isFromVercel, code, next, configurationId } = useVercelParams();
const { isFromVercel, params } = useVercelParams();

const vercelQueryParams = `code=${code}&next=${next}&configurationId=${configurationId}`;
const loginLink = isFromVercel ? `/auth/login?${vercelQueryParams}` : ROUTES.AUTH_LOGIN;
function onSent() {
setShowSentSuccess(true);
}
const loginLink = isFromVercel ? `${ROUTES.AUTH_LOGIN}?${params.toString()}` : ROUTES.AUTH_LOGIN;

return showSentSuccess ? (
<AuthContainer
if (showSentSuccess) {
<AuthLayout
title="Reset Sent!"
description="We've sent a password reset link to the account associated with your email"
>
<Button data-test-id="success-screen-reset" onClick={() => navigate(loginLink)} inherit>
Go Back
</Button>
</AuthContainer>
) : (
<AuthContainer title="Reset Password" description="">
{!token && <PasswordResetRequestForm onSent={onSent} />}
</AuthLayout>;
}

return (
<AuthLayout title="Reset Password" description="">
{!token && <PasswordResetRequestForm onSent={() => setShowSentSuccess(true)} />}
{token && <PasswordResetForm token={token} />}
</AuthContainer>
</AuthLayout>
);
}
Loading
Loading