DEV Community

Danilo Miranda
Danilo Miranda

Posted on

Building an API with AdonisJS (part 2)

This is the second post of the series about creating an API using AdonisJS. If you didn't read the first part here is the link Building an API with AdonisJS.

Now let's continue with the second part. Here we will learn how to:

  • Update the user information (PUT request)
  • Recover user's password
  • Send emails with Adonis
  • Work with migration to update a table structure

Updating user information

Let's start by creating the controller that will allow the user to update his information, like his username and password (in this application the user won't be allowed to update his email).

The logic behind will be very simple:

  • The user will send in the request, his new desired username, his current password and the new desired password.
  • We'll then search for the user in the database
  • Then we check if the current password provided is correct and then update his information with the new provided password

In order to create a new controller, we need to run the following Adonis' command:

adonis make:controller UpdateUserInfo --type http
Enter fullscreen mode Exit fullscreen mode

Now we can go to the file app/controllers/http/UpdateUserInfoController.js and start coding:

Let's make sure we import our User model and we'll also use an Adonis' helper called Hash.

Hash will be responsible for hashing the new provided password, for security reasons.

'use stric'

const User = use('App/Models/User')
const Hash = use('Hash')
Enter fullscreen mode Exit fullscreen mode

Our controller will only require an update method, so inside our controller UpdateUserInfoController let's begin by creating our method:

class UpdateUserInfoController {
  async update ({ request, response, params }) {
Enter fullscreen mode Exit fullscreen mode

And with our logic in mind, we do the following:

  1. Let's get the new info sent by the user in the request:

2.

   const id = params.id
       const { username, password, newPassword } = request
         .only(['username', 'password', 'newPassword'])
Enter fullscreen mode Exit fullscreen mode
  1. Now look for the user in database (using the id):
   const user = await User.findByOrFail('id', id)
Enter fullscreen mode Exit fullscreen mode
  1. Check if password provided matches with the current one:
   const passwordCheck = await Hash.verify(password, user.password)

   if (!passwordCheck) {
         return response
           .status(400)
           .send({ message: { error: 'Incorrect password provided' } })
       }
Enter fullscreen mode Exit fullscreen mode
  1. With the password check done, we are able to update the users information:
   // updating user data
       user.username = username
       user.password = newPassword
Enter fullscreen mode Exit fullscreen mode
  1. To finish all we just need to persist the data in the database, by using the .save() method.
   await user.save()
Enter fullscreen mode Exit fullscreen mode

Your UpdateUserInfoController.js should look like this now:

'use strict'

const User = use('App/Models/User')
const Hash = use('Hash')

class UpdateUserInfoController {
  async update ({ request, response, params }) {
    const id = params.id
    const { username, password, newPassword } = request
      .only(['username', 'password', 'newPassword'])

    // looking for user in DB
    const user = await User.findByOrFail('id', id)

    // checking if old password informed is correct
    const passwordCheck = await Hash.verify(password, user.password)

    if (!passwordCheck) {
      return response
        .status(400)
        .send({ message: { error: 'Incorrect password provided' } })
    }

    // updating user data
    user.username = username
    user.password = newPassword

    // persisting new data (saving)
    await user.save()
  }
}

module.exports = UpdateUserInfoController

Enter fullscreen mode Exit fullscreen mode

Perfect! Now let's test our controller. Head to start/routes.js file

One thing that's really important here is that, some of our routes will only be accessible by an authenticated user, and Adonis' routing has a perfect method to deal with this, called group(). With group you can call a method called middleware and pass as an argument an array containing which middlewares should run before accesing the routes inside the group method.

Route.group(() => {
  // updating username and password
  Route.put('users/:id', 'UpdateUserInfoController.update')
}).middleware(['auth'])
Enter fullscreen mode Exit fullscreen mode

In our case, we only need the auth method, that's provided by default by Adonis. In a few moments we will test this route without and with an user athenticated

First, let's test this route without being authenticated:

This is the new information I want to save for my user:

{
    "password": "123456",
    "newPassword": "123",
    "username": "DanSilva"
}
Enter fullscreen mode Exit fullscreen mode

I won't show here how to send the request using Insomnia because I already did so in the first part

If I send the request while not being authenticated I'll receive a 401 error (unauthorized). For this to work, I must provide, within the request, a JWT token that I get when I sign in, so make sure to sign in to test this route.

After you sign in, copy the token the request returned to you. Create a new PUT method in Insomnia and right below the request URL there is a tab called "Auth". In the dropdown menu that opens select Bearer Token and in the token field paste the token you just copied.

Before we send the request again, let's look at our user's data in the database to make sure it updated after our request.

User info

Perfect. Now let's send the request. Just make sure that your URL is following this strucure

base_url/users/YOUR_USER_ID_HEre

Now send the request. If suceeded the request will return a 204, as we didn't set any message to return.

User info updated

See? The new user info is saved in our database!

Sending emails with AdonisJS

Before we continue to the part where we will create the controller to request a passsword recovery and set a new password with this recovery let's see how we configure Adonis to send emails.

The email provider is not installed by default so we must proceed with its installation. To do so just run the following command:

adonis install @adonisjs/mail
Enter fullscreen mode Exit fullscreen mode

Now we need to register the new provider in our application. Our providers array is located at start/app.js. Open that file and find the variable called providers. This variable is an array containing all the providers essential to make adonis work properly. Just add the following provider at the end of this array:

'@adonisjs/mail/providers/MailProvider'
Enter fullscreen mode Exit fullscreen mode

We still need to configure a few things before we can continue. We need a client to test the functionality of sending emails and there is a perfect tool suited for this task.

We will use Mailtrap. Mailtrap is, as stated in their own website:

Mailtrap is a fake SMTP server to test, view and share emails sent from the development and staging environments without spamming real customers.

When you create your account and go to https://mailtrap.io/inboxes you'll see a page saying you have no inboxes.

Mailtrap inboxes

Just click on "Create Inbox" to create a new one. Access your inbox and you'll be directly redirected to the SMTP setting tab. This is an important tab as we'll be using the information displayed here to finish our configuration in our adonis api.

Host:   smtp.mailtrap.io
Port:   25 or 465 or 2525
Username:   a218f0cd73b5a4
Password:   0a5b3c6c6acc17
Enter fullscreen mode Exit fullscreen mode

We'll insert this data above in our .env file to properly set our mailing service:

MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=465
MAIL_USERNAME=a218f0cd73b5a4
MAIL_PASSWORD=0a5b3c6c6acc17
Enter fullscreen mode Exit fullscreen mode

Make sure that MAIL_USERNAME and MAIL_PASSWORD matches with the data provided to you by mailtrap.

Now we need to head to app/mail.js to finish our email settings.

As we'll use SMTP, in the connection session of the file we'll leave as it is. Just make sure yours is the same as mine (in case you are using SMTP of coure):

connection: Env.get('MAIL_CONNECTION', 'smtp')
Enter fullscreen mode Exit fullscreen mode

Now head to the smtp object make sure it looks like this:

smtp: {
    driver: 'smtp', // make sure here is as SMTP
    pool: true,
    // using Env (provided by Adonis) to retriev the .env variables
    port: Env.get('MAIL_PORT'),
    host: Env.get('MAIL_HOST'),
    secure: false,
    auth: {
      user: Env.get('MAIL_USERNAME'),
      pass: Env.get('MAIL_PASSWORD')
    },
    maxConnections: 5,
    maxMessages: 100,
    rateLimit: 10
  },
Enter fullscreen mode Exit fullscreen mode

Awesome, if all this we finished to configure our application to send emails. It's not that much of a work to be done actually. We only took three steps:

  1. Install Adonis' mail provider
  2. Configure our environment variables to user a mailing service we desire
  3. Configured our mail.js file to grab the information from our environment variables

Requesting the password recovery

Let's start by requering the password recovery. You know when you click "Forgot password" and then you (usually) give your email and then you receive an email with a link to recover your password? So this is what we will be doing now.

For this we will need to check for a validity of the request, I mean, suppose you send your first request, then you have, let's say, 2 days to click on the link sent to you, otherwise it won't be valid anymore.

I'll use a token for the sake of this purpose, so before we begin, we need a token field in user's table in our database. As, at the beginning of our application, we already run the migration that creates the user table, we will need to run a new migration to update the table structure so we can add the token column.

To create a new migration run the command:

adonis make:migration user --action select
Enter fullscreen mode Exit fullscreen mode

Before we continue, let's just take a look at the structure of this command:

adonis make:migration MIGRATION_NAME --action ACTION_NAME(create, select)
Enter fullscreen mode Exit fullscreen mode
  • MIGRATION_NAME_HERE: The name of the migration. Here I recommend you to put the name of the table you are about to update
  • ACTION_NAME: You have to options:
    1. Create: This is used when you want to create a new table
    2. Select: Use it when you want to update the structure of an existing table

Now select the new migration file created, located at database/migrations

Inside you migration file, you'll see a class with two methods, up and down. For now let's focus on the up method as this one is used to create the update. The down method just reverse the alterations you made

up () {
    this.table('users', (table) => {
      // make alterations
    })
  }
Enter fullscreen mode Exit fullscreen mode

You can see that this.table() is making use of two arguments. The first one is the table's name. The value here will be automatically set based on your migration's name and in plural form. If you set, when creating the migration file, the name as user_update, this first argument here would be user_updates and you would probably run into some errors as you don't have table.

The second argument is the function that will run all your updates.

So as I said before, we will need a token field, but we'll also need a token_create_at field, so we can check its validity.

We will create 2 columns, one as a string to store our token and one as a timestamp, to store the moment our token was created.

up () {
    this.table('users', (table) => {
      table.string('token') // token
      table.timestamp('token_created_at') // date when token was created
    })
  }
Enter fullscreen mode Exit fullscreen mode

Just run our migration:

adonis migration:run
Enter fullscreen mode Exit fullscreen mode

Greate, now that we have our user's table updated we will begin by creating our controller. I'll call this one ForgotPassword.

adonis make:controller ForgotPassword --type http
Enter fullscreen mode Exit fullscreen mode

Let's begin by creating the store() method, that will be responsible for dealing with the request of password recovery. This will generate the token and send the email to the user.

First, out of our class, let's import everything we need:

'use strict'

const User = use('App/Models/User') // user model
const Mail = use('Mail') // Adonis' mail

const moment = require('moment') // moment (RUN NPM INSTALL MOMENT)
const crypto = require('crypto') // crypto
Enter fullscreen mode Exit fullscreen mode

We'll need to get the user email, and find him in the database:

// account request password recovery
const { email } = request.only(['email'])

// checking if email is registered
const user = await User.findByOrFail('email', email)
Enter fullscreen mode Exit fullscreen mode

After that we'll generate the token. To generate the token we will use crypto, a native feature that comes with NodeJS. (You can find more info about crypto here: https://nodejs.org/api/crypto.html)

// generating token
const token = await crypto.randomBytes(10).toString('hex')
Enter fullscreen mode Exit fullscreen mode

After we generate the token we convert is to a string using toString().

Now we need to set the validity of the token. For this we need to store the moment the token was created:

user.token_created_at = new Date()
Enter fullscreen mode Exit fullscreen mode

Then we proceed by saving the token in the database and persisting all the information:

user.token = token

// persisting data (saving)
await user.save()
Enter fullscreen mode Exit fullscreen mode

With all that done, we'll send the email to the user:

await Mail.send('emails.recover', { user, token }, (message) => {
    message
        .from('support@danmiranda.io')
        .to(email)
})
Enter fullscreen mode Exit fullscreen mode

Mail.send() uses three parameters:

  1. The email template (we'll talk about that in a moment)
  2. The variables to be sent to the template
  3. The callback function to set things like: from, to, subject, attachements, etc...

First let's talk about the template. As we created this adonis application as an api only, we need to register the view provider in the providers list in start/app.js

'@adonisjs/framework/providers/ViewProvider'
Enter fullscreen mode Exit fullscreen mode

All our views, must be kepts in the directory resources/views, so in the root of the project create the folder resources and inside the folder create views folder. Now inside this folder you can organize your views as you wish. For example, in our application I'll store our email templates inside a emails folder. By now you may have a folder strucure looking like this:

├── resources
│   └── views
│       └── emails
│           └── recover.edge 
Enter fullscreen mode Exit fullscreen mode

recover.edge is our template file. Edge is the official template engine built for AdonisJS. It's syntax looks pretty the same as an HTML file, so there won't be any learning curve for this template.

You can build the text inside this template any way you prefer, but I'll put the one I'm using for you to follow

<h1>Password recovery request</h1>
<p>
  Hello {{ user.username }}, it seems someone requested a password recovery
  for your account registered with the email {{ user.email }}.
</p>

<p>
  If it was you, just click this
<a href="http://127.0.0.1:3333/users/forgotPassword/{{token}}/{{user.email}}">link</a>
</p>

<p>
  If it wasn't you then we recommend you to change your password. Someone may
  have stolen it. 🕵️‍🕵️‍🕵️‍🕵️‍🕵️‍🕵️‍🕵️
</p>
Enter fullscreen mode Exit fullscreen mode

The most important part to note here is the use of double brackets {{}}. You use this syntax to access variable you pass to the template. In our case above we are retrieving the user's username, the user's email and the token.

Now let's take a look back at our Mail.send() function:

await Mail.send('emails.recover', { user, token }, (message) => {
    message
        .from('support@danmiranda.io')
        .to(email)
})
Enter fullscreen mode Exit fullscreen mode

As we previously said, the first argument is the template. As adonis will directly read the directory resources/views we only need to specify the remaining directory inside this views folder. Because we first created a folder called emails and then we stored our template we specify it in the first argument in a syntax like accessing an javascript's object's property, in our case emails.recover.

The second argument, our variables { user, token }. Here we'll send the whole user object so we don't need to pass a lot of variables inside here.

And finally, the third argument, the callback function. In our example we'll only set the from() address and the to() address. If you want to see other options you have available please click this link.

For now, your store method must look like this:

async store ({ request }) {
    try {
      // account request password recovery
      const { email } = request.only(['email'])

      // checking if email is registered
      const user = await User.findByOrFail('email', email)

      // generating token
      const token = await crypto.randomBytes(10).toString('hex')

      // registering when token was created and saving token
      user.token_created_at = new Date()
      user.token = token

      // persisting data (saving)
      await user.save()

      await Mail.send('emails.recover', { user, token }, (message) => {
        message
          .from('support@danmiranda.io')
          .to(email)
      })

      return user
    } catch (err) {
      console.log(err)
    }
Enter fullscreen mode Exit fullscreen mode

Let's add a route to deal with this request.

Route.post('users/forgotPassword', 'ForgotPasswordController.store')
Enter fullscreen mode Exit fullscreen mode

When you test the request, our request will return our user, so you'll be able to see the token generated:

{
  "id": 10,
  "username": "DanSilva",
  "email": "danmiranda@danmiranda.io",
  "password": "$2a$10$3p5Ci56Zc2h7i0nC7NrfFuuorTuS/7qdAPjudPBwDTzvYrZLbOa8i",
  "created_at": "2019-03-03 15:40:02",
  "updated_at": "2019-03-04 22:49:59",
  "token": "79ee3379e35eeabdbcca", // HERE IS THE TOKEN
  "token_created_at": "2019-03-05T01:49:59.958Z"
}
Enter fullscreen mode Exit fullscreen mode

Also, go to your mailtrap's inbox and you'll probably see the email sent

Mailtrap email

Great! We finished creating the controller to handle with the recover password request. In the next and final section we'll create the method to update the password to a new one, based on the token and if it's still valid or not

Updating and recovering the password

If you look at the link sent within the email you see something like this:

http://127.0.0.1:3333/users/forgotPassword/79ee3379e35eeabdbcca/danmiranda@danmiranda.io

It's basically following this structure:

base_url/users/forgotPassword/:token/:email

We will use this url to set our route that will trigger ou controller's method.

The controller's update method will follow by this logic:

  • We get the token and the user's email coming in the URL request
  • We get the new password the user desires
  • Look for user (using the email) in the database
  • Checks if the token coming from the URL is still the same in the database (usefull in case the user requested a new password recovery an tries to use an old link)
  • Check if token is still valid
  • Update password and reset token

Let's get to work then...

To get parameters coming in the URL we use the params coming from the request context.

async update ({ request, response, params }) {
    const tokenProvided = params.token // retrieving token in URL
    const emailRequesting = params.email // email requesting recovery
Enter fullscreen mode Exit fullscreen mode

Now the new password the user wants

const { newPassword } = request.only(['newPassword'])
Enter fullscreen mode Exit fullscreen mode

Let's look for the user

const user = await User.findByOrFail('email', emailRequesting)
Enter fullscreen mode Exit fullscreen mode

Now we work with the token, firts checking if the link is not using an old token, and then checking if the current token is still valid

// checking if token is still the same
// just to make sure that the user is not using an old link
// after requesting the password recovery again
const sameToken = tokenProvided === user.token

if (!sameToken) {
    return response
        .status(401)
        .send({ message: {
            error: 'Old token provided or token already used'
        } })
}

// checking if token is still valid (48 hour period)
const tokenExpired = moment()
.subtract(2, 'days')
.isAfter(user.token_created_at)

if (tokenExpired) {
    return response.status(401).send({ message: { error: 'Token expired' } })
}
Enter fullscreen mode Exit fullscreen mode

And finally, with all the checks for the token provided done, and successfully passed we update the password and reset the token:

// saving new password
user.password = newPassword

// deleting current token
user.token = null
user.token_created_at = 0

// persisting data (saving)
await user.save()
Enter fullscreen mode Exit fullscreen mode

Your update() method should be like this by now:

async update ({ request, response, params }) {
    const tokenProvided = params.token // retrieving token in URL
    const emailRequesting = params.email // email requesting recovery

    const { newPassword } = request.only(['newPassword'])

    // looking for user with the registered email
    const user = await User.findByOrFail('email', emailRequesting)

    // checking if token is still the same
    // just to make sure that the user is not using an old link
    // after requesting the password recovery again
    const sameToken = tokenProvided === user.token

    if (!sameToken) {
      return response
        .status(401)
        .send({ message: {
          error: 'Old token provided or token already used'
        } })
    }

    // checking if token is still valid (48 hour period)
    const tokenExpired = moment()
      .subtract(2, 'days')
      .isAfter(user.token_created_at)

    if (tokenExpired) {
      return response.status(401).send({ message: { error: 'Token expired' } })
    }

    // saving new password
    user.password = newPassword

    // deleting current token
    user.token = null
    user.token_created_at = 0

    // persisting data (saving)
    await user.save()
  }
Enter fullscreen mode Exit fullscreen mode

And your whole ForgotPassowrdController should be like this:

'use strict'

const User = use('App/Models/User')
const Mail = use('Mail')

const moment = require('moment')
const crypto = require('crypto')

class ForgotPasswordController {
  /**
   * this method will store a new request made by the user
   * when he requires a password recover it'll generate a
   * token to allow him to reset his password
   */
  async store ({ request }) {
    try {
      // account request password recovery
      const { email } = request.only(['email'])

      // checking if email is registered
      const user = await User.findByOrFail('email', email)

      // generating token
      const token = await crypto.randomBytes(10).toString('hex')

      // registering when token was created and saving token
      user.token_created_at = new Date()
      user.token = token

      // persisting data (saving)
      await user.save()

      await Mail.send('emails.recover', { user, token }, (message) => {
        message
          .from('support@danmiranda.io')
          .to(email)
      })

      return user
    } catch (err) {
      console.log(err)
    }
  }

  async update ({ request, response, params }) {
    const tokenProvided = params.token // retrieving token in URL
    const emailRequesting = params.email // email requesting recovery

    const { newPassword } = request.only(['newPassword'])

    // looking for user with the registered email
    const user = await User.findByOrFail('email', emailRequesting)

    // checking if token is still the same
    // just to make sure that the user is not using an old link
    // after requesting the password recovery again
    const sameToken = tokenProvided === user.token

    if (!sameToken) {
      return response
        .status(401)
        .send({ message: {
          error: 'Old token provided or token already used'
        } })
    }

    // checking if token is still valid (48 hour period)
    const tokenExpired = moment()
      .subtract(2, 'days')
      .isAfter(user.token_created_at)

    if (tokenExpired) {
      return response.status(401).send({ message: { error: 'Token expired' } })
    }

    // saving new password
    user.password = newPassword

    // deleting current token
    user.token = null
    user.token_created_at = 0

    // persisting data (saving)
    await user.save()
  }
}

module.exports = ForgotPasswordController

Enter fullscreen mode Exit fullscreen mode

Now let's test our last method. First let's add the route, following that structure I previously mentioned:

base_url/users/forgotPassword/:token/:email and add a PUT request in our route

Route.put('users/forgotPassword/:token/:email', 'ForgotPasswordController.update')
Enter fullscreen mode Exit fullscreen mode

First, I'll test a case where the token number is incorrect:

With a request like this I'll receive as return a 401 error and the following JSON:

{
  "message": {
    "error": "Old token provided or token already used"
  }
}
Enter fullscreen mode Exit fullscreen mode

The last, example, before we test the successful one is an invalid token. To test this I'll manually alter in the database the date the token was generated to more then 2 days ago.

With that, I'll also receive a 401 error and a JSON telling the token is expired

Now the test we were waiting for. I changed again the date the token was created to match the limit of 2 days. I won't receive any message in the body, just a 204 status. In this example I set the new password to "12".

If I try to sign in with the old password, "123456" I'll receive an error, but if I try with the new password, "12", everything should be fine.

Wow!! This one was a long post, so I'll stop here for now. The next one will cover the steps for a user to create a new appointment, I'll see you there!!

Top comments (16)

Collapse
 
mavguimaraes profile image
mavguimaraes

I am studying your tutorial on Building an API with Adonisjs (2nd part) and when I click on the link to recover the password I get the following:
HttpException
E_ROUTE_NOT_FOUND: Route not found GET /users/forgotPassword/ba101e9c3d63dd821ab2/maguimar@fe.up.pt
I do not know what could be wrong since the code is the same as the tutorial

Collapse
 
nilomiranda profile image
Danilo Miranda

Hey Mav, really happy to see that you are using my guide, I hope it's of good use for you.

Are you clicking in the link generated inside the email? Or are you just copying the token up to the email part of the link?

If you wish, send me a DM on twitter and I'll help you out 🙂🙂

Collapse
 
njoanc profile image
NYIRAMWIZA

I have the same issue. I am clicking on the link found inside the email. Even when I try with postman, I get the same error

Thread Thread
 
10yine profile image
michael elias

hello

Collapse
 
njoanc profile image
NYIRAMWIZA

I have the same issue

Collapse
 
fozz2k profile image
Mark Forrest

Use PUT not GET in your REST client

Collapse
 
christopaixao profile image
Christopher Paixão

ótima série de posts!
vc poderia me ajudar com um problema que estou tendo?
estou com uma api no adonisjs sendo consumida pelo ionic,
gostaria que ao fazer login o usuario fosse direcionado para uma pagina de perfil, esta página é acessada por localhost:8100/profile/IDUSUARIO.
Colocando o ID manualmente eu consigo visualizar o perfil do usuário porém ao fazer login eu quero retorna o id no resultado.

método login no adonisjs:

async login({ request, auth, response }) {
    const email = request.input("email")
    const password = request.input("password");
    try {
        if (await auth.attempt(email, password, id)) {
            let user = await User.findBy('email', email)
            let accessToken = await auth.generate(user)
            return response.json({ "user": user, "access_token": accessToken})

        }

    }
    catch (e) {
        return response.json({ message: 'You first need to register!' })
    }
}
Collapse
 
iampick profile image
iampick

Hi,

Thank you for your guide. I have a problem when tried to use JWT Auth with insomnia. Where can I check this?

Thank you.

Collapse
 
nilomiranda profile image
Danilo Miranda

Hi,

How are you sending the token via insomnia?

Collapse
 
njoanc profile image
NYIRAMWIZA

Hey! Here how I am sending the token via postman

127.0.0.1:3333/reset/7687930908790...

Collapse
 
arapl3y profile image
Alex Rapley

Thanks for the tutorials! This line was causing me issues: user.token_created_at = 0

However changing it to: user.token_created_at = null fixed the issue

Collapse
 
aleishizawa profile image
Alessandro Navarro Ishizawa

Olá Danilo, muito bom e útil essa série de posts !!!
Estou seguindo os steps, o e-mail está sendo enviado, tokens gerados, só tenho uma dúvida, estou fazendo esse modelo de redefinição de senha para um app em react-native, quando clico no link do e-mail(e abre pelo navegador), da o erro de rota HttpException
E_ROUTE_NOT_FOUND: Route not found GET /users/forgotPassword, pois como no tutorial ela foi definida como PUT, deveria criar outra rota GET para pegar esses parâmetros via URL, ou alterar a mesma para GET ? o que sugere ?

Collapse
 
anthony4m profile image
Anthony Amoah

how do i signin

Collapse
 
raphaeljansson profile image
RaphaelJansson

I had a problem with Mail then I used Nodemailer, it worked very well.

Collapse
 
anthony4m profile image
Anthony Amoah

am following the series and i'm running to a problem i am trying to login using the login function but i keep getting 401 error i dont know what i am doing wrong

Collapse
 
necati12 profile image
necati12

Hi,
I like your work and I can say you. a great job..