Создание приложения, использующего JWT для хранения данных аутентификации
Поскольку одностраничные интерфейсные приложения и мобильные приложения стали более популярными, чем когда-либо, интерфейсная часть отделена от серверной части. Поскольку почти все веб-приложения нуждаются в проверке подлинности, должен существовать способ для внешнего интерфейса или мобильных приложений для безопасного хранения идентификационных данных пользователя.
Веб-токены JSON (JWT) - один из наиболее распространенных способов хранения данных аутентификации в интерфейсных приложениях. В Node.js есть популярные библиотеки, которые могут генерировать и проверять JWT, проверяя его подлинность, проверяя секретный ключ, хранящийся в серверной части, а также проверяя дату истечения срока действия.
Токен кодируется в стандартном формате, понятном большинству приложений. Обычно он содержит идентификационные данные пользователя, такие как идентификатор пользователя, имя пользователя и т. Д. Он выдается пользователю, когда пользователь может успешно пройти аутентификацию.
В этой части мы создадим приложение, которое использует JWT для хранения данных аутентификации. На внутренней стороне мы будем использовать платформу Express, которая работает на Node.js, и пакет jsonwebtoken
для генерации и проверки токена. Для внешнего интерфейса мы будем использовать фреймворк Angular и модуль @auth0/angular-jwt
для Angular. В нашем приложении, когда пользователь вводит имя пользователя и пароль, и они находятся в нашей базе данных, JWT будет сгенерирован из нашего секретного ключа, возвращен пользователю и сохранен во внешнем приложении в локальном хранилище. Всякий раз, когда пользователю необходимо получить доступ к аутентифицированным маршрутам на серверной части, ему понадобится токен. В серверном приложении будет функция, называемая промежуточным программным обеспечением, для проверки действительного токена. Действительный токен - это токен, срок действия которого не истек и который проверяется на соответствие нашему секретному ключу. Там также будут страницы регистрации и настройки учетных данных пользователя в дополнение к странице входа.
Теперь, когда у нас есть план, мы можем начать с создания папок внешнего и внутреннего приложений. Сделайте по одному для каждого. Затем мы начинаем писать серверное приложение. Сначала мы устанавливаем несколько пакетов и генерируем скелетный код Express. Мы запускаем npx express-generator
, чтобы сгенерировать код. Затем нам нужно установить несколько пакетов. Мы делаем это, запустив npm i @babel/register express-jwt sequelize bcrypt sequelize-cli dotenv jsonwebtoken body-parser cors
. @babel/register
позволяет нам использовать новейшие функции JavaScript. express-jwt
генерирует JWT и проверяет его на секретность. bcrypt
выполняет хеширование и обработку наших паролей. sequelize
- это наша ORM для выполнения CRUD. cors
позволяет нашему приложению Angular взаимодействовать с нашей серверной частью, разрешая междоменное взаимодействие. dotenv
позволяет нам хранить переменные среды в .env
файле. body-parser
необходим Express для анализа запросов JSON.
Затем мы выполняем миграции нашей базы данных. Запустите npx sequelize-cli init
, чтобы сгенерировать скелетный код нашей базы данных для сопоставления объектов. Затем запускаем:
npx sequelize-cli model:generate --name User --attributes username:string, password:string, email:string
Делаем еще одну миграцию и ставим:
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return Promise.all([ queryInterface.addConstraint( "Users", ["email"], { type: "unique", name: 'emailUnique' }), queryInterface.addConstraint( "Users", ["userName"], { type: "unique", name: 'userNameUnique' }), }, down: (queryInterface, Sequelize) => { return Promise.all([ queryInterface.removeConstraint( "Users", 'emailUnique' ), queryInterface.removeConstraint( "Users", 'userNameUnique' ), ]) } };
Это гарантирует, что у нас не будет повторяющихся записей с одним и тем же именем пользователя или адресом электронной почты.
Это создает User
модель и таблицу Users
после запуска npx sequelize-cli db:migrate
.
Напишем код. Поместите в app.js
следующее:
require("@babel/register"); require("babel-polyfill"); require('dotenv').config(); const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); const user = require('./controllers/userController'); const app = express(); app.use(cors()) app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use((req, res, next) => { res.locals.session = req.session; next(); }); app.use('/user', user); app.get('*', (req, res) => { res.redirect('/home'); }); app.listen((process.env.PORT || 8080), () => { console.log('App running on port 8080!'); });
Нам нужно:
require("@babel/register"); require("babel-polyfill");
использовать новейшие функции JavaScript.
А нам понадобятся:
require('dotenv').config();
чтобы прочитать нашу конфигурацию в .env
файле.
Это точка входа. Вскоре мы создадим userController
в папке controllers
.
app.use(‘/user’, user);
направляет любой URL, начинающийся с user
, в userController
файл.
Затем мы добавляем файл userController.js
:
const express = require('express'); const bcrypt = require('bcrypt'); const router = express.Router(); const models = require('../models'); const jwt = require('jsonwebtoken'); import { authCheck } from '../middlewares/authCheck'; router.post('/login', async (req, res) => { const secret = process.env.JWT_SECRET; const userName = req.body.userName; const password = req.body.password; if (!userName || !password) { return res.send({ error: 'User name and password required' }) } const users = await models.User.findAll({ where: { userName } }) const user = users[0]; if (!user) { res.status(401); return res.send({ error: 'Invalid username or password' }); } try { const compareRes = await bcrypt.compare(password, user.hashedPassword); if (compareRes) { const token = jwt.sign( { data: { userName, userId: user.id } }, secret, { expiresIn: 60 * 60 } ); return res.send({ token }); } else { res.status(401); return res.send({ error: 'Invalid username or password' }); } } catch (ex) { logger.error(ex); res.status(401); return res.send({ error: 'Invalid username or password' }); } }); router.post('/signup', async (req, res) => { const userName = req.body.userName; const email = req.body.email; const password = req.body.password; try { const hashedPassword = await bcrypt.hash(password, 10) await models.User.create({ userName, email, hashedPassword }) return res.send({ message: 'User created' }); } catch (ex) { logger.error(ex); res.status(400); return res.send({ error: ex }); } }); router.put('/updateUser', authCheck, async (req, res) => { const userName = req.body.userName; const email = req.body.email; const token = req.headers.authorization; const decoded = jwt.verify(token, process.env.JWT_SECRET); const userId = decoded.data.userId; try { await models.User.update({ userName, email }, { where: { id: userId } }) return res.send({ message: 'User created' }); } catch (ex) { logger.error(ex); res.status(400); return res.send({ error: ex }); } }); router.put('/updatePassword', authCheck, async (req, res) => { const token = req.headers.authorization; const password = req.body.password; const decoded = jwt.verify(token, process.env.JWT_SECRET); const userId = decoded.data.userId; try { const hashedPassword = await bcrypt.hash(password, saltRounds) await models.User.update({ hashedPassword }, { where: { id: userId } }) return res.send({ message: 'User created' }); } catch (ex) { logger.error(ex); res.status(400); return res.send({ error: ex }); } }); module.exports = router;
Маршрут login
ищет запись пользователя, а затем, если она найдена, проверяет хешированный пароль с помощью compare
функции bcrypt
. Если оба успешны, создается JWT. Маршрут signup
получает полезную нагрузку JSON, состоящую из имени пользователя и пароля, и сохраняет их. Обратите внимание, что перед сохранением пароля выполняется хеширование и обработка. Пароли не должны храниться в виде обычного текста. bcrypt.hash
принимает два аргумента. Это первый пароль в виде обычного текста, а второй - количество раундов соли.
updatePassword
маршрут - это аутентифицированный маршрут. Он проверяет токен и, если он действителен, продолжает сохранять пароль пользователя, выполняя поиск User
с идентификатором пользователя, который соответствует декодированному токену. Далее мы добавим промежуточное ПО authCheck
.
Создайте папку middlewares
и создайте в ней authCheck.js
.
const jwt = require('jsonwebtoken'); const secret = process.env.JWT_SECRET; export const authCheck = (req, res, next) => { if (req.headers.authorization) { const token = req.headers.authorization; jwt.verify(token, secret, (err, decoded) => { if (err) { res.send(401); } else { next(); } }); } else { res.send(401); } }
Это позволяет нам проверять аутентификацию на аутентифицированных маршрутах без повторения кода. Мы помещаем if
между URL-адресом и нашим основным кодом маршрута в каждом аутентифицированном маршруте, импортируя его и ссылаясь на него.
Мы создаем .env
файл из корня папки внутреннего приложения со следующим содержимым:
DB_HOST='localhost' DB_NAME='login-app' DB_USERNAME='db-username' DB_PASSWORD='db-password' JWT_SECRET='secret'
Бэкэнд-приложение готово. Теперь перейдем к клиентскому приложению Angular.
Переключитесь в папку своего внешнего приложения. Чтобы создать приложение Angular, вам понадобится Angular CLI.
Чтобы установить его, запустите npm i -g @angular/cli
в командной строке Node.js. Затем запустите ng new frontend
, чтобы сгенерировать скелетный код для своего внешнего приложения.
Также установите @angular/material
согласно документации по Angular.
После этого замените значение по умолчанию app.module.ts
следующим:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule, MatCheckboxModule, MatInputModule, MatMenuModule, MatSidenavModule, MatToolbarModule, MatTableModule, MatDialogModule, MAT_DIALOG_DEFAULT_OPTIONS, MatDatepickerModule, MatSelectModule, MatCardModule } from '@angular/material'; import { MatFormFieldModule } from '@angular/material/form-field'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { StoreModule } from '@ngrx/store'; import { reducers } from './reducers'; import { FormsModule } from '@angular/forms'; import { TopBarComponent } from './top-bar/top-bar.component'; import { HomePageComponent } from './home-page/home-page.component'; import { LoginPageComponent } from './login-page/login-page.component'; import { SignUpPageComponent } from './sign-up-page/sign-up-page.component'; import { SettingsPageComponent } from './settings-page/settings-page.component'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { SessionService } from './session.service'; import { HttpReqInterceptor } from './http-req-interceptor'; import { UserService } from './user.service'; import { CapitalizePipe } from './capitalize.pipe'; @NgModule({ declarations: [ AppComponent, TopBarComponent, HomePageComponent, LoginPageComponent, SignUpPageComponent, SettingsPageComponent, ], imports: [ BrowserModule, AppRoutingModule, StoreModule.forRoot(reducers), BrowserAnimationsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, MatMenuModule, MatSidenavModule, MatToolbarModule, MatTableModule, FormsModule, HttpClientModule, MatDialogModule, MatDatepickerModule, MatMomentDateModule, MatSelectModule, MatCardModule, NgxMaterialTimepickerModule ], providers: [ SessionService, { provide: HTTP_INTERCEPTORS, useClass: HttpReqInterceptor, multi: true }, UserService, { provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { hasBackdrop: false } }, ], bootstrap: [AppComponent], }) export class AppModule { }
Это создает все зависимости и компоненты, которые мы добавим. Чтобы упростить аутентифицированные запросы с помощью нашего токена, мы создаем перехватчик HTTP-запросов, создав http-req-interceptor.ts
:
import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpResponse, HttpErrorResponse, HttpRequest } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../environments/environment' import { map, filter, tap } from 'rxjs/operators'; import { Router } from '@angular/router'; @Injectable() export class HttpReqInterceptor implements HttpInterceptor { constructor( public router: Router ) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { let modifiedReq = req.clone({}); if (localStorage.getItem('token')) { modifiedReq = modifiedReq.clone({ setHeaders: { authorization: localStorage.getItem('token') } }); } return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => { if (event instanceof HttpResponse) {} }); } }
Мы устанавливаем токен во всех запросах, кроме запроса на вход.
В нашем environments/environment.ts
у нас есть:
export const environment = { production: false, apiUrl: 'http://localhost:8080' };
Это указывает на URL нашей серверной части.
Теперь нам нужно сделать нашу боковую навигацию. Мы хотим добавить @ngrx/store
для хранения состояния боковой навигационной панели. Устанавливаем пакет, запустив npm install @ngrx/store --save
. Мы добавляем наш редуктор, запустив ng add @ngrx/store
, чтобы добавить наши редукторы.
Мы добавляем menu-reducers.ts
для централизованной установки состояния в нашем хранилище потоков:
const TOGGLE_MENU = 'TOGGLE_MENU'; function menuReducer(state, action) { switch (action.type) { case TOGGLE_MENU: state = action.payload; return state; default: return state } } export { menuReducer, TOGGLE_MENU };
Чтобы связать наш редуктор с другими частями приложения, введите в index.ts
следующее:
import { menuReducer } from './menu-reducer'; import { tweetsReducer } from './tweets-reducer'; export const reducers = { menu: menuReducer, };
Чтобы получить внешний вид нашего Материального дизайна, добавьте в style.css
следующее:
/* You can add global styles to this file, and also import other style files */ @import "~@angular/material/prebuilt-themes/indigo-pink.css"; body { font-family: "Roboto", sans-serif; margin: 0; } form { mat-form-field { width: 95vw; margin: 0 auto; } } .center { text-align: center; }
Между тегами head
в index.html
мы добавляем:
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Затем мы добавляем службу для пользовательских функций, запустив ng g service user
. Будет создано user.service.ts
. Затем мы помещаем:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; import { Router } from '@angular/router'; import { JwtHelperService } from "@auth0/angular-jwt"; const helper = new JwtHelperService(); @Injectable({ providedIn: 'root' }) export class UserService { constructor( private http: HttpClient, private router: Router ) { } signUp(data) { return this.http.post(`${environment.apiUrl}/user/signup`, data); } updateUser(data) { return this.http.put(`${environment.apiUrl}/user/updateUser`, data); } updatePassword(data) { return this.http.put(`${environment.apiUrl}/user/updatePassword`, data); } login(data) { return this.http.post(`${environment.apiUrl}/user/login`, data); } logOut() { localStorage.clear(); this.router.navigate(['/']); } isAuthenticated() { try { const token = localStorage.getItem('token'); const decodedToken = helper.decodeToken(token); const isExpired = helper.isTokenExpired(token); return !!decodedToken && !isExpired; } catch (ex) { return false; } } }
Каждая функция запрашивает подписку на HTTP-запрос, за исключением функции isAuthenticated
, которая используется для проверки действительности токена.
Нам также нужна маршрутизация для нашего приложения, чтобы мы могли видеть страницы при переходе по URL-адресам, перечисленным ниже. В app-routing.module.ts
мы помещаем:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HomePageComponent } from './home-page/home-page.component'; import { LoginPageComponent } from './login-page/login-page.component'; import { SignUpPageComponent } from './sign-up-page/sign-up-page.component'; import { TweetsPageComponent } from './tweets-page/tweets-page.component'; import { SettingsPageComponent } from './settings-page/settings-page.component'; import { PasswordResetRequestPageComponent } from './password-reset-request-page/password-reset-request-page.component'; import { PasswordResetPageComponent } from './password-reset-page/password-reset-page.component'; import { IsAuthenticatedGuard } from './is-authenticated.guard'; const routes: Routes = [ { path: 'login', component: LoginPageComponent }, { path: 'signup', component: SignUpPageComponent }, { path: 'settings', component: SettingsPageComponent, canActivate: [IsAuthenticatedGuard] }, { path: '**', component: HomePageComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Теперь мы создаем детали, на которые есть ссылки в файле выше. Нам нужно запретить людям доступ к аутентифицированным маршрутам без токена, поэтому нам нужен охранник в Angular. Мы делаем это, запустив ng g guard isAuthenticated
. Это генерирует is-authenticated.guard.ts
.
Мы помещаем в is-authenticated.guard.ts
следующее:
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { UserService } from './user.service'; @Injectable({ providedIn: 'root' }) export class IsAuthenticatedGuard implements CanActivate { constructor( private userService: UserService, private router: Router ) { } canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { const isAuthenticated = this.userService.isAuthenticated(); if (!isAuthenticated) { localStorage.clear(); this.router.navigate(['/']); } return isAuthenticated; } }
Это использует нашу isAuthenticated
функцию из UserService
для проверки действительного токена. Если он недействителен, мы очищаем его и перенаправляем обратно на главную страницу.
Теперь создаем формы для входа в систему, устанавливая данные пользователя после входа. Мы запускаем ng g component homePage
, ng g component loginPage
, ng g component topBar
, ng g component signUpPage
и ng g component settingsPage
. Это для форм и компонентов верхней панели.
Домашняя страница - это просто статическая страница. У нас должны быть созданы home-page.component.html
и home-page.component.ts
после выполнения команд в нашем последнем абзаце.
В home-page.component.html
мы помещаем:
<div class="center"> <h1>Home Page</h1> </div>
Теперь делаем нашу страницу авторизации. В login-page.component.ts
мы помещаем:
<div class="center"> <h1>Log In</h1> </div> <form #loginForm='ngForm' (ngSubmit)='login(loginForm)'> <mat-form-field> <input matInput placeholder="Username" required #userName='ngModel' name='userName' [(ngModel)]='loginData.userName'> <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)"> <div *ngIf="userName.errors.required"> Username is required. </div> </mat-error> </mat-form-field> <br> <mat-form-field> <input matInput placeholder="Password" type='password' required #password='ngModel' name='password' [(ngModel)]='loginData.password'> <mat-error *ngIf="password.invalid && (password.dirty || password.touched)"> <div *ngIf="password.errors.required"> Password is required. </div> </mat-error> </mat-form-field> <br> <button mat-raised-button type='submit'>Log In</button> <a mat-raised-button routerLink='/passwordResetRequest'>Reset Password</a> </form>
В login-page.component.ts
мы помещаем:
import { Component, OnInit } from '@angular/core'; import { UserService } from '../user.service'; import { NgForm } from '@angular/forms'; import { Router } from '@angular/router'; @Component({ selector: 'app-login-page', templateUrl: './login-page.component.html', styleUrls: ['./login-page.component.scss'] }) export class LoginPageComponent implements OnInit { loginData: any = <any>{}; constructor( private userService: UserService, private router: Router ) { } ngOnInit() { } login(loginForm: NgForm) { if (loginForm.invalid) { return; } this.userService.login(this.loginData) .subscribe((res: any) => { localStorage.setItem('token', res.token); this.router.navigate(['/settings']); }, err => { alert('Invalid username or password'); }) } }
Следим за тем, чтобы все поля были заполнены. Если это так, данные для входа будут отправлены, а токен будет сохранен в локальном хранилище, если аутентификация прошла успешно. В противном случае будет отображаться предупреждение об ошибке.
Затем на нашей странице регистрации sign-up-page.component.html
мы помещаем:
<div class="center"> <h1>Sign Up</h1> </div> <br> <form #signUpForm='ngForm' (ngSubmit)='signUp(signUpForm)'> <mat-form-field> <input matInput placeholder="Username" required #userName='ngModel' name='userName' [(ngModel)]='signUpData.userName'> <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)"> <div *ngIf="userName.errors.required"> Username is required. </div> </mat-error> </mat-form-field> <br> <mat-form-field> <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email' [(ngModel)]='signUpData.email'> <mat-error *ngIf="email.invalid && (email.dirty || email.touched)"> <div *ngIf="email.errors.required"> Email is required. </div> <div *ngIf="email.invalid"> Email is invalid. </div> </mat-error> </mat-form-field> <br> <mat-form-field> <input matInput placeholder="Password" type='password' required #password='ngModel' name='password' [(ngModel)]='signUpData.password'> <mat-error *ngIf="password.invalid && (password.dirty || password.touched)"> <div *ngIf="password.errors.required"> Password is required. </div> </mat-error> </mat-form-field> <br> <button mat-raised-button type='submit'>Sign Up</button> </form>
и в sign-up-page.component.ts
мы помещаем:
import { Component, OnInit } from '@angular/core'; import { UserService } from '../user.service'; import { NgForm } from '@angular/forms'; import { Router } from '@angular/router'; import _ from 'lodash'; @Component({ selector: 'app-sign-up-page', templateUrl: './sign-up-page.component.html', styleUrls: ['./sign-up-page.component.scss'] }) export class SignUpPageComponent implements OnInit { signUpData: any = <any>{}; constructor( private userService: UserService, private router: Router ) { } ngOnInit() { } signUp(signUpForm: NgForm) { if (signUpForm.invalid) { return; } this.userService.signUp(this.signUpData) .subscribe(res => { this.login(); }, err => { console.log(err); if ( _.has(err, 'error.error.errors') && Array.isArray(err.error.error.errors) && err.error.error.errors.length > 0 ) { alert(err.error.error.errors[0].message); } }) } login() { this.userService.login(this.signUpData) .subscribe((res: any) => { localStorage.setItem('token', res.token); this.router.navigate(['/tweets']); }) } }
Эти два фрагмента кода получают данные регистрации и отправляют их в серверную часть, которая сохранит файл, если они все действительны.
Аналогично в settings-page.component.html
:
<div class="center"> <h1>Settings</h1> </div> <br> <div> <h2>Update User Info</h2> </div> <br> <form #updateUserForm='ngForm' (ngSubmit)='updateUser(updateUserForm)'> <mat-form-field> <input matInput placeholder="Username" required #userName='ngModel' name='userName' [(ngModel)]='updateUserData.userName'> <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)"> <div *ngIf="userName.errors.required"> Username is required. </div> </mat-error> </mat-form-field> <br> <mat-form-field> <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email' [(ngModel)]='updateUserData.email'> <mat-error *ngIf="email.invalid && (email.dirty || email.touched)"> <div *ngIf="email.errors.required"> Email is required. </div> <div *ngIf="email.invalid"> Email is invalid. </div> </mat-error> </mat-form-field> <br> <button mat-raised-button type='submit'>Update User Info</button> </form> <br> <div> <h2>Update Password</h2> </div> <br> <form #updatePasswordForm='ngForm' (ngSubmit)='updatePassword(updatePasswordForm)'> <mat-form-field> <input matInput placeholder="Password" type='password' required #password='ngModel' name='password' [(ngModel)]='updatePasswordData.password'> <mat-error *ngIf="password.invalid && (password.dirty || password.touched)"> <div *ngIf="password.errors.required"> Password is required. </div> </mat-error> </mat-form-field> <br> <button mat-raised-button type='submit'>Update Password</button> </form> <br> <div *ngIf='currentTwitterUser.id' class="title"> <h2>Connected to Twitter Account</h2> <div> <button mat-raised-button (click)='redirectToTwitter()'>Connect to Different Twitter Account</button> </div> </div> <div *ngIf='!currentTwitterUser.id' class="title"> <h2>Not Connected to Twitter Account</h2> <div> <button mat-raised-button (click)='redirectToTwitter()'>Connect to Twitter Account</button> </div> </div>
В settings-page.component.html
мы помещаем:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { SessionService } from '../session.service'; import { NgForm } from '@angular/forms'; import { UserService } from '../user.service'; @Component({ selector: 'app-settings-page', templateUrl: './settings-page.component.html', styleUrls: ['./settings-page.component.scss'] }) export class SettingsPageComponent implements OnInit { currentTwitterUser: any = <any>{}; elements: any[] = []; displayedColumns: string[] = ['key', 'value']; updateUserData: any = <any>{}; updatePasswordData: any = <any>{}; constructor( private sessionService: SessionService, private userService: UserService, private router: Router ) { } ngOnInit() { } updateUser(updateUserForm: NgForm) { if (updateUserForm.invalid) { return; } this.userService.updateUser(this.updateUserData) .subscribe(res => { alert('Updated user info successful.'); }, err => { alert('Updated user info failed.'); }) } updatePassword(updatePasswordForm: NgForm) { if (updatePasswordForm.invalid) { return; } this.userService.updatePassword(this.updatePasswordData) .subscribe(res => { alert('Updated password successful.'); }, err => { alert('Updated password failed.'); }) } }
Подобно другим страницам, это отправляет полезную нагрузку запроса на изменение данных пользователя и пароля на наш сервер.
Наконец, чтобы сделать нашу верхнюю панель, мы добавляем в top-bar.component.html
следующее:
<mat-toolbar> <a (click)='toggleMenu()' class="menu-button"> <i class="material-icons"> menu </i> </a> Twitter Automator </mat-toolbar>
И в top-bar.component.ts
:
import { Component, OnInit } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { TOGGLE_MENU } from '../reducers/menu-reducer'; @Component({ selector: 'app-top-bar', templateUrl: './top-bar.component.html', styleUrls: ['./top-bar.component.scss'] }) export class TopBarComponent implements OnInit { menuOpen: boolean; constructor( private store: Store<any> ) { store.pipe(select('menu')) .subscribe(menuOpen => { this.menuOpen = menuOpen; }) } ngOnInit() { } toggleMenu() { this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen }); } }
В app.component.ts
мы помещаем:
import { Component, HostListener } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { TOGGLE_MENU } from './reducers/menu-reducer'; import { UserService } from './user.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { menuOpen: boolean; constructor( private store: Store<any>, private userService: UserService ) { store.pipe(select('menu')) .subscribe(menuOpen => { this.menuOpen = menuOpen; }) } isAuthenticated() { return this.userService.isAuthenticated(); } @HostListener('document:click', ['$event']) public onClick(event) { const isOutside = !event.target.className.includes("menu-button") && !event.target.className.includes("material-icons") && !event.target.className.includes("mat-drawer-inner-container") if (isOutside) { this.menuOpen = false; this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen }); } } logOut() { this.userService.logOut(); } }
а в app.component.html
у нас есть:
<mat-sidenav-container class="example-container"> <mat-sidenav mode="side" [opened]='menuOpen'> <ul> <li> <b> Twitter Automator </b> </li> <li> <a routerLink='/login' *ngIf='!isAuthenticated()'>Log In</a> </li> <li> <a routerLink='/signup' *ngIf='!isAuthenticated()'>Sign Up</a> </li> <li> <a href='#' (click)='logOut()' *ngIf='isAuthenticated()'>Log Out</a> </li> <li> <a routerLink='/tweets' *ngIf='isAuthenticated()'>Tweets</a> </li> <li> <a routerLink='/settings' *ngIf='isAuthenticated()'>Settings</a> </li> </ul> </mat-sidenav> <mat-sidenav-content> <app-top-bar></app-top-bar> <div id='content'> <router-outlet></router-outlet> </div> </mat-sidenav-content> </mat-sidenav-container>
Это позволяет нам переключать наше боковое меню навигации. Обратите внимание, что у нас есть:
@HostListener('document:click', ['$event']) public onClick(event) { const isOutside = !event.target.className.includes("menu-button") && !event.target.className.includes("material-icons") && !event.target.className.includes("mat-drawer-inner-container") if (isOutside) { this.menuOpen = false; this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen }); } }
для обнаружения щелчков боковой навигационной панели. Если мы щелкнем снаружи, то есть не щелкнем ни один элемент с этими классами, мы закроем меню. this.store.dispatch
распространяет закрытое состояние на все компоненты.
В итоге имеем:
Следуйте за мной в Twitter: https://twitter.com/AuMayeung