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.
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
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 install
to download dependencies - Run
npm start
to 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/
.
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.
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.
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.js
User.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.js
res.json({ success: true, token: `Bearer ${token}`, user: { id: user._id, name: user.name, username: user.username, email: user.email }
-
Use
ExtractJwt.fromAuthHeaderAsBearerToken()
method inpassport.js
module.exports = (passport) => { let opts = {}; opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
-
Use
jwt_payload.data._id
as the argument to theUser.getUserById
method inpassport.js
passport.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.')); }
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.
-
Import
FormsModule
intoapp.module.ts
import { 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.component
andlogin.component
files (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
*ngIf
directive instead ofangular2-flash-messages
since 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>
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
HTTPClientModule
intoapp.module.ts
import { HttpClientModule } from '@angular/common/http'; ... ... imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), // BrowserModule if not using Angular Universal FormsModule, HttpClientModule, RouterModule.forRoot(appRoutes), ],
-
Import
HTTPClient
into 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 thedata
argument 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');
-
HTTPHeaders
has replaced the deprecatedHeaders
API. 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 }); }
-
The
tokenNotExpired()
function was not implemented since theAngular2-jwt
package 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_token
should be set as an argument fortokenNotExpired()
loggedIn() { tokenNotExpired(id_token); }
-
NotFoundComponent
added toapp.module.ts
to catch all Error: 404 - Page Not Found routes in Angular. There is a separate error handler for 404s with API requests inserver.js
const 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; } } ...
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.js
as directed in Part 10 - Modify
registerUser(user)
,authenticateUser(user)
, andgetProfile()
functions inauth.service.ts
to 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
start
andpostinstall
scripts inpackage.json
to:"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
devDependencies
todependencies
inpackage.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.
To get more help on the Angular CLI use ng help
or go check out the Angular CLI README.