Skip to content

Commit 81fceec

Browse files
Add router config and create global services:
- Confirm dialog (modal-like) - App Config (persisted settings) - Requires-auth Transition Hook - Authentication service - Session Storage fake REST API - Google Analytics Transition Hook
1 parent 7bafd62 commit 81fceec

14 files changed

+424
-1
lines changed

src/app/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Contents
2+
3+
### The main app module bootstrap
4+
5+
- *app.states*.js: Defines the top-level states such as home, welcome, and login
6+
7+
### Components for the Top-level states
8+
9+
- *app.component*.js: A component which displays the header nav-bar for authenticated in users
10+
- *home.component*.js: A component that has links to the main submodules
11+
- *login.component*.js: A component for authenticating a guest user
12+
- *welcome.component*.js: A component which displays a welcome screen for guest users

src/app/app.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { LoginComponent } from './login.component';
99
import { HomeComponent } from './home.component';
1010
import { UIRouterModule, UIView } from 'ui-router-ng2';
1111
import { APP_STATES } from './app.states';
12+
import { GlobalModule } from './global/global.module';
13+
import { routerConfigFn } from './router.config';
1214

1315
@NgModule({
1416
declarations: [
@@ -20,8 +22,10 @@ import { APP_STATES } from './app.states';
2022
imports: [
2123
UIRouterModule.forRoot({
2224
states: APP_STATES,
23-
otherwise: { state: 'home' }
25+
otherwise: { state: 'home' },
26+
config: routerConfigFn,
2427
}),
28+
GlobalModule,
2529
BrowserModule,
2630
FormsModule,
2731
HttpModule

src/app/global/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Contents
2+
3+
### Global services
4+
- *appConfig*.service.js: Stores and retrieves the user's application preferences
5+
- *auth*.service.js: Simulates an authentication service
6+
- *dialog*.service.js: Provides a dialog confirmation service
7+
8+
### Directives
9+
- *dialog*.directive.js: Provides a dialog directive used by the dialog service
10+
11+
### Router Hooks
12+
13+
- *requiresAuth*.hook.js: A transition hook which allows a state to declare that it requires an authenticated user
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Injectable } from '@angular/core';
2+
3+
/**
4+
* This service stores and retrieves user preferences in session storage
5+
*/
6+
@Injectable()
7+
export class AppConfigService {
8+
sort = '+date';
9+
emailAddress: string = undefined;
10+
restDelay = 100;
11+
12+
constructor() {
13+
this.load();
14+
}
15+
16+
load() {
17+
try {
18+
return Object.assign(this, JSON.parse(sessionStorage.getItem('appConfig')));
19+
} catch (Error) { }
20+
21+
return this;
22+
}
23+
24+
save() {
25+
sessionStorage.setItem('appConfig', JSON.stringify(Object.assign({}, this)));
26+
}
27+
}
28+

src/app/global/auth.hook.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { TransitionService } from 'ui-router-core';
2+
import { AuthService } from './auth.service';
3+
4+
/**
5+
* This file contains a Transition Hook which protects a
6+
* route that requires authentication.
7+
*
8+
* This hook redirects to /login when both:
9+
* - The user is not authenticated
10+
* - The user is navigating to a state that requires authentication
11+
*/
12+
export function requiresAuthHook(transitionService: TransitionService) {
13+
// Matches if the destination state's data property has a truthy 'requiresAuth' property
14+
const requiresAuthCriteria = {
15+
to: (state) => state.data && state.data.requiresAuth
16+
};
17+
18+
// Function that returns a redirect for the current transition to the login state
19+
// if the user is not currently authenticated (according to the AuthService)
20+
21+
const redirectToLogin = (transition) => {
22+
const authService: AuthService = transition.injector().get(AuthService);
23+
const $state = transition.router.stateService;
24+
if (!authService.isAuthenticated()) {
25+
return $state.target('login', undefined, { location: false });
26+
}
27+
};
28+
29+
// Register the "requires auth" hook with the TransitionsService
30+
transitionService.onBefore(requiresAuthCriteria, redirectToLogin, {priority: 10});
31+
}

src/app/global/auth.service.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Injectable } from '@angular/core';
2+
import { AppConfigService } from './app-config.service';
3+
import { wait } from '../util/util';
4+
5+
/**
6+
* This service emulates an Authentication Service.
7+
*/
8+
@Injectable()
9+
export class AuthService {
10+
// data
11+
usernames: string[] = ['[email protected]', '[email protected]', '[email protected]'];
12+
13+
constructor(public appConfig: AppConfigService) { }
14+
15+
/**
16+
* Returns true if the user is currently authenticated, else false
17+
*/
18+
isAuthenticated() {
19+
return !!this.appConfig.emailAddress;
20+
}
21+
22+
/**
23+
* Fake authentication function that returns a promise that is either resolved or rejected.
24+
*
25+
* Given a username and password, checks that the username matches one of the known
26+
* usernames (this.usernames), and that the password matches 'password'.
27+
*
28+
* Delays 800ms to simulate an async REST API delay.
29+
*/
30+
authenticate(username, password) {
31+
const appConfig = this.appConfig;
32+
33+
// checks if the username is one of the known usernames, and the password is 'password'
34+
const checkCredentials = () => new Promise<string>((resolve, reject) => {
35+
const validUsername = this.usernames.indexOf(username) !== -1;
36+
const validPassword = password === 'password';
37+
38+
return (validUsername && validPassword) ? resolve(username) : reject('Invalid username or password');
39+
});
40+
41+
return wait(800)
42+
.then(checkCredentials)
43+
.then((authenticatedUser) => {
44+
appConfig.emailAddress = authenticatedUser;
45+
appConfig.save();
46+
});
47+
}
48+
49+
/** Logs the current user out */
50+
logout() {
51+
this.appConfig.emailAddress = undefined;
52+
this.appConfig.save();
53+
}
54+
}

src/app/global/dialog.component.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { wait } from '../util/util';
3+
4+
@Component({
5+
selector: 'app-dialog',
6+
template: `
7+
<div class="backdrop" [class.active]="visible"></div>
8+
<div class='wrapper'>
9+
<div class="content">
10+
<h4 *ngIf="message">{{message}}</h4>
11+
<div [hidden]="!details">{{details}}</div>
12+
13+
<div style="padding-top: 1em;">
14+
<button class="btn btn-primary" (click)="yes()">{{yesMsg}}</button>
15+
<button class="btn btn-primary" (click)="no()">{{noMsg}}</button>
16+
</div>
17+
</div>
18+
</div>
19+
`,
20+
styles: []
21+
})
22+
export class DialogComponent implements OnInit {
23+
visible: boolean;
24+
25+
message: string;
26+
details: string;
27+
yesMsg: string;
28+
noMsg: string;
29+
30+
promise: Promise<any>;
31+
32+
constructor() {
33+
this.promise = new Promise((resolve, reject) => {
34+
this.yes = () => {
35+
this.visible = false;
36+
wait(300).then(resolve);
37+
};
38+
39+
this.no = () => {
40+
this.visible = false;
41+
wait(300).then(reject);
42+
};
43+
});
44+
}
45+
46+
yes() {}
47+
no() {}
48+
49+
ngOnInit() {
50+
wait(50).then(() => this.visible = true);
51+
}
52+
}

src/app/global/dialog.service.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Injectable, ViewContainerRef, ComponentFactoryResolver, Injector, ComponentFactory } from '@angular/core';
2+
import { DialogComponent } from './dialog.component';
3+
4+
@Injectable()
5+
export class DialogService {
6+
vcRef: ViewContainerRef;
7+
private factory: ComponentFactory<DialogComponent>;
8+
9+
constructor(resolver: ComponentFactoryResolver) {
10+
this.factory = resolver.resolveComponentFactory(DialogComponent);
11+
}
12+
13+
confirm(message, details = 'Are you sure?', yesMsg = 'Yes', noMsg = 'No') {
14+
const componentRef = this.vcRef.createComponent(this.factory);
15+
const component = componentRef.instance;
16+
17+
component.message = message;
18+
component.details = details;
19+
component.yesMsg = yesMsg;
20+
component.noMsg = noMsg;
21+
22+
const destroy = () => componentRef.destroy();
23+
component.promise.then(destroy, destroy);
24+
return component.promise;
25+
}
26+
}

src/app/global/global.module.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NgModule } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { AppConfigService } from './app-config.service';
4+
import { AuthService } from './auth.service';
5+
import { DialogComponent } from './dialog.component';
6+
import { DialogService } from './dialog.service';
7+
8+
@NgModule({
9+
imports: [
10+
CommonModule
11+
],
12+
providers: [
13+
AppConfigService,
14+
AuthService,
15+
DialogService,
16+
],
17+
declarations: [DialogComponent],
18+
entryComponents: [DialogComponent],
19+
})
20+
export class GlobalModule { }

src/app/router.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { UIRouter } from 'ui-router-core';
2+
import { googleAnalyticsHook } from './util/ga';
3+
import { requiresAuthHook } from './global/auth.hook';
4+
5+
export function routerConfigFn(router: UIRouter) {
6+
const transitionService = router.transitionService;
7+
requiresAuthHook(transitionService);
8+
googleAnalyticsHook(transitionService);
9+
}

0 commit comments

Comments
 (0)