Создание приложения, использующего 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