mean-auth-app-universal
mean-auth-app-universal copied to clipboard
MEAN stack project w/ auth (Passport) and SSR (Angular Universal)
MEAN Auth App with Angular Universal
Last Update: 3/7/2018
This project has been updated with the most recent MEAN versions listed below and has been integrated with Angular Universal for SEO and social media compatibility using server-side rendering. The code of this project is a result of my code-along at the end of part 9 of the video series MEAN Stack Front to Back by Brad Traversy. His original code repo may be found here. Instructions on how to deploy this code to Heroku are also included.

Versions Used
This project was generated with Angular CLI v1.7.3
- MongoDB v3.6.2 (Mongoose 5.0.9)
- Express v4.16.2
- Angular v5.2.8
- Node.js v9.8.0
Starting the Project
To begin working with this project, perform the following tasks:
- Clone this repository to your local machine
- Start up your local instance of MongoDB
- Run
npm installto download dependencies - Run
npm startto generate the /dist folder for your Angular front-end, the /dist-server folder for Angular Universal, and to start your Node.js server onhttp://localhost:3000/.
Development server
Run ng serve for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files. Please note that this will not start your MongoDB connection.
Code Updates to Original Project
The original project by Brad Traversy was made in mid Febuary 2017 with Angular 2. Since that time, there have been numerous code-breaking changes, especially with the release of Angular 5 in early November 2017. Hopefully, this repository will help others code-along to this outstanding video series more easily.
Back-End Updates [Parts 1 - 4]
The following changes should be made to your code when developing your back-end. Most of these updates reflect changes to third-party packages such as Passport.js, bcrpyt, and mongoose.
-
Mongoose has been updated to version 5 and the connection is configured as a Promise.
mongoose.connect(config.database) .then(() => console.log('MongoDB Connected')) .catch(err => console.log(err)); -
Use
{data: user}in thejwt.sign()method inusers.jsUser.comparePassword(password, user.password, (err, isMatch) => { if(err) throw err; if(isMatch) { const token = jwt.sign({data: user}, config.secret, { -
Use
Bearer ${token}(make sure to include the space) in theUser.comparePassword()method inusers.jsres.json({ success: true, token: `Bearer ${token}`, user: { id: user._id, name: user.name, username: user.username, email: user.email } -
Use
ExtractJwt.fromAuthHeaderAsBearerToken()method inpassport.jsmodule.exports = (passport) => { let opts = {}; opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); -
Use
jwt_payload.data._idas the argument to theUser.getUserByIdmethod inpassport.jspassport.use(new JwtStrategy(opts, (jwt_payload, done) => { User.getUserById(jwt_payload.data._id, (err, user) => { -
Nested callback functions have been converted to Promises (optional - example shown below)
module.exports.addUser = function(newUser, callback){ bcrypt.genSalt(10, (err, salt) => { bcrypt.hash(newUser.password, salt, (err, hash) => { if(err) throw err; newUser.password = hash; newUser.save(callback); }); }); }becomes
module.exports.addUser = (newUser, callback) => { bcrypt.genSalt(10) .then((salt) => bcrypt.hash(newUser.password, salt)) .then((hash) => { newUser.password = hash; newUser.save(callback); }).catch((err) => console.log('There was an error adding a user.')); }
Front-End Updates [Parts 5 - 9]
The code in this section mainly focuses on building the front-end with Angular. This project has been integrated with Angular Universal for server side rendering and because of this, there have been several changes to the original code for compatibility reasons in addition to various UI changes. It's also important to note that the code in this repo is structured differently than in the video series, but the updated changes can be used in your own code-along projects.
Forms
-
Import
FormsModuleintoapp.module.tsimport { FormsModule } from '@angular/common/http'; ... ... imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), // BrowserModule if not using Angular Universal FormsModule, HttpClientModule, RouterModule.forRoot(appRoutes), ], -
Template-Driven Forms are used to implement forms in the
register.componentandlogin.componentfiles (example below).<form #loginForm="ngForm" (ngSubmit)="onLoginSubmit(loginForm)"> <div class="form-group"> <label for="username">Username <sup class="red">*</sup></label> <input required name="username" id="username" #username="ngModel" ngModel type="email" class="form-control"> </div> ... -
Validation used with
*ngIfdirective instead ofangular2-flash-messagessince this package was not compatible with Angular Universal. Code adapted to continue to validate for all fields required and an appropriate e-mail input just as in the original video. The only difference is that the flash messages do not disappear, but the functionality of each webpage was adapted to accomodate for this (example below).<h2 class="page-header">Register</h2> <div class="alert alert-danger" *ngIf="errSwitch" role="alert">{{ errMsg }}</div> <div class="alert alert-success" *ngIf="succSwitch" role="alert">{{ succMsg }}</div>
HTTP Client
HTTPClient is used to connect our HTTP API endpoints as the old Http API used in the video series has been deprecated in Angular 5. The following syntax changes should be made in order to properly connect your back-end APIs.
-
Import
HTTPClientModuleintoapp.module.tsimport { HttpClientModule } from '@angular/common/http'; ... ... imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), // BrowserModule if not using Angular Universal FormsModule, HttpClientModule, RouterModule.forRoot(appRoutes), ], -
Import
HTTPClientinto files when using the clientimport { HttpClient } from '@angular/common/http'; -
JSON is an assumed default and no longer needs to be explicitly parsed (example from
registerUser(user)function inauth.service.ts).return this.http.post('http://localhost:3000/users/register', user, { headers }) .map(res => res.json());becomes
return this.http.post('http://localhost:3000/users/register', user, { headers }); -
Subscribe to observables as usual, but to avoid a pre-compilation error for
data.success, set thedataargument to typeany(example fromlogin.component.ts).this.authService.authenticateUser(user).subscribe((data: any) => { if (data.success) { this.authService.storeUserData(data.token, data.user); this.router.navigateByUrl('/dashboard'); -
HTTPHeadershas replaced the deprecatedHeadersAPI. HTTPHeaders are immutable so after importing them from@angular/common/http, they must be explicitly appended to the defined header itself. HTTPHeaders are used inauth.service.ts.getProfile() { this.loadToken(); let headers = new HttpHeaders(); headers.append('Content-Type', 'application/json'); headers.append('Authorization', this.authToken); return this.http.get('http://localhost:3000/users/profile', { headers }); }becomes
getProfile() { this.loadToken(); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'application/json'); headers = headers.append('Authorization', this.authToken); return this.http.get('http://localhost:3000/users/profile', { headers }); }
Other
-
The
tokenNotExpired()function was not implemented since theAngular2-jwtpackage was not compatibile with Angular Universal. Instead, a simple workaround was implemented, which unfortunately does not save the user's application state after exiting the browser window.loggedIn() { if (this.authToken) { return true; } else { return false; } }If you are not using Angular Universal,
id_tokenshould be set as an argument fortokenNotExpired()loggedIn() { tokenNotExpired(id_token); } -
NotFoundComponentadded toapp.module.tsto catch all Error: 404 - Page Not Found routes in Angular. There is a separate error handler for 404s with API requests inserver.jsconst appRoutes: Routes = [ { path: '', component: HomeComponent }, ... ... { path: '**', component: NotFoundComponent } ]; -
Class for fade-in animation added for each page transition in
styles.css(optional).fadein { -webkit-animation: fadein 1s; /* Safari, Chrome and Opera > 12.1 */ -moz-animation: fadein 1s; /* Firefox < 16 */ -ms-animation: fadein 1s; /* Internet Explorer */ -o-animation: fadein 1s; /* Opera < 12.1 */ animation: fadein 1s; } @keyframes fadein { from { opacity: 0; } to { opacity: 1; } } ...
Deploying to Heroku
This code may be deployed to Heroku and connected to mLab making it accessible on the internet. This guide has been slightly modified from Part 10 of the video series as this code has been structured differently.
- Set up mLab by modifying
config/database.jsas directed in Part 10 - Modify
registerUser(user),authenticateUser(user), andgetProfile()functions inauth.service.tsto connect to mLab (example below)
becomesregisterUser(user) { user.username = user.username.toLowerCase(); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'application/json'); return this.http.post('http://localhost:3000/users/register', user, { headers }); }registerUser(user) { user.username = user.username.toLowerCase(); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'application/json'); return this.http.post('users/register', user, { headers }); } - Modify the
startandpostinstallscripts inpackage.jsonto:"scripts": { "ng": "ng", "start": "node server.js", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", "postinstall": "ng build --prod && ng build --prod --app universal --output-hashing=none" }, - Move the following dependecies from
devDependenciestodependenciesinpackage.json:"dependencies": { ... "@angular/cli": "1.6.2", "@angular/compiler-cli": "^5.0.0", ... "typescript": "~2.4.2", ... }, - Deploy to Heroku as directed in Part 10.
Further help
To get more help on the Angular CLI use ng help or go check out the Angular CLI README.