This article was updated June 15, 2018. This series and associated repository use Angular 6, CLI 6, RxJS 6, and revised recommendations regarding the storage of sensitive data.

TL;DR: This 8-part tutorial series covers building and deploying a full-stack JavaScript application from the ground up with hosted MongoDB, Express, Angular, and Node.js (MEAN stack). The completed code is available in the mean-rsvp-auth0 GitHub repo and a deployed sample app is available at https://rsvp.kmaida.net. Part 7 of the tutorial series covers deleting data, retrieving relational data from MongoDB, and renewing authentication tokens.

Real-World Angular Series

You can view all sections of the tutorial series here:

Part 7: Relational Data and Token Renewal

The sixth part of this tutorial covered reactive forms with custom validation.

The seventh installment in the series covers deleting events, retrieving relational data from MongoDB to list events a user has RSVPed to, and silently renewing authentication tokens.

Angular: Delete Event

Let's pick up right where we left off last time. Our app's administrator can now create and update events. We also need to be able to delete events. We already added a DELETE API route in Part 6. Now let's call this endpoint in our Angular app. We'll do so in our Update Event component.

We want to make deleting an event slightly more involved than simply clicking a button. We also want to avoid showing the user a modal or pop-up message making them confirm their action. To delete an event, we'll have the user confirm the title of the event by entering it into a text field.

Note: This is how GitHub has users confirm deletion of repositories.

We don't want to confuse users about what they should be entering as a title by showing them both the update and delete forms at the same time, so we'll add tabs to the Update Event component.

Add Tabs to Update Event Component Class

The code necessary to add tabs is minimal. There are no initial data calls necessary to delete events, so our tabs for updating events will be less involved compared to the tabs for viewing an event's details vs. displaying RSVPs.

Open the update-event.component.ts file:

// src/app/pages/admin/update-event/update-event.component.ts ... export class UpdateEventComponent implements OnInit, OnDestroy { ... tabSub: Subscription; tab: string; ngOnInit() { ... // Subscribe to query params to watch for tab changes this.tabSub = this.route.queryParams .subscribe(queryParams => { this.tab = queryParams['tab'] || 'edit'; }); } ... ngOnDestroy() { ... this.tabSub.unsubscribe(); } }

We already have all the imports necessary to add tabs to our component class. We'll add properties for a tabSub subscription and tab string to store the name of the current tab.

In ngOnInit() , we'll add a subscribtion to queryParams to set the local tab property to the contents of the tab query parameter (or 'edit' if no parameter is available).

Finally, we'll unsubscribe from the tabSub in the ngOnDestroy() lifecycle method.

Add Tabs to Update Event Component Template

Let's add the markup necessary to display tabs and dynamic content in our Update Event component template. Open the update-event.component.html file:

<!-- src/app/pages/admin/update-event/update-event.component.html --> ... <ng-template [ngIf]="utils.isLoaded(loading)"> <div *ngIf="event" class="card"> <div class="card-header"> <ul class="nav nav-tabs card-header-tabs"> <li class="nav-item"> <a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'edit' }" [ngClass]="{ 'active': utils.tabIs(tab, 'edit') }">Edit</a> </li> <li class="nav-item"> <a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'delete' }" [ngClass]="{ 'active': utils.tabIs(tab, 'delete') }">Delete</a> </li> </ul> </div> <div class="card-body"> <!-- Edit event form --> <app-event-form *ngIf="utils.tabIs(tab, 'edit')" [event]="event"></app-event-form> <!-- Delete event --> <app-delete-event *ngIf="utils.tabIs(tab, 'delete')" [event]="event"></app-delete-event> </div> </div> <!-- Error loading event --> ... </ng-template>

We can change our <ng-template [ngIf]="event"> to <div *ngIf="event" class="card"> because this element should now render in the page as a container. Then we'll add the necessary markup to create tabs in a card header element. We'll set up the routerLink s with query parameters and [ngClass] to apply a conditional active class for the current tab. Our two tabs will be called "Edit" and "Delete".

Next we'll add a .card-body element containing our conditional tab content. We'll show the <app-event-form> component if the active tab is edit . We'll show an <app-delete-event> component if the delete tab is active. We'll also pass the [event] to the Delete Event component, which we'll create next.

Once we have tabs in place, our Update Event component should like this by default:

Create Delete Event Component

Let's generate our Delete Event component:

$ ng g component pages/admin/update-event/delete-event

This is a child component of Update Event and provides the content for the delete tab.

Delete Event Component Class

Open the delete-event.component.ts and let's add some functionality:

// src/app/pages/admin/update-event/delete-event/delete-event.component.ts import { Component, OnDestroy, Input } from '@angular/core'; import { EventModel } from './../../../../core/models/event.model'; import { Subscription } from 'rxjs'; import { ApiService } from './../../../../core/api.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-delete-event', templateUrl: './delete-event.component.html', styleUrls: ['./delete-event.component.scss'] }) export class DeleteEventComponent implements OnDestroy { @Input() event: EventModel; confirmDelete: string; deleteSub: Subscription; submitting: boolean; error: boolean; constructor( private api: ApiService, private router: Router ) { } removeEvent() { this.submitting = true; // DELETE event by ID this.deleteSub = this.api .deleteEvent$(this.event._id) .subscribe( res => { this.submitting = false; this.error = false; console.log(res.message); // If successfully deleted event, redirect to Admin this.router.navigate(['/admin']); }, err => { console.error(err); this.submitting = false; this.error = true; } ); } ngOnDestroy() { if (this.deleteSub) { this.deleteSub.unsubscribe(); } } }

We'll import OnDestroy and Input , as well as EventModel , Subscription , ApiService , and Router (to redirect after the event has been deleted).

Our parent Update Event component sends the event as an @Input() . We expect this to have the shape EventModel . We'll set a local confirmDelete property to store the string that the user types that needs to match to the event's title to confirm deletion. We also need a deleteSub subscription, and of course, our standard submitting and error states.

We'll add the API service and Router to the constructor. We actually don't need ngOnInit() in this component, so you'll notice we've removed the method and all references to the OnInit lifecycle hook.

Our removeEvent() method will be called from a button that is only enabled once the user has successfully inputted the event's full title in a text field. We'll send the event's _id to our deleteEvent$() API observable. If the event is successfully deleted, we'll need to redirect to the Admin page since there will no longer be any event data available in the Update Event component.

In the ngOnDestroy() method, we'll check if the subscription exists, since it is only created when the user clicks the button to delete the event. If it is present, we'll unsubscribe.

Delete Event Component Template

Now open the delete-event.component.html template and add the following code:

<!-- src/app/pages/admin/update-event/delete-event.component/delete-event.component.html --> <p class="lead"> You are deleting the "<strong [innerHTML]="event.title"></strong>" event. </p> <p class="text-danger"> Deleting this event will also remove all associated RSVPs. Please proceed with caution! </p> <div class="form-group"> <label for="deleteEvent">Confirm event title:</label> <input type="text" id="deleteEvent" class="form-control" [(ngModel)]="confirmDelete"> </div> <!-- Delete button --> <p> <button class="btn btn-danger" (click)="removeEvent()" [disabled]="confirmDelete !== event.title || submitting">Delete Event</button> <app-submitting *ngIf="submitting"></app-submitting> </p> <!-- Error deleting event --> <p *ngIf="error" class="alert alert-danger"> <strong>Oops!</strong> There was an error deleting this event. Please try again. </p>

We'll need to show the name of the event so the admin can confirm the title without too much hassle. Next we'll display some cautionary information.

Then we'll create an extremely simple form. This form is unlike those we created for RSVPing and creating events. In fact, we don't even need true validation. We can handle everything we need with a simple [(ngModel)] directive and a comparison expression.

Note: If you prefer, you may implement a template-driven form here. You can even create a custom validator. However, for the sake of ease and simplicity, this tutorial won't take that approach.

We'll use [(ngModel)] to set up two-way binding between the input field and our confirmDelete property. Our delete button will call the removeEvent() method when clicked, but will be disabled if the value of confirmDelete is not an exact match to the event.title . As usual, we'll disable the button and display our <app-submitting> loading component if the API call is in progress.

Finally, if something went wrong deleting the event, we'll show an error.

Our Update Event component now looks like this when the Delete tab is active:

Angular: Admin Event Links

Now that the functionality for CRUD (Create Read Update Delete) is complete, let's add a couple more buttons to the Admin page to facilitate access to these features.

Add "Create" Link to Admin Page

Let's add a button to the Admin page that links to the Create Event component page. Open the admin.component.html template file and add a paragraph tag with a link:

<!-- src/app/pages/admin/admin.component.html --> ... <ng-template [ngIf]="utils.isLoaded(loading)"> ... <p> <a class="btn btn-success btn-block" routerLink="/admin/event/new">+ Create New Event</a> </p> ...

This link simply leads to the Create Event page.

Add "Edit" and "Delete" Links to Admin Events List

Now let's add a link to each event in the Admin component that will take us straight to the Delete tab for that event. In the admin.component.html template file, update the following:

<!-- src/app/pages/admin/admin.component.html --> ... <!-- Events listing --> <section class="list-group"> <div *ngFor="let event of fs.orderByDate(filteredEvents, 'startDatetime')" class="list-group-item list-group-item-action flex-column align-items-start"> ... <p class="mb-1"> <a class="btn btn-info btn-sm mr-1" [routerLink]="['/admin/event/update', event._id]">Edit</a> <a class="btn btn-danger btn-sm" [routerLink]="['/admin/event/update', event._id]" [queryParams]="{tab: 'delete'}">Delete</a> </p> </div> </section> ...

Let's add an "Edit" and a "Delete" button. The "Edit" button can lead to the Update Event component on its default tab. The "Delete" link should have [queryParams] set to the delete tab.

Our Admin page should now look something like this:

Side Note: Recall that we already added an "Edit" link to our Event Details component. This link should now be active as well.

API: Get Events User Has RSVPed To

Our application is still missing an important feature: a page where the user can collectively view all their RSVPs for upcoming events.

In order to achieve this, we need to get the list of a user's RSVPs and then find all the events that match the RSVP's eventId . MongoDB is not a relational database, but we can use comparison query operators to do this.

Open the server api.js file and add the following route:

// server/api.js ... /* |-------------------------------------- | API Routes |-------------------------------------- */ ... // GET list of upcoming events user has RSVPed to app.get('/api/events/:userId', jwtCheck, (req, res) => { Rsvp.find({userId: req.params.userId}, 'eventId', (err, rsvps) => { const _eventIdsArr = rsvps.map(rsvp => rsvp.eventId); const _rsvpEventsProjection = 'title startDatetime endDatetime'; let eventsArr = []; if (err) { return res.status(500).send({message: err.message}); } if (rsvps) { Event.find( {_id: {$in: _eventIdsArr}, startDatetime: { $gte: new Date() }}, _rsvpEventsProjection, (err, events) => { if (err) { return res.status(500).send({message: err.message}); } if (events) { events.forEach(event => { eventsArr.push(event); }); } res.send(eventsArr); }); } }); }); ...

We'll first use find() to get all RSVPs with a userId matching the user ID passed as a parameter to the route. We'll send a projection of eventId , which means that the returned results will only contain this single key/value. We'll then create an array of event IDs ( _eventIdsArr ) using the Array .map() method to get just the ID strings. We can then use this array to find() only events that have _id s matching items in the array.

The only properties we'll need for display of the event list in the My RSVPs component are title , startDatetime , and endDatetime . We'll create a projection for these called _rsvpEventsProjection .

After handling errors for retrieving RSVPs, we can then find() events with an _id present in the _eventIdsArr . This is done using the MongoDB $in comparison query operator. We only want upcoming events, so we'll indicate that startDatetime should be greater than or equal to ( $gte ) the current datetime. We'll then pass the _rsvpEventsProjection we just created to get back only the properties we need.

After handling errors for retrieving events, we'll push any results found to an array and send() the array.

We're now ready to use our /api/events/:userId endpoint to get a list of upcoming events that the user has RSVPed to.

Angular: Add User's Events Endpoint to API Service

Let's add our new API endpoint to our API service. Open the api.service.ts file and add this method:

// src/app/core/api.service.ts ... // GET all events a specific user has RSVPed to (login required) getUserEvents$(userId: string): Observable<EventModel[]> { return this.http .get<EventModel[]>(`${ENV.BASE_API}events/${userId}`, { headers: new HttpHeaders().set('Authorization', this._authHeader) }) .pipe( catchError((error) => this._handleError(error)) ); } ...

Angular: My RSVPs (Profile)

We now have an API endpoint providing a list of upcoming events a user has responded to. Let's make a My RSVPs (profile) component to display this information to the authenticated user.

Create My RSVPs Component

First we'll generate our new component. Run the following command:

$ ng g component pages/my-rsvps

We now have our My RSVPs component scaffolded.

Update App Routing Module

The My RSVPs component is a routed component, so let's add it to our app-routing.module.ts :

// src/app/core/app-routing.module.ts ... import { MyRsvpsComponent } from './pages/my-rsvps/my-rsvps.component'; const routes: Routes = [ ..., { path: 'my-rsvps', component: MyRsvpsComponent, canActivate: [ AuthGuard ] }, ... ]; ...

The user must be authenticated in order to have a user ID and stored RSVPs, so we'll implement AuthGuard for this route.

Add Capitalize Utility to Service

We're going to display the identity provider (IdP) that the user is currently logged in with. In order to display this from the data given by the user's ID, we'll create a small utility method that capitalizes the first letter of a string.

Open the utils.service.ts file and add this method:

// src/app/core/utils.service.ts ... capitalize(str: string): string { // Capitalize first letter of string return str.charAt(0).toUpperCase() + str.slice(1); } ...

My RSVPs Component Class

Let's implement our My RSVPs component class. Open the my-rsvps.component.ts file and add:

// src/app/pages/my-rsvps/my-rsvps.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { AuthService } from './../../auth/auth.service'; import { ApiService } from './../../core/api.service'; import { UtilsService } from './../../core/utils.service'; import { FilterSortService } from './../../core/filter-sort.service'; import { Subscription } from 'rxjs'; import { EventModel } from './../../core/models/event.model'; @Component({ selector: 'app-my-rsvps', templateUrl: './my-rsvps.component.html', styleUrls: ['./my-rsvps.component.scss'] }) export class MyRsvpsComponent implements OnInit, OnDestroy { pageTitle = 'My RSVPs'; loggedInSub: Subscription; eventListSub: Subscription; eventList: EventModel[]; loading: boolean; error: boolean; userIdp: string; constructor( private title: Title, public auth: AuthService, private api: ApiService, public fs: FilterSortService, public utils: UtilsService ) { } ngOnInit() { this.loggedInSub = this.auth.loggedIn$.subscribe( loggedIn => { this.loading = true; if (loggedIn) { this._getEventList(); } } ); this.title.setTitle(this.pageTitle); } private _getEventList() { // Get events user has RSVPed to this.eventListSub = this.api .getUserEvents$(this.auth.userProfile.sub) .subscribe( res => { this.eventList = res; this.loading = false; }, err => { console.error(err); this.loading = false; this.error = true; } ); } ngOnDestroy() { this.loggedInSub.unsubscribe(); this.eventListSub.unsubscribe(); } }

We'll use our standard imports for routed components with an API call, as well as the FilterSortService to order the events by date. Then we'll add our standard properties to manage page title, the event list subscription, etc.

In our ngOnInit() method, we'll set the page title and _getEventList() , which subscribes to the getUserEvents$() observable we created earlier, passing the user's ID (the auth.userProfile.sub property) to the API endpoint.

Finally, we'll unsubscribe from our API observable in the ngOnDestroy() method.

My RSVPs Component Template

Open the my-rsvps.component.html template file:

<!-- src/app/pages/my-rsvps/my-rsvps.component.html --> <h1 class="text-center">{{ pageTitle }}</h1> <p class="lead" *ngIf="auth.loggedIn && auth.userProfile"> Hello, <strong [innerHTML]="auth.userProfile.name"></strong>! <ng-template [ngIf]="auth.isAdmin"> You may <a routerLink="/admin">create and administer events</a>. </ng-template> </p> <app-loading *ngIf="loading"></app-loading> <ng-template [ngIf]="utils.isLoaded(loading)"> <ng-template [ngIf]="eventList"> <!-- Event list retrieved but no RSVPs yet --> <p *ngIf="!eventList.length" class="lead"> You have not RSVPed to any events yet. Check out the <a routerLink="/">homepage</a> to see a list of upcoming events. </p> <ng-template [ngIf]="eventList.length"> <p class="lead">You have <strong>RSVPed</strong> for the following upcoming events:</p> <!-- Events listing --> <div class="list-group"> <a *ngFor="let event of fs.orderByDate(eventList, 'startDatetime')" [routerLink]="['/event', event._id]" [queryParams]="{ tab: 'rsvp' }" class="list-group-item list-group-item-action flex-column align-items-start"> <div class="d-flex w-100 justify-content-between"> <h5 class="mb-1" [innerHTML]="event.title"></h5> <small>{{ utils.eventDates(event.startDatetime, event.endDatetime) }}</small> </div> <small class="mb-1">Click to view or update this RSVP</small> </a> </div> </ng-template> </ng-template> <!-- Error loading events --> <p *ngIf="error" class="alert alert-danger"> <strong>Oops!</strong> There was an error getting your RSVP data. </p> </ng-template>

We'll ensure the user is logged in and welcome them by name. If the user has admin privileges, we'll show a message with a link to the Admin page where they can create and administer events.

We'll then check whether data has been loaded from the API. If the user hasn't RSVPed to any events yet, we'll show a message letting them know to check out the events listed on the homepage.

If events are present, we'll show a listing with titles, dates, and links to each event's RSVP tab. This way the user can easily view or update their RSVP.

Last, we'll show an error if there was a problem retrieving data from the API.

The My RSVPs component should now look like this in the browser:

Update Header Component

We have a route for our My RSVPs component, but no links to it in the application. Let's add some in our header.component.html :

<!-- src/app/header/header.component.html --> <header id="header" class="header"> ... <span *ngIf="auth.loggedIn && auth.userProfile"> <a routerLink="/my-rsvps">{{ auth.userProfile.name }}</a> ... <nav id="nav" class="nav" role="navigation"> <ul class="nav-list"> ... <li> <a *ngIf="auth.loggedIn" routerLink="/my-rsvps" routerLinkActive="active">My RSVPs</a> </li> ... </ul> </nav> ...

We'll link the authenticated user's name to the /my-rsvps route. We'll also add a link in the navigation sidebar. This link should only appear if the user is logged in.

Angular: Silently Renew Token with Auth0

You may have noticed throughout development that your access token periodically expires if the same session is left open for longer than two hours. This can result in unexpected loss of access to the API, or the UI still displaying elements that aren't actually accessible to unauthenticated users.

In order to prevent session disruption, we're going to implement automatic authentication renewal with Auth0. The auth0.js library has a method for performing silent authentication to acquire new tokens. If you recall, we have actually already implemented token renewal way back in Part 2: Angular Authentication in our Authentication service's renewToken() method. We'll now set up a way to silently and automatically call this method when the user's token expires during an active browser session.

Important Note: If you are using Auth0 social connections in your app, please make sure that you have set the connections up to use your own client app keys. If you're using Auth0 dev keys, token renewal will always return login_required . Each social connection's details has a link with explicit instructions on how to acquire your own key for the particular IdP.

Token renewal with silent authentication will not reload our app or redirect users to the Auth0 login page. The renewal will take place behind the scenes in an iframe, preventing disruption of the user experience.

Update Auth Service to Support Token Renewal

Now we'll make updates to our AuthService to support scheduled, silent token renewal.

Open the auth.service.ts file and let's get started:

// src/app/core/auth/auth.service.ts ... import { BehaviorSubject, Subscription, of, timer } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; @Injectable() export class AuthService { ... // Subscribe to token expiration stream refreshSub: Subscription; ... private _setSession(authResult, profile?) { ... // Schedule access token renewal this.scheduleRenewal(); } ... scheduleRenewal() { // If last token is expired, do nothing if (!this.tokenValid) { return; } // Unsubscribe from previous expiration observable this.unscheduleRenewal(); // Create and subscribe to expiration observable const expiresIn$ = of(this.expiresAt).pipe( mergeMap( expires => { const now = Date.now(); // Use timer to track delay until expiration // to run the refresh at the proper time return timer(Math.max(1, expires - now)); } ) ); this.refreshSub = expiresIn$ .subscribe( () => { this.renewToken(); this.scheduleRenewal(); } ); } unscheduleRenewal() { if (this.refreshSub) { this.refreshSub.unsubscribe(); } } }

First we'll import Subscription , of , and timer to support creating an observable token expiration timer. We'll then need to import the mergeMap operator from RxJS.

We'll declare a new property for a subscription to a token expiration stream we'll create shortly. This property is called refreshSub and has a type of Subscription .

Next, we'll update the _setSession() method. We need to call scheduleRenewal() to restart the expiration timer when a new token has been retrieved. This will then initiate the process of updating the token, and also fetching the user's info again, just in case something has changed or gone awry. If we're getting a new token, we also want to get the user data again as well for security.

Let's talk about the renewToken() method that already exists, but in a little more depth this time. Here's the method for a refresher:

renewToken() { // Check for valid Auth0 session this._auth0.checkSession({}, (err, authResult) => { if (authResult && authResult.accessToken) { this._getProfile(authResult); } else { this._clearExpiration(); } }); }

This method uses auth0.js to acquire new tokens using the checkSession() method on our existing _auth0 web auth instance. This approach uses postMessage under the hood to implement cross-origin communication between our parent app and the silent authentication taking place in an iframe. On successful acquisition of a new access token, we'll call _getProfile() to retrieve user info and then update the session data.

The first new method is scheduleRenewal() :

scheduleRenewal() { // If last token is expired, do nothing if (!this.tokenValid) { return; } // Unsubscribe from previous expiration observable this.unscheduleRenewal(); // Create and subscribe to expiration observable const expiresIn$ = of(this.expiresAt).pipe( mergeMap( expires => { const now = Date.now(); // Use timer to track delay until expiration // to run the refresh at the proper time return timer(Math.max(1, expires - now)); } ) ); this.refreshSub = expiresIn$ .subscribe( () => { this.renewToken(); this.scheduleRenewal(); } ); }

If the user is authenticated, we'll do a cleanup check and then create an observable called expiresIn$ . We'll use the mergeMap() RxJS operator to flatten the stream. We'll then utilize a timer() that produces a value ( 0 ) when the current access token expires.

Then we'll subscribe to the expiresIn$ observable. As declared with our properties earlier, this subscription is called refreshSub . When the 0 value is produced indicating the token is expired, we'll call renewToken() and scheduleRenewal() to set the session and reset the timer with the fresh token's expiration countdown.

The other new method is unscheduleRenewal() :

unscheduleRenewal() { if (this.refreshSub) { this.refreshSub.unsubscribe(); } }

This method simply checks for the existence of a refreshSub subscription and unsubscribes from it. The method is called on logout, or if we need to subscribe to a new token expiration observable ( expiresIn$ ).

Update Auth0 Application Settings

In order to avoid getting a 403 Forbidden error when silently renewing authentication, we need to make sure our Auth0 Client's allowed origin settings are updated.

Go to your Auth0 Dashboard Clients section and select your app's Client. In the Allowed Web Origins, add http://localhost:4200 and http://localhost:8083 to the list. Save your changes. This enables both our Angular server and Node server domains to be used with web message response mode.

Note: Recall that we are using the loggedIn property to track authentication state in templates (rather than auth.tokenValid ). This is ideal because loggedIn is updated on the fly as the state changes. We also won't get a flash of a logged-out state during a successful token renewal.

Test Token Expiration and Renewal

For testing, you can change the access token expiration time in your Auth0 Dashboard APIs. Select the API you set up for this application. You can then change the Token Expiration For Browser Flows (Seconds) value. It is 7200 seconds by default (2 hours). For testing token renewal, you can change this value to something much shorter. Test it out with the setting at 20 seconds. If you open your browser's Network panel, your login should be persisted and you should see activity (a call to the authorize endpoint) every 20 seconds indicating successful silent token renewal.

Note: Make sure to change the token expiration back once you're finished testing your renewal implementation.

Aside: Securing Applications with Auth0

Are you building a B2C, B2B, or B2E tool? Auth0 can help you focus on what matters the most to you, the special features of your product. Auth0 can improve your product's security with state-of-the-art features like passwordless, breached password surveillance, and multifactor authentication.

We offer a generous free tier so you can get started with modern authentication.

Summary

In Part 7 of our Real-World Angular Series, we've covered deleting events, listing events a user has RSVPed to, and silent renewal of authentication tokens. In the final part of the tutorial series, we'll cover NgModule refactoring, lazy loading, and production deployment with SSL.