This article will explore the implementation of using the AWS Amplify JS package alongside Next.js and an existing AWS Cognito instance, with an entirely custom UI.
As discussed in the original article, BetaBud was originally written using Auth.js (formerly Next-Auth) and the AWS Cognito UI. The main reason for taking this approach was the speed of development. It was quick and easy to get Next-Auth implemented, and the Hosted UI from AWS does a lot of the leg work.
However, this is all pretty boilerplate and doesn’t offer much customisation per product. A quick search about the hosted UI and you will soon find the numerous complaints about how restrictive it is.
There are other alternative plans out there. Many have gone down the fully hosted router, following the success of Supabase. Even Auth.js has evolved to have a hosted version named Clerk.
What about @aws-amplify/ui-react ?
Again another solution that is half way between the AWS hosted solution and rolling your own. However, it still didn't feel integrated enough.
The initial idea was to use Auth.js in tandem with AWS Cognito, creating custom pages for each of the necessary pages. However, it soon transpired this additional dependency was unnecessary. No need to confuse matters with Auth.js, if the AWS Amplify JS package can offer the same functionality.
Often confusion comes with packages like Auth.js as they try to be as generic as possible. This is great as all use cases can be used, yet the documentation can sometimes feel like it is missing. It is difficult to please everyone.
I have a big gripe with AWS Amplify as I believe that it is documented to intentionally upsell. A distinction should be made here, that there is the AWS Amplify service that is paid for, and then the AWS Amplify JS SDK.
AWS Amplify JS SDK | AWS Amplify Service |
---|---|
Library for integrating with AWS Services | A managed service for provisioning back end resources |
User control | Delegates control to AWS |
Free need to pay for the resources provisioned independently | Costs |
The documentation is written in a way that suggests that they are mutually inclusive and can’t be used independently. This is not the case and causes a lot of confusion. You can use the AWS Amplify client-side JS package, without paying for the AWS Amplify service. This is the approach that will be discussed here.
To create BetaBud's authentication and authorisation, what is actually needed?
Access to JWT
The approach used for BetaBud is a separate front end and back end, as has been discussed here.
The front end is written using Next.js, whilst the back end is protected by an API Gateway that uses AWS Cognito as an authorizer. Therefore we need a JWT to communicate with the back end.
Protected pages
Accessing the user
User Login
User Sign Up
The user sign-up is a very important part of a user’s journey. The limited customization of the Hosted UI was a big motivation for making this change. If your site is gaining views but that isn’t translating to users then perhaps the sign-up is a little too cumbersome.
Further adding a custom sign-up allows for greater control over the inputs, validation messages and design. For instance, perhaps you want a Confirm Password input, that is validated against the password, not possible with the Hosted UI.
Whether you should have a Confirm Password is another question.
Reset/Forgotten Password
Further User Management
Let's get started
Many tutorials will use the AWS Amplify CLI to create an application within AWS Amplify itself. However, we already had AWS Cognito standing up, and didn’t want to use the AWS Amplify service at all, merely the JS package for interacting with Cognito.
Further, our project is using Next.js App Router, rather than Page Router, and utilizes the Material UI component library.
The only packages needed to be installed are @aws-amplify/adapter-nextjs and aws-amplify.
Some articles suggest the package size for Amplify is extremely bloated, particularly compared to Next-Auth. That isn’t the case any more as can be seen here.
As I didn’t want to use the CLI at all, I opted to hand-roll my Amplify config to connect to Cognito. Simply declare the UserPoolId
and the UserClientPoolId
.
As we’re using Next.js in addition to the App Router, this configuration needs to be called both on the Client and Server side.
First off let's create a renderless component that will be declared as early as possible in our Root Layout, the component is merely used to trigger the Configure. As you can see it consumes the config that we declared earlier. Be sure to set {SSR: true}
as is done below.
This is then used within our root layout.
Next up we need to configure on the Server side. To do this we need to use the @aws-amplify/adapter-nextjs
and use the createServerRunner
. Again this will consume the aforementioned config.
We can use the cookies as our context which will then allow us to obtain both the current user and session.
Now that we have Amplify configured, we are going to create the middleware that redirects a user if they’re not authenticated. Here we use the runWithAmplifyServerContext that was exported from the Server side setup.
We retrieve the latest session and ensure whether a token exists. If a token doesn’t exist then there will be a redirect to the login page.
Upon redirecting, we will take the current desired URL and append it as a redirect URL query string parameter. This will be used later so that if a user is trying to access a particular page, they’re redirected there upon login.
Additionally, we export a config that defines what routes that middleware should apply to. Here we exclude any API routes, next related content, and any images, amongst any pages where an authenticated user isn’t required.
So we have 2 higher-order components that we’re going to declare. One is for pages where authentication is required - withAuth, and the other is for auth-related pages which should be inaccessible when a user is logged in - withoutAuth.
When a page loads that requires authentication, we often need access both to the user and the current session alike. This is pretty consistent, therefore it makes sense to abstract this and pass those variables through to the page, harnessing a higher-level component.
We wrap each component where these properties are necessary when exporting the page. Therefore we will retrieve the user and session on the server side before the page itself is executed.
We can then access the user and session on a page like so. This will allow us to retrieve the JWT token, meaning that we can pass in the header of all of our HTTP requests like below.
To keep the aforementioned middleware clean and reduce complexity, I have created an additional higher-level component for this edge case.
It would be nice if we could nest middleware so that it is only applicable to the folder it lives within. However, this isn’t currently possible and all middleware applies to the whole application. Therefore, I wanted to avoid such edge case conditional logic from always being executed.
Essentially this just checks whether there is a current user object present once the server receives the request. If so, the user will be redirected to the homepage. This is to prevent pages such as login and sign-up from being accessible once already logged in.
We can then wrap the exports of login and sign up with this HOC.
User management is inevitably going to include a lot of password fields. The standard Textfield one from Material UI, which is merely a nice wrapping of a simple HTML input of type password, could work. However, I wanted to embellish this to add the functionality to view the plain text of the password whilst there is a mouse down on the eye icon.
So starting, by creating a component that could be reused everywhere.
We’re going to start by creating four different pages that harness the higher-level components mentioned above. 3 will require withoutAuth
, and the other will require withAuth
:
The login page serves 3 purposes, primarily to allow login, but also to navigate to forgotten password and signup pages. Therefore it will have the login component, followed by the links to these two pages.
A simple page with the rendering of the other links along with the component itself.
This component will take in 2 field inputs, one of type email and also the password input that we created earlier. Upon signing in, if there are any validation errors then the message will show on the screen.
If successful, the user will be redirected. If there is a redirect url in the query string then this will be their redirection destination. Otherwise, the user will just be redirected to the home page.
Perhaps the most important page, a user sign-up can make or break their commitment to your site and it shouldn’t be underestimated. Create a seamless signup and you have a point of contact for that user.
First, we need to consider the fields that we require in a little more detail. For BetaBud, this is initially quite trivial, but your use case may be more complex. We require:
We’re going to stick to the convention and add additional fields for Confirm Email and Confirm Password. Earlier, I mentioned an article that suggests removing these as it creates Sign Up friction, however, we will add them for now as its functionality the Hosted UI doesn’t allow.
Self-explanatory, simply return the signup component. Due to the page’s interactivity, the need to use client-side state, and that all data is related; there is little value in rendering much more at a page level.
The initial screen will load with the five required fields, as well as a submit form button. These have their client-side validation, along with validation configured when setting up the AWS Cognito instance.
Field | Validation |
---|---|
Username | Required field |
Required field and must be a valid email | |
Confirm Email | Must match the Email field - inherits that field’s validation |
Password | Required field |
Confirm Password | Must match the Password field - inherits that field’s validation |
Further validation is performed on the initial submission of this data to AWS. A user-friendly error message will be returned if there is a failure and displayed. Otherwise, the next action will be performed as dictated by the response.
A user is required to confirm their email with a code that is sent upon signing up. Here, we’re using a slide component, which allows users to fill in the initial data required. After submitting, the screen will transition, to allow them to enter their confirmation code.
Again, should this value be invalid, the error message will show.
The critical page that you hope never gets used. Forgotten Password with AWS Cognito works as many other identity providers do. Input your username, receive a code via email, update your password and provide the code.
Similar to the sign-up page, the page is pretty one-dimensional as it all depends on the Forgotten Password component.
Again, we’re going to make use of the Slide component. The first page will ask for the user’s username, with the next page asking for the confirmation code, as well as an updated password and a confirmation of that. Upon completion, we will redirect the user to the root page but logged in.
There is simple client-side validation to ensure that the passwords match, and then the remaining validation is delegated to the AWS response itself
Manage User is the only new page where the user is required to be logged in to access - note the withAuth higher level component used. This page renders 3 different functionality, therefore they’re all different sub-components:
This list of functionality can be extended down the line, perhaps a user has a display picture or a gender property. However, so far these are the only necessary fields and should be able to be changed.
A user can’t change their username.
Each component will render the read-only static data, and then will allow modification through a modal to keep the page clean.
The Page renders the three components, consuming the user prop from withAuth to allow us to pass the static value of the email to come through.
This simple component will render the current email. Then when selecting the edit, a modal will show where the user can enter their new email. A confirmation code will be sent to the new email, which they will then be prompted to enter. Any error from AWS will then be shown, if successful the modal will close.
Again, a simple card will render the Change Password, with the edit opening a modal. For the change of the password, a user must supply both their current password and enter a new password. Again, if there's an error it will be shown, otherwise, the modal will close.
The Delete Account is slightly different and will need adjusting depending on the service. For BetaBud, not only do we want to delete the User from AWS Cognito, but we want to delete all of their related data to conform with GDPR. This is an additional consideration for when architecting your data store.
The component itself will render a button to delete the user, with a confirmation modal appearing on clicking the button. For BetaBud, the deletion of associated data will be performed by a Lambda trigger that will be explored at a later date. However, here we will just call the delete user functionality.
Should this fail, an error message will show, otherwise the user will be redirected to the Welcome page.
The final piece of the puzzle is updating the top navigation that is on all pages. If a user is authorised then the logged-in bar will be shown rather than the standard bar showing the option to login. This will be dictated by whether the session exists or not.
The second part is the authentication, rendering who it is that is logged in. This will show the username, and link to their profile.
Sanitise the responses from AWS, these shouldn’t be displayed to the user directly. Each error should be tailored in a BetaBud manner, this will also allow for internationalisation when targeting multiple language markets.
Reduce the use of any
type and introduce types for each object used here.
Break down the components further and reduce reliance on the client components. This will reduce the payload size on the page render.
1
2export default {
3 "Auth": {
4 "Cognito": {
5 "userPoolId": process.env.NEXT_PUBLIC_USER_POOL_ID,
6 "userPoolClientId": process.env.NEXT_PUBLIC_USER_CLIENT_POOL_ID
7 }
8 }
9};
1
2'use client'
3
4Amplify.configure(config, { ssr: true });
5
6
7export default function ConfigureAmplifyClientSide() {
8 return null;
9}
1// snippet from /src/app/layout.tsx
2
3export default function RootLayout({ children }: { children: React.ReactNode }) {
4
5 return (
6 <html lang="en">
7 <body>
8 <ConfigureAmplifyClientSide />
1
2export const { runWithAmplifyServerContext } = createServerRunner({ config });
3
4export async function AuthGetCurrentUserServer() {
5 try {
6 const currentUser = await runWithAmplifyServerContext({
7 nextServerContext: { cookies },
8 operation: (contextSpec) => getCurrentUser(contextSpec),
9 });
10 return currentUser;
11 } catch (error) {
12 console.error(error);
13 }
14}
15
16export async function AuthGetCurrentSessionServer() {
17 return await runWithAmplifyServerContext({
18 nextServerContext: { cookies },
19 operation: async (contextSpec) => {
20 try {
21 const session = await fetchAuthSession(contextSpec);
22 return session.tokens;
23 } catch (error) {
24 console.log(error);
25 return null;
26 }
27 }
28 });
29}
1
2export async function middleware(request: NextRequest) {
3 const response = NextResponse.next();
4
5 const url = new URL(request.url);
6
7 if(url.pathname === '/'){
8 return response;
9 }
10
11 const authenticated = await runWithAmplifyServerContext({
12 nextServerContext: { request, response },
13 operation: async (contextSpec) => {
14 try {
15 const session = await fetchAuthSession(contextSpec);
16 return session.tokens !== undefined;
17 } catch (error) {
18 console.log(error);
19 return false;
20 }
21 }
22 });
23
24 if (authenticated) {
25 return response;
26 }
27
28 const paramsObj = { redirectUrl: url.toString() };
29 const searchParams = new URLSearchParams(paramsObj);
30
31 return NextResponse.redirect(new URL(`/login?${searchParams.toString()}`, request.url));
32}
33
34export const config = {
35 matcher: [
36 "/((?!api|_next/static|_next/image|favicon.ico|login|signup|forgottenpassword|welcome|blog|demo).*)",
37 ],
38};
1
2export default function withAuth(WrappedComponent) {
3 return async (props) => {
4 try {
5 const currentUser = await AuthGetCurrentUserServer();
6 const currentSession = await AuthGetCurrentSessionServer();
7
8 if (!!currentUser) {
9 return <WrappedComponent {...props} user={currentUser} session={currentSession} />;
10 }
11
12 return <WrappedComponent {...props} />;
13 } catch (error) {
14 console.error(error);
15 return <p>Something went wrong...</p>;
16 }
17 }
18}
1function PageRequiresAuth({params, user, session}){
2 return <></>;
3}
4
5export default withAuth(PageRequiresAuth);
1
2export default function withoutAuth(WrappedComponent) {
3 return async (props) => {
4 try {
5 const currentUser = await AuthGetCurrentUserServer();
6
7 if (!!currentUser) {
8 redirect('/');
9 }
10
11 return <WrappedComponent {...props} />;
12 } catch (error) {
13 redirect('/');
14 }
15 }
16}
1function Login({params, user, session}){
2 return <></>;
3}
4
5export default withoutAuth(Login);
1
2interface PasswordInputProps {
3 label: string;
4 value?: string | null;
5 onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
6 passError?: string | null;
7 sx?: Record<string, any>; // for custom styles
8 required?: boolean;
9 name?: string;
10 autoComplete?: string;
11}
12
13const PasswordInput = forwardRef(
14 (
15 {
16 label,
17 value = null,
18 onChange = () => {},
19 passError = null,
20 sx = {},
21 required = false,
22 name = 'password',
23 autoComplete = 'current-password',
24 }: PasswordInputProps,
25 ref: Ref<HTMLInputElement>
26 ) => {
27 const [showPassword, setShowPassword] = useState(false);
28
29 const handleMousePassword = () => {
30 setShowPassword((show) => !show);
31 };
32
33 const endAdornment = <InputAdornment position="end">
34 <IconButton
35 aria-label="toggle password visibility"
36 onMouseDown={handleMousePassword}
37 onMouseUp={handleMousePassword}
38 edge="end"
39 >
40 {showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
41 </IconButton>
42 </InputAdornment>;
43
44 return (
45 <FormControl sx={sx} variant="outlined">
46 <InputLabel htmlFor="outlined-adornment-password">{label}</InputLabel>
47 {!!ref ? (
48 <OutlinedInput
49 id="outlined-adornment-password"
50 type={showPassword ? 'text' : 'password'}
51 name={name}
52 autoComplete={autoComplete}
53 inputRef={ref} // Use inputRef instead of ref
54 endAdornment={endAdornment}
55 label="Password"
56 error={!!passError}
57 required={required}
58 />
59 ) : (
60 <OutlinedInput
61 id="outlined-adornment-password"
62 type={showPassword ? 'text' : 'password'}
63 name={name}
64 autoComplete={autoComplete}
65 value={value}
66 onChange={onChange}
67 inputRef={ref} // Use inputRef instead of ref
68 endAdornment={endAdornment}
69 label="Password"
70 error={!!passError}
71 required={required}
72 />
73 )}
74 {!!passError && <FormHelperText>{passError}</FormHelperText>}
75 </FormControl>
76 );
77 }
78);
79
80export default PasswordInput;
1
2function Login() {
3 return <Container maxWidth='sm' sx={{ pt: 22, textAlign: 'center' }}>
4 <Stack spacing={2}>
5 <LoginSection />
6 <Link variant='caption' href='/signup'>Sign Up</Link>
7 <Link variant='caption' href='/forgottenpassword'>Forgotten Password</Link>
8 </Stack>
9 </Container>;
10}
11
12export default withoutAuth(Login);
1
2'use client'
3
4export default function LoginSection() {
5 const router = useRouter();
6 const searchParams = useSearchParams();
7 const { reload } = useMyContext();
8
9 const userRef = useRef(null);
10 const passRef = useRef(null);
11
12 const [userError, setUserError] = useState<string | null>(null);
13 const [passError, setPassError] = useState<string | null>(null);
14 const [submitError, setSubmitError] = useState<string | null>(null);
15
16 const handleSignIn = (e) => {
17 e.preventDefault();
18 let failed = false;
19 setSubmitError(null);
20
21 const user = userRef?.current?.value;
22 const pass = passRef?.current?.value;
23
24 if (!user) {
25 setUserError("Please provide a Username");
26 failed = true;
27 } else {
28 setUserError(null);
29 }
30
31 if (!pass) {
32 setPassError("Please provide a Password");
33 failed = true;
34
35 } else {
36 setPassError(null);
37 }
38
39 if (failed) {
40 return;
41 }
42
43 handleSignInAsync({ username: user, password: pass })
44 .then(({ isSignedIn, nextStep }) => {
45
46 switch (nextStep.signInStep) {
47 case 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED':
48 case 'CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE':
49 case 'CONFIRM_SIGN_IN_WITH_TOTP_CODE':
50 case 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP':
51 case 'CONFIRM_SIGN_IN_WITH_SMS_CODE':
52 case 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION':
53 console.log(nextStep.signInStep);
54 throw Error("Not implemented");
55 break;
56 case 'RESET_PASSWORD':
57 router.push('/resetpassword');
58 break;
59 case 'CONFIRM_SIGN_UP':
60 break;
61 case 'DONE':
62 const redirectUrl = searchParams.get('redirectUrl');
63 reload()
64 .then(() => {
65 if (!!redirectUrl) {
66 router.push(redirectUrl);
67 } else {
68 router.push('/');
69 }
70 });
71 break;
72 }
73 })
74 .catch(e => setSubmitError(e.message));
75 }
76
77 const handleSignInAsync = async ({ username, password }: SignInInput) => {
78 const { isSignedIn, nextStep } = await signIn({ username, password });
79 return { isSignedIn, nextStep };
80 }
81
82 return <FormControl fullWidth onSubmit={handleSignIn} component='form' autoComplete='on'>
83 <Stack spacing={4} sx={{ textAlign: 'center' }}>
84 <TextField label='username'
85 name='username'
86 autoComplete='username'
87 inputRef={userRef}
88 error={!!userError}
89 helperText={userError} />
90 <PasswordInput label='password'
91 autoComplete='current-password'
92 ref={passRef}
93 passError={passError} />
94 <Button type='submit' fullWidth>Submit</Button>
95 {!!submitError && <Typography variant='caption' color='error'>{submitError}</Typography>}
96 </Stack>
97 </FormControl>;
98}
1
2import withoutAuth from "@/components/auth/withoutAuth";
3import SignUpSection from "@/components/views/auth/SignUpSection";
4
5function SignUp() {
6 return <SignUpSection />;
7}
8
9export default withoutAuth(SignUp);
1
2'use client'
3
4type SignUpParameters = {
5 username: string;
6 password: string;
7 email: string;
8};
9
10export default function SignUpSection() {
11 const router = useRouter();
12 const searchParams = useSearchParams();
13 const { reload } = useMyContext();
14
15 const userRef = useRef(null);
16 const emailRef = useRef(null);
17 const confirmEmailRef = useRef(null);
18 const passRef = useRef(null);
19 const confirmPassRef = useRef(null);
20 const codeRef = useRef(null);
21
22 const [userError, setUserError] = useState<string | null>(null);
23 const [emailError, setEmailError] = useState<string | null>(null);
24 const [confirmEmailError, setConfirmEmailError] = useState<string | null>(null);
25 const [passError, setPassError] = useState<string | null>(null);
26 const [confirmPassError, setConfirmPassError] = useState<string | null>(null);
27 const [submitError, setSubmitError] = useState<string | null>(null);
28
29 const [page, setPage] = useState<Number>(0);
30 const [disabled, setDisabled] = useState<boolean>(false);
31
32 const handleSignUp = (e) => {
33 e.preventDefault();
34 let failed = false;
35
36 setSubmitError(null);
37
38 const user = userRef?.current?.value;
39 const email = emailRef?.current?.value;
40 const confirmEmail = confirmEmailRef?.current?.value;
41 const pass = passRef?.current?.value;
42 const confirmPass = confirmPassRef?.current?.value;
43
44 if (!user) {
45 setUserError("Please provide a Username");
46 failed = true;
47 } else {
48 setUserError(null);
49 }
50
51 if (!email) {
52 setEmailError("Please provide a valid Email");
53 failed = true;
54 } else if (email !== confirmEmail) {
55 setConfirmEmailError("Must match email");
56 failed = true;
57 } else {
58 setEmailError(null);
59 }
60
61 if (!pass) {
62 setPassError("Please provide a Password");
63 failed = true;
64 } else if (pass !== confirmPass) {
65 setConfirmPassError("Must match password");
66 failed = true;
67 } else {
68 setPassError(null);
69 }
70
71 if (failed) {
72 return;
73 }
74
75 setDisabled(true);
76 handleSignUpAsync({ username: user, password: pass, email: email })
77 .then(next)
78 .catch(e => {
79 console.log("Error signing up", e);
80 });
81 }
82
83 const handleSignUpAsync = async ({
84 username,
85 password,
86 email,
87 }: SignUpParameters) => {
88 try {
89 return await signUp({
90 username,
91 password,
92 options: {
93 userAttributes: {
94 email,
95 }
96 }
97 });
98 } catch (error) {
99 setSubmitError(error.message);
100 }
101 }
102
103 const handleSignUpConfirmation = (e) => {
104 e.preventDefault();
105 let failed = false;
106
107 setSubmitError(null);
108
109 const user = userRef?.current?.value;
110 const code = codeRef?.current?.value;
111
112
113 if (!user) {
114 failed = true;
115 setUserError("Please provide a Username");
116 } else {
117 setUserError(null);
118 }
119
120 if (!code) {
121 failed = true;
122 }
123
124 if (failed) {
125 return;
126 }
127
128 setDisabled(true);
129 handleSignUpConfirmationAsync({ username: user, confirmationCode: code })
130 .then(next)
131 .catch(e => setSubmitError(e.message));
132 }
133
134 const handleSignUpConfirmationAsync = async ({
135 username,
136 confirmationCode
137 }: ConfirmSignUpInput) => {
138 return await confirmSignUp({
139 username,
140 confirmationCode
141 });
142 }
143
144 const next = ({ isSignUpComplete, nextStep }) => {
145 switch (nextStep.signUpStep) {
146 case 'CONFIRM_SIGN_UP':
147 setPage(1);
148 setDisabled(false);
149 break;
150 case 'DONE':
151 const redirectUrl = searchParams.get('redirectUrl');
152 reload()
153 .then(() => {
154 if (!!redirectUrl) {
155 router.push(redirectUrl);
156 } else {
157 router.push('/');
158 }
159 });
160 break;
161 }
162
163 throw Error("Not implemented");
164 }
165
166 return <Container maxWidth='sm' sx={{ py: 12 }}>
167 <Slide in={page === 0} direction='right' exit={false} mountOnEnter unmountOnExit>
168 <FormControl fullWidth onSubmit={handleSignUp} component='form' autoComplete='on'>
169 <Stack spacing={4}>
170 <TextField label='Username'
171 autoComplete='username'
172 inputRef={userRef}
173 error={!!userError}
174 helperText={userError}
175 required />
176 <TextField label='Email'
177 type='email'
178 inputRef={emailRef}
179 error={!!emailError}
180 helperText={emailError}
181 required />
182 <TextField label='Confirm Email'
183 type='email'
184 inputRef={confirmEmailRef}
185 error={!!confirmEmailError}
186 helperText={confirmEmailError}
187 required />
188 <PasswordInput label='Password'
189 autoComplete='new-password'
190 ref={passRef}
191 passError={passError}
192 required />
193 <PasswordInput label='Confirm Password'
194 ref={confirmPassRef}
195 passError={confirmPassError}
196 required />
197 <Box>
198 <Button type='submit' fullWidth disabled={disabled}>Submit</Button>
199 {!!submitError && <Typography variant='caption' color='error'>{submitError}</Typography>}
200 </Box>
201 </Stack>
202 </FormControl>
203 </Slide>
204 <Slide in={page === 1} direction='left' timeout={{ enter: 500 }} mountOnEnter unmountOnExit>
205 <FormControl fullWidth onSubmit={handleSignUpConfirmation} component='form'>
206 <Stack spacing={4}>
207 <TextField label='Username'
208 value={userRef?.current?.value || ''}
209 disabled />
210 <TextField label='Confirmation Code'
211 inputRef={codeRef}
212 required />
213 <Box>
214 <Button type='submit' fullWidth disabled={disabled}>Submit</Button>
215 {!!submitError && <Typography variant='caption' color='error'>{submitError}</Typography>}
216 </Box>
217 </Stack>
218 </FormControl>
219 </Slide>
220 </Container>;
221}
1
2function ResetPassword() {
3 return <ForgottenPasswordSection />;
4}
5
6export default withoutAuth(ResetPassword);
1
2'use client'
3
4export default function ForgottenPasswordSection() {
5 const router = useRouter();
6
7 const userRef = useRef(null);
8 const codeRef = useRef(null);
9 const passRef = useRef(null);
10 const confirmPassRef = useRef(null);
11
12 const [userError, setUserError] = useState<string | null>(null);
13 const [passError, setPassError] = useState<string | null>(null);
14 const [confirmPassError, setConfirmPassError] = useState<string | null>(null);
15 const [submitError, setSubmitError] = useState<string | null>(null);
16
17 const [page, setPage] = useState<Number>(0);
18 const [disabled, setDisabled] = useState<boolean>(false);
19
20 const handleResetPassword = (e) => {
21 e.preventDefault();
22 setSubmitError(null);
23
24 const user = userRef?.current?.value;
25 handleResetPasswordAsync(user)
26 .then(handleResetPasswordNextSteps)
27 .catch(e => setSubmitError(e.message));
28 }
29
30 const handleResetPasswordAsync = async (username: string) => {
31 const output = await resetPassword({ username });
32 return output;
33 }
34
35 const handleResetPasswordNextSteps = (output: ResetPasswordOutput) => {
36 const { nextStep } = output;
37 switch (nextStep.resetPasswordStep) {
38 case 'CONFIRM_RESET_PASSWORD_WITH_CODE':
39 setPage(1);
40 break;
41 case 'DONE':
42 router.push('/');
43 break;
44 }
45 }
46
47 const handleConfirmResetPassword = (e) => {
48 e.preventDefault();
49 let failed = false;
50
51 setSubmitError(null);
52
53 const user = userRef?.current?.value;
54 const pass = passRef?.current?.value;
55 const confirmPass = confirmPassRef?.current?.value;
56 const code = codeRef?.current?.value;
57
58 if (!user) {
59 setUserError("Please provide a Username");
60 failed = true;
61 } else {
62 setUserError(null);
63 }
64
65 if (!pass) {
66 setPassError("Please provide a Password");
67 failed = true;
68 } else if (pass !== confirmPass) {
69 setConfirmPassError("Must match password");
70 failed = true;
71 } else {
72 setPassError(null);
73 }
74
75 if (failed) {
76 return;
77 }
78
79 setDisabled(true);
80
81 handleConfirmResetPasswordAsync({ username: user, confirmationCode: code, newPassword: pass });
82 }
83
84 const handleConfirmResetPasswordAsync = async ({
85 username,
86 confirmationCode,
87 newPassword
88 }: ConfirmResetPasswordInput) => {
89 await confirmResetPassword({ username, confirmationCode, newPassword });
90 router.push('/');
91 }
92
93 return <Container maxWidth='sm' sx={{ py: 22 }}>
94 <Slide in={page === 0} direction='right' exit={false} mountOnEnter unmountOnExit>
95 <FormControl fullWidth onSubmit={handleResetPassword} component='form' autoComplete='on'>
96 <Stack spacing={4}>
97 <TextField label='Username'
98 autoComplete='username'
99 inputRef={userRef}
100 error={!!userError}
101 helperText={userError}
102 required />
103 <Box>
104 <Button type='submit' fullWidth disabled={disabled}>Reset Password</Button>
105 {!!submitError && <Typography variant='caption' color='error'>{submitError}</Typography>}
106 </Box>
107 </Stack>
108 </FormControl>
109 </Slide>
110 <Slide in={page === 1} direction='left' timeout={{ enter: 500 }} mountOnEnter unmountOnExit>
111 <FormControl fullWidth onSubmit={handleConfirmResetPassword} component='form'>
112 <Stack spacing={4}>
113 <TextField label='Username'
114 value={userRef?.current?.value || ''}
115 disabled />
116 <TextField label='Confirmation Code'
117 inputRef={codeRef}
118 required />
119 <PasswordInput label='Password'
120 autoComplete='new-password'
121 ref={passRef}
122 passError={passError}
123 required />
124 <PasswordInput label='Confirm Password'
125 ref={confirmPassRef}
126 passError={confirmPassError}
127 required />
128 <Box>
129 <Button type='submit' fullWidth disabled={disabled}>Submit</Button>
130 {!!submitError && <Typography variant='caption' color='error'>{submitError}</Typography>}
131 </Box>
132 </Stack>
133 </FormControl>
134 </Slide>
135 </Container>;
136}
1
2export const metadata = {
3 title: 'BetaBud - Manage User'
4};
5
6async function ManageUserPage({ user }) {
7
8 return <Container maxWidth='sm' sx={{ py: 12 }}>
9 <Stack spacing={2}>
10 <Typography variant='h3'>Account Settings</Typography>
11 <ChangeEmailSection user={user} />
12 <ChangePasswordSection />
13 <DeleteAccountSection />
14 </Stack>
15 </Container>;
16}
17
18export default withAuth(ManageUserPage);
1
2'use client'
3
4export default function ChangeEmailSection({ user }) {
5 const [open, setOpen] = useState(false);
6 const [page, setPage] = useState(0);
7 const [email, setEmail] = useState<string | null>();
8 const [confirmCode, setConfirmCode] = useState<string | null>();
9 const [submitError, setSubmitError] = useState<string | null>(null);
10
11 const handleOpen = () => {
12 setOpen(true);
13 };
14
15 const handleClose = () => {
16 setOpen(false);
17 setPage(0);
18 setEmail(null);
19 setConfirmCode(null);
20 setSubmitError(null);
21 };
22
23 const handleChangeEmail = (e) => {
24 e.preventDefault();
25
26 switch (page) {
27 case 0:
28 handleUpdateEmailAsync(email)
29 .then(({ isUpdated, nextStep }) => {
30 if (isUpdated) {
31 setOpen(false);
32 setEmail(null);
33 setConfirmCode(null);
34 return;
35 }
36 if (nextStep.updateAttributeStep === 'CONFIRM_ATTRIBUTE_WITH_CODE') {
37 setSubmitError(null);
38 setPage(1);
39 }
40 })
41 .catch(e => setSubmitError(e.message));
42 break;
43 case 1:
44 handleConfirmUserAttributeAsync({ userAttributeKey: 'email', confirmationCode: confirmCode })
45 .then(() => {
46 setOpen(false);
47 setEmail(null);
48 setConfirmCode(null);
49 return;
50 })
51 .catch(e => setSubmitError(e.message));
52 break;
53 }
54 }
55
56 const handleUpdateEmailAsync = async (
57 updatedEmail: string
58 ) => {
59 const attributes = await updateUserAttributes({
60 userAttributes: {
61 email: updatedEmail
62 }
63 });
64 return attributes.email;
65 }
66
67 const handleConfirmUserAttributeAsync = async ({
68 userAttributeKey,
69 confirmationCode
70 }: ConfirmUserAttributeInput) => {
71 return await confirmUserAttribute({ userAttributeKey, confirmationCode });
72 }
73
74 return <>
75 <Card>
76 <CardHeader title='Email Address'
77 subheader={user.signInDetails.loginId}
78 action={<IconButton><EditIcon onClick={handleOpen} /></IconButton>} />
79 </Card>
80 <Dialog open={open}
81 onClose={handleClose}>
82 <DialogTitle>Change Email</DialogTitle>
83 <DialogContent>
84 <Stack direction='column'>
85 <Slide in={page === 0} direction='right' exit={false} mountOnEnter unmountOnExit>
86 <TextField label='New Email'
87 type='email'
88 onChange={e => setEmail(e.target.value)}
89 value={email}
90 sx={{ my: 2, width: 400, maxWidth: '100%' }} />
91 </Slide>
92 <Slide in={page === 1} direction='left' timeout={{ enter: 500 }} mountOnEnter unmountOnExit>
93 <Stack direction='column'>
94 <TextField label='New Email'
95 type='email'
96 disabled
97 value={email}
98 sx={{ my: 2, width: 400, maxWidth: '100%' }} />
99 <TextField label='Confirmation Code'
100 onChange={e => setConfirmCode(e.target.value)}
101 value={confirmCode}
102 sx={{ my: 2, width: 400, maxWidth: '100%' }} />
103 </Stack>
104 </Slide>
105 {!!submitError && <DialogContentText>{submitError}</DialogContentText>}
106 </Stack>
107 </DialogContent>
108 <DialogActions>
109 <Button variant='outlined'
110 color='primary'
111 onClick={handleClose}
112 autoFocus>Cancel</Button>
113 <Button variant='outlined'
114 color='error'
115 onClick={handleChangeEmail}
116 endIcon={<EditIcon />}>Continue</Button>
117 </DialogActions>
118 </Dialog>
119 </>;
120}
1
2'use client'
3
4export default function ChangePasswordSection() {
5 const [open, setOpen] = useState(false);
6 const [oldPass, setOldPass] = useState<string | null>();
7 const [newPass, setNewPass] = useState<string | null>();
8 const [submitError, setSubmitError] = useState<string | null>(null);
9
10 const handleOpen = () => {
11 setOpen(true);
12 };
13
14 const handleClose = () => {
15 setOpen(false);
16 setOldPass(null);
17 setNewPass(null);
18 setSubmitError(null);
19 };
20
21 const handleChangePassword = () => {
22 handleUpdatePasswordAsync({ oldPassword: oldPass, newPassword: newPass })
23 .then(() => {
24 setOpen(false);
25 setOldPass(null);
26 setNewPass(null);
27 })
28 .catch(e => setSubmitError(e.message));
29 };
30
31 const handleUpdatePasswordAsync = async ({
32 oldPassword,
33 newPassword
34 }: UpdatePasswordInput) => {
35 await updatePassword({ oldPassword, newPassword });
36 }
37
38 return <>
39 <Card>
40 <CardHeader title='Password'
41 action={<IconButton onClick={handleOpen}><EditIcon /></IconButton>} />
42 </Card>
43 <Dialog open={open}
44 onClose={handleClose}>
45 <DialogTitle>Change Password</DialogTitle>
46 <DialogContent>
47 <Stack direction='column'>
48 <PasswordInput label='Old Password'
49 autoComplete='current-password'
50 onChange={e => setOldPass(e.target.value)}
51 value={oldPass}
52 sx={{ my: 2, width: 400, maxWidth: '100%' }} />
53 <PasswordInput label='New Password'
54 autoComplete='new-password'
55 onChange={e => setNewPass(e.target.value)}
56 value={newPass}
57 sx={{ my: 2, width: 400, maxWidth: '100%' }} />
58 {!!submitError && <DialogContentText>{submitError}</DialogContentText>}
59 </Stack>
60 </DialogContent>
61 <DialogActions>
62 <Button variant='outlined'
63 color='primary'
64 onClick={handleClose}
65 autoFocus>Cancel</Button>
66 <Button variant='outlined'
67 color='error'
68 onClick={handleChangePassword}
69 endIcon={<EditIcon />}>Continue</Button>
70 </DialogActions>
71 </Dialog>
72 </>;
73}
1
2'use client'
3
4export default function DeleteAccountSection() {
5 const router = useRouter();
6
7 const [open, setOpen] = useState(false);
8 const [submitError, setSubmitError] = useState<string | null>(null);
9
10 const handleOpen = () => {
11 setOpen(true);
12 };
13
14 const handleClose = () => {
15 setOpen(false);
16 setSubmitError(null);
17 };
18
19 const handleDeleteAccount = () => {
20 handleDeleteUserAsync()
21 .then(() => router.push('/welcome'))
22 .catch(e => setSubmitError(e.message));
23 };
24
25 const handleDeleteUserAsync = async () => {
26 await deleteUser();
27 }
28
29 return <>
30 <Button variant='outlined' color='error' endIcon={<DeleteIcon />} onClick={handleOpen} disabled={open}>
31 Delete Account
32 </Button>
33 <Dialog open={open}
34 onClose={handleClose}>
35 <DialogTitle>Delete Account</DialogTitle>
36 <DialogContent>
37 <DialogContentText>
38 Are you sure you wish to delete your account?
39 </DialogContentText>
40 {!!submitError && <DialogContentText>{submitError}</DialogContentText>}
41 </DialogContent>
42 <DialogActions>
43 <Button variant='outlined'
44 color='primary'
45 onClick={handleClose}
46 autoFocus>Cancel</Button>
47 <Button variant='outlined'
48 color='error'
49 onClick={handleDeleteAccount}
50 endIcon={<DeleteIcon />}>Continue</Button>
51 </DialogActions>
52 </Dialog>
53 </>;
54}
1
2'use client'
3
4export default function ProfileMenu() {
5 const router = useRouter();
6
7 const [session, setSession] = useState<AuthTokens | null>(null);
8 const [user, setUser] = useState<AuthUser | null>(null);
9
10 useEffect(() => {
11 const load = async () => {
12 const loadedSession = await loadSession();
13 if (!!loadedSession) {
14 const loadedUser = await currentAuthenticatedUser();
15 setSession(loadedSession);
16 setUser(loadedUser);
17 } else {
18 setSession(null);
19 setUser(null);
20 }
21 }
22
23 load();
24 }, []);
25
26 const handleSignOut = () => {
27 handleSignOutAsync()
28 .then(() => {
29 router.push('/welcome');
30 })
31 .catch(e => console.log('error signing out: ', e));
32 }
33
34 async function handleSignOutAsync() {
35 await signOut();
36 }
37
38 return (<Fragment>
39 {!session && (
40 <Fragment>
41 <Button href='/welcome' sx={{ color: 'primary.contrastText', ml: 'auto', mr: 0 }}>Welcome</Button>
42 <Button href='/login' sx={{ color: 'primary.contrastText', ml: 'auto', mr: 0 }}>Login</Button>
43 </Fragment>
44 )}
45 {session && (
46 <Fragment>
47 <Link variant='subtitle2'
48 color='primary.contrastText'
49 sx={{ mx: 2 }}
50 href='/manageuser'
51 underline='none'>{user.username}</Link>
52 <Tooltip title='Logout'>
53 <IconButton size="large"
54 aria-label="account of current user"
55 aria-controls="menu-appbar"
56 color='inherit'
57 onClick={handleSignOut}>
58 <LogoutIcon />
59 </IconButton>
60 </Tooltip>
61 </Fragment>
62 )}
63 </Fragment>);
64}