Search code examples
reactjsnext.jsnext-router

How to make a reset password form route with next.js


Please see EDIT below.

I am stuck with a reset password problem in my project. I am able to send an email to the user with the link for resetting the password with an hashed token and the id of the user inside.

99% of the code is already done (backend and frontend), the only thing is that I am not able to get the hashed token and the id on the client side in order to call the right API end point and so show the user the form for resetting the password and have the logic that checks the token, expire date, etc.

I am not using react-router-dom (even though I tried to use it only for this route and I am not able to making it working anyway).

Basically the link that I send to the user is this one:

`http://localhost:3000/reset-password-form/${hashedResetToken}/${user._id}`

my route looks like this:

router.patch('/reset-password-form/:resetToken/:userId', shopController.resetPasswordPage);

Now, I tried to add the functionality of react-router-dom like this into the reset-password-form.js file inside of the page folder like this:

import React from 'react';
import ResetPasswordPage from '../components/ResetPasswordPage';
import { BrowserRouter as Router, Switch, Route} from 'react-router-dom';

const resetPasswordPage = () => {
    return (
      <>
        <Router>
            <Switch>
                <Route exact path="/reset-password-form/:resetToken/:userId" component={ResetPasswordPage} />
            </Switch>
        </Router>
        <ResetPasswordPage />
      </>
    )
}

export default resetPasswordPage;

And, the problem is that I cannot make a call to the right API end point cause I am not able to get the params that I need. Component looks like this (it is just a few lines of code, not the whole component):

fetchData = e => {
    e.preventDefault();
    const resetToken = this.props.match.params.resetToken;
    const userId = this.props.match.params.userId;

    fetch(`http://localhost:8090/reset-password-form/${resetToken}/${userId}`, {

So right now if I click on the link I am getting a 404 page not found.

How can I get these params on the client side?

EDIT:

So now I'm using next/router as it was suggested.

First, I noticed that if the token gets hashed with a / inside next/router is not able to read the token and it gives me a 404 page (probably because it thinks that there are more than 2 parameters?). I tried to adjust the routes without the / with no success.

For debugging purposes my reset-password-form/[resetToken]/[userId].js into the pages folder looks like this:

const resetPasswordPage = () => {
    const router = useRouter();

    return (
      <>
        <p>user id: {router.query.userId}</p>
        <p>token: {router.query.resetToken}</p>
      </>
    )
}

and this gives me as a result the hashed token (only if it is generated without a / inside otherwise I will get a 404 page) and the userId.

After debugging I removed the 2 p tags and I am passing the ResetPasswordPage component inside the return, passing as props the parameters that I need and get them like this (ResetPasswordPage component):

fetchData = e => {
    e.preventDefault();
    const resetToken = this.props.resetToken;
    const userId = this.props.userId;

    fetch(`http://localhost:8090/reset-password-form/${resetToken}/${userId}`, {

but if I do so I will get Error: React.Children.only expected to receive a single React element child.

If I remove the other 2 children from the ResetPasswordPage component the page that I want gets rendered (again only if the token is generated without a /). Is there any way to render also the other 2 children (I do have the <> </> tags between the 3 components)? Those are important for the page to work (they show a nav link and error/success messages).

How can I avoid the fact that if the token gets hashed with a / the user will get a 404 page?

EDIT 2.0

Full code of ResetPasswordPage component:

import React, { Component } from 'react';
import Link from 'next/link';
import Form from '../components/styles/Form';
import Logo from '../components/styles/Logo';
import MessageStyles from '../components/styles/MessageStyles';

class ResetPasswordPage extends Component {
    state = {
        loading: false,
        message: null,
        password: '',
        confirmPassword: '', 
    }

    handleChange = e => {
        this.setState({ [e.target.name]: e.target.value });
    }

    fetchData = e => {
        e.preventDefault();

        const resetToken = this.props.resetToken;
        const userId = this.props.userId;

        fetch(`http://localhost:8090/reset-password-form/${resetToken}/${userId}`, {
            method: 'PATCH',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',

            },
            credentials: 'include',
            body: JSON.stringify({
                password: this.state.password,
                confirmPassword: this.state.confirmPassword,

            })
        })
        .then(res => {
            return res.json();
        })
        .then(resData => {
            console.log(resData);
        })
        .then(() => {
            setTimeout(() => {
                window.location.replace('/auth/login');
            }, 3000);
        })
        .catch(err => console.log(err))
    }

    render() {
        return (  
          <>
            <Logo> // by removing only the logo component with the link inside I solved the issue of Error: React.Children.only expected to receive a single React element child
                <Link href="/shop" />
            </Logo>
            <MessageStyles><p id="message-test">{this.state.message}</p></MessageStyles>
            <Form onSubmit={this.fetchData}>
                <fieldset aria-busy={this.state.loading} disabled={this.state.loading}>
                    <h1>Chang{this.state.loading ? 'ing' : 'e'} Your Password</h1>
                    <label htmlFor="password">
                        <input
                            name="password"
                            type="password"
                            onChange={this.handleChange}
                            value={this.state.password}
                            className={this.state.password.length < 5 ? 'invalid' : '' }
                        />
                    </label>
                    <label htmlFor="confirm password">
                        <input
                            name="confirmPassword"
                            type="password"
                            onChange={this.handleChange}
                            value={this.state.confirmPassword}
                            className={this.state.password !== this.state.confirmPassword ? 'invalid' : '' }
                        />
                    </label>
                    <button>Chang{this.state.loading ? 'ing' : 'e'} Your Password</button>
                </fieldset>
            </Form>
          </>
        )
    }
}

export default ResetPasswordPage;

So if I remove Logo and MessageStyle page gets rendered.


Solution

  • So in order to solve the issue I did the following:

    renamed the file reset-password-form.js into the pages folder like this "reset-password-form/[resetToken]/[userId].js".

    reset-password-form/[resetToken]/[userId].js looks like this:

    import React from 'react';
    import { useRouter } from 'next/router';
    import ResetPasswordPage from '../../../components/ResetPasswordPage';
    
    const resetPasswordPage = () => {
    
    const router = useRouter();
    
    return (
    
        <ResetPasswordPage userId={router.query.userId} resetToken={router.query.resetToken} />
    
    )
    
    }
    
    export default resetPasswordPage;
    

    And into the ResetPasswordPage I got the params that I need by getting it from the props like this:

    resetToken = this.props.resetToken;
    userId = this.props.userId;
    

    and for the / inside the token you need to encode/decode the token encodeURIComponent and decodeURIComponent