diff --git a/packages/backend/src/controllers/api/v1/users/accept-invitation.js b/packages/backend/src/controllers/api/v1/users/accept-invitation.js
index 9ff08b38..8c6763ac 100644
--- a/packages/backend/src/controllers/api/v1/users/accept-invitation.js
+++ b/packages/backend/src/controllers/api/v1/users/accept-invitation.js
@@ -12,9 +12,7 @@ export default async (request, response) => {
.throwIfNotFound();
if (!user.isInvitationTokenValid()) {
- throw new Error(
- 'Invitation link is not valid or expired. You can use reset password to get a new link.'
- );
+ return response.status(422).end();
}
await user.acceptInvitation(password);
diff --git a/packages/backend/src/graphql/mutations/create-user.ee.js b/packages/backend/src/graphql/mutations/create-user.ee.js
index 3649b041..13fd2f17 100644
--- a/packages/backend/src/graphql/mutations/create-user.ee.js
+++ b/packages/backend/src/graphql/mutations/create-user.ee.js
@@ -23,7 +23,7 @@ const createUser = async (_parent, params, context) => {
const userPayload = {
fullName,
email,
- status: 'pending',
+ status: 'invited',
};
try {
diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js
index 21816a39..54e79228 100644
--- a/packages/backend/src/models/user.js
+++ b/packages/backend/src/models/user.js
@@ -35,7 +35,7 @@ class User extends Base {
password: { type: 'string' },
status: {
type: 'string',
- enum: ['active', 'pending'],
+ enum: ['active', 'invited'],
default: 'active',
},
resetPasswordToken: { type: ['string', 'null'] },
@@ -234,11 +234,12 @@ class User extends Base {
return await this.$query().patch({
invitationToken: null,
invitationTokenSentAt: null,
+ status: 'active',
password,
});
}
- async isResetPasswordTokenValid() {
+ isResetPasswordTokenValid() {
if (!this.resetPasswordTokenSentAt) {
return false;
}
@@ -250,7 +251,7 @@ class User extends Base {
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
}
- async isInvitationTokenValid() {
+ isInvitationTokenValid() {
if (!this.invitationTokenSentAt) {
return false;
}
diff --git a/packages/backend/src/serializers/user.js b/packages/backend/src/serializers/user.js
index 2cdec6d5..d6a147d8 100644
--- a/packages/backend/src/serializers/user.js
+++ b/packages/backend/src/serializers/user.js
@@ -8,6 +8,7 @@ const userSerializer = (user) => {
email: user.email,
createdAt: user.createdAt.getTime(),
updatedAt: user.updatedAt.getTime(),
+ status: user.status,
fullName: user.fullName,
};
diff --git a/packages/web/src/components/AcceptInvitationForm/index.jsx b/packages/web/src/components/AcceptInvitationForm/index.jsx
new file mode 100644
index 00000000..bd98a918
--- /dev/null
+++ b/packages/web/src/components/AcceptInvitationForm/index.jsx
@@ -0,0 +1,134 @@
+import { yupResolver } from '@hookform/resolvers/yup';
+import LoadingButton from '@mui/lab/LoadingButton';
+import Paper from '@mui/material/Paper';
+import Alert from '@mui/material/Alert';
+import Typography from '@mui/material/Typography';
+import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
+import * as React from 'react';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import * as yup from 'yup';
+import Form from 'components/Form';
+import TextField from 'components/TextField';
+import * as URLS from 'config/urls';
+import useAcceptInvitation from 'hooks/useAcceptInvitation';
+import useFormatMessage from 'hooks/useFormatMessage';
+
+const validationSchema = yup.object().shape({
+ password: yup.string().required('acceptInvitationForm.mandatoryInput'),
+ confirmPassword: yup
+ .string()
+ .required('acceptInvitationForm.mandatoryInput')
+ .oneOf([yup.ref('password')], 'acceptInvitationForm.passwordsMustMatch'),
+});
+
+export default function ResetPasswordForm() {
+ const enqueueSnackbar = useEnqueueSnackbar();
+ const formatMessage = useFormatMessage();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const acceptInvitation = useAcceptInvitation();
+ const token = searchParams.get('token');
+
+ const handleSubmit = async (values) => {
+ await acceptInvitation.mutateAsync({
+ password: values.password,
+ token,
+ });
+
+ enqueueSnackbar(formatMessage('acceptInvitationForm.invitationAccepted'), {
+ variant: 'success',
+ SnackbarProps: {
+ 'data-test': 'snackbar-accept-invitation-success',
+ },
+ });
+
+ navigate(URLS.LOGIN);
+ };
+
+ return (
+
+ theme.palette.text.disabled,
+ pb: 2,
+ mb: 2,
+ }}
+ gutterBottom
+ >
+ {formatMessage('acceptInvitationForm.title')}
+
+
+
+ );
+}
diff --git a/packages/web/src/components/UserList/index.jsx b/packages/web/src/components/UserList/index.jsx
index 7c383fae..0427a898 100644
--- a/packages/web/src/components/UserList/index.jsx
+++ b/packages/web/src/components/UserList/index.jsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import Stack from '@mui/material/Stack';
+import Chip from '@mui/material/Chip';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -64,6 +65,15 @@ export default function UserList() {
+
+
+ {formatMessage('userList.status')}
+
+
+
@@ -100,6 +110,12 @@ export default function UserList() {
+
+
+
+
+
+
`/executions/${executionId}`;
export const LOGIN = '/login';
export const LOGIN_CALLBACK = `${LOGIN}/callback`;
export const SIGNUP = '/sign-up';
+export const ACCEPT_INVITATON = '/accept-invitation';
export const FORGOT_PASSWORD = '/forgot-password';
export const RESET_PASSWORD = '/reset-password';
export const APPS = '/apps';
diff --git a/packages/web/src/hooks/useAcceptInvitation.js b/packages/web/src/hooks/useAcceptInvitation.js
new file mode 100644
index 00000000..5628f559
--- /dev/null
+++ b/packages/web/src/hooks/useAcceptInvitation.js
@@ -0,0 +1,15 @@
+import { useMutation } from '@tanstack/react-query';
+
+import api from 'helpers/api';
+
+export default function useAcceptInvitation() {
+ const mutation = useMutation({
+ mutationFn: async (payload) => {
+ const { data } = await api.post('/v1/users/invitation', payload);
+
+ return data;
+ },
+ });
+
+ return mutation;
+}
diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json
index 82b4d9c3..26d0c233 100644
--- a/packages/web/src/locales/en.json
+++ b/packages/web/src/locales/en.json
@@ -153,6 +153,14 @@
"resetPasswordForm.passwordFieldLabel": "Password",
"resetPasswordForm.confirmPasswordFieldLabel": "Confirm password",
"resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.",
+ "acceptInvitationForm.passwordsMustMatch": "Passwords must match.",
+ "acceptInvitationForm.mandatoryInput": "{inputName} is required.",
+ "acceptInvitationForm.title": "Accept invitation",
+ "acceptInvitationForm.submit": "Set your password",
+ "acceptInvitationForm.passwordFieldLabel": "Password",
+ "acceptInvitationForm.confirmPasswordFieldLabel": "Confirm password",
+ "acceptInvitationForm.invitationAccepted": "The password has been set. Now, you can login.",
+ "acceptInvitationForm.invalidToken": "Invitation link is not valid or expired. You can use reset password to get a new link.",
"usageAlert.informationText": "Tasks: {consumedTaskCount}/{allowedTaskCount} (Resets {relativeResetDate})",
"usageAlert.viewPlans": "View plans",
"jsonViewer.noDataFound": "We couldn't find anything matching your search",
@@ -190,7 +198,6 @@
"deleteUserButton.cancel": "Cancel",
"deleteUserButton.confirm": "Delete",
"deleteUserButton.successfullyDeleted": "The user has been deleted.",
- "editUserPage.title": "Edit user",
"createUserPage.title": "Create user",
"userForm.fullName": "Full name",
"userForm.email": "Email",
@@ -199,11 +206,14 @@
"createUser.submit": "Create",
"createUser.successfullyCreated": "The user has been created.",
"createUser.invitationEmailInfo": "Invitation email will be sent if SMTP credentials are valid. Otherwise, you can share the invitation link manually: ",
+ "editUserPage.title": "Edit user",
+ "editUser.status": "Status",
"editUser.submit": "Update",
"editUser.successfullyUpdated": "The user has been updated.",
"userList.fullName": "Full name",
"userList.email": "Email",
"userList.role": "Role",
+ "userList.status": "Status",
"rolesPage.title": "Role management",
"rolesPage.createRole": "Create role",
"deleteRoleButton.title": "Delete role",
diff --git a/packages/web/src/pages/AcceptInvitation/index.jsx b/packages/web/src/pages/AcceptInvitation/index.jsx
new file mode 100644
index 00000000..42b1d72e
--- /dev/null
+++ b/packages/web/src/pages/AcceptInvitation/index.jsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Container from 'components/Container';
+import AcceptInvitationForm from 'components/AcceptInvitationForm';
+
+export default function AcceptInvitation() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/packages/web/src/pages/EditUser/index.jsx b/packages/web/src/pages/EditUser/index.jsx
index 72094845..779badb2 100644
--- a/packages/web/src/pages/EditUser/index.jsx
+++ b/packages/web/src/pages/EditUser/index.jsx
@@ -3,6 +3,8 @@ import LoadingButton from '@mui/lab/LoadingButton';
import Grid from '@mui/material/Grid';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
+import Chip from '@mui/material/Chip';
+import Typography from '@mui/material/Typography';
import MuiTextField from '@mui/material/TextField';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react';
@@ -82,6 +84,7 @@ export default function EditUser() {
+
)}
@@ -89,6 +92,18 @@ export default function EditUser() {
{!isUserLoading && (