Amplication & Angular: Аутентификация на фронтенде

Добро пожаловать в этот учебник по созданию полнофункционального приложения с помощью Amplication.

Мы будем шаг за шагом создавать приложение Todos, используя Angular для фронтенда и Amplication для бэкенда.

Если вы застряли, у вас возникли вопросы или вы просто хотите поздороваться с другими разработчиками Amplication, такими же как вы, то присоединяйтесь к нашему Discord!


Оглавление

  • Шаг 1 — Добавление модуля HttpClientModule
  • Шаг 2 — Запросы авторизации
  • Шаг 3 — Компонент Auth
  • Шаг 4 — Вход в систему
  • Шаг 5 — Подведение итогов

Шаг 1 — Добавление модуля HttpClientModule

Чтобы позволить пользователям войти в приложение Todos, нам нужно будет запросить у них имя пользователя и пароль, а затем проверить их на бэкенде. Для выполнения HTTP-запроса к бэкенду мы будем использовать Angular HttpClientModule. Сначала откройте web/src/app/app.module.ts и добавьте import HttpClientModule:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
+ import { HttpClientModule } from '@angular/common/http';
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем добавьте HttpClientModule в imports в декораторе @NgModule:

@NgModule({
   declarations: [
      AppComponent,
      TaskComponent,
      TasksComponent,
      CreateTaskComponent
   ],
   imports: [
      BrowserModule,
      ReactiveFormsModule,
+      HttpClientModule
   ],
   providers: [],
   bootstrap: [AppComponent]
})
export class AppModule { }
Вход в полноэкранный режим Выход из полноэкранного режима

Мы захотим абстрагировать некоторые переменные, такие как наш API url, в многократно используемый ресурс. В web/src/environments/environment.ts и web/src/environments/environment.prod.ts добавьте следующие свойства в экспорт environment:

export const environment = {
   production: false,
+   apiUrl: 'http://localhost:3000',
+   jwtKey: 'accessToken',
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы хотим настроить Angular HttpClientModule на использование токена доступа пользователя при выполнении запросов к бэкенду и легкий доступ к библиотеке axios, поэтому нам понадобится настроить перехватчик, а также некоторые другие функции. В терминале перейдите в каталог web и запустите:

ng g s JWT
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем замените содержимое сгенерированного файла (web/src/app/jwt.service.ts) следующим кодом:

import { Injectable } from '@angular/core';
import {
   HttpInterceptor,
   HttpEvent,
   HttpRequest,
   HttpHandler,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';

@Injectable({
   providedIn: 'root',
})
export class JWTService implements HttpInterceptor {
   get jwt(): string {
      return localStorage.getItem(environment.jwtKey) || '';
   }

   set jwt(accessToken: string) {
      localStorage.setItem(environment.jwtKey, accessToken);
   }

   get isStoredJwt(): boolean {
      return Boolean(this.jwt);
   }

   intercept(
      request: HttpRequest<any>,
      next: HttpHandler
   ): Observable<HttpEvent<any>> {
      if (request.url.startsWith(environment.apiUrl)) {
         request = request.clone({
            setHeaders: { Authorization: `Bearer ${this.jwt}` },
         });
      }

      return next.handle(request);
   }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь каждый запрос, который делает Angular HttpClientModule, будет принимать токен доступа JWT пользователя, который будет храниться в локальном хранилище, и присваивать его заголовку Authorization каждого запроса.

Кроме того, мы добавили getter, который проверяет, существует ли уже токен доступа в локальном хранилище, и setter для сохранения токена доступа в локальном хранилище.

Наконец, нам нужно настроить JWTService в AppModule. Откройте web/src/app/app.module.ts и импортируйте JWTService и HTTP_INTERCEPTORS:

- import { HttpClientModule } from '@angular/common/http';
+ import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

+ import { JWTService } from './jwt.service';

import { AppComponent } from './app.component';
Вход в полноэкранный режим Выход из полноэкранного режима

Затем добавьте и настройте JWTService в providers декоратора @NgModule:

-  providers: [],
+  providers: [
+     { provide: HTTP_INTERCEPTORS, useClass: JWTService, multi: true },
+  ],
   bootstrap: [AppComponent]
})
export class AppModule { }
Вход в полноэкранный режим Выход из полноэкранного режима

Шаг 2 — Запросы авторизации

Вместо того чтобы вызывать конечные точки API непосредственно из наших компонентов, мы абстрагируем логику запросов, чтобы в случае необходимости внести изменения в поведение кода мы могли сделать это в одном месте.

В терминале перейдите в каталог web и запустите:

ng g s auth
Войти в полноэкранный режим Выйти из полноэкранного режима

В верхней части вновь созданного файла (web/src/app/auth.service.ts) мы импортируем JWTService и HttpClient и некоторые другие зависимости.

import { Injectable } from '@angular/core';
+ import { HttpClient } from '@angular/common/http';
+ import { of } from 'rxjs';
+ import { catchError, mergeMap } from 'rxjs/operators';
+ import { JWTService } from './jwt.service';
+ import { environment } from '../environments/environment';
Вход в полноэкранный режим Выход из полноэкранного режима

В AuthService установите JWTService и HttpClient в качестве аргументов конструктора:

export class AuthService {
   constructor(private http: HttpClient, private jwt: JWTService) { }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь добавьте метод me:

me() {
   const url = new URL('/api/me', environment.apiUrl).href;
   return this.jwt.isStoredJwt
      ? this.http.get(url).pipe(catchError(() => of(null)))
      : of(null);
}
Войти в полноэкранный режим Выйти из полноэкранного режима

me проверит, сохранен ли у нас маркер доступа, потому что если его нет, то этот запрос не будет успешным. Если токен существует, то будет сделан запрос GET к конечной точке /api/me, которую мы создали в шаге 3. При успешном выполнении запроса будет возвращен объект user текущего пользователя.

Далее добавьте метод login:

login(username: string, password: string) {
   const url = new URL('/api/login', environment.apiUrl).href;
   return this.http
      .post(url, {
         username,
         password,
      })
      .pipe(
         catchError(() => of(null)),
         mergeMap((result: any) => {
            if (!result) {
               alert('Could not login');
               return of();
            }
            this.jwt.jwt = result.accessToken;
            return this.me();
         })
      );
}
Вход в полноэкранный режим Выход из полноэкранного режима

login сделает запрос POST к конечной точке /api/login, отправив имя пользователя и пароль нашего пользователя. В случае неудачи запроса, например, когда пользователь не существует, появится предупреждение, уведомляющее пользователя о неудаче. При успешном запросе маркер доступа будет сохранен в локальном хранилище, а затем будет вызвана функция me, которая вернет объект user текущего пользователя.

Затем добавьте метод signup:

signup(username: string, password: string) {
   const url = new URL('/api/signup', environment.apiUrl).href;
   return this.http
      .post(url, {
         username,
         password,
      })
      .pipe(
         catchError(() => of(null)),
         mergeMap((result: any) => {
            if (!result) {
               alert('Could not sign up');
               return of();
            }
            this.jwt.jwt = result.accessToken;
            return this.me();
         })
      );
}
Вход в полноэкранный режим Выход из полноэкранного режима

signup сделает запрос POST к конечной точке /api/signup, которую мы также создали в Шаге 3, отправив имя пользователя и пароль нашего нового пользователя. В случае неудачи запроса, например, если имя пользователя уже используется, появится предупреждение, уведомляющее пользователя о неудаче. В случае успешного запроса токен доступа будет сохранен в локальном хранилище, а затем будет вызвана функция me для возврата объекта user текущего пользователя.

Наконец, нам нужно добавить AuthService в AppModule. Откройте web/src/app/app.module.ts и импортируйте AuthService:

+ import { AuthService } from './auth.service';
import { JWTService } from './jwt.service';
Войдите в полноэкранный режим Выход из полноэкранного режима

Затем добавьте и настройте AuthService в providers в декораторе @NgModule:

   providers: [
      { provide: HTTP_INTERCEPTORS, useClass: JWTService, multi: true },
+      AuthService,
   ],
   bootstrap: [AppComponent]
})
export class AppModule { }
Вход в полноэкранный режим Выход из полноэкранного режима

Шаг 3 — Компонент Auth

Нам нужен компонент, который будет собирать имя пользователя и пароль от пользователя, а затем делать соответствующий запрос с помощью функций, которые мы только что добавили. В терминале перейдите в каталог web и запустите:

ng g c auth
Войти в полноэкранный режим Выйти из полноэкранного режима

Откройте следующие файлы и замените их содержимое на следующее:

web/src/app/auth/auth.component.ts

import { Component, Output, EventEmitter } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AuthService } from '../auth.service';

@Component({
   selector: 'app-auth',
   templateUrl: './auth.component.html',
   styleUrls: ['./auth.component.css'],
})
export class AuthComponent {
   @Output() setUser = new EventEmitter<string>();
   authForm = this.fb.group({
      username: '',
      password: '',
      confirm: '',
   });
   isLogin = true;

   constructor(private fb: FormBuilder, private auth: AuthService) {}

   onSubmit() {
      const { username, password, confirm }: { [key: string]: string } =
         this.authForm.getRawValue();

      if (!username || !password) return;

      let authResult;

      if (!this.isLogin && password !== confirm) {
         return alert('Passwords do not match');
      } else if (!this.isLogin) {
         authResult = this.auth.signup(username.toLowerCase(), password);
      } else {
         authResult = this.auth.login(username.toLowerCase(), password);
      }

      authResult.subscribe({ next: (result: any) => this.setUser.emit(result) });
   }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

web/src/app/auth/auth.component.html

<form [formGroup]="authForm" (ngSubmit)="onSubmit()">
   <h2>{{isLogin ? "Login" : "Sign Up"}}</h2>
   <input name="username" type="text" placeholder="username" formControlName="username" required />
   <input name="password" type="password" placeholder="password" formControlName="password" required />
   <input *ngIf="!isLogin" name="confirmPassword" type="password" placeholder="confirm password"
    formControlName="confirm" required />

   <button type="submit">Submit</button>
   <button type="button" (click)="isLogin = !isLogin">
      {{isLogin ? "Need an account?" : "Already have an account?"}}
   </button>
</form>
Войти в полноэкранный режим Выход из полноэкранного режима

Этот компонент отображает форму, предлагающую пользователю ввести имя пользователя и пароль для входа в систему. Если у пользователя еще нет учетной записи, то кнопка в нижней части страницы переключает форму на создание новой учетной записи, что добавляет новое поле для подтверждения пароля.

При отправке вызывается метод login или signup из AuthService, и результат обрабатывается эмиттером события @Output() setUser.

Шаг 4 — Вход в систему

Когда компонент аутентификации создан, нам остается только показать его пользователям. Начните с добавления свойства user к AppComponent в web/src/app/app.component.ts, например:

export class AppComponent {
   tasks: any[] = [];
+   user: any;
Войти в полноэкранный режим Выйти из полноэкранного режима

Далее мы добавим метод в AppComponent для установки свойства user. Хотя мы могли бы напрямую установить значение, в конечном итоге мы захотим вызвать некоторый код, когда пользователь будет установлен, поэтому мы реализуем это таким образом.

setUser(user: any) {
   this.user = user;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Затем обновите шаблон AppComponent (web/src/app/app.component.html), чтобы он выглядел следующим образом:

<ng-container *ngIf="user; else auth">
   <app-create-task (addTask)="addTask($event)"></app-create-task>
   <app-tasks [tasks]="tasks" (completed)="completed($event)"></app-tasks>
</ng-container>

<ng-template #auth>
   <app-auth (setUser)="setUser($event)"></app-auth>
</ng-template>
Вход в полноэкранный режим Выход из полноэкранного режима

Итак, на верхнем уровне шаблона компонента у нас есть два дочерних элемента, <ng-container> и <ng-template>. Поведение <ng-container> очень похоже на то, как <> используется в React, где мы удерживаем элементы без добавления дополнительных элементов в DOM. Контейнер <ng-container> отображается, если свойство user существует в AppComponent, иначе отображается содержимое <ng-template>. Внутри <ng-template> мы добавили элемент app-auth. Когда элемент app-auth (AuthComponent) испускает событие setUser, свойство user элемента AppComponent присваивается его методом setUser. Если есть значение user, то мы переключим шаблон на отображение списка дел.

Не ожидается, что пользователь будет входить в систему каждый раз, особенно если учесть, что мы храним токен доступа JWT пользователя. Мы обновим AppComponent, чтобы вызывать метод me из AuthService при инициации компонента. Таким образом, мы сможем присвоить свойство user как можно раньше.

Начните с импорта OnInit и AuthService, а затем установите AppComponent для реализации хука жизненного цикла OnInit.

- import { Component } from '@angular/core';
+ import { Component, OnInit } from '@angular/core';
+ import { AuthService } from './auth.service';

@Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css']
})
- export class AppComponent {
+ export class AppComponent implements OnInit {
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем добавьте конструктор, в котором AuthService будет задан в качестве единственного аргумента.

constructor(private auth: AuthService) {}
Вход в полноэкранный режим Выход из полноэкранного режима

Затем добавьте эту реализацию крючка жизненного цикла OnInit:

ngOnInit(): void {
   this.auth.me().subscribe({ next: (user) => (this.user = user) });
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь, если свойство user имеет значение, что происходит только при входе в систему, приложение будет показывать задачи пользователя. Если свойство user не имеет значения, то пользователю будет показан экран авторизации, который при входе или регистрации пользователя установит свойство user с помощью события setUser элемента app-auth (AuthComponent).

Шаг 5 — Завершение

Запустите приложение и попробуйте создать новую учетную запись.

Вернитесь на следующей неделе к пятому шагу, или посетите сайт Amplication docs для полного руководства сейчас!

Чтобы просмотреть изменения для этого шага, посетите здесь.

Оцените статью
Procodings.ru
Добавить комментарий