diff --git a/packages/backend/src/graphql/mutations/forgot-password.ee.ts b/packages/backend/src/graphql/mutations/forgot-password.ee.ts index 0d0ef97f..5e28a429 100644 --- a/packages/backend/src/graphql/mutations/forgot-password.ee.ts +++ b/packages/backend/src/graphql/mutations/forgot-password.ee.ts @@ -1,3 +1,4 @@ +import appConfig from '../../config/app'; import User from '../../models/user'; import emailQueue from '../../queues/email'; import { @@ -30,6 +31,8 @@ const forgotPassword = async (_parent: unknown, params: Params) => { template: 'reset-password-instructions', params: { token: user.resetPasswordToken, + webAppUrl: appConfig.webAppUrl, + fullName: user.fullName, }, }; @@ -40,7 +43,7 @@ const forgotPassword = async (_parent: unknown, params: Params) => { await emailQueue.add(jobName, jobPayload, jobOptions); - return; + return true; }; export default forgotPassword; diff --git a/packages/backend/src/graphql/mutations/reset-password.ee.ts b/packages/backend/src/graphql/mutations/reset-password.ee.ts index c8a350bc..d68fc55a 100644 --- a/packages/backend/src/graphql/mutations/reset-password.ee.ts +++ b/packages/backend/src/graphql/mutations/reset-password.ee.ts @@ -24,7 +24,7 @@ const resetPassword = async (_parent: unknown, params: Params) => { await user.resetPassword(password); - return; + return true; }; export default resetPassword; diff --git a/packages/backend/src/helpers/authentication.ts b/packages/backend/src/helpers/authentication.ts index c1368e0a..e746d452 100644 --- a/packages/backend/src/helpers/authentication.ts +++ b/packages/backend/src/helpers/authentication.ts @@ -31,6 +31,7 @@ const authentication = shield( login: allow, createUser: allow, forgotPassword: allow, + resetPassword: allow, }, }, { diff --git a/packages/backend/src/views/emails/reset-password-instructions.ee.hbs b/packages/backend/src/views/emails/reset-password-instructions.ee.hbs index 1fb678f4..8397c863 100644 --- a/packages/backend/src/views/emails/reset-password-instructions.ee.hbs +++ b/packages/backend/src/views/emails/reset-password-instructions.ee.hbs @@ -1,16 +1,23 @@ - Title + Reset password instructions - Hello {{ email }} +

+ Hello {{ fullName }}, +

- Someone has requested a link to change your password, and you can do this through the link below. +

+ Someone has requested a link to change your password, and you can do this through the link below. +

- Change my password +

+ Change my password +

- If you didn't request this, please ignore this email. - Your password won't change until you access the link above and create a new one. +

+ If you didn't request this, please ignore this email. Your password won't change until you access the link above and create a new one. +

diff --git a/packages/backend/src/workers/email.ts b/packages/backend/src/workers/email.ts index d8ff037c..953f9c41 100644 --- a/packages/backend/src/workers/email.ts +++ b/packages/backend/src/workers/email.ts @@ -8,13 +8,13 @@ import appConfig from '../config/app'; export const worker = new Worker( 'email', async (job) => { - const { email, subject, templateName, params } = job.data; + const { email, subject, template, params } = job.data; await mailer.sendMail({ to: email, from: appConfig.fromEmail, subject: subject, - html: compileEmail(templateName, params), + html: compileEmail(template, params), }); }, { connection: redisConfig } diff --git a/packages/web/src/components/DeleteAccountDialog/index.ee.tsx b/packages/web/src/components/DeleteAccountDialog/index.ee.tsx index 59b9f308..d7310de5 100644 --- a/packages/web/src/components/DeleteAccountDialog/index.ee.tsx +++ b/packages/web/src/components/DeleteAccountDialog/index.ee.tsx @@ -15,11 +15,11 @@ import useAuthentication from 'hooks/useAuthentication'; import useFormatMessage from 'hooks/useFormatMessage'; import useCurrentUser from 'hooks/useCurrentUser'; -type TDeleteAccountDialogProps = { +type DeleteAccountDialogProps = { onClose: () => void; } -export default function DeleteAccountDialog(props: TDeleteAccountDialogProps) { +export default function DeleteAccountDialog(props: DeleteAccountDialogProps) { const [deleteUser] = useMutation(DELETE_USER); const formatMessage = useFormatMessage(); const currentUser = useCurrentUser(); diff --git a/packages/web/src/components/ForgotPasswordForm/index.ee.tsx b/packages/web/src/components/ForgotPasswordForm/index.ee.tsx new file mode 100644 index 00000000..69b362d8 --- /dev/null +++ b/packages/web/src/components/ForgotPasswordForm/index.ee.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { useMutation } from '@apollo/client'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import LoadingButton from '@mui/lab/LoadingButton'; + +import { FORGOT_PASSWORD } from 'graphql/mutations/forgot-password.ee'; +import Form from 'components/Form'; +import TextField from 'components/TextField'; +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function ForgotPasswordForm() { + const formatMessage = useFormatMessage(); + const [forgotPassword, { data, loading }] = useMutation(FORGOT_PASSWORD); + + const handleSubmit = async (values: any) => { + await forgotPassword({ + variables: { + input: values, + }, + }); + }; + + return ( + + theme.palette.text.disabled, + pb: 2, + mb: 2, + }} + gutterBottom + > + {formatMessage('forgotPasswordForm.title')} + + +
+ + + + {formatMessage('forgotPasswordForm.submit')} + + + {data && theme.palette.success.main }}> + {formatMessage('forgotPasswordForm.instructionsSent')} + } + +
+ ); +} diff --git a/packages/web/src/components/LoginForm/index.tsx b/packages/web/src/components/LoginForm/index.tsx index fab8fae2..7070da10 100644 --- a/packages/web/src/components/LoginForm/index.tsx +++ b/packages/web/src/components/LoginForm/index.tsx @@ -11,59 +11,11 @@ import * as URLS from 'config/urls'; import { LOGIN } from 'graphql/mutations/login'; import Form from 'components/Form'; import TextField from 'components/TextField'; - -function renderFields(props: { loading: boolean }) { - const { loading = false } = props; - - return () => { - return ( - <> - - - - - - Login - - - - Don't have an Automatisch account yet?  - - Sign up - - - - ); - }; -} +import useFormatMessage from 'hooks/useFormatMessage'; function LoginForm() { const navigate = useNavigate(); + const formatMessage = useFormatMessage(); const authentication = useAuthentication(); const [login, { loading }] = useMutation(LOGIN); @@ -85,8 +37,6 @@ function LoginForm() { authentication.updateToken(token); }; - const render = React.useMemo(() => renderFields({ loading }), [loading]); - return ( - Login + {formatMessage('loginForm.title')} -
+ + + + + + + {formatMessage('loginForm.forgotPasswordText')} + + + + {formatMessage('loginForm.submit')} + + + + {formatMessage('loginForm.noAccount')} +   + + {formatMessage('loginForm.signUp')} + + +
); } diff --git a/packages/web/src/components/ResetPasswordForm/index.ee.tsx b/packages/web/src/components/ResetPasswordForm/index.ee.tsx new file mode 100644 index 00000000..3ec40073 --- /dev/null +++ b/packages/web/src/components/ResetPasswordForm/index.ee.tsx @@ -0,0 +1,122 @@ +import * as React from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { useMutation } from '@apollo/client'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import LoadingButton from '@mui/lab/LoadingButton'; +import { useSnackbar } from 'notistack'; +import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; + +import * as URLS from 'config/urls'; +import Form from 'components/Form'; +import TextField from 'components/TextField'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { RESET_PASSWORD } from 'graphql/mutations/reset-password.ee'; + +const validationSchema = yup.object().shape({ + password: yup.string().required('resetPasswordForm.mandatoryInput'), + confirmPassword: yup + .string() + .required('resetPasswordForm.mandatoryInput') + .oneOf([yup.ref('password')], 'resetPasswordForm.passwordsMustMatch'), +}); + +export default function ResetPasswordForm() { + const { enqueueSnackbar } = useSnackbar(); + const formatMessage = useFormatMessage(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const [resetPassword, { data, loading }] = useMutation(RESET_PASSWORD); + + const token = searchParams.get('token'); + + const handleSubmit = async (values: any) => { + await resetPassword({ + variables: { + input: { + password: values.password, + token, + }, + }, + }); + + enqueueSnackbar(formatMessage('resetPasswordForm.passwordUpdated'), { variant: 'success' }); + + navigate(URLS.LOGIN); + }; + + return ( + + theme.palette.text.disabled, + pb: 2, + mb: 2, + }} + gutterBottom + > + {formatMessage('resetPasswordForm.title')} + + +
( + <> + + + + + + {formatMessage('resetPasswordForm.submit')} + + + )} + /> + + ); +} diff --git a/packages/web/src/components/SignUpForm/index.ee.tsx b/packages/web/src/components/SignUpForm/index.ee.tsx index eb7a55f6..9c14c5e5 100644 --- a/packages/web/src/components/SignUpForm/index.ee.tsx +++ b/packages/web/src/components/SignUpForm/index.ee.tsx @@ -5,13 +5,13 @@ import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import LoadingButton from '@mui/lab/LoadingButton'; import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; import useAuthentication from 'hooks/useAuthentication'; import * as URLS from 'config/urls'; import { CREATE_USER } from 'graphql/mutations/create-user.ee'; import Form from 'components/Form'; import TextField from 'components/TextField'; -import { yupResolver } from '@hookform/resolvers/yup'; import { LOGIN } from 'graphql/mutations/login'; import useFormatMessage from 'hooks/useFormatMessage'; @@ -22,7 +22,7 @@ const validationSchema = yup.object().shape({ confirmPassword: yup .string() .required('signupForm.mandatoryInput') - .oneOf([yup.ref('password')], 'signupForm.passwordMustMatch'), + .oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'), }); const initialValues = { diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts index 062d88f4..5500a6ab 100644 --- a/packages/web/src/config/urls.ts +++ b/packages/web/src/config/urls.ts @@ -6,6 +6,8 @@ export const EXECUTION = (executionId: string): string => export const LOGIN = '/login'; export const SIGNUP = '/sign-up'; +export const FORGOT_PASSWORD = '/forgot-password'; +export const RESET_PASSWORD = '/reset-password'; export const APPS = '/apps'; export const NEW_APP_CONNECTION = '/apps/new'; diff --git a/packages/web/src/graphql/mutations/forgot-password.ee.ts b/packages/web/src/graphql/mutations/forgot-password.ee.ts new file mode 100644 index 00000000..9373875e --- /dev/null +++ b/packages/web/src/graphql/mutations/forgot-password.ee.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const FORGOT_PASSWORD = gql` + mutation ForgotPassword($input: ForgotPasswordInput) { + forgotPassword(input: $input) + } +`; diff --git a/packages/web/src/graphql/mutations/reset-password.ee.ts b/packages/web/src/graphql/mutations/reset-password.ee.ts new file mode 100644 index 00000000..aaf991f6 --- /dev/null +++ b/packages/web/src/graphql/mutations/reset-password.ee.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const RESET_PASSWORD = gql` + mutation ResetPassword($input: ResetPasswordInput) { + resetPassword(input: $input) + } +`; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 22afbb8e..87192f13 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -112,6 +112,24 @@ "signupForm.passwordFieldLabel": "Password", "signupForm.confirmPasswordFieldLabel": "Confirm password", "signupForm.submit": "Sign up", - "signupForm.passwordMustMatch": "Passwords must match.", - "signupForm.mandatoryInput": "{inputName} is required." -} + "signupForm.passwordsMustMatch": "Passwords must match.", + "signupForm.mandatoryInput": "{inputName} is required.", + "loginForm.title": "Login", + "loginForm.emailFieldLabel": "Email", + "loginForm.passwordFieldLabel": "Password", + "loginForm.forgotPasswordText": "Forgot password?", + "loginForm.submit": "Login", + "loginForm.noAccount": "Don't have an Automatisch account yet?", + "loginForm.signUp": "Sign up", + "forgotPasswordForm.title": "Forgot password", + "forgotPasswordForm.submit": "Send reset instructions", + "forgotPasswordForm.instructionsSent": "The instructions have been sent!", + "forgotPasswordForm.emailFieldLabel": "Email", + "resetPasswordForm.passwordsMustMatch": "Passwords must match.", + "resetPasswordForm.mandatoryInput": "{inputName} is required.", + "resetPasswordForm.title": "Reset password", + "resetPasswordForm.submit": "Reset password", + "resetPasswordForm.passwordFieldLabel": "Password", + "resetPasswordForm.confirmPasswordFieldLabel": "Confirm password", + "resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login." +} \ No newline at end of file diff --git a/packages/web/src/pages/ForgotPassword/index.ee.tsx b/packages/web/src/pages/ForgotPassword/index.ee.tsx new file mode 100644 index 00000000..4b75e703 --- /dev/null +++ b/packages/web/src/pages/ForgotPassword/index.ee.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Container from 'components/Container'; +import ForgotPasswordForm from 'components/ForgotPasswordForm/index.ee'; + +export default function ForgotPassword(): React.ReactElement { + return ( + + + + + + ); +} diff --git a/packages/web/src/pages/ResetPassword/index.ee.tsx b/packages/web/src/pages/ResetPassword/index.ee.tsx new file mode 100644 index 00000000..eb41213e --- /dev/null +++ b/packages/web/src/pages/ResetPassword/index.ee.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Container from 'components/Container'; +import ResetPasswordForm from 'components/ResetPasswordForm/index.ee'; + +export default function ResetPassword(): React.ReactElement { + return ( + + + + + + ); +} diff --git a/packages/web/src/routes.tsx b/packages/web/src/routes.tsx index 5120663c..c06b8467 100644 --- a/packages/web/src/routes.tsx +++ b/packages/web/src/routes.tsx @@ -9,6 +9,8 @@ import Flows from 'pages/Flows'; import Flow from 'pages/Flow'; import Login from 'pages/Login'; import SignUp from 'pages/SignUp/index.ee'; +import ForgotPassword from 'pages/ForgotPassword/index.ee'; +import ResetPassword from 'pages/ResetPassword/index.ee'; import EditorRoutes from 'pages/Editor/routes'; import * as URLS from 'config/urls'; import settingsRoutes from './settingsRoutes'; @@ -90,6 +92,24 @@ export default ( } /> + + + + } + /> + + + + + } + /> +