Creating pages that require authentication is simple in Angular. A guard is a piece of Angular code that is designed to control access to routes.
In app-routing.module.ts
, we have an array of Route
objects and in each entry, you can add a canActivate
property where you can pass an array of guards.
The guard allows you to check for the requirements to access your routes. You return true
, or a promise that resolves to true
, to allow people to access your route. Otherwise, return false
or return your promise to false
.
In this story, we use the API we wrote in a previous piece.
To build the Angular app, you need the Angular CLI. To install it, run npm i -g @angular/cli
in your Node.js command prompt. Then, run ng new frontend
to generate the skeleton code for your front-end app.
Also, install @angular/material
according to the Angular documentation to make our UI look pretty.
After that, replace the default app.module.ts
with the following:
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 { }
This creates all our dependencies and components that we’ll add. To make authenticated requests easy with our token, we create an HTTP request interceptor by creating 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) {}
});
}
}
We set the token in all requests, except the login request.
In our environments/environment.ts
, we have:
export const environment = {
production: false,
apiUrl: 'http://localhost:8080'
};
This points to our back end’s URL.
Now, we need to make our side nav. We want to add @ngrx/store
to store the side nav’s state. We install the package by running npm install @ngrx/store --save
.
We add our reducer by running ng add @ngrx/store
to add our reducers.
We add menu-reducers.ts
to set state centrally in our flux store and in that file we enter:
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 };
In index.ts
of the same folder, we put:
import { menuReducer } from './menu-reducer';
import { tweetsReducer } from './tweets-reducer';export const reducers = {
menu: menuReducer,
};
To link our reducer to other parts of the app.
In style.css
, to get our Material Design look, we put:
/* 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;
}
In index.html
, we add the following in between the head
tags:
<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">
Then, we add a service for the user functions by running ng g service user
. That will create user.service.ts
. We then put:
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;
}
}}
Each function requests a subscription for an HTTP request, except for the isAuthenticated
function which is used to check for the token’s validity.
We also need routing for our app so we can see the pages when we go to the URLs listed below.
In app-routing.module.ts
, we put:
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 { }
Now, we create the parts that are referenced in the file above.
We need to prevent people from accessing authenticated routes without a token, so we need a guard in Angular. We make that by running ng g guard isAuthenticated
. This generates is-authenticated.guard.ts
.
We put the following in 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;
}}
This uses our isAuthenticated
function from UserService
to check for a valid token. If it’s not valid, we clear it and redirect back to home page.
Now, we create the forms for logging in setting the user data after logging in.
We run ng g component homePage
, ng g component loginPage
, ng g component topBar
, ng g component signUpPage
, and ng g component settingsPage
. These are for the forms and the top bar components.
The home page is just a static page. We should have home-page.component.html
and home-page.component.ts
generated after running the commands in our last paragraph.
In home-page.component.html
, we put:
<div class="center">
<h1>Home Page</h1>
</div>
Now we make our login page. In login-page.component.ts
, we put:
<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>
In login-page.component.ts
, we put:
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');
})
}
}
We make sure that all fields are filled. If they are, the login data will be sent and the token will be saved to local storage if authentication is successful. Otherwise an error alert will be displayed.
In our sign-up page, sign-up-page.component.html
, we put:
<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>
And, in sign-up-page.component.ts
, we put:
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']);
})
}
}
The two pieces of code get the sign-up data and send it to the back end which will save the file if they are all valid.
Similarly, in the 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>
In settings-page.component.html
, we put:
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.');
})
}
}
Similar to other pages, this sends a request payload for changing user data and passwords to our back end.
Finally, to make our top bar, we put the following in top-bar.component.html
:
<mat-toolbar>
<a (click)='toggleMenu()' class="menu-button">
<i class="material-icons">
menu
</i>
</a>
Twitter Automator
</mat-toolbar>
And in 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 });
}
}
In app.component.ts
, we put:
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();
}
}
And in app.component.html
, we have:
<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>
This allows us to toggle our side nav menu. Note that we have:
@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 });
}
}
To detect clicks outside the side nav. If we click outside, i.e. we’re not clicking on any element with those classes, then we close the menu.
this.store.dispatch
propagates the closed state to all components.