Skip to content

Commit

Permalink
Ms2/signin merge users (#360)
Browse files Browse the repository at this point in the history
* style(ms2/signin): shift signin modal up

* style(ms2/signin): sign in as guest

* style(ms2/signin): changed sorting of providers

* style(ms2/signin): new layout

* style(ms2/signin): continue in as guest

* style(ms2/signin)

* style(ms2): changed info color to gray

* feat(ms2): show guests a warning

* typos

* feat(ms2): show new guests modal for creating process

* lint

* fix: missing dependency

* feat(ms2): update email if user is signed in and is verifying email

* feat(ms2/profile): modal to change email

* feat(ms2): verificationToken store

* fix(ms2/signin): remove dangerous sign in code

* feat(ms2): verificationToken server actions

* feat(ms2/profile): request email change

* feat(ms2/change-email): page for confirming email change

* refactor(ms2): moved signin-email template to lib/email

* refactor(ms2/signin-link-email): changed parameter format

* feat(ms2/profile): close modal after email change request

* feat(ms2/change-email): send change email link

* style(ms2/change-email): better feedback

* feat(ms2/change-email): cancel email change

* fix(ms2/e2e-tests): sign in as guest

* fix(ms2/signin): use callbackUrl if there is one

* fix(ms2/e2e-tests): use new sign in

* 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.

* fix(ms2/environments): remove folders when removing environment

* feat(ms2/users): update guest users

* feat(ms2/folders): get all folders

* feat(ms2/folder): move folders to other environments

* feat(ms2/processes): get processes without ability

* typo

* feat(ms2): transfer guest processes to account

* fix(ms2/signin): allow guests to sign in to dev users

* 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.

* fix: merge conflict

* feat(ms2/DTOs): updateProcess, moveFolder, updateFolderMetaData

* refactor(ms2/transfer-processes): now works with JWT for passing the guestId

* ms2: type fix

* fix(ms2/user): type fix + only allow users to update their data if they aren't guests

* typo

* fix: added key

* fix: typo in filename

* fix(ms2/process-transfer-page): combined buttons + changed invalid token message

* fix: import from DTOs

---------

Co-authored-by: Kai Rohwer <[email protected]>
  • Loading branch information
FelipeTrost and OhKai authored Dec 16, 2024
1 parent 0d81afe commit 93517e2
Show file tree
Hide file tree
Showing 20 changed files with 386 additions and 78 deletions.
12 changes: 11 additions & 1 deletion src/management-system-v2/app/(auth)/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }) => {
Expand All @@ -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);
Expand All @@ -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 <SignIn providers={providers} userType={userType} />;
return (
<SignIn providers={providers} userType={userType} guestReferenceToken={guestReferenceToken} />
);
};

export default SignInPage;
16 changes: 12 additions & 4 deletions src/management-system-v2/app/(auth)/signin/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -150,7 +154,9 @@ const SignIn: FC<{
if (provider.type === 'credentials') {
return (
<Form
onFinish={(values) => signIn(provider.id, { ...values, callbackUrl })}
onFinish={(values) =>
signIn(provider.id, { ...values, callbackUrl: callbackUrlWithGuestRef })
}
key={provider.id}
layout="vertical"
>
Expand All @@ -168,7 +174,9 @@ const SignIn: FC<{
return (
<>
<Form
onFinish={(values) => signIn(provider.id, { ...values, callbackUrl })}
onFinish={(values) =>
signIn(provider.id, { ...values, callbackUrl: callbackUrlWithGuestRef })
}
key={provider.id}
layout="vertical"
>
Expand Down Expand Up @@ -211,7 +219,7 @@ const SignIn: FC<{
style={{ width: '1.5rem', height: 'auto' }}
/>
}
onClick={() => signIn(provider.id, { callbackUrl })}
onClick={() => signIn(provider.id, { callbackUrl: callbackUrlWithGuestRef })}
/>
</Tooltip>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.');
Expand All @@ -35,7 +34,7 @@ const Process = async ({ params: { processId, environmentId }, searchParams }: P
? process.versions.find((version) => version.id === selectedVersionId)
: undefined;

// Since the user is able to minimize and close the page, everyting is in a
// Since the user is able to minimize and close the page, everything is in a
// client component from here.
return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Adapter = {
return getUserById(id);
},
updateUser: async (user: AuthenticatedUser) => {
return updateUser(user.id, user);
return updateUser(user.id, { ...user, isGuest: false });
},
getUserByEmail: async (email: string) => {
return getUserByEmail(email) ?? null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,25 @@ 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?.isGuest && account?.provider !== 'guest-loguin') {
// Guest account signs in with proper auth
if (
sessionUser?.isGuest &&
account?.provider !== 'guest-signin' &&
!email?.verificationRequest
) {
// Check if the user's cookie is correct
const sessionUserInDb = await getUserById(sessionUser.id);
if (!sessionUserInDb || !sessionUserInDb.isGuest) throw new Error('Something went wrong');

const user = _user as Partial<AuthenticatedUser>;
const guestUser = await getUserById(sessionUser.id);
const userSigningIn = await getUserById(_user.id);

if (guestUser?.isGuest) {
updateUser(guestUser.id, {
if (!userSigningIn) {
await updateUser(sessionUser.id, {
firstName: user.firstName ?? undefined,
lastName: user.lastName ?? undefined,
username: user.username ?? undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/management-system-v2/app/shared-viewer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,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);
}
Expand Down
71 changes: 71 additions & 0 deletions src/management-system-v2/app/transfer-processes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { getCurrentUser } from '@/components/auth';
import Content from '@/components/content';
import { getProcesses, getUserById } from '@/lib/data/DTOs';
import { Card, Result } from 'antd';
import { redirect } from 'next/navigation';
import ProcessTransferButtons from './transfer-processes-confirmation-buttons';
import { getGuestReference } from '@/lib/reference-guest-user-token';

export default async function TransferProcessesPage({
searchParams,
}: {
searchParams: {
callbackUrl?: string;
referenceToken?: string;
};
}) {
const { userId, session } = await getCurrentUser();
if (!session) redirect('api/auth/signin');
if (session.user.isGuest) redirect('/');

const callbackUrl = decodeURIComponent(searchParams.callbackUrl || '/');

const token = decodeURIComponent(searchParams.referenceToken || '');
const referenceToken = getGuestReference(token);
if ('error' in referenceToken) {
let message = 'Invalid link';
if (referenceToken.error === 'TokenExpiredError') message = 'Link expired';

return (
<Content title="Transfer Processes">
<Result
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."
/>
</Content>
);
}
const guestId = referenceToken.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 = 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.isGuest) redirect(callbackUrl);

// NOTE: this ignores folders
const guestProcesses = await getProcesses(guestId);

// If the guest has no processes -> nothing to do
if (guestProcesses.length === 0) redirect(callbackUrl);

return (
<Content title="Transfer Processes">
<Card
title="Would you like to transfer your processes?"
style={{ maxWidth: '70ch', margin: 'auto' }}
>
Your guest account had {guestProcesses.length} process{guestProcesses.length !== 1 && 'es'}.
<br />
Would you like to transfer them to your account?
<ProcessTransferButtons referenceToken={token} callbackUrl={callbackUrl} />
</Card>
</Content>
);
}
88 changes: 88 additions & 0 deletions src/management-system-v2/app/transfer-processes/server-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use server';

import { getCurrentUser } from '@/components/auth';
import { Folder } from '@/lib/data/folder-schema';
import {
getProcesses,
getFolders,
getRootFolder,
moveFolder,
updateFolderMetaData,
updateProcess,
getUserById,
deleteUser,
} from '@/lib/data/DTOs';
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';

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)
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 = (await getRootFolder(session.user.id)).id;
const guestRootFolderId = (await getRootFolder(guestId)).id;

// no ability check necessary, owners of personal spaces can do anything
const guestProcesses = await getProcesses(guestId);
for (const process of guestProcesses) {
const processUpdate: Partial<Process> = {
environmentId: session.user.id,
creatorId: session.user.id,
};
if (process.folderId === guestRootFolderId) processUpdate.folderId = userRootFolderId;
await updateProcess(process.id, processUpdate);
}

const guestFolders = await getFolders(guestId);
for (const folder of guestFolders) {
if (folder.id === guestRootFolderId) continue;

const folderData: Partial<Folder> = { 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(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)
return userError('Invalid guest id', UserErrorType.PermissionError);

deleteUser(guestId);

redirect(redirectUrl);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import { Space, Button } from 'antd';
import { ReactNode, useTransition } from 'react';
import {
transferProcesses as serverTransferProcesses,
discardProcesses as serverDiscardProcesses,
} from './server-actions';

export default function ProcessTransferButtons({
referenceToken,
callbackUrl,
}: {
referenceToken: string;
callbackUrl?: string;
children?: ReactNode;
}) {
const [discardingProcesses, startDiscardingProcesses] = useTransition();
function discardProcesses() {
startDiscardingProcesses(async () => {
await serverDiscardProcesses(referenceToken, callbackUrl);
});
}

const [transferring, startTransfer] = useTransition();
function transferProcesses() {
startTransfer(async () => {
await serverTransferProcesses(referenceToken, callbackUrl);
});
}

return (
<Space style={{ width: '100%', justifyContent: 'right' }}>
<Button onClick={discardProcesses} loading={discardingProcesses} disabled={transferring}>
No
</Button>
<Button
type="primary"
onClick={transferProcesses}
loading={transferring}
disabled={discardingProcesses}
>
Yes
</Button>
</Space>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, isGuest: false });

deleteVerificationToken(tokenParams);
}
Loading

0 comments on commit 93517e2

Please sign in to comment.