Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5ed014b
style(ms2/signin): shift signin modal up
FelipeTrost Jun 21, 2024
2ef53a9
style(ms2/signin): sign in as guest
FelipeTrost Jun 21, 2024
5b479b1
style(ms2/signin): changed sorting of providers
FelipeTrost Jun 24, 2024
8e3fb01
style(ms2/signin): new layout
FelipeTrost Jun 24, 2024
9b9ed49
style(ms2/signin): continue in as guest
FelipeTrost Jun 24, 2024
ce3da11
style(ms2/signin)
FelipeTrost Jun 29, 2024
7f845f0
style(ms2): changed info color to gray
FelipeTrost Jun 30, 2024
3aedc62
feat(ms2): show guests a warning
FelipeTrost Jun 30, 2024
4f748a0
typos
FelipeTrost Jun 30, 2024
6fc71a5
feat(ms2): show new guests modal for creating process
FelipeTrost Jun 30, 2024
dbedff1
merge main
FelipeTrost Jun 30, 2024
3386e0f
lint
FelipeTrost Jul 2, 2024
0ed9a20
fix: missing dependency
FelipeTrost Jul 2, 2024
0d0d17f
Merge remote-tracking branch 'origin/main' into ms2/signin-style
FelipeTrost Jul 2, 2024
4e3eeb8
Merge branch 'main' into ms2/signin-style
OhKai Jul 7, 2024
8bf39bc
feat(ms2): update email if user is signed in and is verifying email
FelipeTrost Jul 8, 2024
057907e
feat(ms2/profile): modal to change email
FelipeTrost Jul 8, 2024
6e358e9
Merge remote-tracking branch 'origin/main' into ms2/change-email
FelipeTrost Jul 8, 2024
506d584
feat(ms2): verificationToken store
FelipeTrost Jul 10, 2024
c4fb15c
fix(ms2/signin): remove dangerous sign in code
FelipeTrost Jul 10, 2024
edc70cb
feat(ms2): verificationToken server actions
FelipeTrost Jul 10, 2024
cb923c7
feat(ms2/profile): request email change
FelipeTrost Jul 10, 2024
7c567ee
feat(ms2/change-email): page for confirming email change
FelipeTrost Jul 10, 2024
c0f19a1
refactor(ms2): moved signin-email template to lib/email
FelipeTrost Jul 11, 2024
17ba3a0
refactor(ms2/signin-link-email): changed parameter format
FelipeTrost Jul 11, 2024
a60fc8e
feat(ms2/profile): close modal after email change request
FelipeTrost Jul 11, 2024
64e61cf
feat(ms2/change-email): send change email link
FelipeTrost Jul 11, 2024
3d35269
style(ms2/change-email): better feedback
FelipeTrost Jul 11, 2024
d50a5eb
feat(ms2/change-email): cancel email change
FelipeTrost Jul 11, 2024
fde2b45
Merge branch 'main' into ms2/signin-style
OhKai Jul 11, 2024
bb57561
fix(ms2/e2e-tests): sign in as guest
FelipeTrost Jul 18, 2024
7bc7416
Merge branch 'ms2/signin-style' of github.com:PROCEED-Labs/proceed in…
FelipeTrost Jul 18, 2024
bea3c5f
Merge remote-tracking branch 'origin/main' into ms2/signin-style
FelipeTrost Jul 18, 2024
d78f13a
fix(ms2/signin): use callbackUrl if there is one
FelipeTrost Jul 19, 2024
713b8ac
fix(ms2/e2e-tests): use new sign in
FelipeTrost Jul 19, 2024
299724b
Merge branch 'ms2/change-email' into ms2/signin-merge-users
FelipeTrost Jul 19, 2024
61f3148
fix(ms2/signin): check if verification request
FelipeTrost Jul 19, 2024
4191315
Merge branch 'ms2/signin-style' into ms2/signin-merge-users
FelipeTrost Jul 19, 2024
2b75ada
fix(ms2/environments): remove folders when removing environment
FelipeTrost Jul 21, 2024
84620aa
feat(ms2/users): update guest users
FelipeTrost Jul 21, 2024
b25c072
feat(ms2/folders): get all folders
FelipeTrost Jul 21, 2024
6671e75
feat(ms2/folder): move folders to other environments
FelipeTrost Jul 21, 2024
111fb82
feat(ms2/processes): get processes without ability
FelipeTrost Jul 22, 2024
cc4ae73
typo
FelipeTrost Jul 22, 2024
cefc7ec
feat(ms2): transfer guest processes to account
FelipeTrost Jul 22, 2024
65fcc61
fix(ms2/signin): allow guests to sign in to dev users
FelipeTrost Jul 22, 2024
a039486
refactor(ms2/e2e-openmodal): allow any return type
FelipeTrost Aug 2, 2024
3b0b46b
fix(ms2/e2e-waitForHydration): better selector
FelipeTrost Aug 2, 2024
0319472
feat(ms2/e2e-processListPage): export selectors
FelipeTrost Aug 2, 2024
bfc8fd7
feat(ms2/tests): test user for preview deployments
FelipeTrost Aug 7, 2024
b909aed
feat(ms2/tests): add test user in dev
FelipeTrost Aug 7, 2024
3ad484f
refactr(ms2/e2e): use processListPage.processLocatorByDefinitionId
FelipeTrost Aug 7, 2024
8652905
feat(ms2/e2e): transferring processes
FelipeTrost Aug 7, 2024
61da2e2
feat(ms2/e2e): email signin
FelipeTrost Aug 8, 2024
e14e0ee
Merge remote-tracking branch 'origin/main' into ms2/signine-e2e-tests
FelipeTrost Aug 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"uuid": "9.0.1",
"webpack": "^4.35.3",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.2.1"
"webpack-dev-server": "^3.2.1",
"imapflow": "^1.0.164"
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'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';
Expand All @@ -17,7 +17,11 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => {
const [changeNameModalOpen, setChangeNameModalOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState<ReactNode | undefined>(undefined);

const { message: messageApi } = App.useApp();
const [changeEmailModalOpen, setChangeEmailModalOpen] = useState(false);
const [errors, parseEmail] = useParseZodErrors(z.object({ email: z.string().email() }));
const [changeEmailForm] = Form.useForm();

const { message: messageApi, notification } = App.useApp();

async function deleteUser() {
try {
Expand All @@ -33,6 +37,26 @@ 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;

setChangeEmailModalOpen(false);
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 || '';

Expand Down Expand Up @@ -65,6 +89,31 @@ const UserProfile: FC<{ userData: User }> = ({ userData }) => {
}}
/>

<Modal
title="Change your email"
open={changeEmailModalOpen}
closeIcon={null}
onCancel={() => setChangeEmailModalOpen(false)}
onOk={changeEmailForm.submit}
destroyOnClose
>
<Alert
type="warning"
message="We'll send a sign in link to your new email, if you don't open it in this browser your email won't be changed"
style={{ marginBottom: '1rem' }}
/>
<Form
initialValues={userData}
form={changeEmailForm}
layout="vertical"
onFinish={requestEmailChange}
>
<Form.Item label="Email" name="email" required {...antDesignInputProps(errors, 'email')}>
<Input type="email" />
</Form.Item>
</Form>
</Modal>

<Space direction="vertical" className={styles.Container}>
<Card className={styles.Card} style={{ margin: 'auto' }}>
{errorMessage && (
Expand Down
20 changes: 11 additions & 9 deletions src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, VerificationToken>();

const Adapter = {
createUser: async (
user: Omit<AuthenticatedUser, 'id'> | { email: string; emailVerified: Date },
Expand All @@ -25,21 +28,20 @@ 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;
},
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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,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,
Expand Down Expand Up @@ -63,16 +66,26 @@ 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-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<AuthenticatedUser>;
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,
Expand Down Expand Up @@ -221,6 +234,32 @@ if (process.env.NODE_ENV === 'development') {
);
}

// add the test user in preview deployments and dev
// dev is for local testing
const url = process.env.NEXTAUTH_URL;
if (
(url && url.endsWith('app.run') && url.startsWith('https://pr-')) ||
process.env.NODE_ENV === 'development'
) {
nextAuthOptions.providers.push(
CredentialsProvider({
id: 'test-user',
name: 'Continue With Test User',
credentials: {},
async authorize() {
return addUser({
guest: false,
email: `test-user-${crypto.randomUUID()}@proceed-labs.org`,
firstName: 'Test',
lastName: 'Test',
username: 'test-user',
emailVerified: new Date(),
});
},
}),
);
}

export type ExtractedProvider =
| {
id: string;
Expand Down
81 changes: 81 additions & 0 deletions src/management-system-v2/app/change-email/change-email-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client';

import { Space, Button, App, Card, Typography } from 'antd';
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 ChangeEmailCard({
previousEmail,
newEmail,
}: {
previousEmail?: string;
newEmail: string;
}) {
const { message } = App.useApp();
const params = useSearchParams();
const router = useRouter();
const session = useSession();

const [loading, setLoading] = useState<'changing' | 'cancelling' | undefined>();
async function changeEmail(cancel: boolean = false) {
try {
setLoading(cancel ? 'cancelling' : 'changing');

const response = await serverChangeEmail(params.get('token')!, params.get('email')!, cancel);

if (response?.error) throw response.error.message;

if (cancel) {
message.open({ content: 'Email change cancelled', type: 'success' });
} else {
message.open({ content: 'Email changed', type: 'success' });
session.update();
}

router.push('/profile');
} catch (e) {
const content = typeof e === 'string' ? e : 'An error occurred';

message.open({ content, type: 'error' });
setLoading(undefined);
}
}

return (
<Content title="Change Email">
<Card
title="Are you sure you want to change your email?"
style={{ width: '90%', maxWidth: '80ch', margin: 'auto' }}
>
{previousEmail ? (
<>
<Typography.Text code>{previousEmail}</Typography.Text>
<ArrowRightOutlined style={{ margin: '0 1rem' }} />
<Typography.Text code>{newEmail}</Typography.Text>
</>
) : (
<>
Your email will now be <Typography.Text code>{newEmail}</Typography.Text>
</>
)}
<br />
<Space style={{ marginTop: '1rem' }}>
<Button
type="default"
onClick={() => changeEmail(true)}
loading={loading === 'cancelling'}
>
Cancel
</Button>
<Button type="primary" onClick={() => changeEmail()} loading={loading === 'changing'}>
Confirm
</Button>
</Space>
</Card>
</Content>
);
}
34 changes: 34 additions & 0 deletions src/management-system-v2/app/change-email/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getCurrentUser } from '@/components/auth';
import { getTokenHash, notExpired } from '@/lib/change-email/utils';
import { getVerificationToken } from '@/lib/data/legacy/verification-tokens';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import ChangeEmailCard from './change-email-card';

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 || session.user.guest) redirect('/');
const previousEmail = session.user.email;

const verificationToken = getVerificationToken({
identifier: email,
token: await getTokenHash(token),
});

if (
!verificationToken ||
!verificationToken.updateEmail ||
verificationToken.userId !== userId ||
!(await notExpired(verificationToken))
)
redirect('/');

return <ChangeEmailCard previousEmail={previousEmail} newEmail={email} />;
}
54 changes: 54 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,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 (
<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?
<TransferProcessesConfirmationButtons guestId={guestId} callbackUrl={callbackUrl} />
</Card>
</Content>
);
}
Loading