As you've probably noticed, the AdonisJS starter kit came with authentication working out of the box. I firmly believe in learning by doing, so in this lesson, all we're going to do is remove the route-level portions of our authentication that've been provided by the starter kit. We'll redo it ourselves so that we're familiar with everything that's going on.
So, to start, we can delete the route definitions that've been provided to us in our start/routes.ts file. You should just be left with the below after removing.
import { controllers } from '#generated/controllers' import { middleware } from '#start/kernel' import router from '@adonisjs/core/services/router' router.where('id', router.matchers.number()) router.on('/').render('pages/home').as('home') router.on('/terms').render('pages/terms') router .resource('challenges', controllers.Challenges) .use(['index', 'edit', 'create'], middleware.requestLogger())Copied!
- start
- routes.ts
Next, we can remove the new_account_controller, session_controller, and user validator.
rm app/controllers/new_account_controller.ts && rm app/controllers/session_controller.ts && rm app/validators/user.tsCopied!
Perfect! Now all that remains are config-level things that we'll cover as we go through implementing authentication. Then, we've already learned through everything happening in our auth pages, so we can leave those as well and just do a quick refresher.
Starting with our login page.
@layout() <div class="form-container"> <div> <h1> Login </h1> <p> Enter your email and password below to log in </p> </div> <div> @form({ route: 'session.store', method: 'POST' }) <div> @field.root({ name: 'email' }) @!field.label({ text: 'Email' }) @!input.control({ type: 'email' }) @!field.error() @end </div> <div> @field.root({ name: 'password' }) @!field.label({ text: 'Password' }) @!input.control({ type: 'password' }) @!field.error() @end </div> <div> @!button({ text: 'Login', type: 'submit' }) </div> @end </div> </div> @endCopied!
- resources
- views
- pages
- auth
- login.edge
For starters, we have the form component that accepts a method, route, routeParams, and routeOptions to build out the final action URL for the form's submission. This component will also add our CSRF field when performing POST, PUT, PATCH, and DELETE requests.
Then, we have the field components that build out the input fields. The root injects the fields' context, which includes the input's name, old value from the previous submission, and error state. The label, control, and error then use this context to build out their applicable section of the input.
Finally, the button adds a submit button to our form!
Then, the only difference in our signup page is the addition of two fields.
@layout() <div class="form-container"> <div> <h1> Signup </h1> <p> Enter your details below to create your account </p> </div> <div> @form({ route: 'new_account.store', method: 'POST' }) <div> @field.root({ name: 'fullName' }) @!field.label({ text: 'Full name' }) @!input.control() @!field.error() @end </div> <div> @field.root({ name: 'email' }) @!field.label({ text: 'Email' }) @!input.control({ type: 'email', autocomplete: 'email' }) @!field.error() @end </div> <div> @field.root({ name: 'password' }) @!field.label({ text: 'Password' }) @!input.control({ type: 'password', autocomplete: 'new-password' }) @!field.error() @end </div> <div> @field.root({ name: 'passwordConfirmation' }) @!field.label({ text: 'Confirm password' }) @!input.control({ type: 'password', autocomplete: 'new-password' }) @!field.error() @end </div> <div> @!button({ text: 'Sign up', type: 'submit' }) </div> @end </div> </div> @endCopied!
- resources
- views
- pages
- auth
- signup.edge
The two added fields here are for the user's fullName and then passwordConfirmation to ensure the user knows the password they've entered. An alternative here would be a single password field with a show/hide value toggle.
Lastly, we have in our header partial an if/else for our authentication.
<header> <div> <div> @link({ route: 'home', text: 'Home' }) {{-- svg ... --}} @end </div> <div> <nav> @if(auth.user) @!avatar({ initials: auth.user.initials }) @form({ route: 'session.destroy', method: 'POST' }) @!button({ text: 'Logout' }) @end @else @!link({ route: 'new_account.create', text: 'Signup' }) @!link({ route: 'session.create', text: 'Login' }) @end </nav> </div> </div> </header>Copied!
- resource
- views
- partials
- header.edge
If the user is logged in and the user has been populated for the request, auth.user will contain the authenticated user. So, when that is truthy, we'll show an avatar with the user's initials and a form to logout.
Note that the logout form sends a POST request. This is done to prevent CSRF attacks. Someone could force users to logout through malicious means if we were to use a GET request. Plus, among other things, it also helps protect against a user accidentally logging out due to pre-fetching. Pre-fetching is when a page is eagerly loaded before the user actually clicks the link.
If the user is not logged in, links to signup or login will be shown. Now, since we've deleted these routes, any pages referencing these routes will now throw a route not found error. Since our header is everywhere, we'll go ahead and just remove this section for now, and we'll add it back as we reimplement things.
<header> <div> <div> @link({ route: 'home', text: 'Home' }) {{-- svg ... --}} @end </div> <div> <nav> {{-- TODO --}} </nav> </div> </div> </header>Copied!
- resource
- views
- partials
- header.edge
Great, let's get started!