Skip to content

Latest commit

 

History

History
202 lines (150 loc) · 5.74 KB

10-auth-service.md

File metadata and controls

202 lines (150 loc) · 5.74 KB

Authentication Service & Login Flow

Services allow state and functionality (i.e., regular functions, actions) to be shared across various parts of an Ember app.

In our case, there are various things that may need to "see" or "use" authentication state/functionality (i.e., a currentUser, the logOut and logIn functions, etc...):

  • Creating a new chat message
  • Preventing unauthenticated users from entering the app
  • Logging in
  • Logging out

We could use a component for this, but it would increase the complexity of our templates and would involve passing extra named args (the things that look like {{ @firstName }}) through the component tree.

<Auth as |authApi|>
  <TeamSelector @auth={{authApi}} />
  <TeamSidebar @auth={{authApi}} />
  <main class="flex-1 flex flex-col bg-white overflow-hidden channel">
    <ChannelHeader @auth={{authApi}} />

    <div class="py-4 flex-1 overflow-y-scroll channel-messages-list" role="list">
      <ChatMessage />
      <ChatMessage />
    </div>

    <ChannelFooter @auth={{authApi}} />
  </main>
</Auth>

This is pretty ugly, and gets uglier as more of these cross-cutting areas (state and functionality that many parts of the app need to know about) are added, and more things need to access them. Thankfully, Services allow a better way of accomplishing the same thing...


💡 Mike's Tip: Services in Angular & React

Sharing state "horizontally" across many different concerns in an app is not unique to Ember.js. React's Context API and Angular's Dependency Injection system are designed to solve the same problem.


Run the following:

ember generate service auth

This will result in two new files being created.

First, let's flesh out the service so that we can use it in a few places.

import Service, { inject as service } from '@ember/service';

const AUTH_KEY = 'shlack-auth-id';

export default class AuthService extends Service {
  /**
   * @type {Router}
   */
  @service router;

  /**
   *
   * @param {string} userId
   */
  loginWithUserId(userId) {
    window.localStorage.setItem(AUTH_KEY, userId);
    this.router.transitionTo('teams');
  }

  get currentUserId() {
    return window.localStorage.getItem(AUTH_KEY);
  }
}

Delegating "logging in" to the login component

Let's use this service in our <LoginForm /> component in app/components/login-form.js.

Start by adding these imports:

import { inject as service } from '@ember/service';
import AuthService from 'shlack/services/auth';

Inject the service onto the component.

  /**
   * @type {AuthService}
   */
  @service auth;

Update the handleSignIn function to make use of it.

   handleSignIn(value) {
-    console.log(value);
+    if (typeof value === 'string' && value.length > 0)
+      this.auth.loginWithUserId(value);
   }

Rendering authentication data in the chat UI

Let's show the ID of the currently logged in user in the chat sidebar component. We just have a .hbs file so far, so let's upgrade it to a proper component.

BE SURE NOT TO OVERWRITE THE TEMPLATE

ember generate component team-sidebar

In the newly-created app/components/team-sidebar.js, add the following service injection:

 import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import AuthService from 'shlack/services/auth';

 export default class TeamSidebarComponent extends Component {
+  /**
+   * @type {AuthService}
+   */
+  @service auth;
 }

In app/templates/components/team-sidebar.hbs use the currentUserId value from the service.

         <span class="team-sidebar__current-user-name text-white opacity-75 text-sm">
-          Mike North
+          Mike North ({{this.auth.currentUserId}})
         </span>
       </div>
     </div>

Tests

Update the component test at tests/integration/components/team-sidebar-test.js so that it uses the following assertion:

assert.deepEqual(
  this.element.textContent
    .trim()
    .replace(/\s*\n+\s*/g, '\n')
    .split('\n'),
  ['LinkedIn', 'Mike North (1)', 'Channels', '#', 'general', 'Logout']
);

Create a new acceptance test for logging in.

ember generate acceptance-test login

Open tests/acceptance/login-test.js and replace the example assertions with:

test('starting logged out, then logging in', async function(assert) {
await visit('/login');
assert.equal(currentURL(), '/login');

await fillIn('select', '1');
await click('form input[type="submit"]');

assert.equal(currentURL(), '/teams');
});

Being sure to import what you need from @ember/test-helpers.

import { visit, currentURL, fillIn, click } from '@ember/test-helpers';

Completed File

view here