build ok,
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { confirmSignIn } from 'aws-amplify/auth';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { DynamicLogo } from '@/components/core/logo';
|
||||
|
||||
const schema = zod
|
||||
.object({
|
||||
password: zod.string().min(6, { message: 'Password should be at least 6 characters' }),
|
||||
confirmPassword: zod.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
type Values = zod.infer<typeof schema>;
|
||||
|
||||
const defaultValues = { password: '', confirmPassword: '' } satisfies Values;
|
||||
|
||||
export function NewPasswordRequiredForm(): React.JSX.Element {
|
||||
const [isPending, setIsPending] = React.useState<boolean>(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
async (values: Values): Promise<void> => {
|
||||
setIsPending(true);
|
||||
|
||||
try {
|
||||
const { nextStep } = await confirmSignIn({ challengeResponse: values.password });
|
||||
|
||||
if (nextStep.signInStep === 'DONE') {
|
||||
// UserProvider will handle Router refresh
|
||||
// After refresh, GuestGuard will handle the redirect
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled next step: ${nextStep.signInStep}`);
|
||||
} catch (err) {
|
||||
setError('root', { type: 'server', message: (err as { message: string }).message });
|
||||
setIsPending(false);
|
||||
}
|
||||
},
|
||||
[setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
|
||||
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
|
||||
</Box>
|
||||
</div>
|
||||
<Typography variant="h5">New password required</Typography>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack spacing={2}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.password)}>
|
||||
<InputLabel>Password</InputLabel>
|
||||
<OutlinedInput {...field} type="password" />
|
||||
{errors.password ? <FormHelperText>{errors.password.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.confirmPassword)}>
|
||||
<InputLabel>Confirm password</InputLabel>
|
||||
<OutlinedInput {...field} type="password" />
|
||||
{errors.confirmPassword ? <FormHelperText>{errors.confirmPassword.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
|
||||
<Button disabled={isPending} type="submit" variant="contained">
|
||||
Confirm
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { resetPassword } from '@aws-amplify/auth';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import { toast } from '@/components/core/toaster';
|
||||
|
||||
export interface ResetPasswordButtonProps {
|
||||
children: React.ReactNode;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function ResetPasswordButton({ children, email }: ResetPasswordButtonProps): React.JSX.Element {
|
||||
const [isPending, setIsPending] = React.useState<boolean>(false);
|
||||
const [submitError, setSubmitError] = React.useState<string>();
|
||||
|
||||
const handle = React.useCallback(async (): Promise<void> => {
|
||||
setIsPending(true);
|
||||
setSubmitError(undefined);
|
||||
|
||||
try {
|
||||
await resetPassword({ username: email });
|
||||
|
||||
setIsPending(false);
|
||||
toast.success('Recovery code sent');
|
||||
} catch (err) {
|
||||
setSubmitError((err as { message: string }).message);
|
||||
setIsPending(false);
|
||||
}
|
||||
}, [email]);
|
||||
|
||||
return (
|
||||
<Stack spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<Button disabled={isPending} onClick={handle}>
|
||||
{children}
|
||||
</Button>
|
||||
{submitError ? <Alert color="error">{submitError}</Alert> : null}
|
||||
<Typography sx={{ textAlign: 'center' }} variant="body2">
|
||||
Wait a few minutes then try again
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { resetPassword } from '@aws-amplify/auth';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { DynamicLogo } from '@/components/core/logo';
|
||||
|
||||
const schema = zod.object({ email: zod.string().min(1, { message: 'Email is required' }).email() });
|
||||
|
||||
type Values = zod.infer<typeof schema>;
|
||||
|
||||
const defaultValues = { email: '' } satisfies Values;
|
||||
|
||||
export function ResetPasswordForm(): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
|
||||
const [isPending, setIsPending] = React.useState<boolean>(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
async (values: Values): Promise<void> => {
|
||||
setIsPending(true);
|
||||
|
||||
try {
|
||||
await resetPassword({ username: values.email });
|
||||
const searchParams = new URLSearchParams({ email: values.email });
|
||||
router.push(`${paths.auth.cognito.updatePassword}?${searchParams.toString()}`);
|
||||
} catch (err) {
|
||||
setError('root', { type: 'server', message: (err as { message: string }).message });
|
||||
setIsPending(false);
|
||||
}
|
||||
},
|
||||
[router, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
|
||||
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
|
||||
</Box>
|
||||
</div>
|
||||
<Typography variant="h5">Reset password</Typography>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.email)}>
|
||||
<InputLabel>Email address</InputLabel>
|
||||
<OutlinedInput {...field} type="email" />
|
||||
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
|
||||
<Button disabled={isPending} type="submit" variant="contained">
|
||||
Send recovery code
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
}
|
161
002_source/cms/src/components/auth/cognito/sign-in-form.tsx
Normal file
161
002_source/cms/src/components/auth/cognito/sign-in-form.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import Link from '@mui/material/Link';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Eye as EyeIcon } from '@phosphor-icons/react/dist/ssr/Eye';
|
||||
import { EyeSlash as EyeSlashIcon } from '@phosphor-icons/react/dist/ssr/EyeSlash';
|
||||
import { resendSignUpCode, signIn } from 'aws-amplify/auth';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { DynamicLogo } from '@/components/core/logo';
|
||||
|
||||
const schema = zod.object({
|
||||
email: zod.string().min(1, { message: 'Email is required' }).email(),
|
||||
password: zod.string().min(1, { message: 'Password is required' }),
|
||||
});
|
||||
|
||||
type Values = zod.infer<typeof schema>;
|
||||
|
||||
const defaultValues = { email: '', password: '' } satisfies Values;
|
||||
|
||||
export function SignInForm(): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
|
||||
const [showPassword, setShowPassword] = React.useState<boolean>();
|
||||
|
||||
const [isPending, setIsPending] = React.useState<boolean>(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
async (values: Values): Promise<void> => {
|
||||
setIsPending(true);
|
||||
|
||||
try {
|
||||
const { nextStep } = await signIn({ username: values.email, password: values.password });
|
||||
|
||||
if (nextStep.signInStep === 'DONE') {
|
||||
// UserProvider will handle Router refresh
|
||||
// After refresh, GuestGuard will handle the redirect
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextStep.signInStep === 'CONFIRM_SIGN_UP') {
|
||||
await resendSignUpCode({ username: values.email });
|
||||
const searchParams = new URLSearchParams({ email: values.email });
|
||||
router.push(`${paths.auth.cognito.signUpConfirm}?${searchParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED') {
|
||||
router.push(paths.auth.cognito.newPasswordRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled next step: ${nextStep.signInStep}`);
|
||||
} catch (err) {
|
||||
setError('root', { type: 'server', message: (err as { message: string }).message });
|
||||
setIsPending(false);
|
||||
}
|
||||
},
|
||||
[router, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
|
||||
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
|
||||
</Box>
|
||||
</div>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h5">Sign in</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Don't have an account?{' '}
|
||||
<Link component={RouterLink} href={paths.auth.cognito.signUp} variant="subtitle2">
|
||||
Sign up
|
||||
</Link>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack spacing={2}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack spacing={2}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.email)}>
|
||||
<InputLabel>Email address</InputLabel>
|
||||
<OutlinedInput {...field} type="email" />
|
||||
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.password)}>
|
||||
<InputLabel>Password</InputLabel>
|
||||
<OutlinedInput
|
||||
{...field}
|
||||
endAdornment={
|
||||
showPassword ? (
|
||||
<EyeIcon
|
||||
cursor="pointer"
|
||||
fontSize="var(--icon-fontSize-md)"
|
||||
onClick={(): void => {
|
||||
setShowPassword(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EyeSlashIcon
|
||||
cursor="pointer"
|
||||
fontSize="var(--icon-fontSize-md)"
|
||||
onClick={(): void => {
|
||||
setShowPassword(true);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label="Password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
/>
|
||||
{errors.password ? <FormHelperText>{errors.password.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
|
||||
<Button disabled={isPending} type="submit" variant="contained">
|
||||
Sign in
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
<div>
|
||||
<Link component={RouterLink} href={paths.auth.cognito.resetPassword} variant="subtitle2">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { autoSignIn, confirmSignUp } from 'aws-amplify/auth';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { DynamicLogo } from '@/components/core/logo';
|
||||
|
||||
const schema = zod.object({ confirmationCode: zod.string().min(1, { message: 'Code is required' }) });
|
||||
|
||||
type Values = zod.infer<typeof schema>;
|
||||
|
||||
const defaultValues = { confirmationCode: '' } satisfies Values;
|
||||
|
||||
export interface SignUpConfirmFormProps {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function SignUpConfirmForm({ email }: SignUpConfirmFormProps): React.JSX.Element {
|
||||
const [isPending, setIsPending] = React.useState<boolean>(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
async (values: Values): Promise<void> => {
|
||||
setIsPending(true);
|
||||
|
||||
try {
|
||||
const { nextStep } = await confirmSignUp({ username: email, confirmationCode: values.confirmationCode });
|
||||
|
||||
if (nextStep.signUpStep === 'DONE') {
|
||||
// Unless you disabled `autoSignIn` in signUp
|
||||
// UserProvider will handle Router refresh
|
||||
// After refresh, GuestGuard will handle the redirect
|
||||
// Otherwise you should redirect to the sign in page.
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextStep.signUpStep === 'COMPLETE_AUTO_SIGN_IN') {
|
||||
await autoSignIn();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled next step: ${nextStep.signUpStep}`);
|
||||
} catch (err) {
|
||||
setError('root', { type: 'server', message: (err as { message: string }).message });
|
||||
setIsPending(false);
|
||||
}
|
||||
},
|
||||
[email, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
|
||||
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
|
||||
</Box>
|
||||
</div>
|
||||
<Typography variant="h5">Confirm your email</Typography>
|
||||
<Typography>
|
||||
We've sent a verification email to{' '}
|
||||
<Typography component="span" variant="subtitle1">
|
||||
"{email}"
|
||||
</Typography>
|
||||
.
|
||||
</Typography>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack spacing={2}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmationCode"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.confirmationCode)}>
|
||||
<InputLabel>Confirmation code</InputLabel>
|
||||
<OutlinedInput {...field} />
|
||||
{errors.confirmationCode ? <FormHelperText>{errors.confirmationCode.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
|
||||
<Button disabled={isPending} type="submit" variant="contained">
|
||||
Confirm
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
}
|
170
002_source/cms/src/components/auth/cognito/sign-up-form.tsx
Normal file
170
002_source/cms/src/components/auth/cognito/sign-up-form.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { signUp } from '@aws-amplify/auth';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import Link from '@mui/material/Link';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { DynamicLogo } from '@/components/core/logo';
|
||||
|
||||
const schema = zod.object({
|
||||
firstName: zod.string().min(1, { message: 'First name is required' }),
|
||||
lastName: zod.string().min(1, { message: 'Last name is required' }),
|
||||
email: zod.string().min(1, { message: 'Email is required' }).email(),
|
||||
password: zod.string().min(6, { message: 'Password should be at least 6 characters' }),
|
||||
terms: zod.boolean().refine((value) => value, 'You must accept the terms and conditions'),
|
||||
});
|
||||
|
||||
type Values = zod.infer<typeof schema>;
|
||||
|
||||
const defaultValues = { firstName: '', lastName: '', email: '', password: '', terms: false } satisfies Values;
|
||||
|
||||
export function SignUpForm(): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
|
||||
const [isPending, setIsPending] = React.useState<boolean>(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
async (values: Values): Promise<void> => {
|
||||
setIsPending(true);
|
||||
|
||||
try {
|
||||
const { nextStep } = await signUp({
|
||||
username: values.email,
|
||||
password: values.password,
|
||||
// @ts-expect-error -- Type error
|
||||
options: { autoSignIn: true },
|
||||
});
|
||||
|
||||
if (nextStep.signUpStep === 'DONE') {
|
||||
// UserProvider will handle Router refresh
|
||||
// After refresh, GuestGuard will handle the redirect
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextStep.signUpStep === 'CONFIRM_SIGN_UP') {
|
||||
const searchParams = new URLSearchParams({ email: values.email });
|
||||
router.push(`${paths.auth.cognito.signUpConfirm}?${searchParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled next step: ${nextStep.signUpStep}`);
|
||||
} catch (err) {
|
||||
setError('root', { type: 'server', message: (err as { message: string }).message });
|
||||
setIsPending(false);
|
||||
}
|
||||
},
|
||||
[router, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
|
||||
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
|
||||
</Box>
|
||||
</div>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h5">Sign up</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Already have an account?{' '}
|
||||
<Link component={RouterLink} href={paths.auth.cognito.signIn} variant="subtitle2">
|
||||
Sign in
|
||||
</Link>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack spacing={2}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.firstName)}>
|
||||
<InputLabel>First name</InputLabel>
|
||||
<OutlinedInput {...field} />
|
||||
{errors.firstName ? <FormHelperText>{errors.firstName.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.lastName)}>
|
||||
<InputLabel>Last name</InputLabel>
|
||||
<OutlinedInput {...field} />
|
||||
{errors.lastName ? <FormHelperText>{errors.lastName.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.email)}>
|
||||
<InputLabel>Email address</InputLabel>
|
||||
<OutlinedInput {...field} type="email" />
|
||||
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.password)}>
|
||||
<InputLabel>Password</InputLabel>
|
||||
<OutlinedInput {...field} type="password" />
|
||||
{errors.password ? <FormHelperText>{errors.password.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="terms"
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<FormControlLabel
|
||||
control={<Checkbox {...field} />}
|
||||
label={
|
||||
<React.Fragment>
|
||||
I have read the <Link>terms and conditions</Link>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
{errors.terms ? <FormHelperText error>{errors.terms.message}</FormHelperText> : null}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
|
||||
<Button disabled={isPending} type="submit" variant="contained">
|
||||
Create account
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@@ -0,0 +1,144 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { confirmResetPassword } from '@aws-amplify/auth';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import Link from '@mui/material/Link';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { DynamicLogo } from '@/components/core/logo';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
|
||||
import { ResetPasswordButton } from './reset-password-button';
|
||||
|
||||
const schema = zod
|
||||
.object({
|
||||
confirmationCode: zod.string().min(1, { message: 'Confirmation code is required' }),
|
||||
password: zod.string().min(6, { message: 'Password should be at least 6 characters' }),
|
||||
confirmPassword: zod.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
type Values = zod.infer<typeof schema>;
|
||||
|
||||
const defaultValues = { confirmationCode: '', password: '', confirmPassword: '' } satisfies Values;
|
||||
|
||||
export interface UpdatePasswordFormProps {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function UpdatePasswordForm({ email }: UpdatePasswordFormProps): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
|
||||
const [isPending, setIsPending] = React.useState<boolean>(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
async (values: Values): Promise<void> => {
|
||||
setIsPending(true);
|
||||
|
||||
try {
|
||||
await confirmResetPassword({
|
||||
username: email,
|
||||
newPassword: values.password,
|
||||
confirmationCode: values.confirmationCode,
|
||||
});
|
||||
toast.success('Password updated');
|
||||
router.push(paths.auth.cognito.signIn);
|
||||
} catch (err) {
|
||||
setError('root', { type: 'server', message: (err as { message: string }).message });
|
||||
setIsPending(false);
|
||||
}
|
||||
},
|
||||
[router, email, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<div>
|
||||
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
|
||||
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
|
||||
</Box>
|
||||
</div>
|
||||
<Typography variant="h5">Update password</Typography>
|
||||
<Stack spacing={1}>
|
||||
<Typography>
|
||||
If an account exists with email{' '}
|
||||
<Typography component="span" variant="subtitle1">
|
||||
"{email}"
|
||||
</Typography>
|
||||
, you will receive a recovery email.
|
||||
</Typography>
|
||||
<div>
|
||||
<Link component={RouterLink} href={paths.auth.cognito.resetPassword} variant="subtitle2">
|
||||
Use another email
|
||||
</Link>
|
||||
</div>
|
||||
</Stack>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack spacing={2}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmationCode"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.confirmationCode)}>
|
||||
<InputLabel>Confirmation Code</InputLabel>
|
||||
<OutlinedInput {...field} type="password" />
|
||||
{errors.confirmationCode ? <FormHelperText>{errors.confirmationCode.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.password)}>
|
||||
<InputLabel>New password</InputLabel>
|
||||
<OutlinedInput {...field} type="password" />
|
||||
{errors.password ? <FormHelperText>{errors.password.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormControl error={Boolean(errors.confirmPassword)}>
|
||||
<InputLabel>Confirm password</InputLabel>
|
||||
<OutlinedInput {...field} type="password" />
|
||||
{errors.confirmPassword ? <FormHelperText>{errors.confirmPassword.message}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
|
||||
<Button disabled={isPending} type="submit" variant="contained">
|
||||
Update password
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
<ResetPasswordButton email={email}>Resend</ResetPasswordButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user