From 5ed014b02bf3f03348c3e40a6c4a014a18ec85fa Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 21 Jun 2024 12:00:56 +0200 Subject: [PATCH 01/45] style(ms2/signin): shift signin modal up --- src/management-system-v2/app/(auth)/signin/signin.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index dccabbd80..a007dd0c6 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -51,6 +51,7 @@ const SignIn: FC<{ style={{ maxWidth: '400px', width: '90%', + top: 0, }} styles={{ mask: { backdropFilter: 'blur(5px)', WebkitBackdropFilter: 'blur(5px)' }, From 2ef53a9c5b5eb3163e51a5ed9eaa0db15f2e26e1 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 21 Jun 2024 12:16:47 +0200 Subject: [PATCH 02/45] style(ms2/signin): sign in as guest --- src/management-system-v2/app/(auth)/signin/signin.tsx | 7 +++++++ .../app/api/auth/[...nextauth]/auth-options.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index a007dd0c6..2e9d617f4 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -73,6 +73,13 @@ const SignIn: FC<{ key={provider.id} layout="vertical" > + {provider.id === 'guest-signin' && ( + + )} {Object.keys(provider.credentials).map((key) => ( diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index 8e8a9de96..25b487ea7 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -19,7 +19,7 @@ const nextAuthOptions: AuthOptions = { }, providers: [ CredentialsProvider({ - name: 'Continue as a Guest', + name: 'Sign in as Guest', id: 'guest-signin', credentials: {}, async authorize() { From 5b479b15d7db3b57b3cf568d71a9597181fcfb77 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 24 Jun 2024 11:12:36 +0200 Subject: [PATCH 03/45] style(ms2/signin): changed sorting of providers --- .../app/(auth)/signin/page.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/management-system-v2/app/(auth)/signin/page.tsx b/src/management-system-v2/app/(auth)/signin/page.tsx index 0f65ba27b..af3bea446 100644 --- a/src/management-system-v2/app/(auth)/signin/page.tsx +++ b/src/management-system-v2/app/(auth)/signin/page.tsx @@ -19,15 +19,20 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin (provider) => !isGuest || !['guest-signin', 'development-users'].includes(provider.id), ); - providers = providers.sort((a, b) => { - if (a.type === 'email') { - return -2; - } - if (a.type === 'credentials') { - return -1; - } - - return 1; + providers = providers.toSorted((a, b) => { + if (a.id === 'guest-signin') return 1; + if (b.id === 'guest-signin') return -1; + + if (a.type === 'oauth') return 1; + if (b.type === 'oauth') return -1; + + if (a.id === 'development-users') return -1; + if (b.id === 'development-users') return 1; + + if (a.type === 'email') return -1; + if (b.type === 'email') return 1; + + return 0; }); return ; From 8e3fb017c3f2c00cdced9df82ea31ca8ab66c5a5 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 24 Jun 2024 11:13:47 +0200 Subject: [PATCH 04/45] style(ms2/signin): new layout --- .../app/(auth)/signin/signin.tsx | 187 +++++++++--------- 1 file changed, 99 insertions(+), 88 deletions(-) diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index 2e9d617f4..b2e8a9a82 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -1,16 +1,7 @@ 'use client'; -import { FC, Fragment, ReactNode, useEffect, useState } from 'react'; -import { - Typography, - Alert, - Form, - Input, - Button, - Image as AntDesignImage, - Divider, - Modal, -} from 'antd'; +import { FC, useEffect, useState } from 'react'; +import { Typography, Alert, Form, Input, Button, Divider, Modal, Space, Tooltip } from 'antd'; import styles from './login.module.scss'; import { useSearchParams } from 'next/navigation'; @@ -26,6 +17,12 @@ const SignIn: FC<{ const callbackUrl = searchParams.get('callbackUrl') ?? '/'; const authError = searchParams.get('error'); + const oauthProviders = providers.filter((provider) => provider.type === 'oauth'); + const guestProvider = providers.find((provider) => provider.id === 'guest-signin'); + const credentials = providers.filter( + (provider) => provider.type !== 'oauth' && provider.id !== 'guest-signin', + ); + // We need to wait until the component is mounted on the client // to open the modal, otherwise it will cause a hydration mismatch const [open, setOpen] = useState(false); @@ -64,88 +61,102 @@ const SignIn: FC<{ {authError && } - {providers.map((provider, idx) => { - let loginMethod: ReactNode; - if (provider.type === 'credentials') { - loginMethod = ( -
signIn(provider.id, { ...values, callbackUrl })} - key={provider.id} - layout="vertical" - > - {provider.id === 'guest-signin' && ( - - )} - {Object.keys(provider.credentials).map((key) => ( - - + + {credentials.map((provider) => { + if (provider.type === 'credentials') { + return ( + signIn(provider.id, { ...values, callbackUrl })} + key={provider.id} + layout="vertical" + > + {Object.keys(provider.credentials).map((key) => ( + + + + ))} + + + ); + } else if (provider.type === 'email') { + return ( +
signIn(provider.id, { ...values, callbackUrl })} + key={provider.id} + layout="vertical" + > + + - ))} - -
- ); - } else if (provider.type === 'oauth') { - loginMethod = ( - + + ); + } + })} + + + {oauthProviders.map((provider, idx) => { + if (provider.type !== 'oauth') return null; + return ( + + - ); - } else if (provider.type === 'email') { - loginMethod = ( - signIn(provider.id, { ...values, callbackUrl })} - key={provider.id} - layout="vertical" - > - - - - - - ); - } - return ( - - {loginMethod} - {idx < providers.length - 1 && provider.type !== 'oauth' && ( - - - OR - - - )} - - ); - })} + + + + )} + By signing in, you agree to our Terms of Service From 9b9ed4930a4eb419d400e5035a62a3b5eb7d9a76 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 24 Jun 2024 13:08:42 +0200 Subject: [PATCH 05/45] style(ms2/signin): continue in as guest --- .../app/api/auth/[...nextauth]/auth-options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index 25b487ea7..9845ae57a 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -19,7 +19,7 @@ const nextAuthOptions: AuthOptions = { }, providers: [ CredentialsProvider({ - name: 'Sign in as Guest', + name: 'Continue as guest', id: 'guest-signin', credentials: {}, async authorize() { From ce3da116e473075754d8f72ef19534df1c50b8f8 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sat, 29 Jun 2024 10:03:17 +0200 Subject: [PATCH 06/45] style(ms2/signin) --- .../app/(auth)/signin/page.tsx | 8 +- .../app/(auth)/signin/signin.tsx | 296 ++++++++++++------ 2 files changed, 198 insertions(+), 106 deletions(-) diff --git a/src/management-system-v2/app/(auth)/signin/page.tsx b/src/management-system-v2/app/(auth)/signin/page.tsx index af3bea446..61d72eeb8 100644 --- a/src/management-system-v2/app/(auth)/signin/page.tsx +++ b/src/management-system-v2/app/(auth)/signin/page.tsx @@ -16,7 +16,7 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin let providers = getProviders(); providers = providers.filter( - (provider) => !isGuest || !['guest-signin', 'development-users'].includes(provider.id), + (provider) => !isGuest || !['development-users'].includes(provider.id), ); providers = providers.toSorted((a, b) => { @@ -35,7 +35,11 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin return 0; }); - return ; + let userType; + if (!session) userType = 'none' as const; + else userType = isGuest ? ('guest' as const) : ('user' as const); + + return ; }; export default SignInPage; diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index b2e8a9a82..234ed2d8e 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -1,7 +1,19 @@ 'use client'; import { FC, useEffect, useState } from 'react'; -import { Typography, Alert, Form, Input, Button, Divider, Modal, Space, Tooltip } from 'antd'; +import { + Typography, + Alert, + Form, + Input, + Button as AntDesignButton, + Divider, + Modal, + Space, + Tooltip, + ButtonProps, + ConfigProvider, +} from 'antd'; import styles from './login.module.scss'; import { useSearchParams } from 'next/navigation'; @@ -10,9 +22,43 @@ import Image from 'next/image'; import { signIn } from 'next-auth/react'; import { type ExtractedProvider } from '@/app/api/auth/[...nextauth]/auth-options'; +const verticalGap = '1rem'; + +const divider = ( + + OR + +); + +const Button = (props: ButtonProps) => ( +
+ +
+); + +const signInTitle = ( + + SIGN IN + +); + const SignIn: FC<{ providers: ExtractedProvider[]; -}> = ({ providers }) => { + userType: 'guest' | 'user' | 'none'; +}> = ({ providers, userType }) => { const searchParams = useSearchParams(); const callbackUrl = searchParams.get('callbackUrl') ?? '/'; const authError = searchParams.get('error'); @@ -31,86 +77,126 @@ const SignIn: FC<{ }, [setOpen]); return ( - - } - open={open} - closeIcon={null} - footer={null} - style={{ - maxWidth: '400px', - width: '90%', - top: 0, - }} - styles={{ - mask: { backdropFilter: 'blur(5px)', WebkitBackdropFilter: 'blur(5px)' }, + - - Sign in - + + } + open={open} + closeIcon={null} + footer={null} + style={{ + maxWidth: '60ch', + width: '90%', + top: 0, + }} + styles={{ + mask: { backdropFilter: 'blur(5px)', WebkitBackdropFilter: 'blur(5px)' }, + header: { paddingBottom: verticalGap }, + }} + className={styles.Card} + > + {authError && ( + + )} - {authError && } + {userType === 'none' ? ( + + TRY PROCEED + + ) : ( + signInTitle + )} - - {credentials.map((provider) => { - if (provider.type === 'credentials') { - return ( -
signIn(provider.id, { ...values, callbackUrl })} - key={provider.id} - layout="vertical" - > - {Object.keys(provider.credentials).map((key) => ( - - - - ))} - -
- ); - } else if (provider.type === 'email') { - return ( -
signIn(provider.id, { ...values, callbackUrl })} - key={provider.id} - layout="vertical" - > - + signIn(guestProvider.id, { ...values, callbackUrl })} + key={guestProvider.id} + layout="vertical" + > + + + {divider} + + )} + + {userType === 'none' && signInTitle} + + + {credentials.map((provider) => { + if (provider.type === 'credentials') { + return ( +
signIn(provider.id, { ...values, callbackUrl })} + key={provider.id} + layout="vertical" > - - - -
- ); - } - })} + {Object.keys(provider.credentials).map((key) => ( + + + + ))} + + + ); + } else if (provider.type === 'email') { + return ( + <> +
signIn(provider.id, { ...values, callbackUrl })} + key={provider.id} + layout="vertical" + > + + + + +
- + + + ); + } + })} + + + {divider} + + {oauthProviders.map((provider, idx) => { if (provider.type !== 'oauth') return null; return ( - - - - - - )} - - - By signing in, you agree to our Terms of Service - -
+ + + + + + )} + + + By using the PROCEED Platform, you agree to the{' '} + Terms of Service and the storage of functionally essential + cookies on your device. + +
+ ); }; From 7f845f0a16767f17f0b79d3340a15c7289072ac2 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 30 Jun 2024 21:22:42 +0200 Subject: [PATCH 07/45] style(ms2): changed info color to gray --- src/management-system-v2/components/theme.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/management-system-v2/components/theme.tsx b/src/management-system-v2/components/theme.tsx index 0da472627..55cea3e24 100644 --- a/src/management-system-v2/components/theme.tsx +++ b/src/management-system-v2/components/theme.tsx @@ -32,6 +32,8 @@ const Theme: FC = ({ children }) => { screenSMMin: 601, screenSM: 601, screenXSMax: 600, + colorInfoBg: '#fafafa', // gray-3 (ant design colors) + colorInfoBorder: '#d9d9d9', // gray-2 (ant design colors) }, components: { Layout: { From 3aedc6265c11fda914a4d020e55967980639019c Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 30 Jun 2024 21:23:15 +0200 Subject: [PATCH 08/45] feat(ms2): show guests a warning --- .../components/header-actions.tsx | 79 ++++++++++++++----- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/src/management-system-v2/components/header-actions.tsx b/src/management-system-v2/components/header-actions.tsx index dc8c317e0..c87ca5eda 100644 --- a/src/management-system-v2/components/header-actions.tsx +++ b/src/management-system-v2/components/header-actions.tsx @@ -1,9 +1,19 @@ 'use client'; -import { UserOutlined } from '@ant-design/icons'; -import { Avatar, Button, Dropdown, Space, Tooltip } from 'antd'; +import { UserOutlined, WarningOutlined } from '@ant-design/icons'; +import { + Alert, + Avatar, + Button, + ConfigProvider, + Dropdown, + Modal, + Space, + Tooltip, + theme, +} from 'antd'; import { signIn, signOut, useSession } from 'next-auth/react'; -import { FC, ReactNode } from 'react'; +import { FC, ReactNode, useState } from 'react'; import Assistant from '@/components/assistant'; import UserAvatar from './user-avatar'; import SpaceLink from './space-link'; @@ -13,6 +23,8 @@ const HeaderActions: FC = () => { const session = useSession(); const isGuest = session.data?.user.guest; const loggedIn = session.status === 'authenticated'; + const token = theme.useToken(); + const [guestWarningOpen, setGuestWarningOpen] = useState(false); if (!process.env.NEXT_PUBLIC_USE_AUTH) { return null; @@ -22,10 +34,10 @@ const HeaderActions: FC = () => { return ( - + + <> + + ); return ( - - {enableChatbot && } - {actionButton} - + setGuestWarningOpen(false)} + okButtonProps={{ + children: 'Continue as guest', }} + okText="Sign in" + onOk={() => signIn()} > - - - - - + + + + {enableChatbot && } + {actionButton} + + + + + + + ); }; From 4f748a0d3193bb757af7483820e4894baab3df8e Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 30 Jun 2024 21:46:59 +0200 Subject: [PATCH 09/45] typos --- src/management-system-v2/components/processes/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/components/processes/index.tsx b/src/management-system-v2/components/processes/index.tsx index 2004b021a..f341e0478 100644 --- a/src/management-system-v2/components/processes/index.tsx +++ b/src/management-system-v2/components/processes/index.tsx @@ -259,7 +259,7 @@ const Processes = ({ } catch (e) { message.open({ type: 'error', - content: `Someting went wrong`, + content: `Something went wrong`, }); } }); @@ -272,7 +272,7 @@ const Processes = ({ moveItems, }; - // Here all the loading states shoud be ORed together + // Here all the loading states should be ORed together const loading = movingItem; return ( From 6fc71a50f5294b1024892cb58244f88979b1403e Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 30 Jun 2024 22:03:37 +0200 Subject: [PATCH 10/45] feat(ms2): show new guests modal for creating process --- .../app/(auth)/signin/signin.tsx | 4 +++- .../components/process-creation-button.tsx | 4 +++- .../components/processes/index.tsx | 23 +++++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index 234ed2d8e..74591c4da 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -126,7 +126,9 @@ const SignIn: FC<{ {userType === 'none' && guestProvider && ( <>
signIn(guestProvider.id, { ...values, callbackUrl })} + onFinish={(values) => + signIn(guestProvider.id, { ...values, callbackUrl: '/processes?createprocess' }) + } key={guestProvider.id} layout="vertical" > diff --git a/src/management-system-v2/components/process-creation-button.tsx b/src/management-system-v2/components/process-creation-button.tsx index e8219f94f..4afd2f6fa 100644 --- a/src/management-system-v2/components/process-creation-button.tsx +++ b/src/management-system-v2/components/process-creation-button.tsx @@ -14,6 +14,7 @@ import { spaceURL } from '@/lib/utils'; type ProcessCreationButtonProps = ButtonProps & { customAction?: (values: { name: string; description: string }) => Promise; wrapperElement?: ReactNode; + defaultOpen?: boolean; }; /** @@ -23,9 +24,10 @@ type ProcessCreationButtonProps = ButtonProps & { const ProcessCreationButton: React.FC = ({ wrapperElement, customAction, + defaultOpen = false, ...props }) => { - const [isProcessModalOpen, setIsProcessModalOpen] = useState(false); + const [isProcessModalOpen, setIsProcessModalOpen] = useState(defaultOpen); const router = useRouter(); const environment = useEnvironment(); const folderId = useParams<{ folderId: string }>().folderId ?? ''; diff --git a/src/management-system-v2/components/processes/index.tsx b/src/management-system-v2/components/processes/index.tsx index f341e0478..0e5f322d8 100644 --- a/src/management-system-v2/components/processes/index.tsx +++ b/src/management-system-v2/components/processes/index.tsx @@ -1,7 +1,7 @@ 'use client'; import styles from './processes.module.scss'; -import { ComponentProps, useState, useTransition } from 'react'; +import { ComponentProps, useEffect, useState, useTransition } from 'react'; import { Space, Button, Tooltip, Grid, App, Drawer, Dropdown, Card, Badge, Spin } from 'antd'; import { ExportOutlined, @@ -162,7 +162,26 @@ const Processes = ({ { dependencies: [selectedRowKeys.length] }, ); - const createProcessButton = ; + const createProcessButton = ( + + ); + + useEffect(() => { + const searchParams = new URLSearchParams(document.location.search); + if (searchParams.has('createprocess')) { + searchParams.delete('createprocess'); + router.replace( + window.location.origin + window.location.pathname + '?' + searchParams.toString(), + ); + } + }, []); + const defaultDropdownItems = []; if (ability.can('create', 'Process')) defaultDropdownItems.push({ From 3386e0f637216bbede51ca914bf933f0b2f4f015 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 2 Jul 2024 12:16:15 +0200 Subject: [PATCH 11/45] lint --- src/management-system-v2/components/header-actions.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/management-system-v2/components/header-actions.tsx b/src/management-system-v2/components/header-actions.tsx index 87e2aa21f..7780decf9 100644 --- a/src/management-system-v2/components/header-actions.tsx +++ b/src/management-system-v2/components/header-actions.tsx @@ -14,7 +14,6 @@ import { Tooltip, Typography, theme, - } from 'antd'; import { signIn, signOut, useSession } from 'next-auth/react'; import { FC, useContext, useState } from 'react'; From 0ed9a2083e2ca6d9ae3951c528aa34af35b7150b Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 2 Jul 2024 14:21:39 +0200 Subject: [PATCH 12/45] fix: missing dependency --- src/management-system-v2/components/processes/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/components/processes/index.tsx b/src/management-system-v2/components/processes/index.tsx index 2149f0c54..32784a34d 100644 --- a/src/management-system-v2/components/processes/index.tsx +++ b/src/management-system-v2/components/processes/index.tsx @@ -1,7 +1,7 @@ 'use client'; import styles from './processes.module.scss'; -import { ComponentProps, useEffect, useState, useTransition } from 'react'; +import { ComponentProps, useEffect, useRef, useState, useTransition } from 'react'; import { Space, Button, Tooltip, Grid, App, Drawer, Dropdown, Card, Badge, Spin } from 'antd'; import { ExportOutlined, From 8bf39bc53b6de37de1b002466bb9c0445948b746 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 8 Jul 2024 11:36:47 +0200 Subject: [PATCH 13/45] feat(ms2): update email if user is signed in and is verifying email --- .../api/auth/[...nextauth]/auth-options.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index 426957755..be8876d66 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -58,10 +58,27 @@ const nextAuthOptions: AuthOptions = { return session; }, - signIn: async ({ account, user: _user }) => { + signIn: async ({ account, user: _user, email }) => { const session = await getServerSession(nextAuthOptions); const sessionUser = session?.user; + if ( + sessionUser && + !sessionUser.guest && + account?.provider === 'email' && + !(_user as Partial).emailVerified && + !email?.verificationRequest + ) { + const userSigninIn = getUserById(_user.id); + + if (!userSigninIn) { + updateUser(sessionUser.id, { + email: _user.email as string, + emailVerified: new Date(), + }); + } + } + if (sessionUser?.guest && account?.provider !== 'guest-loguin') { const user = _user as Partial; const guestUser = getUserById(sessionUser.id); From 057907ead322c168141731aebc765607ac2ee3b7 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 8 Jul 2024 13:08:18 +0200 Subject: [PATCH 14/45] feat(ms2/profile): modal to change email --- .../[environmentId]/profile/user-profile.tsx | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx index b05c24b8d..1353ea574 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx @@ -1,21 +1,27 @@ 'use client'; import { FC, ReactNode, useState } from 'react'; -import { Space, Card, Typography, App, Table, Alert } from 'antd'; +import { Space, Card, Typography, App, Table, Alert, Modal, Form, Input } from 'antd'; import styles from './user-profile.module.scss'; import { RightOutlined } from '@ant-design/icons'; -import { signOut } from 'next-auth/react'; +import { signIn, signOut } from 'next-auth/react'; import ConfirmationButton from '@/components/confirmation-button'; import UserDataModal from './user-data-modal'; import { User } from '@/lib/data/user-schema'; import { deleteUser as deleteUserServerAction } from '@/lib/data/users'; import UserAvatar from '@/components/user-avatar'; import { CloseOutlined } from '@ant-design/icons'; +import useParseZodErrors, { antDesignInputProps } from '@/lib/useParseZodErrors'; +import { z } from 'zod'; const UserProfile: FC<{ userData: User }> = ({ userData }) => { const [changeNameModalOpen, setChangeNameModalOpen] = useState(false); const [errorMessage, setErrorMessage] = useState(undefined); + const [changeEmailModalOpen, setChangeEmailModalOpen] = useState(false); + const [errors, parseEmail] = useParseZodErrors(z.object({ email: z.string().email() })); + const [changeEmailForm] = Form.useForm(); + const { message: messageApi } = App.useApp(); async function deleteUser() { @@ -64,6 +70,35 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => { }} /> + setChangeEmailModalOpen(false)} + onOk={changeEmailForm.submit} + destroyOnClose + > + + { + const data = parseEmail(values); + if (!data) return; + signIn('email', { email: values.email, callbackUrl: '/profile' }); + }} + > + + + + + + {errorMessage && ( @@ -107,6 +142,7 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => { key: 'email', title: 'Email', value: !userData.guest ? userData.email : 'Guest', + action: () => setChangeEmailModalOpen(true), }, ]} columns={[ @@ -114,7 +150,7 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => { { dataIndex: 'value' }, { key: 'action', - render: (_, row) => row.action && , + render: () => , }, ]} onRow={(row) => From 506d584fcd1d1e89362a37f654672f678fc03f32 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 10 Jul 2024 10:25:52 +0200 Subject: [PATCH 15/45] feat(ms2): verificationToken store --- .../app/api/auth/[...nextauth]/adapter.ts | 18 ++--- .../lib/data/legacy/store.js | 1 + .../lib/data/legacy/verification-tokens.ts | 68 +++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 src/management-system-v2/lib/data/legacy/verification-tokens.ts diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts b/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts index 295be62ff..b31256ac4 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts @@ -6,11 +6,14 @@ import { addOauthAccount, getOauthAccountByProviderId, } from '@/lib/data/legacy/iam/users'; +import { + createVerificationToken, + deleteVerificationToken, + getVerificationToken, +} from '@/lib/data/legacy/verification-tokens'; import { AuthenticatedUser } from '@/lib/data/user-schema'; import { type Adapter, AdapterAccount, VerificationToken } from 'next-auth/adapters'; -const invitationTokens = new Map(); - const Adapter = { createUser: async ( user: Omit | { email: string; emailVerified: Date }, @@ -31,15 +34,14 @@ const Adapter = { return getUserByEmail(email) ?? null; }, createVerificationToken: async (token: VerificationToken) => { - invitationTokens.set(token.identifier, token); - return token; + return createVerificationToken(token); }, - useVerificationToken: async ({ identifier }: { identifier: string; token: string }) => { + useVerificationToken: async (params: { identifier: string; token: string }) => { // next-auth checks if the token is expired - const storedToken = invitationTokens.get(identifier); - invitationTokens.delete(identifier); + const token = getVerificationToken(params); + if (token) deleteVerificationToken(params); - return storedToken ?? null; + return token ?? null; }, linkAccount: async (account: AdapterAccount) => { return addOauthAccount({ diff --git a/src/management-system-v2/lib/data/legacy/store.js b/src/management-system-v2/lib/data/legacy/store.js index 751979839..d119b56b7 100644 --- a/src/management-system-v2/lib/data/legacy/store.js +++ b/src/management-system-v2/lib/data/legacy/store.js @@ -57,6 +57,7 @@ if (!global.stores) { stores.folders = { store: getStore('folders') }; stores.machineConfig = { store: getStore('machineConfig') }; stores.systemAdmins = { store: getStore('systemAdmins') }; + stores.verificationTokens = { store: getStore('verificationTokens') }; } /** diff --git a/src/management-system-v2/lib/data/legacy/verification-tokens.ts b/src/management-system-v2/lib/data/legacy/verification-tokens.ts new file mode 100644 index 000000000..24306ad22 --- /dev/null +++ b/src/management-system-v2/lib/data/legacy/verification-tokens.ts @@ -0,0 +1,68 @@ +import store from './store.js'; +import { z } from 'zod'; + +const verificationTokenSchema = z.union([ + z.object({ + token: z.string(), + identifier: z.string(), + expires: z.date(), + updateEmail: z.literal(false).optional(), + }), + z.object({ + token: z.string(), + identifier: z.string(), + expires: z.date(), + updateEmail: z.literal(true), + userId: z.string(), + }), +]); + +export type VerificationToken = z.infer; + +// @ts-ignore +let firstInit = !global.verificationTokensMetaObject; + +export let verificationTokensMetaObject: Record = + // @ts-ignore + global.verificationTokensMetaObject || (global.verificationTokensMetaObject = {}); + +/** initializes the folders meta information objects */ +export function init() { + if (!firstInit) return; + + const storedTokens = store.get('verificationTokens') as (VerificationToken & { id: string })[]; + + for (const token of storedTokens) verificationTokensMetaObject[token.token] = token; +} + +init(); + +export function getVerificationToken(params: Pick) { + const token = verificationTokensMetaObject[params.token]; + if (!token || token.identifier !== params.identifier) return; + + return token as VerificationToken; +} + +export function deleteVerificationToken(params: Pick) { + const token = verificationTokensMetaObject[params.token]; + if (!token || token.identifier !== params.identifier) throw new Error('Token not found'); + + store.remove('verificationTokens', token.token); + delete verificationTokensMetaObject[token.token]; +} + +export function createVerificationToken(tokenInput: VerificationToken) { + const token = verificationTokenSchema.parse(tokenInput); + + if (verificationTokensMetaObject[token.token]) { + throw new Error('Token already exists'); + } + + const storeToken = { ...token, id: token.token }; // id because the store needs an id + + verificationTokensMetaObject[token.token] = storeToken; + store.add('verificationTokens', storeToken); + + return token; +} From c4fb15c6a1d3bb9df9a74722361555a829bf26b9 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 10 Jul 2024 21:41:10 +0200 Subject: [PATCH 16/45] fix(ms2/signin): remove dangerous sign in code --- .../api/auth/[...nextauth]/auth-options.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index b55953a2a..b6857d703 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -60,27 +60,10 @@ const nextAuthOptions: AuthOptions = { return session; }, - signIn: async ({ account, user: _user, email }) => { + signIn: async ({ account, user: _user }) => { const session = await getServerSession(nextAuthOptions); const sessionUser = session?.user; - if ( - sessionUser && - !sessionUser.guest && - account?.provider === 'email' && - !(_user as Partial).emailVerified && - !email?.verificationRequest - ) { - const userSigninIn = getUserById(_user.id); - - if (!userSigninIn) { - updateUser(sessionUser.id, { - email: _user.email as string, - emailVerified: new Date(), - }); - } - } - if (sessionUser?.guest && account?.provider !== 'guest-loguin') { const user = _user as Partial; const guestUser = getUserById(sessionUser.id); From edc70cbb608cd9d65c54cd3642657fcc78626a1b Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 10 Jul 2024 21:43:03 +0200 Subject: [PATCH 17/45] feat(ms2): verificationToken server actions --- .../lib/change-email/server-actions.ts | 57 +++++++++++++++++++ .../lib/change-email/utils.ts | 57 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/management-system-v2/lib/change-email/server-actions.ts create mode 100644 src/management-system-v2/lib/change-email/utils.ts diff --git a/src/management-system-v2/lib/change-email/server-actions.ts b/src/management-system-v2/lib/change-email/server-actions.ts new file mode 100644 index 000000000..35952fbfd --- /dev/null +++ b/src/management-system-v2/lib/change-email/server-actions.ts @@ -0,0 +1,57 @@ +'use server'; + +import { z } from 'zod'; +import { userError } from '../user-error'; +import { createChangeEmailVerificationToken, getTokenHash, notExpired } from './utils'; +import { getCurrentUser } from '@/components/auth'; +import { + createVerificationToken, + getVerificationToken, + deleteVerificationToken, +} from '@/lib/data/legacy/verification-tokens'; +import { updateUser } from '@/lib/data/legacy/iam/users'; + +export async function requestEmailChange(newEmail: string) { + try { + const { session } = await getCurrentUser(); + if (!session || session.user.guest) + return userError('You must be signed in to change your email'); + const userId = session.user.id; + + const email = z.string().email().parse(newEmail); + + const { verificationToken, redirectUrl } = await createChangeEmailVerificationToken({ + email, + userId, + }); + + createVerificationToken(verificationToken); + + // TODO: send email + console.log(redirectUrl); + } catch (e) { + if (e instanceof z.ZodError) return userError('Invalid email'); + + return userError('Something went wrong'); + } +} + +export async function changeEmail(token: string, identifier: string, cancel: boolean = false) { + const { session, userId } = await getCurrentUser(); + if (!session || session.user.guest) + return userError('You must be signed in to change your email'); + + const tokenParams = { identifier, token: await getTokenHash(token) }; + const verificationToken = getVerificationToken(tokenParams); + if ( + !verificationToken || + !verificationToken.updateEmail || + verificationToken.userId !== userId || + !(await notExpired(verificationToken)) + ) + return userError('Invalid token'); + + if (!cancel) updateUser(userId, { email: verificationToken.identifier }); + + deleteVerificationToken(tokenParams); +} diff --git a/src/management-system-v2/lib/change-email/utils.ts b/src/management-system-v2/lib/change-email/utils.ts new file mode 100644 index 000000000..6b6eae9ae --- /dev/null +++ b/src/management-system-v2/lib/change-email/utils.ts @@ -0,0 +1,57 @@ +import 'server-only'; + +import nextAuthOptions from '@/app/api/auth/[...nextauth]/auth-options'; +import { z } from 'zod'; +import { VerificationToken } from '../data/legacy/verification-tokens'; + +async function createHash(message: string) { + const msgUint8 = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + + return hashHex; +} + +export function getTokenHash(token: string) { + return createHash(`${token}${nextAuthOptions.secret}`); +} + +export async function createChangeEmailVerificationToken({ + email, + userId, +}: { + email: string; + userId: string; +}) { + const identifier = z.string().email().parse(email); + + const token = crypto.randomUUID(); + const expires = new Date(Date.now() + 1000 * 60 * 60 * 24); + + const verificationToken = { + token: await getTokenHash(token), + expires, + identifier, + updateEmail: true, + userId, + } satisfies VerificationToken; + + const redirectUrl = new URL( + '/change-email?' + + new URLSearchParams({ + token, + email: identifier, + }), + process.env.NEXTAUTH_URL ?? 'http://localhost:3000', + ).toString(); + + return { verificationToken, redirectUrl }; +} + +export async function notExpired( + verificationToken: Extract, +) { + if (verificationToken.expires.valueOf() < Date.now()) return false; + return true; +} From cb923c7d0cb32b8cae303b4506dcba542c824129 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 10 Jul 2024 21:43:57 +0200 Subject: [PATCH 18/45] feat(ms2/profile): request email change --- .../[environmentId]/profile/user-profile.tsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx index 1353ea574..d1bb85a51 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx @@ -4,7 +4,7 @@ import { FC, ReactNode, useState } from 'react'; import { Space, Card, Typography, App, Table, Alert, Modal, Form, Input } from 'antd'; import styles from './user-profile.module.scss'; import { RightOutlined } from '@ant-design/icons'; -import { signIn, signOut } from 'next-auth/react'; +import { signOut } from 'next-auth/react'; import ConfirmationButton from '@/components/confirmation-button'; import UserDataModal from './user-data-modal'; import { User } from '@/lib/data/user-schema'; @@ -13,6 +13,7 @@ import UserAvatar from '@/components/user-avatar'; import { CloseOutlined } from '@ant-design/icons'; import useParseZodErrors, { antDesignInputProps } from '@/lib/useParseZodErrors'; import { z } from 'zod'; +import { requestEmailChange as serverRequestEmailChange } from '@/lib/change-email/server-actions'; const UserProfile: FC<{ userData: User }> = ({ userData }) => { const [changeNameModalOpen, setChangeNameModalOpen] = useState(false); @@ -22,7 +23,7 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => { const [errors, parseEmail] = useParseZodErrors(z.object({ email: z.string().email() })); const [changeEmailForm] = Form.useForm(); - const { message: messageApi } = App.useApp(); + const { message: messageApi, notification } = App.useApp(); async function deleteUser() { try { @@ -38,6 +39,25 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => { } } + async function requestEmailChange(values: unknown) { + try { + const data = parseEmail(values); + if (!data) return; + + const response = await serverRequestEmailChange(data.email); + if (response && 'error' in response) throw response; + + notification.success({ + message: 'Email change request successful', + description: 'Check your Email for the verification link', + }); + } catch (e: unknown) { + //@ts-ignore + const content = (e?.error?.message as ReactNode) ? e.error.message : 'An error ocurred'; + messageApi.error({ content }); + } + } + const firstName = userData.guest ? 'Guest' : userData.firstName || ''; const lastName = userData.guest ? '' : userData.lastName || ''; @@ -87,11 +107,7 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => { initialValues={userData} form={changeEmailForm} layout="vertical" - onFinish={(values) => { - const data = parseEmail(values); - if (!data) return; - signIn('email', { email: values.email, callbackUrl: '/profile' }); - }} + onFinish={requestEmailChange} > From 7c567eeb5da044289e116f4ec4627f8b1985f925 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 10 Jul 2024 21:44:17 +0200 Subject: [PATCH 19/45] feat(ms2/change-email): page for confirming email change --- .../app/change-email/confirmation-buttons.tsx | 44 +++++++++++++++++++ .../app/change-email/page.tsx | 42 ++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/management-system-v2/app/change-email/confirmation-buttons.tsx create mode 100644 src/management-system-v2/app/change-email/page.tsx diff --git a/src/management-system-v2/app/change-email/confirmation-buttons.tsx b/src/management-system-v2/app/change-email/confirmation-buttons.tsx new file mode 100644 index 000000000..51b31f538 --- /dev/null +++ b/src/management-system-v2/app/change-email/confirmation-buttons.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { Space, Button, App } from 'antd'; +import { useTransition } from 'react'; +import { changeEmail as serverChangeEmail } from '@/lib/change-email/server-actions'; +import { useRouter, useSearchParams } from 'next/navigation'; + +export default function ConfirmationButtons() { + const { message } = App.useApp(); + const params = useSearchParams(); + const router = useRouter(); + + const [changingEmail, startChangingEmail] = useTransition(); + function changeEmail() { + startChangingEmail(async () => { + try { + const response = await serverChangeEmail(params.get('token')!, params.get('email')!); + + if (response?.error) throw response.error.message; + + message.open({ content: 'Email changed', type: 'success' }); + router.push('/profile'); + } catch (e) { + const content = typeof e === 'string' ? e : 'An error occurred'; + + message.open({ content, type: 'error' }); + } + }); + } + + const [cancelling, startCancel] = useTransition(); + function cancel() {} + + return ( + + + + + ); +} diff --git a/src/management-system-v2/app/change-email/page.tsx b/src/management-system-v2/app/change-email/page.tsx new file mode 100644 index 000000000..d8983e04e --- /dev/null +++ b/src/management-system-v2/app/change-email/page.tsx @@ -0,0 +1,42 @@ +import { getCurrentUser } from '@/components/auth'; +import Content from '@/components/content'; +import { requestEmailChange } from '@/lib/change-email/server-actions'; +import { getTokenHash, notExpired } from '@/lib/change-email/utils'; +import { getVerificationToken } from '@/lib/data/legacy/verification-tokens'; +import { Button, Card, Space } from 'antd'; +import { redirect } from 'next/navigation'; +import { z } from 'zod'; +import ConfirmationButtons from './confirmation-buttons'; + +const searchParamsScema = z.object({ email: z.string().email(), token: z.string() }); + +export default async function ChangeEmailPage({ searchParams }: { searchParams: unknown }) { + const parsedSearchkParams = searchParamsScema.safeParse(searchParams); + if (!parsedSearchkParams.success) redirect('/'); + const { email, token } = parsedSearchkParams.data; + + const { session } = await getCurrentUser(); + const userId = session?.user.id; + if (!userId) redirect('/'); + + const verificationToken = getVerificationToken({ + identifier: email, + token: await getTokenHash(token), + }); + + if ( + !verificationToken || + !verificationToken.updateEmail || + verificationToken.userId !== userId || + !(await notExpired(verificationToken)) + ) + redirect('/'); + + return ( + + + + + + ); +} From c0f19a13bbc0248c1289e1ce0cd909dca939c240 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Thu, 11 Jul 2024 12:33:03 +0200 Subject: [PATCH 20/45] refactor(ms2): moved signin-email template to lib/email --- .../api/auth/[...nextauth] => lib/email}/signin-link-email.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/management-system-v2/{app/api/auth/[...nextauth] => lib/email}/signin-link-email.tsx (100%) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/signin-link-email.tsx b/src/management-system-v2/lib/email/signin-link-email.tsx similarity index 100% rename from src/management-system-v2/app/api/auth/[...nextauth]/signin-link-email.tsx rename to src/management-system-v2/lib/email/signin-link-email.tsx From 17ba3a07c5d940dd37ed7fe224be3fd4c065868d Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Thu, 11 Jul 2024 13:00:02 +0200 Subject: [PATCH 21/45] refactor(ms2/signin-link-email): changed parameter format --- .../api/auth/[...nextauth]/auth-options.ts | 7 ++-- .../lib/email/signin-link-email.tsx | 33 ++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index b6857d703..f66001fa6 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -11,7 +11,7 @@ import Adapter from './adapter'; import { AuthenticatedUser, User } from '@/lib/data/user-schema'; import { sendEmail } from '@/lib/email/mailer'; import { randomUUID } from 'crypto'; -import renderSigninLinkEmail from './signin-link-email'; +import renderSigninLinkEmail from '@/lib/email/signin-link-email'; const nextAuthOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, @@ -30,7 +30,10 @@ const nextAuthOptions: AuthOptions = { }), EmailProvider({ sendVerificationRequest(params) { - const signinMail = renderSigninLinkEmail(params.url, params.expires); + const signinMail = renderSigninLinkEmail({ + signInLink: params.url, + expires: params.expires, + }); sendEmail({ to: params.identifier, diff --git a/src/management-system-v2/lib/email/signin-link-email.tsx b/src/management-system-v2/lib/email/signin-link-email.tsx index ee9be1a07..42203011d 100644 --- a/src/management-system-v2/lib/email/signin-link-email.tsx +++ b/src/management-system-v2/lib/email/signin-link-email.tsx @@ -18,8 +18,17 @@ import * as React from 'react'; const baseUrl = process.env.NEXTAUTH_URL ?? ''; -function SigninUrlMail({ signInLink, expires }: { signInLink: string; expires: Date }) { - const expiresIn = expires.getTime() - Date.now(); +type MailProps = { + signInLink: string; + expires: Date; + linkText?: string; + headerText?: string; + description?: string; + footerText?: string; +}; + +function SigninUrlMail(mailProps: MailProps) { + const expiresIn = mailProps.expires.getTime() - Date.now(); const linkDuration: number = Math.floor(expiresIn / 1000 / 60 / 60); return ( @@ -66,11 +75,12 @@ function SigninUrlMail({ signInLink, expires }: { signInLink: string; expires: D marginBottom: '15px', }} > - Sign in to PROCEED + {mailProps.headerText ?? 'Sign in to PROCEED'} - Hi, with this mail you can sign in to your PROCEED account. If you don't have - an account yet, a new one will be created for you. Just click on the following link: + {mailProps.description ?? + `Hi, with this mail you can sign in to your PROCEED account. If you don't have + an account yet, a new one will be created for you. Just click on the following link:`} - Sign in Link + {mailProps.linkText ?? 'Sign in Link'}
- If you have not initiated the sign in, you can simply ignore this mail. Your account - is still secure as you can only sign in by email. The PROCEED Crew + {mailProps.footerText ?? + `If you have not initiated the sign in, you can simply ignore this mail. Your account + is still secure as you can only sign in by email. The PROCEED Crew`}
@@ -164,8 +175,8 @@ const text = { margin: '24px 0', }; -export default function renderSigninLinkEmail(signInLink: string, expires: Date) { - const email = ; +export default function renderSigninLinkEmail(mailProps: MailProps) { + const email = ; return { html: render(email), text: render(email, { plainText: true }) }; } From a60fc8ecae45f81460b4f8f3a85c0214f99d8383 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Thu, 11 Jul 2024 13:06:54 +0200 Subject: [PATCH 22/45] feat(ms2/profile): close modal after email change request --- .../app/(dashboard)/[environmentId]/profile/user-profile.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx index d1bb85a51..d96602429 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx @@ -47,6 +47,7 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => { const response = await serverRequestEmailChange(data.email); if (response && 'error' in response) throw response; + setChangeEmailModalOpen(false); notification.success({ message: 'Email change request successful', description: 'Check your Email for the verification link', From 64e61cf00c5d4acfe807bc4a9dbfeb0205c750f5 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Thu, 11 Jul 2024 13:20:34 +0200 Subject: [PATCH 23/45] feat(ms2/change-email): send change email link --- .../lib/change-email/server-actions.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/lib/change-email/server-actions.ts b/src/management-system-v2/lib/change-email/server-actions.ts index 35952fbfd..488b183f9 100644 --- a/src/management-system-v2/lib/change-email/server-actions.ts +++ b/src/management-system-v2/lib/change-email/server-actions.ts @@ -10,6 +10,8 @@ import { deleteVerificationToken, } from '@/lib/data/legacy/verification-tokens'; import { updateUser } from '@/lib/data/legacy/iam/users'; +import { sendEmail } from '../email/mailer'; +import renderSigninLinkEmail from '../email/signin-link-email'; export async function requestEmailChange(newEmail: string) { try { @@ -27,8 +29,22 @@ export async function requestEmailChange(newEmail: string) { createVerificationToken(verificationToken); - // TODO: send email - console.log(redirectUrl); + const signinMail = renderSigninLinkEmail({ + signInLink: redirectUrl, + expires: verificationToken.expires, + headerText: 'Change your email address', + description: + 'Hi, you have requested to change the email address associated with your PROCEED account. Please click the link below to confirm this change:', + footerText: + 'If you did not request this email change, you can ignore this email. Your account remains secure and can only be accessed with your original email address. The PROCEED Crew', + }); + + sendEmail({ + to: email, + subject: 'PROCEED: Change your email address', + html: signinMail.html, + text: signinMail.text, + }); } catch (e) { if (e instanceof z.ZodError) return userError('Invalid email'); From 3d3526967bee424244837dc82064f450949ec4d5 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Thu, 11 Jul 2024 15:46:23 +0200 Subject: [PATCH 24/45] style(ms2/change-email): better feedback --- .../app/change-email/change-email-card.tsx | 71 +++++++++++++++++++ .../app/change-email/confirmation-buttons.tsx | 44 ------------ .../app/change-email/page.tsx | 16 ++--- 3 files changed, 75 insertions(+), 56 deletions(-) create mode 100644 src/management-system-v2/app/change-email/change-email-card.tsx delete mode 100644 src/management-system-v2/app/change-email/confirmation-buttons.tsx diff --git a/src/management-system-v2/app/change-email/change-email-card.tsx b/src/management-system-v2/app/change-email/change-email-card.tsx new file mode 100644 index 000000000..5708769af --- /dev/null +++ b/src/management-system-v2/app/change-email/change-email-card.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { Space, Button, App, Card, Typography } from 'antd'; +import { useTransition } from 'react'; +import { changeEmail as serverChangeEmail } from '@/lib/change-email/server-actions'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { ArrowRightOutlined } from '@ant-design/icons'; +import Content from '@/components/content'; + +export default function ConfirmationButtons({ + previousEmail, + newEmail, +}: { + previousEmail?: string; + newEmail: string; +}) { + const { message } = App.useApp(); + const params = useSearchParams(); + const router = useRouter(); + + const [changingEmail, startChangingEmail] = useTransition(); + function changeEmail() { + startChangingEmail(async () => { + try { + const response = await serverChangeEmail(params.get('token')!, params.get('email')!); + + if (response?.error) throw response.error.message; + + message.open({ content: 'Email changed', type: 'success' }); + router.push('/profile'); + } catch (e) { + const content = typeof e === 'string' ? e : 'An error occurred'; + + message.open({ content, type: 'error' }); + } + }); + } + + const [cancelling, startCancel] = useTransition(); + function cancel() {} + + return ( + + + {!previousEmail ? ( + <> + {previousEmail} + + {newEmail} + + ) : ( + <> + Your email will now be {newEmail} + + )} +
+ + + + +
+
+ ); +} diff --git a/src/management-system-v2/app/change-email/confirmation-buttons.tsx b/src/management-system-v2/app/change-email/confirmation-buttons.tsx deleted file mode 100644 index 51b31f538..000000000 --- a/src/management-system-v2/app/change-email/confirmation-buttons.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { Space, Button, App } from 'antd'; -import { useTransition } from 'react'; -import { changeEmail as serverChangeEmail } from '@/lib/change-email/server-actions'; -import { useRouter, useSearchParams } from 'next/navigation'; - -export default function ConfirmationButtons() { - const { message } = App.useApp(); - const params = useSearchParams(); - const router = useRouter(); - - const [changingEmail, startChangingEmail] = useTransition(); - function changeEmail() { - startChangingEmail(async () => { - try { - const response = await serverChangeEmail(params.get('token')!, params.get('email')!); - - if (response?.error) throw response.error.message; - - message.open({ content: 'Email changed', type: 'success' }); - router.push('/profile'); - } catch (e) { - const content = typeof e === 'string' ? e : 'An error occurred'; - - message.open({ content, type: 'error' }); - } - }); - } - - const [cancelling, startCancel] = useTransition(); - function cancel() {} - - return ( - - - - - ); -} diff --git a/src/management-system-v2/app/change-email/page.tsx b/src/management-system-v2/app/change-email/page.tsx index d8983e04e..696a3f008 100644 --- a/src/management-system-v2/app/change-email/page.tsx +++ b/src/management-system-v2/app/change-email/page.tsx @@ -1,12 +1,9 @@ import { getCurrentUser } from '@/components/auth'; -import Content from '@/components/content'; -import { requestEmailChange } from '@/lib/change-email/server-actions'; import { getTokenHash, notExpired } from '@/lib/change-email/utils'; import { getVerificationToken } from '@/lib/data/legacy/verification-tokens'; -import { Button, Card, Space } from 'antd'; import { redirect } from 'next/navigation'; import { z } from 'zod'; -import ConfirmationButtons from './confirmation-buttons'; +import ChangeEmailCard from './change-email-card'; const searchParamsScema = z.object({ email: z.string().email(), token: z.string() }); @@ -17,7 +14,8 @@ export default async function ChangeEmailPage({ searchParams }: { searchParams: const { session } = await getCurrentUser(); const userId = session?.user.id; - if (!userId) redirect('/'); + if (!userId || session.user.guest) redirect('/'); + const previousEmail = session.user.email; const verificationToken = getVerificationToken({ identifier: email, @@ -32,11 +30,5 @@ export default async function ChangeEmailPage({ searchParams }: { searchParams: ) redirect('/'); - return ( - - - - - - ); + return ; } From d50a5eba9939a4a97545c28d655373cc0a883d90 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Thu, 11 Jul 2024 15:59:55 +0200 Subject: [PATCH 25/45] feat(ms2/change-email): cancel email change --- .../app/change-email/change-email-card.tsx | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/management-system-v2/app/change-email/change-email-card.tsx b/src/management-system-v2/app/change-email/change-email-card.tsx index 5708769af..026ab7749 100644 --- a/src/management-system-v2/app/change-email/change-email-card.tsx +++ b/src/management-system-v2/app/change-email/change-email-card.tsx @@ -1,13 +1,14 @@ 'use client'; import { Space, Button, App, Card, Typography } from 'antd'; -import { useTransition } from 'react'; +import { useState } from 'react'; import { changeEmail as serverChangeEmail } from '@/lib/change-email/server-actions'; import { useRouter, useSearchParams } from 'next/navigation'; import { ArrowRightOutlined } from '@ant-design/icons'; import Content from '@/components/content'; +import { useSession } from 'next-auth/react'; -export default function ConfirmationButtons({ +export default function ChangeEmailCard({ previousEmail, newEmail, }: { @@ -17,27 +18,32 @@ export default function ConfirmationButtons({ const { message } = App.useApp(); const params = useSearchParams(); const router = useRouter(); + const session = useSession(); - const [changingEmail, startChangingEmail] = useTransition(); - function changeEmail() { - startChangingEmail(async () => { - try { - const response = await serverChangeEmail(params.get('token')!, params.get('email')!); + const [loading, setLoading] = useState<'changing' | 'cancelling' | undefined>(); + async function changeEmail(cancel: boolean = false) { + try { + setLoading(cancel ? 'cancelling' : 'changing'); - if (response?.error) throw response.error.message; + const response = await serverChangeEmail(params.get('token')!, params.get('email')!, cancel); - message.open({ content: 'Email changed', type: 'success' }); - router.push('/profile'); - } catch (e) { - const content = typeof e === 'string' ? e : 'An error occurred'; + if (response?.error) throw response.error.message; - message.open({ content, type: 'error' }); + if (cancel) { + message.open({ content: 'Email change cancelled', type: 'success' }); + } else { + message.open({ content: 'Email changed', type: 'success' }); + session.update(); } - }); - } - const [cancelling, startCancel] = useTransition(); - function cancel() {} + router.push('/profile'); + } catch (e) { + const content = typeof e === 'string' ? e : 'An error occurred'; + + message.open({ content, type: 'error' }); + setLoading(undefined); + } + } return ( @@ -45,7 +51,7 @@ export default function ConfirmationButtons({ title="Are you sure you want to change your email?" style={{ width: '90%', maxWidth: '80ch', margin: 'auto' }} > - {!previousEmail ? ( + {previousEmail ? ( <> {previousEmail} @@ -58,10 +64,14 @@ export default function ConfirmationButtons({ )}
- - From bb57561d6c3fde987b80491631d5954619d9eb03 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Thu, 18 Jul 2024 13:08:40 +0200 Subject: [PATCH 26/45] fix(ms2/e2e-tests): sign in as guest --- tests/ms2/ms2.page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ms2/ms2.page.ts b/tests/ms2/ms2.page.ts index 105f55106..8f02666f0 100644 --- a/tests/ms2/ms2.page.ts +++ b/tests/ms2/ms2.page.ts @@ -13,7 +13,7 @@ export class MS2Page { const modal = await openModal(this.page, async () => { this.page.goto('/'); }); - await modal.getByRole('button', { name: 'Continue as a Guest' }).click(); + await modal.getByRole('button', { name: 'Create a Process' }).click(); await this.page.waitForURL('**/processes'); } From d78f13acea3957b0f334dde5781460317500343c Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 19 Jul 2024 13:10:12 +0200 Subject: [PATCH 27/45] fix(ms2/signin): use callbackUrl if there is one --- src/management-system-v2/app/(auth)/signin/signin.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index 74591c4da..143a4270b 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -60,7 +60,7 @@ const SignIn: FC<{ userType: 'guest' | 'user' | 'none'; }> = ({ providers, userType }) => { const searchParams = useSearchParams(); - const callbackUrl = searchParams.get('callbackUrl') ?? '/'; + const callbackUrl = searchParams.get('callbackUrl') ?? undefined; const authError = searchParams.get('error'); const oauthProviders = providers.filter((provider) => provider.type === 'oauth'); @@ -127,7 +127,10 @@ const SignIn: FC<{ <>
- signIn(guestProvider.id, { ...values, callbackUrl: '/processes?createprocess' }) + signIn(guestProvider.id, { + ...values, + callbackUrl: callbackUrl || '/processes?createprocess', + }) } key={guestProvider.id} layout="vertical" From 713b8ac94c67a57262c35f2c678da6c82068973f Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 19 Jul 2024 13:11:09 +0200 Subject: [PATCH 28/45] fix(ms2/e2e-tests): use new sign in --- .../ms2/processes/process-modeler/process-modeler.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/ms2/processes/process-modeler/process-modeler.spec.ts b/tests/ms2/processes/process-modeler/process-modeler.spec.ts index c02592d1b..acd3fe655 100644 --- a/tests/ms2/processes/process-modeler/process-modeler.spec.ts +++ b/tests/ms2/processes/process-modeler/process-modeler.spec.ts @@ -345,10 +345,12 @@ test('share-modal', async ({ processListPage, ms2Page }) => { await newPage.waitForURL(`${clipboardData}`); // Add the shared process to the workspace - await newPage.getByRole('button', { name: 'Add to your workspace' }).click(); - await newPage.waitForURL(/signin\?callbackUrl=([^]+)/); + await openModal(newPage, async () => { + await newPage.getByRole('button', { name: 'Add to your workspace' }).click(); + await newPage.waitForURL(/signin\?callbackUrl=([^]+)/); + }); - await newPage.getByRole('button', { name: 'Continue as a Guest' }).click(); + await newPage.getByRole('button', { name: 'Create a Process' }).click(); await newPage.waitForURL(/shared-viewer\?token=([^]+)/); await newPage.getByRole('button', { name: 'My Space' }).click(); From 61f3148413a3f0e21c6f4fcfa7c9f66bbbd1bb8d Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 19 Jul 2024 15:32:30 +0200 Subject: [PATCH 29/45] fix(ms2/signin): check if verification request before this if a guest tries to sign in to a email, his email was automatically updated to the email he submitted, without him having to get the email. --- .../app/api/auth/[...nextauth]/auth-options.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index f66001fa6..af17d2686 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -63,11 +63,15 @@ const nextAuthOptions: AuthOptions = { return session; }, - signIn: async ({ account, user: _user }) => { + signIn: async ({ account, user: _user, email }) => { const session = await getServerSession(nextAuthOptions); const sessionUser = session?.user; - if (sessionUser?.guest && account?.provider !== 'guest-loguin') { + if ( + sessionUser?.guest && + account?.provider !== 'guest-loguin' && + !email?.verificationRequest + ) { const user = _user as Partial; const guestUser = getUserById(sessionUser.id); From 2b75adab36f3abdeff71c66ee83263f3cd45816b Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 21 Jul 2024 23:03:49 +0200 Subject: [PATCH 30/45] fix(ms2/environments): remove folders when removing environment --- .../lib/data/legacy/iam/environments.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/iam/environments.ts b/src/management-system-v2/lib/data/legacy/iam/environments.ts index 173b9a704..7a0a38b72 100644 --- a/src/management-system-v2/lib/data/legacy/iam/environments.ts +++ b/src/management-system-v2/lib/data/legacy/iam/environments.ts @@ -6,8 +6,7 @@ import { adminPermissions } from '@/lib/authorization/permissionHelpers'; import { addRoleMappings } from './role-mappings'; import { addMember, membershipMetaObject, removeMember } from './memberships'; import { Environment, EnvironmentInput, environmentSchema } from '../../environment-schema'; -import { getProcessMetaObjects, removeProcess } from '../_process'; -import { createFolder } from '../folders'; +import { createFolder, deleteFolder, getRootFolder } from '../folders'; // @ts-ignore let firstInit = !global.environmentMetaObject; @@ -118,21 +117,20 @@ export function deleteEnvironment(environmentId: string, ability?: Ability) { if (ability && !ability.can('delete', 'Environment')) throw new UnauthorizedError(); - const roles = Object.values(roleMetaObjects); - for (const role of roles) { - if (role.environmentId === environmentId) { - deleteRole(role.id); // also deletes role mappings - } - } + // NOTE: when using a db I think it would be faster to just delete processes and folders where de + // environmentId matches + const rootFolder = getRootFolder(environmentId); + if (!rootFolder) throw new Error('Root folder not found'); + deleteFolder(rootFolder.id); - const processes = Object.values(getProcessMetaObjects()); - for (const process of processes) { - if (process.environmentId === environmentId) { - removeProcess(process.id); + if (environment.organization) { + const roles = Object.values(roleMetaObjects); + for (const role of roles) { + if (role.environmentId === environmentId) { + deleteRole(role.id); // also deletes role mappings + } } - } - if (environment.organization) { const environmentMemberships = membershipMetaObject[environmentId]; if (environmentMemberships) { for (const { userId } of environmentMemberships) { From 84620aa49b64596de7c3942f8f355b5ad0b501dd Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 21 Jul 2024 23:31:52 +0200 Subject: [PATCH 31/45] feat(ms2/users): update guest users --- .../app/api/auth/[...nextauth]/adapter.ts | 2 +- .../lib/change-email/server-actions.ts | 2 +- .../lib/data/legacy/iam/users.ts | 12 ++++++++++-- src/management-system-v2/lib/data/user-schema.ts | 1 + src/management-system-v2/lib/data/users.tsx | 2 +- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts b/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts index b31256ac4..d31b85de9 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts @@ -28,7 +28,7 @@ const Adapter = { return getUserById(id); }, updateUser: async (user: AuthenticatedUser) => { - return updateUser(user.id, user); + return updateUser(user.id, { ...user, guest: false }); }, getUserByEmail: async (email: string) => { return getUserByEmail(email) ?? null; diff --git a/src/management-system-v2/lib/change-email/server-actions.ts b/src/management-system-v2/lib/change-email/server-actions.ts index 488b183f9..2449cd20c 100644 --- a/src/management-system-v2/lib/change-email/server-actions.ts +++ b/src/management-system-v2/lib/change-email/server-actions.ts @@ -67,7 +67,7 @@ export async function changeEmail(token: string, identifier: string, cancel: boo ) return userError('Invalid token'); - if (!cancel) updateUser(userId, { email: verificationToken.identifier }); + if (!cancel) updateUser(userId, { email: verificationToken.identifier, guest: false }); deleteVerificationToken(tokenParams); } diff --git a/src/management-system-v2/lib/data/legacy/iam/users.ts b/src/management-system-v2/lib/data/legacy/iam/users.ts index cc9e7f4e5..51c40ccf6 100644 --- a/src/management-system-v2/lib/data/legacy/iam/users.ts +++ b/src/management-system-v2/lib/data/legacy/iam/users.ts @@ -7,6 +7,8 @@ import { OauthAccount, AuthenticatedUser, AuthenticatedUserSchema, + GuestUser, + GuestUserSchema, } from '../../user-schema'; import { addEnvironment, deleteEnvironment } from './environments'; import { OptionalKeys } from '@/lib/typescript-utils.js'; @@ -133,7 +135,12 @@ export function deleteUser(userId: string) { return user; } -export function updateUser(userId: string, inputUser: Partial) { +export function updateUser( + userId: string, + inputUser: + | (Partial & { guest: false }) + | (Partial & { guest: true }), +) { const user = getUserById(userId, { throwIfNotFound: true }); const isGoingToBeGuest = inputUser.guest !== undefined ? inputUser.guest : user.guest; @@ -142,7 +149,8 @@ export function updateUser(userId: string, inputUser: Partial if (isGoingToBeGuest) { updatedUser = { id: user.id, - guest: true, + signedInWithUserId: user.guest ? user.signedInWithUserId : undefined, + ...GuestUserSchema.parse(inputUser), }; } else { const newUserData = AuthenticatedUserSchema.partial().parse(inputUser); diff --git a/src/management-system-v2/lib/data/user-schema.ts b/src/management-system-v2/lib/data/user-schema.ts index 45b923cf8..3983e4156 100644 --- a/src/management-system-v2/lib/data/user-schema.ts +++ b/src/management-system-v2/lib/data/user-schema.ts @@ -39,6 +39,7 @@ export type AuthenticatedUser = z.infer & { id: export const GuestUserSchema = z.object({ guest: z.literal(true), id: z.string().optional(), + signedInWithUserId: z.string().optional(), }); export type GuestUser = z.infer & { id: string }; diff --git a/src/management-system-v2/lib/data/users.tsx b/src/management-system-v2/lib/data/users.tsx index e8f7cee1e..b61922212 100644 --- a/src/management-system-v2/lib/data/users.tsx +++ b/src/management-system-v2/lib/data/users.tsx @@ -58,7 +58,7 @@ export async function updateUser(newUserDataInput: AuthenticatedUserData) { const newUserData = AuthenticatedUserDataSchema.parse(newUserDataInput); - _updateUser(userId, newUserData); + _updateUser(userId, { ...newUserData, guest: false }); } catch (_) { return userError('Error updating user'); } From b25c072948e60f9ec95cc340e3a5ce76f9e62ed5 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 21 Jul 2024 23:45:36 +0200 Subject: [PATCH 32/45] feat(ms2/folders): get all folders --- .../lib/data/legacy/folders.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/management-system-v2/lib/data/legacy/folders.ts b/src/management-system-v2/lib/data/legacy/folders.ts index e5797517f..9bc90fd8e 100644 --- a/src/management-system-v2/lib/data/legacy/folders.ts +++ b/src/management-system-v2/lib/data/legacy/folders.ts @@ -107,6 +107,21 @@ export function getFolderById(folderId: string, ability?: Ability) { return folderData.folder; } +export function getFolders(environmentId?: string, ability?: Ability) { + const _folders = environmentId + ? Object.values(foldersMetaObject.folders).filter( + (folder) => folder?.folder.environmentId === environmentId, + ) + : Object.values(foldersMetaObject.folders); + + const folders = _folders.map((f) => f!.folder); + + if (ability) + return folders.filter((folder) => ability.can('view', toCaslResource('Folder', folder))); + + return folders; +} + export function getFolderChildren(folderId: string, ability?: Ability) { const folderData = foldersMetaObject.folders[folderId]; if (!folderData) throw new Error('Folder not found'); From 6671e750d834b0a1e6c74dbc2a9c15532acf74ac Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 21 Jul 2024 23:46:47 +0200 Subject: [PATCH 33/45] feat(ms2/folder): move folders to other environments --- .../lib/data/legacy/folders.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/folders.ts b/src/management-system-v2/lib/data/legacy/folders.ts index 9bc90fd8e..398996117 100644 --- a/src/management-system-v2/lib/data/legacy/folders.ts +++ b/src/management-system-v2/lib/data/legacy/folders.ts @@ -223,7 +223,7 @@ function _deleteFolder( export function updateFolderMetaData( folderId: string, - newMetaDataInput: Partial, + newMetaDataInput: Partial>, ability?: Ability, ) { const folderData = foldersMetaObject.folders[folderId]; @@ -232,12 +232,15 @@ export function updateFolderMetaData( if (ability && !ability.can('update', toCaslResource('Folder', folderData.folder))) throw new Error('Permission denied'); - const newMetaData = FolderUserInputSchema.partial().parse(newMetaDataInput); - if ( - newMetaDataInput.environmentId && - newMetaDataInput.environmentId != folderData.folder.environmentId - ) - throw new Error('environmentId cannot be changed'); + const newMetaData = FolderSchema.omit({ + parentId: true, + id: true, + // if there is an ability, we interpret this as a user updating the folder + environmentId: ability ? true : undefined, + createdBy: ability ? true : undefined, + }) + .partial() + .parse(newMetaDataInput); const newFolder: Folder = { ...folderData.folder, @@ -275,7 +278,8 @@ export function moveFolder(folderId: string, newParentId: string, ability?: Abil const newParentData = foldersMetaObject.folders[newParentId]; if (!newParentData) throw new Error('New parent folder not found'); - if (newParentData.folder.environmentId !== folderData.folder.environmentId) + // only perform this check when an ability is present (it means that a user is moving the folder) + if (ability && newParentData.folder.environmentId !== folderData.folder.environmentId) throw new Error('Cannot move folder to a different environment'); const oldParentData = foldersMetaObject.folders[folderData.folder.parentId]; @@ -305,6 +309,7 @@ export function moveFolder(folderId: string, newParentId: string, ability?: Abil store.update('folders', oldParentData.folder.id, oldParentData.folder); folderData.folder.parentId = newParentId; + folderData.folder.environmentId = newParentData.folder.environmentId; newParentData.children.push({ type: 'folder', id: folderData.folder.id }); newParentData.folder.lastEdited = new Date().toISOString(); store.update('folders', newParentData.folder.id, newParentData.folder); From 111fb82872cc268e802e5ac0256570c903be808d Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 22 Jul 2024 11:56:26 +0200 Subject: [PATCH 34/45] feat(ms2/processes): get processes without ability --- .../lib/data/legacy/_process.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/_process.ts b/src/management-system-v2/lib/data/legacy/_process.ts index d53cbb572..12930d259 100644 --- a/src/management-system-v2/lib/data/legacy/_process.ts +++ b/src/management-system-v2/lib/data/legacy/_process.ts @@ -49,18 +49,16 @@ export function getProcessMetaObjects() { } /** Returns all processes for a user */ -export async function getProcesses(ability: Ability, includeBPMN = false) { +export async function getProcesses(ability?: Ability, includeBPMN = false) { const processes = Object.values(processMetaObjects); - const userProcesses = await Promise.all( - ability - .filter('view', 'Process', processes) - .map(async (process) => - !includeBPMN ? process : { ...process, bpmn: getProcessBpmn(process.id) }, - ), - ); + const userProcesses = ability ? ability.filter('view', 'Process', processes) : processes; - return userProcesses; + return await Promise.all( + userProcesses.map(async (process) => + !includeBPMN ? process : { ...process, bpmn: getProcessBpmn(process.id) }, + ), + ); } export async function getProcess(processDefinitionsId: string, includeBPMN = false) { From cc4ae73b6bdae17ad4de209dd42f255e2c6eba2f Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 22 Jul 2024 11:57:27 +0200 Subject: [PATCH 35/45] typo --- src/management-system-v2/lib/data/legacy/_process.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/_process.ts b/src/management-system-v2/lib/data/legacy/_process.ts index 12930d259..ac012dd0e 100644 --- a/src/management-system-v2/lib/data/legacy/_process.ts +++ b/src/management-system-v2/lib/data/legacy/_process.ts @@ -202,7 +202,7 @@ export function moveProcess({ if (!dontUpdateOldFolder) { const oldFolder = foldersMetaObject.folders[process.folderId]; - if (!oldFolder) throw new Error("Consistensy Error: Process' folder not found"); + if (!oldFolder) throw new Error("Consistency Error: Process' folder not found"); const processOldFolderIdx = oldFolder.children.findIndex( (item) => 'type' in item && item.type === 'process' && item.id === processDefinitionsId, ); @@ -299,7 +299,7 @@ export async function addProcessVersion(processDefinitionsId: string, bpmn: stri await saveProcessVersion(processDefinitionsId, versionInformation.version || 0, bpmn); - // add information about the new version to the meta information and inform others about its existance + // add information about the new version to the meta information and inform others about its existence const newVersions = existingProcess.versions ? [...existingProcess.versions] : []; //@ts-ignore From cefc7ec1b7e9b6fae70d25cc27327dbcc2d18aa7 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 22 Jul 2024 12:26:36 +0200 Subject: [PATCH 36/45] feat(ms2): transfer guest processes to account --- .../app/(auth)/signin/signin.tsx | 15 +++- .../api/auth/[...nextauth]/auth-options.ts | 14 ++- .../app/transfer-processes/page.tsx | 54 +++++++++++ .../app/transfer-processes/server-actions.ts | 89 +++++++++++++++++++ ...ransfer-processes-confitmation-buttons.tsx | 41 +++++++++ 5 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 src/management-system-v2/app/transfer-processes/page.tsx create mode 100644 src/management-system-v2/app/transfer-processes/server-actions.ts create mode 100644 src/management-system-v2/app/transfer-processes/transfer-processes-confitmation-buttons.tsx diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index 143a4270b..2275ebe7d 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -21,6 +21,7 @@ import Link from 'next/link'; import Image from 'next/image'; import { signIn } from 'next-auth/react'; import { type ExtractedProvider } from '@/app/api/auth/[...nextauth]/auth-options'; +import { User } from '@/lib/data/user-schema'; const verticalGap = '1rem'; @@ -57,12 +58,20 @@ const signInTitle = ( const SignIn: FC<{ providers: ExtractedProvider[]; - userType: 'guest' | 'user' | 'none'; -}> = ({ providers, userType }) => { + user?: User; +}> = ({ providers, user }) => { const searchParams = useSearchParams(); - const callbackUrl = searchParams.get('callbackUrl') ?? undefined; + let callbackUrl = searchParams.get('callbackUrl') ?? undefined; + if (user?.guest) { + callbackUrl = + `/transfer-processes?guestId=${user.id}` + (callbackUrl ? `&callbackUrl=${callbackUrl}` : ''); + } const authError = searchParams.get('error'); + let userType: 'none' | 'guest' | 'user'; + if (!user) userType = 'none'; + else userType = user.guest ? 'guest' : 'user'; + const oauthProviders = providers.filter((provider) => provider.type === 'oauth'); const guestProvider = providers.find((provider) => provider.id === 'guest-signin'); const credentials = providers.filter( diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index 45e67d897..6c0e42758 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -69,14 +69,20 @@ const nextAuthOptions: AuthOptions = { if ( sessionUser?.guest && - account?.provider !== 'guest-loguin' && + account?.provider !== 'guest-signin' && !email?.verificationRequest ) { + // Check if the user's cookie is correct + const sessionUserInDb = getUserById(sessionUser.id); + if (!sessionUserInDb || !sessionUserInDb.guest) throw new Error('Something went wrong'); + const user = _user as Partial; - const guestUser = getUserById(sessionUser.id); + const userSigningIn = getUserById(_user.id); - if (guestUser.guest) { - updateUser(guestUser.id, { + if (userSigningIn) { + updateUser(sessionUser.id, { guest: true, signedInWithUserId: userSigningIn.id }); + } else { + updateUser(sessionUser.id, { firstName: user.firstName ?? undefined, lastName: user.lastName ?? undefined, username: user.username ?? undefined, diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx new file mode 100644 index 000000000..364cb17e8 --- /dev/null +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -0,0 +1,54 @@ +import { getCurrentUser } from '@/components/auth'; +import Content from '@/components/content'; +import { getProcesses } from '@/lib/data/legacy/_process'; +import { getUserById } from '@/lib/data/legacy/iam/users'; +import { Card } from 'antd'; +import { redirect } from 'next/navigation'; +import TransferProcessesConfirmationButtons from './transfer-processes-confitmation-buttons'; + +export default async function TransferProcessesPage({ + searchParams, +}: { + searchParams: { + callbackUrl?: string; + guestId?: string; + }; +}) { + const { userId, session } = await getCurrentUser(); + if (!session) redirect('api/auth/signin'); + if (session.user.guest) redirect('/'); + + const callbackUrl = searchParams.callbackUrl || '/'; + + const guestId = searchParams.guestId; + // guestId === userId if the user signed in with a non existing account, and the guest user was + // turned into an authenticated user + if (!guestId || guestId === userId) redirect(callbackUrl); + + const possibleGuest = getUserById(guestId); + // possibleGuest might be a normal user, this would happen if the user signed in with a new + // account, we only go further then this redirect, if the user signed in with an account that was + // already linked to an existing user + if (!possibleGuest || !possibleGuest.guest || possibleGuest?.signedInWithUserId !== userId) + redirect(callbackUrl); + + // NOTE: this ignores folders + const guestProcesses = (await getProcesses()).filter( + (process) => process.environmentId === guestId, + ); + if (guestProcesses.length === 0) redirect(callbackUrl); + + return ( + + + Your guest account had {guestProcesses.length} process{guestProcesses.length !== 1 && 'es'}. +
+ Would you like to transfer them to your account? + +
+
+ ); +} diff --git a/src/management-system-v2/app/transfer-processes/server-actions.ts b/src/management-system-v2/app/transfer-processes/server-actions.ts new file mode 100644 index 000000000..f9a61a086 --- /dev/null +++ b/src/management-system-v2/app/transfer-processes/server-actions.ts @@ -0,0 +1,89 @@ +'use server'; + +import { getCurrentUser } from '@/components/auth'; +import { Folder } from '@/lib/data/folder-schema'; +import { getProcesses, removeProcess, updateProcess } from '@/lib/data/legacy/_process'; +import { + getFolders, + getRootFolder, + moveFolder, + updateFolderMetaData, +} from '@/lib/data/legacy/folders'; +import { deleteEnvironment } from '@/lib/data/legacy/iam/environments'; +import { deleteUser, getUserById } from '@/lib/data/legacy/iam/users'; +import { Process } from '@/lib/data/process-schema'; +import { UserErrorType, userError } from '@/lib/user-error'; +import { redirect } from 'next/navigation'; + +async function ensureValidRequest(guestId: string) {} + +export async function transferProcesses(guestId: string, callbackUrl: string = '/') { + const { session } = await getCurrentUser(); + if (!session) return userError("You're not signed in", UserErrorType.PermissionError); + if (session.user.guest) + return userError("You can't be a guest to transfer processes", UserErrorType.PermissionError); + + if (guestId === session.user.id) redirect(callbackUrl); + + const possibleGuest = getUserById(guestId); + if ( + !possibleGuest || + !possibleGuest.guest || + possibleGuest?.signedInWithUserId !== session.user.id + ) + return userError('Invalid guest id', UserErrorType.PermissionError); + + // Processes and folders under root folder of guest space guet their folderId changed to the + // root folder of the new owner space, for the rest we just update the environmentId + const userRootFolderId = getRootFolder(session.user.id).id; + const guestRootFolderId = getRootFolder(guestId).id; + + const guestProcesses = (await getProcesses()).filter( + ({ environmentId }) => environmentId === guestId, + ); + for (const process of guestProcesses) { + const processUpdate: Partial = { + environmentId: session.user.id, + owner: session.user.id, + }; + if (process.folderId === guestRootFolderId) processUpdate.folderId = userRootFolderId; + updateProcess(process.id, processUpdate); + } + + const guestFolders = getFolders(guestId); + for (const folder of guestFolders) { + if (folder.id === guestRootFolderId) continue; + + const folderData: Partial = { createdBy: session.user.id }; + + if (folder.parentId === guestRootFolderId) moveFolder(folder.id, userRootFolderId); + else folderData.environmentId = session.user.id; + + updateFolderMetaData(folder.id, folderData); + } + + deleteUser(guestId); + + redirect(callbackUrl); +} + +export async function discardProcesses(guestId: string, redirectUrl: string = '/') { + const { session } = await getCurrentUser(); + if (!session) return userError("You're not signed in", UserErrorType.PermissionError); + if (session.user.guest) + return userError("You can't be a guest to transfer processes", UserErrorType.PermissionError); + + if (guestId === session.user.id) redirect(redirectUrl); + + const possibleGuest = getUserById(guestId); + if ( + !possibleGuest || + !possibleGuest.guest || + possibleGuest?.signedInWithUserId !== session.user.id + ) + return userError('Invalid guest id', UserErrorType.PermissionError); + + deleteUser(guestId); + + redirect(redirectUrl); +} diff --git a/src/management-system-v2/app/transfer-processes/transfer-processes-confitmation-buttons.tsx b/src/management-system-v2/app/transfer-processes/transfer-processes-confitmation-buttons.tsx new file mode 100644 index 000000000..14943af29 --- /dev/null +++ b/src/management-system-v2/app/transfer-processes/transfer-processes-confitmation-buttons.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { Space, Button } from 'antd'; +import { useTransition } from 'react'; +import { + transferProcesses as serverTransferProcesses, + discardProcesses as serverDiscardProcesses, +} from './server-actions'; + +export default function TransferProcessesConfirmationButtons({ + guestId, + callbackUrl, +}: { + guestId: string; + callbackUrl?: string; +}) { + const [transferring, startTransfer] = useTransition(); + function transferProcesses() { + startTransfer(async () => { + await serverTransferProcesses(guestId, callbackUrl); + }); + } + + const [discardingProcesses, startDiscardingProcesses] = useTransition(); + function discardProcesses() { + startDiscardingProcesses(async () => { + await serverDiscardProcesses(guestId, callbackUrl); + }); + } + + return ( + + + + + ); +} From 65fcc61b82e064aecf2b24cba48b3d1db718d840 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 22 Jul 2024 12:27:54 +0200 Subject: [PATCH 37/45] fix(ms2/signin): allow guests to sign in to dev users --- src/management-system-v2/app/(auth)/signin/page.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/management-system-v2/app/(auth)/signin/page.tsx b/src/management-system-v2/app/(auth)/signin/page.tsx index b6173372a..5f98f00d4 100644 --- a/src/management-system-v2/app/(auth)/signin/page.tsx +++ b/src/management-system-v2/app/(auth)/signin/page.tsx @@ -15,8 +15,6 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin let providers = getProviders(); - providers = providers.filter((provider) => !isGuest || 'development-users' !== provider.id); - providers = providers.toSorted((a, b) => { if (a.id === 'guest-signin') return 1; if (b.id === 'guest-signin') return -1; @@ -33,11 +31,7 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin return 0; }); - let userType; - if (!session) userType = 'none' as const; - else userType = isGuest ? ('guest' as const) : ('user' as const); - - return ; + return ; }; export default SignInPage; From a039486543c06e7cc45e4ca7216ea54e1c21cc95 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 2 Aug 2024 09:43:05 +0200 Subject: [PATCH 38/45] refactor(ms2/e2e-openmodal): allow any return type this is done to avoid having to unnecessarily wrap promises so that they have a void return type --- tests/ms2/testUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ms2/testUtils/index.ts b/tests/ms2/testUtils/index.ts index a2c73429b..11fa5510a 100644 --- a/tests/ms2/testUtils/index.ts +++ b/tests/ms2/testUtils/index.ts @@ -55,7 +55,7 @@ export const mockClipboardAPI = async (page: Page) => * * @returns a locator that can be used to get the newly opened modal */ -export async function openModal(page: Page, triggerFunction: () => Promise) { +export async function openModal(page: Page, triggerFunction: () => Promise) { const alreadyOpenCount = await page .locator(`div[aria-modal="true"]:not(.ant-zoom)`) .and(page.locator(`div[aria-modal="true"]:visible`)) From 3b0b46bfe6361f4899b62cedf3d6bc9680ab4a51 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 2 Aug 2024 09:44:20 +0200 Subject: [PATCH 39/45] fix(ms2/e2e-waitForHydration): better selector The previous selector only worked if you where signed in as a guest. I predict this will be a pain point in the future though --- tests/ms2/testUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ms2/testUtils/index.ts b/tests/ms2/testUtils/index.ts index 11fa5510a..05bda25d1 100644 --- a/tests/ms2/testUtils/index.ts +++ b/tests/ms2/testUtils/index.ts @@ -92,7 +92,7 @@ export async function closeModal(modal: Locator, triggerFunction: () => Promise< */ export async function waitForHydration(page: Page) { // this button should be in the header on every page - const accountButton = await page.getByRole('link', { name: 'user' }); + const accountButton = page.locator('a[href$="/profile"].ant-dropdown-trigger'); // the menu that open when hovering over the accountButton only works after the page has been fully hydrated await accountButton.hover(); await page From 0319472a664fabedb075e811473a9b8110446c8d Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 2 Aug 2024 09:48:49 +0200 Subject: [PATCH 40/45] feat(ms2/e2e-processListPage): export selectors --- tests/ms2/processes/process-list.page.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/ms2/processes/process-list.page.ts b/tests/ms2/processes/process-list.page.ts index c6ab281ac..8f9617a08 100644 --- a/tests/ms2/processes/process-list.page.ts +++ b/tests/ms2/processes/process-list.page.ts @@ -104,12 +104,15 @@ export class ProcessListPage { return this.processDefinitionIds; } + processLocatorByDefinitionId(definitionId: string) { + return this.page.locator(`tr[data-row-key="${definitionId}"]`); + } + async removeProcess(definitionId: string) { const { page } = this; const modal = await openModal(page, () => - page - .locator(`tr[data-row-key="${definitionId}"]`) + this.processLocatorByDefinitionId(definitionId) .getByRole('button', { name: 'delete' }) .click(), ); @@ -142,7 +145,7 @@ export class ProcessListPage { .fill(processName ?? 'My Process'); await modal.getByLabel('Process Description').fill(description ?? 'Process Description'); await modal.getByRole('button', { name: 'Create' }).click(); - await page.waitForURL(/processes\/([a-zA-Z0-9-_]+)/); + await page.waitForURL(/processes\/(?!folder)([a-zA-Z0-9-_]+)/); const id = page.url().split('processes/').pop(); @@ -185,6 +188,10 @@ export class ProcessListPage { } } + folderLocatorByName(folderName: string) { + return this.page.locator(`tr:has(span:text-is("${folderName}"))`); + } + async createFolder({ folderName, folderDescription, @@ -204,7 +211,7 @@ export class ProcessListPage { if (folderDescription) await modal.getByLabel('Description').fill(folderDescription); await closeModal(modal, () => page.getByRole('button', { name: 'OK' }).click()); // NOTE: this could break if there is another folder with the same name - const folderRow = page.locator(`tr:has(span:text-is("${folderName}"))`); + const folderRow = this.folderLocatorByName(folderName); await expect(folderRow).toBeVisible(); return folderRow.getAttribute('data-row-key'); From bfc8fd7afca4ccc04b4f844b26aca8dd44fbcf45 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 7 Aug 2024 14:48:41 +0200 Subject: [PATCH 41/45] feat(ms2/tests): test user for preview deployments --- .../api/auth/[...nextauth]/auth-options.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index 6c0e42758..cb7843121 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -218,6 +218,28 @@ if (process.env.NODE_ENV === 'development') { ); } +// only add the test user in preview deployments +const url = process.env.NEXTAUTH_URL; +if (url && url.endsWith('app.run') && url.startsWith('https://pr-')) { + nextAuthOptions.providers.push( + CredentialsProvider({ + id: 'test-user', + name: 'Continue With Test User', + credentials: {}, + async authorize() { + return addUser({ + guest: false, + email: `test-user-${crypto.randomUUID()}@proceed-labs.org`, + firstName: 'Test', + lastName: 'Test', + username: 'test-user', + emailVerified: new Date(), + }); + }, + }), + ); +} + export type ExtractedProvider = | { id: string; From b909aed119e5e96402e92692791f4c32f32f9ed8 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 7 Aug 2024 15:06:34 +0200 Subject: [PATCH 42/45] feat(ms2/tests): add test user in dev --- .../app/api/auth/[...nextauth]/auth-options.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index cb7843121..da97f4667 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -218,9 +218,13 @@ if (process.env.NODE_ENV === 'development') { ); } -// only add the test user in preview deployments +// add the test user in preview deployments and dev +// dev is for local testing const url = process.env.NEXTAUTH_URL; -if (url && url.endsWith('app.run') && url.startsWith('https://pr-')) { +if ( + (url && url.endsWith('app.run') && url.startsWith('https://pr-')) || + process.env.NODE_ENV === 'development' +) { nextAuthOptions.providers.push( CredentialsProvider({ id: 'test-user', From 3ad484f192a9d7080b4b837a80a053fdfefb675b Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 7 Aug 2024 15:15:12 +0200 Subject: [PATCH 43/45] refactr(ms2/e2e): use processListPage.processLocatorByDefinitionId --- tests/ms2/processes/process-list.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/ms2/processes/process-list.spec.ts b/tests/ms2/processes/process-list.spec.ts index 62bc02a94..31ac9ccff 100644 --- a/tests/ms2/processes/process-list.spec.ts +++ b/tests/ms2/processes/process-list.spec.ts @@ -11,10 +11,11 @@ test('create a new process and remove it again', async ({ processListPage }) => await processListPage.goto(); - await expect(page.locator(`tr[data-row-key="${processDefinitionID}"]`)).toBeVisible(); + const processLocator = processListPage.processLocatorByDefinitionId(processDefinitionID); + await expect(processLocator).toBeVisible(); const modal = await openModal(page, () => - page + processLocator .locator(`tr[data-row-key="${processDefinitionID}"]`) .getByRole('button', { name: 'delete' }) .click(), @@ -22,7 +23,7 @@ test('create a new process and remove it again', async ({ processListPage }) => await closeModal(modal, () => modal.getByRole('button', { name: 'OK' }).click()); - await expect(page.locator(`tr[data-row-key="${processDefinitionID}"]`)).not.toBeVisible(); + await expect(processLocator).not.toBeVisible(); processListPage.getDefinitionIds().splice(0, 1); }); From 86529054c1cff7e04e849e4282f93dce660d6910 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 7 Aug 2024 15:21:11 +0200 Subject: [PATCH 44/45] feat(ms2/e2e): transferring processes --- .../authtenticatin-flows.spec.ts | 97 +++++++++++++++++++ tests/ms2/authentication/signin.fitxtures.ts | 29 ++++++ tests/ms2/authentication/signin.page.ts | 32 ++++++ 3 files changed, 158 insertions(+) create mode 100644 tests/ms2/authentication/authtenticatin-flows.spec.ts create mode 100644 tests/ms2/authentication/signin.fitxtures.ts create mode 100644 tests/ms2/authentication/signin.page.ts diff --git a/tests/ms2/authentication/authtenticatin-flows.spec.ts b/tests/ms2/authentication/authtenticatin-flows.spec.ts new file mode 100644 index 000000000..7844f4b49 --- /dev/null +++ b/tests/ms2/authentication/authtenticatin-flows.spec.ts @@ -0,0 +1,97 @@ +import { waitForHydration } from '../testUtils'; +import { expect, test } from './signin.fitxtures'; + +test('Transfer processes from guest', async ({ signinPage, ms2Page, processListPage }) => { + const page = signinPage.page; + + // Sign in as guest + await ms2Page.login(); + + // Create guest assets + const process1 = await processListPage.createProcess({ + processName: 'guest: process 1', + returnToProcessList: true, + }); + const process2 = await processListPage.createProcess({ + processName: 'guest: process 2', + returnToProcessList: true, + }); + + const folderName = crypto.randomUUID(); + await processListPage.createFolder({ folderName }); + const folderLocator = processListPage.folderLocatorByName(folderName); + await folderLocator.click(); + await waitForHydration(page); + + const process3 = await processListPage.createProcess({ processName: 'guest: process 3' }); + await processListPage.goto(); + await folderLocator.click(); + await waitForHydration(page); + await expect(processListPage.processLocatorByDefinitionId(process3)).toBeVisible(); + + // Sign in to an authenticated user + await signinPage.signinAsTestUser(); + + // Transfer processes + await page.waitForURL('**/transfer-processes*'); + await waitForHydration(page); + await page.getByRole('button', { name: 'Yes' }).click(); + // wait for redirect + await page.waitForURL(/^(?!.*trasnfer-processes).*$/); + + // Verify that the guest assets where transferred + await processListPage.goto(); + + await expect(processListPage.processLocatorByDefinitionId(process1)).toBeVisible(); + await expect(processListPage.processLocatorByDefinitionId(process2)).toBeVisible(); + await expect(folderLocator).toBeVisible(); + await folderLocator.click(); + await waitForHydration(page); + await expect(processListPage.processLocatorByDefinitionId(process3)).toBeVisible(); +}); + +test("Don't Transfer processes from guest", async ({ signinPage, ms2Page, processListPage }) => { + const page = signinPage.page; + + // Sign in as guest + await ms2Page.login(); + + // Create guest assets + const process1 = await processListPage.createProcess({ + processName: 'guest: process 1', + returnToProcessList: true, + }); + const process2 = await processListPage.createProcess({ + processName: 'guest: process 2', + returnToProcessList: true, + }); + + const folderName = crypto.randomUUID(); + await processListPage.createFolder({ folderName }); + const folderLocator = processListPage.folderLocatorByName(folderName); + await folderLocator.click(); + await waitForHydration(page); + + const process3 = await processListPage.createProcess({ processName: 'guest: process 3' }); + await processListPage.goto(); + await folderLocator.click(); + await waitForHydration(page); + await expect(processListPage.processLocatorByDefinitionId(process3)).toBeVisible(); + + // Sign in to an authenticated user + await signinPage.signinAsTestUser(); + + // Transfer processes + await page.waitForURL('**/transfer-processes*'); + await waitForHydration(page); + await page.getByRole('button', { name: 'No' }).click(); + // wait for redirect + await page.waitForURL(/^(?!.*trasnfer-processes).*$/); + + // Verify that the guest assets where transferred + await processListPage.goto(); + + await expect(processListPage.processLocatorByDefinitionId(process1)).not.toBeVisible(); + await expect(processListPage.processLocatorByDefinitionId(process2)).not.toBeVisible(); + await expect(folderLocator).not.toBeVisible(); +}); diff --git a/tests/ms2/authentication/signin.fitxtures.ts b/tests/ms2/authentication/signin.fitxtures.ts new file mode 100644 index 000000000..ad71604eb --- /dev/null +++ b/tests/ms2/authentication/signin.fitxtures.ts @@ -0,0 +1,29 @@ +import { test as base } from '@playwright/test'; +import { SigninPage } from './signin.page'; +import { MS2Page } from '../ms2.page'; +import { ProcessListPage } from '../processes/process-list.page'; + +export const test = base.extend<{ + signinPage: SigninPage; + ms2Page: MS2Page; + processListPage: ProcessListPage; +}>({ + ms2Page: async ({ page }, use) => { + const ms2Page = new MS2Page(page); + await use(ms2Page); + }, + processListPage: async ({ page, ms2Page }, use) => { + const processListPage = new ProcessListPage(page); + await use(processListPage); + await processListPage.removeAllProcesses(); + }, + signinPage: async ({ page, processListPage }, use) => { + const signinPage = new SigninPage(page); + await use(signinPage); + + // TODO: move this somewhere else + avoid race conditions + await signinPage.deleteTestUser(); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/ms2/authentication/signin.page.ts b/tests/ms2/authentication/signin.page.ts new file mode 100644 index 000000000..35e4144af --- /dev/null +++ b/tests/ms2/authentication/signin.page.ts @@ -0,0 +1,32 @@ +import { Page } from '@playwright/test'; +import { openModal, waitForHydration } from '../testUtils'; + +export class SigninPage { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async goto() { + await openModal(this.page, () => this.page.goto('/signin')); + } + + async signinAsGuest() {} + + async signinAsTestUser() { + await this.goto(); + await this.page.getByRole('button', { name: 'Continue With Test User' }).click(); + } + + async deleteTestUser() { + await this.page.goto('/profile'); + await waitForHydration(this.page); + await this.page.getByRole('button', { name: 'Delete Account' }).click(); + await this.page + .getByLabel('Delete Account') + .getByRole('button', { name: 'Delete Account' }) + .click(); + await this.page.waitForURL('**/signin*'); + } +} From 61da2e2b7882ce90cf27f4cd4849ae1f723578bf Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Thu, 8 Aug 2024 14:14:00 +0200 Subject: [PATCH 45/45] feat(ms2/e2e): email signin --- package.json | 3 +- .../authtenticatin-flows.spec.ts | 38 ++- tests/ms2/testUtils/index.ts | 73 +++++ yarn.lock | 253 +++++++++++++++++- 4 files changed, 356 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index d7837baea..51baf3d9f 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "uuid": "9.0.1", "webpack": "^4.35.3", "webpack-cli": "^3.3.0", - "webpack-dev-server": "^3.2.1" + "webpack-dev-server": "^3.2.1", + "imapflow": "^1.0.164" } } diff --git a/tests/ms2/authentication/authtenticatin-flows.spec.ts b/tests/ms2/authentication/authtenticatin-flows.spec.ts index 7844f4b49..a023fd589 100644 --- a/tests/ms2/authentication/authtenticatin-flows.spec.ts +++ b/tests/ms2/authentication/authtenticatin-flows.spec.ts @@ -1,4 +1,5 @@ -import { waitForHydration } from '../testUtils'; +import { Locator, mergeExpects } from '@playwright/test'; +import { closeModal, getSigninLink, openModal, waitForHydration } from '../testUtils'; import { expect, test } from './signin.fitxtures'; test('Transfer processes from guest', async ({ signinPage, ms2Page, processListPage }) => { @@ -95,3 +96,38 @@ test("Don't Transfer processes from guest", async ({ signinPage, ms2Page, proces await expect(processListPage.processLocatorByDefinitionId(process2)).not.toBeVisible(); await expect(folderLocator).not.toBeVisible(); }); + +test('Sign in with E-mail', async ({ signinPage }) => { + const page = signinPage.page; + + await openModal(page, () => page.goto('/signin')); + + const sentDate = new Date(); + await page.getByPlaceholder('E-Mail').fill(process.env.TEST_EMAIL); + await page.getByRole('button', { name: 'Continue with E-Mail' }).click(); + + const signInLink = await getSigninLink(sentDate); + + // It could be the case that the email already had a profile + page.goto(signInLink); + + await new Promise((res) => setTimeout(res, 7000)); + const profileModal = page + .locator(`div[aria-modal="true"]:not(.ant-zoom)`) + .getByText('You need to complete your profile') + .first(); + const newUser = await profileModal.isVisible(); + + if (newUser) { + await page.getByLabel('First Name').fill('Test'); + await page.getByLabel('Last Name').fill('User'); + await page.getByLabel('Username').fill(`${crypto.randomUUID().slice(0, 35)}`); + + await closeModal(profileModal, () => page.getByRole('button', { name: 'Submit' }).click()); + } + + await page.goto('/profile'); + await waitForHydration(page); + + await expect(page.getByRole('cell', { name: process.env.TEST_EMAIL })).toBeVisible(); +}); diff --git a/tests/ms2/testUtils/index.ts b/tests/ms2/testUtils/index.ts index 05bda25d1..3826ae355 100644 --- a/tests/ms2/testUtils/index.ts +++ b/tests/ms2/testUtils/index.ts @@ -104,3 +104,76 @@ export async function waitForHydration(page: Page) { await page.mouse.move(0, 0); await page.getByRole('menuitem', { name: 'Account Settings' }).waitFor({ state: 'hidden' }); } + +export async function useMailInbox(use: (client: any) => Promise) { + const { ImapFlow } = require('imapflow'); + + const client = new ImapFlow({ + host: process.env.TEST_EMAIL_HOST, + port: 993, + secure: true, + auth: { + user: process.env.TEST_EMAIL, + pass: process.env.TEST_EMAIL_PASSWORD, + }, + logger: false, + }); + let lock: any; + let response: T; + + try { + await client.connect(); + + lock = await client.getMailboxLock('INBOX'); + + return await use(client); + } finally { + if (lock) lock.release(); + await client.logout(); + } +} + +/** + * get signin link from email inbox + * + * NOTE: this function deletes all emails from no-reply until it finds a match that respects the + * sentDate, thus it should not be used concurrently + * */ +export async function getSigninLink(sentDate: Date) { + return await useMailInbox(async (client) => { + const sender = 'no-reply@proceed-labs.org'; + let foundLink: string | undefined | null; + + // for timing + const start = Date.now(); + const waitForEmailTimeout = 20_000; + const receivedDateError = 10_000; + const coolDown = 2000; + + while (Date.now() - start < waitForEmailTimeout && !foundLink) { + const seenMails: string[] = []; + + for await (let message of client.fetch( + { from: sender }, + { bodyParts: ['TEXT'], envelope: true }, + )) { + seenMails.push(message.seq); + const text: string = message.bodyParts.get('text').toString().replace(/=\r\n/g, ''); + const afterSentDate = + message.envelope.date.getTime() >= sentDate.getTime() - receivedDateError; + // + let signinLink = text.match(/\n(https:\/\/.*callback\/email.*)/)?.[1]; + signinLink = signinLink?.replace(/=3D/g, '='); + + if (signinLink && afterSentDate) foundLink = signinLink; + } + + // you can't delete emails whilst reading them + for (const seq of seenMails) await client.messageDelete(seq); + + await new Promise((res) => setTimeout(res, coolDown)); + } + + return foundLink; + }); +} diff --git a/yarn.lock b/yarn.lock index bf3cf0173..60d8999fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1350,7 +1350,7 @@ htm "^3.1.1" preact "^10.11.2" -"@casl/ability-v6@npm:@casl/ability@^6.5.0", "@casl/ability@6.7.1": +"@casl/ability-v6@npm:@casl/ability@^6.5.0": version "6.7.1" resolved "https://registry.yarnpkg.com/@casl/ability/-/ability-6.7.1.tgz#89691083aafd1cfc4ae9519ffbcb0e7cb77ac201" integrity sha512-e+Vgrehd1/lzOSwSqKHtmJ6kmIuZbGBlM2LBS5IuYGGKmVHuhUuyh3XgTn1VIw9+TO4gqU+uptvxfIRBUEdJuw== @@ -1364,6 +1364,13 @@ dependencies: "@ucast/mongo2js" "^1.3.0" +"@casl/ability@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@casl/ability/-/ability-6.7.1.tgz#89691083aafd1cfc4ae9519ffbcb0e7cb77ac201" + integrity sha512-e+Vgrehd1/lzOSwSqKHtmJ6kmIuZbGBlM2LBS5IuYGGKmVHuhUuyh3XgTn1VIw9+TO4gqU+uptvxfIRBUEdJuw== + dependencies: + "@ucast/mongo2js" "^1.3.0" + "@casl/vue@1.x": version "1.2.3" resolved "https://registry.yarnpkg.com/@casl/vue/-/vue-1.2.3.tgz#ba835f71746334cddc6a97aa879e7b77d7472451" @@ -4738,6 +4745,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + autoprefixer@^9.5.1: version "9.8.8" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a" @@ -5828,6 +5840,14 @@ buffer@^5.1.0, buffer@^5.2.1, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builder-util-runtime@8.9.2: version "8.9.2" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.9.2.tgz#a9669ae5b5dcabfe411ded26678e7ae997246c28" @@ -8750,6 +8770,21 @@ encodeurl@^1.0.2, encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encoding-japanese@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.0.0.tgz#fa0226e5469e7b5b69a04fea7d5481bd1fa56936" + integrity sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ== + +encoding-japanese@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.1.0.tgz#5d3c2b652c84ca563783b86907bf5cdfe9a597e2" + integrity sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w== + +encoding-japanese@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz#0ef2d2351250547f432a2dd155453555c16deb59" + integrity sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A== + end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -9706,7 +9741,7 @@ eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -events@^3.0.0, events@^3.2.0: +events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -10058,6 +10093,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + fast-text-encoding@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" @@ -11762,7 +11802,7 @@ iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.13, iconv-lite@^0.4.17, iconv dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: +iconv-lite@0.6.3, iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -11823,6 +11863,21 @@ ignore@^5.1.1, ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== +imapflow@^1.0.164: + version "1.0.164" + resolved "https://registry.yarnpkg.com/imapflow/-/imapflow-1.0.164.tgz#c883fd263fab4b0096ecde66e59f65222659d554" + integrity sha512-+KAmLrpqq2Q0Ts1imMP4svydfhYznlvlLHhgtTb8NiIcccZvdRNfdHVP8/RrGaw0hy0TOaluawsm/6q+TqdLPw== + dependencies: + encoding-japanese "2.2.0" + iconv-lite "0.6.3" + libbase64 "1.3.0" + libmime "5.3.5" + libqp "2.1.0" + mailsplit "5.4.0" + nodemailer "6.9.14" + pino "9.2.0" + socks "2.8.3" + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -12048,6 +12103,14 @@ ioredis@^5.0.1: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -13732,6 +13795,11 @@ js2xmlparser@^4.0.2: dependencies: xmlcreate "^2.0.4" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -14277,11 +14345,51 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libbase64@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-1.2.1.tgz#fb93bf4cb6d730f29b92155b6408d1bd2176a8c8" + integrity sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew== + +libbase64@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-1.3.0.tgz#053314755a05d2e5f08bbfc48d0290e9322f4406" + integrity sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg== + +libmime@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/libmime/-/libmime-5.2.0.tgz#c4ed5cbd2d9fdd27534543a68bb8d17c658d51d8" + integrity sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw== + dependencies: + encoding-japanese "2.0.0" + iconv-lite "0.6.3" + libbase64 "1.2.1" + libqp "2.0.1" + +libmime@5.3.5: + version "5.3.5" + resolved "https://registry.yarnpkg.com/libmime/-/libmime-5.3.5.tgz#acd95a32f58dab55c8a9d269c5b4509e7ad6ae31" + integrity sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg== + dependencies: + encoding-japanese "2.1.0" + iconv-lite "0.6.3" + libbase64 "1.3.0" + libqp "2.1.0" + libphonenumber-js@^1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.1.tgz#2596683e1876bfee74082bb49339fe0a85ae34f9" integrity sha512-Wze1LPwcnzvcKGcRHFGFECTaLzxOtujwpf924difr5zniyYv1C2PiW0419qDR7m8lKDxsImu5mwxFuXhXpjmvw== +libqp@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/libqp/-/libqp-2.0.1.tgz#b8fed76cc1ea6c9ceff8888169e4e0de70cd5cf2" + integrity sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg== + +libqp@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/libqp/-/libqp-2.1.0.tgz#ce84bffd86b76029032093bd866d316e12a3d3f5" + integrity sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A== + lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" @@ -14642,6 +14750,15 @@ machine-uuid@^1.2.0: dependencies: uuid "^3.1.0" +mailsplit@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/mailsplit/-/mailsplit-5.4.0.tgz#9f4692fadd9013e9ce632147d996931d2abac6ba" + integrity sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA== + dependencies: + libbase64 "1.2.1" + libmime "5.2.0" + libqp "2.0.1" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -15616,6 +15733,11 @@ nodemailer@6.9.13: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.13.tgz#5b292bf1e92645f4852ca872c56a6ba6c4a3d3d6" integrity sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA== +nodemailer@6.9.14: + version "6.9.14" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75" + integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA== + nodemon@^2.0.12: version "2.0.22" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258" @@ -15951,6 +16073,11 @@ omggif@1.0.9: resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.9.tgz#dcb7024dacd50c52b4d303f04802c91c057c765f" integrity sha512-VYAQRSZo7qoBcwB5G29YqVPLnxvDkWulE3x35kwH3bq4GdH/ZkHrcPPhxVfaOGYGZ5KV2/55UpcjcyNIO1qZoQ== +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + on-finished@2.4.1, on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -16660,6 +16787,36 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.2.0.tgz#e77a9516f3a3e5550d9b76d9f65ac6118ef02bdd" + integrity sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^7.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + pirates@^4.0.4, pirates@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -17296,6 +17453,11 @@ process-nextick-args@^2.0.1, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -17609,6 +17771,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" @@ -18129,6 +18296,17 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-web-to-node-stream@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" @@ -18152,6 +18330,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + realpath-native@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -19224,7 +19407,7 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -smart-buffer@^4.0.2: +smart-buffer@^4.0.2, smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== @@ -19377,6 +19560,21 @@ sockjs@^0.3.21: uuid "^8.3.2" websocket-driver "^0.7.4" +socks@2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +sonic-boom@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" + integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ== + dependencies: + atomic-sleep "^1.0.0" + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -19535,7 +19733,12 @@ split2@^3.0.0, split2@^3.1.0: dependencies: readable-stream "^3.0.0" -sprintf-js@^1.1.2: +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sprintf-js@^1.1.2, sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== @@ -19741,7 +19944,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -19776,6 +19979,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -19851,7 +20063,7 @@ string.prototype.trimstart@^1.0.7, string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.0.0, string_decoder@^1.1.1: +string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -19865,7 +20077,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19893,6 +20105,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -20356,6 +20575,13 @@ thread-loader@^3.0.0: neo-async "^2.6.2" schema-utils "^3.0.0" +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" @@ -21979,7 +22205,7 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22014,6 +22240,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"