From 5ed014b02bf3f03348c3e40a6c4a014a18ec85fa Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 21 Jun 2024 12:00:56 +0200 Subject: [PATCH 01/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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 971be9280591261012b04e59bc05c7f1a0b29106 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 26 Nov 2024 18:07:02 +0100 Subject: [PATCH 38/48] refactor(ms2): get processes takes in an spaceId and an ability This change was needed, because the function was redundant and was implemented in a way that caused it to fail. It is redundant to use a userId and an ability, because both tell you for which user we're supposed to get the processes. And it fails because, it returns the processes where the creator was the user, this is wrong, because it doesn't take into consideration abilities. For me is better that the function takes in a spaceId, and returns all processes for a spaceId, and if you want to get the processes for a user you can provide his ability. --- .../processes/[processId]/page.tsx | 7 +++---- .../app/shared-viewer/page.tsx | 2 +- src/management-system-v2/lib/data/DTOs.ts | 6 +++--- src/management-system-v2/lib/data/db/process.ts | 14 ++++++++------ .../lib/data/legacy/_process.ts | 15 +++++++-------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[processId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[processId]/page.tsx index 6d974976a..0eb98aa17 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[processId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[processId]/page.tsx @@ -1,4 +1,4 @@ -import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; +import { getCurrentEnvironment } from '@/components/auth'; import Wrapper from './wrapper'; import styles from './page.module.scss'; import Modeler from './modeler'; @@ -18,11 +18,10 @@ const Process = async ({ params: { processId, environmentId }, searchParams }: P //console.log('processId', processId); //console.log('query', searchParams); const selectedVersionId = searchParams.version ? +searchParams.version : undefined; - const { ability } = await getCurrentEnvironment(environmentId); - const { userId } = await getCurrentUser(); + const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); // Only load bpmn if no version selected. const process = await getProcess(processId, !selectedVersionId); - const processes = await getProcesses(userId, ability, false); + const processes = await getProcesses(activeEnvironment.spaceId, ability, false); if (!ability.can('view', toCaslResource('Process', process))) { throw new Error('Forbidden.'); diff --git a/src/management-system-v2/app/shared-viewer/page.tsx b/src/management-system-v2/app/shared-viewer/page.tsx index 513eaf26a..451a3ece1 100644 --- a/src/management-system-v2/app/shared-viewer/page.tsx +++ b/src/management-system-v2/app/shared-viewer/page.tsx @@ -54,7 +54,7 @@ const getProcessInfo = async ( const { ability, activeEnvironment } = await getCurrentEnvironment(session?.user.id); ({ spaceId } = activeEnvironment); // get all the processes the user has access to - const ownedProcesses = await getProcesses(userId, ability); + const ownedProcesses = await getProcesses(spaceId, ability); // check if the current user is the owner of the process(/has access to the process) => if yes give access regardless of sharing status isOwner = ownedProcesses.some((process) => process.id === definitionId); } diff --git a/src/management-system-v2/lib/data/DTOs.ts b/src/management-system-v2/lib/data/DTOs.ts index 371984a11..adfc96e50 100644 --- a/src/management-system-v2/lib/data/DTOs.ts +++ b/src/management-system-v2/lib/data/DTOs.ts @@ -28,10 +28,10 @@ export async function getProcess(processDefinitionsId: string, includeBPMN = fal : await processModuleLegacy.getProcess(processDefinitionsId, includeBPMN); } -export async function getProcesses(userId: string, ability: Ability, includeBPMN = false) { +export async function getProcesses(environmentId: string, ability?: Ability, includeBPMN = false) { return enableUseDB - ? await processModuleDB.getProcesses(userId, ability, includeBPMN) - : await processModuleLegacy.getProcesses(userId, ability, includeBPMN); + ? await processModuleDB.getProcesses(environmentId, ability, includeBPMN) + : await processModuleLegacy.getProcesses(environmentId, ability, includeBPMN); } export async function getProcessBpmn(processDefinitionsId: string) { diff --git a/src/management-system-v2/lib/data/db/process.ts b/src/management-system-v2/lib/data/db/process.ts index 5fae76ec9..871ae436d 100644 --- a/src/management-system-v2/lib/data/db/process.ts +++ b/src/management-system-v2/lib/data/db/process.ts @@ -17,11 +17,14 @@ import db from '@/lib/data'; import { v4 } from 'uuid'; import { UserErrorType, userError } from '@/lib/user-error'; -/** Returns all processes for a user */ -export async function getProcesses(userId: string, ability: Ability, includeBPMN = false) { - const userProcesses = await db.process.findMany({ +/** + * Returns all processes in an environment + * If you want the processes for a specific user, you have to provide his ability + * */ +export async function getProcesses(environmentId: string, ability?: Ability, includeBPMN = false) { + const spaceProcesses = await db.process.findMany({ where: { - creatorId: userId, + environmentId, }, select: { id: true, @@ -46,10 +49,9 @@ export async function getProcesses(userId: string, ability: Ability, includeBPMN }, }); - //TODO: ability check ? is it really necessary in this case? //TODO: add pagination - return userProcesses; + return ability ? ability.filter('view', 'Process', spaceProcesses) : spaceProcesses; } export async function getProcess(processDefinitionsId: string, includeBPMN = false) { diff --git a/src/management-system-v2/lib/data/legacy/_process.ts b/src/management-system-v2/lib/data/legacy/_process.ts index 85a36fb42..7738bedcd 100644 --- a/src/management-system-v2/lib/data/legacy/_process.ts +++ b/src/management-system-v2/lib/data/legacy/_process.ts @@ -57,16 +57,15 @@ export function getProcessMetaObjects() { } /** Returns all processes for a user */ -export async function getProcesses(userId: string, ability: Ability, includeBPMN = false) { - const processes = Object.values(processMetaObjects); +export async function getProcesses(environmentId: string, ability?: Ability, includeBPMN = false) { + const spaceProcesses = Object.values(processMetaObjects).filter( + (process) => process.environmentId === environmentId, + ); - const userProcesses = ability ? ability.filter('view', 'Process', processes) : processes; + const processes = ability ? ability.filter('view', 'Process', spaceProcesses) : spaceProcesses; - return await Promise.all( - userProcesses.map(async (process) => - !includeBPMN ? process : { ...process, bpmn: getProcessBpmn(process.id) }, - ), - ); + if (!includeBPMN) return processes; + return processes.map((process) => ({ ...process, bpmn: getProcessBpmn(process.id) })); } export async function getProcess(processDefinitionsId: string, includeBPMN = false) { From 0969b792edee71ec080b2013e2cfa56bd26a5459 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 26 Nov 2024 18:14:33 +0100 Subject: [PATCH 39/48] fix: merge conflict --- .../app/api/auth/[...nextauth]/auth-options.ts | 5 ++--- .../lib/data/legacy/iam/users.ts | 18 ------------------ .../lib/data/user-schema.ts | 1 - 3 files changed, 2 insertions(+), 22 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 a61894da9..35d78cbe6 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 @@ -67,6 +67,7 @@ const nextAuthOptions: AuthOptions = { const session = await getServerSession(nextAuthOptions); const sessionUser = session?.user; + // Guest account signs in with proper auth if ( sessionUser?.isGuest && account?.provider !== 'guest-signin' && @@ -79,9 +80,7 @@ const nextAuthOptions: AuthOptions = { const user = _user as Partial; const userSigningIn = await getUserById(_user.id); - if (userSigningIn) { - await updateUser(sessionUser.id, { isGuest: true, signedInWithUserId: userSigningIn.id }); - } else { + if (!userSigningIn) { await updateUser(sessionUser.id, { firstName: user.firstName ?? undefined, lastName: user.lastName ?? undefined, 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 992fca8f4..5e2d38f28 100644 --- a/src/management-system-v2/lib/data/legacy/iam/users.ts +++ b/src/management-system-v2/lib/data/legacy/iam/users.ts @@ -142,33 +142,15 @@ export async function deleteUser(userId: string) { return user; } -<<<<<<< HEAD -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; -======= export async function updateUser(userId: string, inputUser: Partial) { const user = await getUserById(userId, { throwIfNotFound: true }); const isGoingToBeGuest = inputUser.isGuest !== undefined ? inputUser.isGuest : user?.isGuest; ->>>>>>> origin/main let updatedUser: User; if (isGoingToBeGuest) { updatedUser = { -<<<<<<< HEAD - id: user.id, - signedInWithUserId: user.guest ? user.signedInWithUserId : undefined, - ...GuestUserSchema.parse(inputUser), -======= id: user!.id, isGuest: true, ->>>>>>> origin/main }; } 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 0dfefb797..0e2d772b5 100644 --- a/src/management-system-v2/lib/data/user-schema.ts +++ b/src/management-system-v2/lib/data/user-schema.ts @@ -39,7 +39,6 @@ export type AuthenticatedUser = z.infer & { id: export const GuestUserSchema = z.object({ isGuest: z.literal(true), id: z.string().optional(), - signedInWithUserId: z.string().optional(), }); export type GuestUser = z.infer & { id: string }; From 3da52a942a6e7887f513f87cc5e4e84fbddb5a57 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 26 Nov 2024 18:18:50 +0100 Subject: [PATCH 40/48] feat(ms2/DTOs): updateProcess, moveFolder, updateFolderMetaData --- src/management-system-v2/lib/data/DTOs.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/management-system-v2/lib/data/DTOs.ts b/src/management-system-v2/lib/data/DTOs.ts index adfc96e50..c7a87315f 100644 --- a/src/management-system-v2/lib/data/DTOs.ts +++ b/src/management-system-v2/lib/data/DTOs.ts @@ -45,6 +45,12 @@ export async function getProcessVersionBpmn(processDefinitionsId: string, versio : await processModuleLegacy.getProcessVersionBpmn(processDefinitionsId, version); } +export async function updateProcess(...args: Parameters) { + return enableUseDB + ? await processModuleDB.updateProcess(...args) + : await processModuleLegacy.updateProcess(...args); +} + export async function getFolderContents(folderId: string, ability?: Ability) { return enableUseDB ? await folderModuleDB.getFolderContents(folderId, ability) @@ -63,6 +69,20 @@ export async function getFolderById(folderId: string, ability?: Ability) { : await folderModuleLegacy.getFolderById(folderId, ability); } +export async function moveFolder(folderId: string, newParentId: string, ability?: Ability) { + return enableUseDB + ? await folderModuleDB.moveFolder(folderId, newParentId, ability) + : await folderModuleLegacy.moveFolder(folderId, newParentId, ability); +} + +export async function updateFolderMetaData( + ...args: Parameters +) { + return enableUseDB + ? await folderModuleDB.updateFolderMetaData(...args) + : await folderModuleLegacy.updateFolderMetaData(...args); +} + export async function deleteEnvironment(environmentId: string, ability?: Ability) { return enableUseDB ? await environmentModuleDB.deleteEnvironment(environmentId, ability) From 30737b5574bf00547a723791e3c69f9f6b4c10eb Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 26 Nov 2024 19:00:41 +0100 Subject: [PATCH 41/48] refactor(ms2/transfer-processes): now works with JWT for passing the guestId --- .../app/(auth)/signin/page.tsx | 12 +++- .../app/(auth)/signin/signin.tsx | 16 +++-- .../app/transfer-processes/page.tsx | 59 +++++++++++++------ .../app/transfer-processes/server-actions.ts | 49 ++++++++------- ...ransfer-processes-confitmation-buttons.tsx | 47 ++++++++++----- src/management-system-v2/lib/env-vars.ts | 3 + .../lib/reference-guest-user-token.ts | 32 ++++++++++ 7 files changed, 155 insertions(+), 63 deletions(-) create mode 100644 src/management-system-v2/lib/reference-guest-user-token.ts diff --git a/src/management-system-v2/app/(auth)/signin/page.tsx b/src/management-system-v2/app/(auth)/signin/page.tsx index 0ede1380d..14eb62854 100644 --- a/src/management-system-v2/app/(auth)/signin/page.tsx +++ b/src/management-system-v2/app/(auth)/signin/page.tsx @@ -2,6 +2,9 @@ import { getProviders } from '@/app/api/auth/[...nextauth]/auth-options'; import { getCurrentUser } from '@/components/auth'; import { redirect } from 'next/navigation'; import SignIn from './signin'; +import { generateGuestReferenceToken } from '@/lib/reference-guest-user-token'; + +const dayInMS = 1000 * 60 * 60 * 24; // take in search query const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: string } }) => { @@ -13,6 +16,11 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin redirect(callbackUrl); } + // NOTE: expiration should be the same as the expiration for sign in mails + const guestReferenceToken = isGuest + ? generateGuestReferenceToken({ guestId: session.user.id }, new Date(Date.now() + dayInMS)) + : undefined; + let providers = getProviders(); providers = providers.filter((provider) => !isGuest || 'development-users' !== provider.id); @@ -37,7 +45,9 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin if (!session) userType = 'none' as const; else userType = isGuest ? ('guest' as const) : ('user' as const); - return ; + 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 af30cc870..73f2c759f 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -58,9 +58,13 @@ const signInTitle = ( const SignIn: FC<{ providers: ExtractedProvider[]; userType: 'guest' | 'user' | 'none'; -}> = ({ providers, userType }) => { + guestReferenceToken?: string; +}> = ({ providers, userType, guestReferenceToken }) => { const searchParams = useSearchParams(); const callbackUrl = searchParams.get('callbackUrl') ?? undefined; + const callbackUrlWithGuestRef = guestReferenceToken + ? `/transfer-processes?referenceToken=${guestReferenceToken}&callbackUrl=${callbackUrl}` + : callbackUrl; const authError = searchParams.get('error'); const oauthProviders = providers.filter((provider) => provider.type === 'oauth'); @@ -150,7 +154,9 @@ const SignIn: FC<{ if (provider.type === 'credentials') { return ( signIn(provider.id, { ...values, callbackUrl })} + onFinish={(values) => + signIn(provider.id, { ...values, callbackUrl: callbackUrlWithGuestRef }) + } key={provider.id} layout="vertical" > @@ -168,7 +174,9 @@ const SignIn: FC<{ return ( <> signIn(provider.id, { ...values, callbackUrl })} + onFinish={(values) => + signIn(provider.id, { ...values, callbackUrl: callbackUrlWithGuestRef }) + } key={provider.id} layout="vertical" > @@ -211,7 +219,7 @@ const SignIn: FC<{ style={{ width: '1.5rem', height: 'auto' }} /> } - onClick={() => signIn(provider.id, { callbackUrl })} + onClick={() => signIn(provider.id, { callbackUrl: callbackUrlWithGuestRef })} /> ); diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx index 364cb17e8..c610b49c9 100644 --- a/src/management-system-v2/app/transfer-processes/page.tsx +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -1,41 +1,63 @@ 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 { getProcesses, getUserById } from '@/lib/data/DTOs'; +import { Card, Result, Space } from 'antd'; import { redirect } from 'next/navigation'; -import TransferProcessesConfirmationButtons from './transfer-processes-confitmation-buttons'; +import { DiscardButton, TransferButton } from './transfer-processes-confitmation-buttons'; +import { getGuestReference } from '@/lib/reference-guest-user-token'; export default async function TransferProcessesPage({ searchParams, }: { searchParams: { callbackUrl?: string; - guestId?: string; + referenceToken?: string; }; }) { const { userId, session } = await getCurrentUser(); if (!session) redirect('api/auth/signin'); - if (session.user.guest) redirect('/'); + if (session.user.isGuest) redirect('/'); - const callbackUrl = searchParams.callbackUrl || '/'; + const callbackUrl = decodeURIComponent(searchParams.callbackUrl || '/'); + + const token = decodeURIComponent(searchParams.referenceToken || ''); + const referenceToken = getGuestReference(token); + if ('error' in referenceToken) { + let message = 'Invalid token'; + if (referenceToken.error === 'TokenExpiredError') message = 'Expired token'; + + return ( + + + Continue without guest processes{' '} + , + ]} + /> + + ); + } + const guestId = referenceToken.guestId; - 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 + const possibleGuest = await getUserById(guestId); + // possibleGuest might be a normal user, this would happen if the user signed in with an existing + // accocunt, generating the token above, and before using it, he 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); + if (!possibleGuest || !possibleGuest.isGuest) redirect(callbackUrl); // NOTE: this ignores folders - const guestProcesses = (await getProcesses()).filter( - (process) => process.environmentId === guestId, - ); + const guestProcesses = await getProcesses(guestId); + + // If the guest has no processes -> nothing to do if (guestProcesses.length === 0) redirect(callbackUrl); return ( @@ -47,7 +69,10 @@ export default async function TransferProcessesPage({ 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 index 01b12f88d..555ef0a50 100644 --- a/src/management-system-v2/app/transfer-processes/server-actions.ts +++ b/src/management-system-v2/app/transfer-processes/server-actions.ts @@ -1,55 +1,54 @@ 'use server'; -import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; -import { getProcesses } from '@/lib/data/DTOs'; +import { getCurrentUser } from '@/components/auth'; import { Folder } from '@/lib/data/folder-schema'; import { + getProcesses, getFolders, getRootFolder, moveFolder, updateFolderMetaData, -} from '@/lib/data/legacy/folders'; -import { deleteEnvironment } from '@/lib/data/legacy/iam/environments'; + updateProcess, +} from '@/lib/data/DTOs'; import { deleteUser, getUserById } from '@/lib/data/legacy/iam/users'; import { Process } from '@/lib/data/process-schema'; +import { getGuestReference } from '@/lib/reference-guest-user-token'; 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 = '/') { +export async function transferProcesses(referenceToken: string, callbackUrl: string = '/') { const { session } = await getCurrentUser(); if (!session) return userError("You're not signed in", UserErrorType.PermissionError); if (session.user.isGuest) return userError("You can't be a guest to transfer processes", UserErrorType.PermissionError); + const reference = getGuestReference(referenceToken); + if ('error' in reference) return userError(reference.error); + const guestId = reference.guestId; + if (guestId === session.user.id) redirect(callbackUrl); const possibleGuest = await getUserById(guestId); - if ( - !possibleGuest || - !possibleGuest.isGuest || - possibleGuest?.signedInWithUserId !== session.user.id - ) + if (!possibleGuest || !possibleGuest.isGuest) 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 userRootFolderId = (await getRootFolder(session.user.id)).id; + const guestRootFolderId = (await getRootFolder(guestId)).id; - const { ability } = await getCurrentEnvironment(possibleGuest.id); - const guestProcesses = await getProcesses(guestId, ability); + // no ability check necessary, owners of personal spaces can do anything + const guestProcesses = await getProcesses(guestId); for (const process of guestProcesses) { const processUpdate: Partial = { environmentId: session.user.id, - owner: session.user.id, + creatorId: session.user.id, }; if (process.folderId === guestRootFolderId) processUpdate.folderId = userRootFolderId; - updateProcess(process.id, processUpdate); + await updateProcess(process.id, processUpdate); } - const guestFolders = getFolders(guestId); + const guestFolders = await getFolders(guestId); for (const folder of guestFolders) { if (folder.id === guestRootFolderId) continue; @@ -66,20 +65,20 @@ export async function transferProcesses(guestId: string, callbackUrl: string = ' redirect(callbackUrl); } -export async function discardProcesses(guestId: string, redirectUrl: string = '/') { +export async function discardProcesses(referenceToken: string, redirectUrl: string = '/') { const { session } = await getCurrentUser(); if (!session) return userError("You're not signed in", UserErrorType.PermissionError); if (session.user.isGuest) return userError("You can't be a guest to transfer processes", UserErrorType.PermissionError); + const reference = getGuestReference(referenceToken); + if ('error' in reference) return userError(reference.error); + const guestId = reference.guestId; + if (guestId === session.user.id) redirect(redirectUrl); const possibleGuest = await getUserById(guestId); - if ( - !possibleGuest || - !possibleGuest.isGuest || - possibleGuest?.signedInWithUserId !== session.user.id - ) + if (!possibleGuest || !possibleGuest.isGuest) return userError('Invalid guest id', UserErrorType.PermissionError); deleteUser(guestId); 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 index 14943af29..5d2105617 100644 --- 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 @@ -1,41 +1,56 @@ 'use client'; import { Space, Button } from 'antd'; -import { useTransition } from 'react'; +import { ReactNode, useTransition } from 'react'; import { transferProcesses as serverTransferProcesses, discardProcesses as serverDiscardProcesses, } from './server-actions'; -export default function TransferProcessesConfirmationButtons({ - guestId, +export function DiscardButton({ + referenceToken, callbackUrl, + children = 'No', }: { - guestId: string; + referenceToken: string; callbackUrl?: string; + children?: ReactNode; }) { - const [transferring, startTransfer] = useTransition(); - function transferProcesses() { - startTransfer(async () => { - await serverTransferProcesses(guestId, callbackUrl); - }); - } - const [discardingProcesses, startDiscardingProcesses] = useTransition(); function discardProcesses() { startDiscardingProcesses(async () => { - await serverDiscardProcesses(guestId, callbackUrl); + await serverDiscardProcesses(referenceToken, callbackUrl); }); } return ( - ); } + +export function TransferButton({ + referenceToken, + callbackUrl, + children = 'Yes', +}: { + referenceToken: string; + callbackUrl?: string; + children?: ReactNode; +}) { + const [transferring, startTransfer] = useTransition(); + function transferProcesses() { + startTransfer(async () => { + await serverTransferProcesses(referenceToken, callbackUrl); + }); + } + + return ( + + ); +} diff --git a/src/management-system-v2/lib/env-vars.ts b/src/management-system-v2/lib/env-vars.ts index 8face3c3b..5a9fdaedc 100644 --- a/src/management-system-v2/lib/env-vars.ts +++ b/src/management-system-v2/lib/env-vars.ts @@ -52,12 +52,15 @@ const environmentVariables = { TWITTER_CLIENT_SECRET: z.string(), SHARING_ENCRYPTION_SECRET: z.string(), + + GUEST_REFERENCE_SECRET: z.string(), }, development: { SHARING_ENCRYPTION_SECRET: z.string().default('T8VB/r1dw0kJAXjanUvGXpDb+VRr4dV5y59BT9TBqiQ='), INVITATION_ENCRYPTION_SECRET: z .string() .default('T8VB/r1dw0kJAXjanUvGXpDb+VRr4dV5y59BT9TBqiQ='), + GUEST_REFERENCE_SECRET: z.string().default('T8VB/r1dw0kJAXjanUvGXpDb+VRr4dV5y59BT9TBqiQ='), }, test: {}, } satisfies EnvironmentVariables; diff --git a/src/management-system-v2/lib/reference-guest-user-token.ts b/src/management-system-v2/lib/reference-guest-user-token.ts new file mode 100644 index 000000000..19d6e45b1 --- /dev/null +++ b/src/management-system-v2/lib/reference-guest-user-token.ts @@ -0,0 +1,32 @@ +/** + * When a user authenticates himself, and it is detected, that he was signed in as a guest + * a link with this token is set as the redirect url, after he signs he will be asked + * if he wants to transfer the guest processes. + * The token is necessary, because otherwise you could write any guest user id + * this is a small attack surface, but it is better to be safe. + * * */ +import { z } from 'zod'; +import { env } from './env-vars'; +import jwt from 'jsonwebtoken'; + +const referenceSchema = z.object({ guestId: z.string() }); +type Reference = z.infer; + +export function generateGuestReferenceToken(invitation: Reference, expiration: Date) { + // in seconds + const expiresIn = (expiration.getTime() - Date.now()) / 1000; + + return jwt.sign(invitation, env.GUEST_REFERENCE_SECRET, { expiresIn }); +} + +export function getGuestReference(token: string) { + try { + const payload = jwt.verify(token, env.GUEST_REFERENCE_SECRET); + return referenceSchema.parse(payload); + } catch (error) { + if (error instanceof jwt.JsonWebTokenError && error.name === 'TokenExpiredError') { + return { error: 'TokenExpiredError' as const }; + } + return { error: 'error' as const }; + } +} From 0c177f20a250ebd82546998ab3a8fe0c19d6d39d Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 27 Nov 2024 16:42:20 +0100 Subject: [PATCH 42/48] ms2: type fix --- src/management-system-v2/lib/data/users.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/data/users.tsx b/src/management-system-v2/lib/data/users.tsx index d7ab7e826..1406d278f 100644 --- a/src/management-system-v2/lib/data/users.tsx +++ b/src/management-system-v2/lib/data/users.tsx @@ -53,7 +53,12 @@ export async function deleteUser() {
    {conflictingOrgsNames.map((name, idx) => (
  • - {name}: manage roles here + {name}:{' '} + + manage roles here +
  • ))}
From 21611935a7f17b76db3558afb73e7bde4111bf87 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 27 Nov 2024 16:42:35 +0100 Subject: [PATCH 43/48] fix(ms2/user): type fix + only allow users to update their data if they aren't guests --- src/management-system-v2/lib/data/users.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/data/users.tsx b/src/management-system-v2/lib/data/users.tsx index 1406d278f..5a054a3d9 100644 --- a/src/management-system-v2/lib/data/users.tsx +++ b/src/management-system-v2/lib/data/users.tsx @@ -78,10 +78,15 @@ export async function updateUser(newUserDataInput: AuthenticatedUserData) { try { const { userId } = await getCurrentUser(); + const user = await getUserById(userId); + + if (user?.isGuest) { + return userError('Guest users cannot be updated'); + } const newUserData = AuthenticatedUserDataSchema.parse(newUserDataInput); - _updateUser(userId, { ...newUserData, guest: false }); + _updateUser(userId, { ...newUserData }); } catch (_) { return userError('Error updating user'); } From 407c601defefb6cf65992ac6e9b865a8a8c2b7d6 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 9 Dec 2024 10:47:50 +0100 Subject: [PATCH 44/48] typo --- src/management-system-v2/app/transfer-processes/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx index c610b49c9..e3641d4f2 100644 --- a/src/management-system-v2/app/transfer-processes/page.tsx +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -31,7 +31,7 @@ export default async function TransferProcessesPage({ Continue without guest processes{' '} From 8f65c0bca35f757743475837fdd49d7df0db6aa8 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 9 Dec 2024 10:48:08 +0100 Subject: [PATCH 45/48] fix: added key --- src/management-system-v2/app/transfer-processes/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx index e3641d4f2..835026d8c 100644 --- a/src/management-system-v2/app/transfer-processes/page.tsx +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -33,7 +33,7 @@ export default async function TransferProcessesPage({ title={message} subTitle="If you want to transfer the processes from your guest account, you need to sign in with your email from your guest account again." extra={[ - + Continue without guest processes{' '} , ]} From 0d36d9f160abc67958380788c5ffbd452384e0ae Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 9 Dec 2024 10:51:25 +0100 Subject: [PATCH 46/48] fix: typo in filename --- src/management-system-v2/app/transfer-processes/page.tsx | 2 +- ...-buttons.tsx => transfer-processes-confirmation-buttons.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/management-system-v2/app/transfer-processes/{transfer-processes-confitmation-buttons.tsx => transfer-processes-confirmation-buttons.tsx} (100%) diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx index 835026d8c..6b4462dc6 100644 --- a/src/management-system-v2/app/transfer-processes/page.tsx +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -3,7 +3,7 @@ import Content from '@/components/content'; import { getProcesses, getUserById } from '@/lib/data/DTOs'; import { Card, Result, Space } from 'antd'; import { redirect } from 'next/navigation'; -import { DiscardButton, TransferButton } from './transfer-processes-confitmation-buttons'; +import { DiscardButton, TransferButton } from './transfer-processes-confirmation-buttons'; import { getGuestReference } from '@/lib/reference-guest-user-token'; export default async function TransferProcessesPage({ 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-confirmation-buttons.tsx similarity index 100% rename from src/management-system-v2/app/transfer-processes/transfer-processes-confitmation-buttons.tsx rename to src/management-system-v2/app/transfer-processes/transfer-processes-confirmation-buttons.tsx From 2c8d66e59f93be77155957dfc5564b506da2ac87 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 9 Dec 2024 11:06:19 +0100 Subject: [PATCH 47/48] fix(ms2/process-transfer-page): combined buttons + changed invalid token message --- .../app/transfer-processes/page.tsx | 18 +++------ ...ransfer-processes-confirmation-buttons.tsx | 37 +++++++------------ 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx index 6b4462dc6..073b072be 100644 --- a/src/management-system-v2/app/transfer-processes/page.tsx +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -1,9 +1,9 @@ import { getCurrentUser } from '@/components/auth'; import Content from '@/components/content'; import { getProcesses, getUserById } from '@/lib/data/DTOs'; -import { Card, Result, Space } from 'antd'; +import { Card, Result } from 'antd'; import { redirect } from 'next/navigation'; -import { DiscardButton, TransferButton } from './transfer-processes-confirmation-buttons'; +import ProcessTransferButtons from './transfer-processes-confirmation-buttons'; import { getGuestReference } from '@/lib/reference-guest-user-token'; export default async function TransferProcessesPage({ @@ -23,8 +23,8 @@ export default async function TransferProcessesPage({ const token = decodeURIComponent(searchParams.referenceToken || ''); const referenceToken = getGuestReference(token); if ('error' in referenceToken) { - let message = 'Invalid token'; - if (referenceToken.error === 'TokenExpiredError') message = 'Expired token'; + let message = 'Invalid link'; + if (referenceToken.error === 'TokenExpiredError') message = 'Link expired'; return ( @@ -32,11 +32,6 @@ export default async function TransferProcessesPage({ status="error" title={message} subTitle="If you want to transfer the processes from your guest account, you need to sign in with your email from your guest account again." - extra={[ - - Continue without guest processes{' '} - , - ]} /> ); @@ -69,10 +64,7 @@ export default async function TransferProcessesPage({ 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/transfer-processes-confirmation-buttons.tsx b/src/management-system-v2/app/transfer-processes/transfer-processes-confirmation-buttons.tsx index 5d2105617..174c57a54 100644 --- a/src/management-system-v2/app/transfer-processes/transfer-processes-confirmation-buttons.tsx +++ b/src/management-system-v2/app/transfer-processes/transfer-processes-confirmation-buttons.tsx @@ -7,10 +7,9 @@ import { discardProcesses as serverDiscardProcesses, } from './server-actions'; -export function DiscardButton({ +export default function ProcessTransferButtons({ referenceToken, callbackUrl, - children = 'No', }: { referenceToken: string; callbackUrl?: string; @@ -23,24 +22,6 @@ export function DiscardButton({ }); } - return ( - - - - ); -} - -export function TransferButton({ - referenceToken, - callbackUrl, - children = 'Yes', -}: { - referenceToken: string; - callbackUrl?: string; - children?: ReactNode; -}) { const [transferring, startTransfer] = useTransition(); function transferProcesses() { startTransfer(async () => { @@ -49,8 +30,18 @@ export function TransferButton({ } return ( - + + + + ); } From 86e4c58715200a233a26aa56226e794724083108 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 9 Dec 2024 16:46:33 +0100 Subject: [PATCH 48/48] fix: import from DTOs --- .../app/transfer-processes/server-actions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/app/transfer-processes/server-actions.ts b/src/management-system-v2/app/transfer-processes/server-actions.ts index 555ef0a50..50519b208 100644 --- a/src/management-system-v2/app/transfer-processes/server-actions.ts +++ b/src/management-system-v2/app/transfer-processes/server-actions.ts @@ -9,8 +9,9 @@ import { moveFolder, updateFolderMetaData, updateProcess, + getUserById, + deleteUser, } from '@/lib/data/DTOs'; -import { deleteUser, getUserById } from '@/lib/data/legacy/iam/users'; import { Process } from '@/lib/data/process-schema'; import { getGuestReference } from '@/lib/reference-guest-user-token'; import { UserErrorType, userError } from '@/lib/user-error';