Ready to get started?

Join Adocasts Plus for $8/mo, or sign into an existing Adocasts Plus account, to get access to all of our lessons.

robot mascot smiling

Building with AdonisJS & Inertia #5.0

User Registration with InertiaJS

In This Lesson

We'll complete our user registration flow by validating our registration form data, creating a new user, logging that user in, and forwarding them to the next page in the flow.

Created by
@tomgobich
Published

Chapters

00:00 - Creating Our Register Controller
01:05 - Using Our Register Controller Methods On Our Routes
02:40 - Handling Our Register Form Submission
03:39 - Introducing Actions
05:45 - Completing Our WebRegister Action
07:20 - Injecting & Using Our WebRegister Action
07:56 - Testing Our Registration Flow

Join the Discussion 8 comments

Create a free account to join in on the discussion
  1. @tdturn2
    @tdturn2

    When changing over routes to:

    router.get('/register', [RegisterController, 'show']).as('register.show').use(middleware.guest())

    I'm' getting Error: The import "#controllers/auth/register_controller" is not imported dynamically from start/routes.ts. You must use dynamic import to make it reloadable (HMR) with hot-hook.

    is my register_controller.ts not setup or registered properly?

    I can work around this, but not as clean as your walkthrough

    router.get('/register', async (ctx) => { const { default: RegisterController } = await import('#controllers/auth/register_controller') return new RegisterController().show(ctx) }).as('register.show').use(middleware.guest())

    1
    1. Responding to tdturn2
      @tomgobich

      Hi tdturn2!

      First, your import is valid and will work, it just won't work with hot module reloading (HMR). HMR is enabled by default in newer AdonisJS 6 projects. Rather than fully restarting the application when a change is made in development, HMR enables the single updated spot to be updated on the fly.

      AdonisJS uses NodeJS Loader Hooks to perform HMR, and loader hooks require dynamic imports to work and hot-swap modules when updated. If you'd like to read more on this, they walk through the full what & whys in the documentation.

      So rather than importing your controller like this, which will not work with HMR:

      import RegisterController from '#controllers/auth/register_controller'
      
      router.get('/register', [RegisterController, 'show'])
      Copied!

      You can instead dynamically import the controller, which will work with HMR:

      const RegisterController = import('#controllers/auth/register_controller')
      
      router.get('/register', [RegisterController, 'show'])
      Copied!

      If you have your text editor fix lint errors on save, this change will happen automatically when you save your file. You'll see this happen for me in the lesson around the 2:20 mark. If you're using VSCode, you can turn this on by installing the ESLint extension, and then adding the below to your JSON user settings.

      {
        "editor.codeActionsOnSave": {
          "source.fixAll.eslint": "explicit"
        },
      }
      Copied!
      2
      1. Responding to tomgobich
        @tdturn2
        @tdturn2

        Awesome, that was it! Thank you!

        1
        1. Responding to tdturn2
          @tomgobich

          Anytime!! 😊

          1
  2. @gregory

    Hello Tom,

    I would like to implement email verification, but I’m not sure what I should do in the store method of the RegisterController.

    I would like to show this message to the user after registration:

    Verify your email

    You need to verify your email to activate your account..

    A verification link has been sent to your email address. Please check your inbox and click the link to verify your email.

    The link is valid for the next 15 minutes.

    If you don’t see the email, check your spam or junk folder.

    Should I use return inertia.render('auth/email-verification') or return response.redirect().toRoute('email.verify.index')?

    export default class RegisterController {
      async show({ inertia }: HttpContext) {
        return inertia.render('auth/register')
      }
    
      async store({ request, response, inertia }: HttpContext) {
        const payload = await request.validateUsing(registerValidator)
        const role = await Role.getBySlug('user')
    
        const user = await User.create({
          roleId: role.id,
          firstName: payload.firstName,
          lastName: payload.lastName,
          unconfirmedEmail: payload.email,
          password: payload.password,
        })
    
        const signedUrl = router
          .builder()
          .prefixUrl(env.get('BASE_URL'))
          .params({
            id: user.id,
          })
          .qs({
            email: encryption.encrypt(user.unconfirmedEmail, '15 minutes'),
          })
          .makeSigned('email.verify', {
            expiresIn: '15 minutes',
          })
    
        await mail.sendLater(new VerificationEmailNotification(user, signedUrl))
    
        // return response.redirect().toRoute('email.verify.index')
        // or
        // return inertia.render('auth/email-verification')
      }
    }
    
    Copied!

    If I use inertia.render('auth/email-verification'), it will display my React page but remain on app_url/register.

    router
      .get('/email/verify/:id', [EmailVerificationController, 'verify'])
      .as('email.verify')
      .use(middleware.guest())
    
    export default class EmailVerificationController {
      async verify({ request, response }: HttpContext) {
        if (!request.hasValidSignature()) {
          return response.badRequest('Invalid or expired URL')
        }
    
        const email = encryption.decrypt(request.qs()['email'] ?? '')
        const id = request.param('id')
        const user = await User.findOrFail(id)
    
        if (email !== user.unconfirmedEmail) {
          return response.badRequest('Invalid or expired URL')
        }
    
        user.status = UserStatus.ACTIVE
        user.email = user.unconfirmedEmail
        user.unconfirmedEmail = null
        user.verifiedAt = DateTime.now()
        await user.save()
    
        console.log(user)
      }
    }
    
    Copied!

    If I use return response.redirect().toRoute('email.verify.index'), it will redirect to app_url/email/verify/sent. This mean I will need to declare two routes instead of one.

    router
      .get('/email/verify/sent', [EmailVerificationController, 'index'])
      .as('email.verify.index')
      .use(middleware.guest())
    router
      .get('/email/verify/:id', [EmailVerificationController, 'verify'])
      .as('email.verify')
      .use(middleware.guest())
    
    export default class EmailVerificationController {
      async index({ inertia }: HttpContext) {
        return inertia.render('auth/email-verification')
      }
    
      async verify({ request, response }: HttpContext) {
        // logic here
      }
    }
    Copied!
    1
    1. Responding to gregory
      @tomgobich

      Hi @gregory! Great question! I would opt for redirecting to /email/verify/sent because if the user refreshes the page, with this being a URL of its own, they'll be returned right back to this message. If you were to just return inertia.render() this page, if the user refreshes, they'll be shown the registration form again since they aren't logged in within RegisterController.store. That might cause them to question if they actually registered, resulting in them attempting to register again.

      0
  3. @usources
    @usources

    Hi! I'm trying to add a new confirmPassword field only for validation purposes; it has no effect on the user model.

    Everything looks good until creating the user model because the user model does not have a confirmPassword field.

    Is there any way to omit a value from the validateUsing function?

    I followed this approach, but I'm not sure if it's the best one:

    import User from '#models/user'
    import { registerValidator } from '#validators/auth'
    import { inject } from '@adonisjs/core'
    import { HttpContext } from '@adonisjs/core/http'
    import { Infer } from '@vinejs/vine/types'
    
    type Params = {
      data: Infer<typeof registerValidator>
    }
    
    @inject()
    export default class WebRegister {
      constructor(protected ctx: HttpContext) {}
    
      async handle({ data }: Params) {
        const { confirmPassword, ...userData } = data
    
        const user = await User.create(userData)
    
        await this.ctx?.auth.use('web').login(user)
    
        return { user }
      }
    }
    
    Copied!
    0
    1. Responding to usources
      @tomgobich

      Hi usources!

      Yeah, what you're doing there is perfectly valid, and what I do in most cases where I don't need to omit a ton of fields. Alternatively, you could use an omit utility, like what is offered from Lodash, but what you have is absolutely fine!

      1