Skip to content

Commit a98aca1

Browse files
committed
feat(frontend): use @ngrx/effects for authentication
1 parent 1de985b commit a98aca1

File tree

14 files changed

+181
-109
lines changed

14 files changed

+181
-109
lines changed

src/main/typescript/app/app.component.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {ReactiveFormsModule, FormsModule} from '@angular/forms';
77
import {CoreModule} from './core/core.module';
88
import {BaseRequestOptions, Http, ConnectionBackend, RequestOptions} from '@angular/http';
99
import {MockBackend} from '@angular/http/testing';
10-
import {AuthService} from './core/store/auth/auth.service';
1110

1211
describe('AppComponent', () => {
1312

@@ -36,8 +35,7 @@ describe('AppComponent', () => {
3635
return new Http(backend, defaultOptions);
3736
},
3837
deps: [MockBackend, BaseRequestOptions]
39-
},
40-
AuthService
38+
}
4139
]
4240
});
4341

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {Component, OnInit} from '@angular/core';
2-
import {AuthService} from './core/store/auth/auth.service';
2+
import {RootState} from './core/store/index';
3+
import {Store} from '@ngrx/store';
4+
import {LoadAndProcessTokenAction} from './core/store/auth/auth.actions';
35

46
@Component({
57
selector: 'shardis-root',
@@ -8,11 +10,11 @@ import {AuthService} from './core/store/auth/auth.service';
810
})
911
export class AppComponent implements OnInit {
1012

11-
constructor(private authService: AuthService) {
13+
constructor(private store: Store<RootState>) {
1214
}
1315

1416
ngOnInit() {
15-
this.authService.dispatchToken();
17+
this.store.dispatch(new LoadAndProcessTokenAction());
1618
}
1719

1820
}

src/main/typescript/app/auth/login/login.component.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {RouterTestingModule} from '@angular/router/testing';
55
import {CoreModule} from '../../core/core.module';
66
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
77
import {ClarityModule} from 'clarity-angular';
8-
import {AuthService} from '../../core/store/auth/auth.service';
98
import {MockBackend} from '@angular/http/testing';
109
import {Http, ConnectionBackend, RequestOptions, BaseRequestOptions} from '@angular/http';
1110

@@ -33,8 +32,7 @@ describe('LoginComponent', () => {
3332
return new Http(backend, defaultOptions);
3433
},
3534
deps: [MockBackend, BaseRequestOptions]
36-
},
37-
AuthService
35+
}
3836
]
3937
})
4038
.compileComponents();

src/main/typescript/app/auth/login/login.component.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {Component, OnInit} from '@angular/core';
2-
import {AuthService} from '../../core/store/auth/auth.service';
32
import {Observable} from 'rxjs/Observable';
43
import {Store} from '@ngrx/store';
54
import {RootState} from '../../core/store/index';
6-
import {ClearAuthErrorAction} from '../../core/store/auth/auth.actions';
5+
import {ClearAuthErrorAction, AuthenticateAction, AuthenticateActionPayload} from '../../core/store/auth/auth.actions';
76

87
@Component({
98
selector: 'shardis-login',
@@ -16,14 +15,14 @@ export class LoginComponent implements OnInit {
1615
password = 'xxxxxx';
1716
authError$: Observable<string>;
1817

19-
constructor(private authService: AuthService, private store: Store<RootState>) {
18+
constructor(private store: Store<RootState>) {
2019
this.authError$ = store.select(s => s.auth.error);
2120
}
2221

2322
authenticate() {
2423
console.log('authenticate');
2524
this.store.dispatch(new ClearAuthErrorAction());
26-
this.authService.authenticate(this.username, this.password);
25+
this.store.dispatch(new AuthenticateAction(new AuthenticateActionPayload(this.username, this.password)));
2726
}
2827

2928
ngOnInit() {

src/main/typescript/app/core/core.module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {RouterStoreModule} from '@ngrx/router-store';
44
import {StoreDevtoolsModule} from '@ngrx/store-devtools';
55
import {StoreModule} from '@ngrx/store';
66
import {reducer} from './store/index';
7-
import {AuthService} from './store/auth/auth.service';
7+
import {EffectsModule} from '@ngrx/effects';
8+
import {AuthEffects} from './store/auth/auth.effects';
89

910
const store = StoreModule.provideStore(reducer);
1011

@@ -14,9 +15,9 @@ const store = StoreModule.provideStore(reducer);
1415
store,
1516
RouterStoreModule.connectRouter(),
1617
StoreDevtoolsModule.instrumentOnlyWithExtension(),
18+
EffectsModule.run(AuthEffects)
1719
],
1820
providers: [
19-
AuthService
2021
]
2122
})
2223
export class CoreModule {

src/main/typescript/app/core/store/auth/auth.actions.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,87 @@ import {Action} from '@ngrx/store';
22
import {uniqueActionType} from '../shared/unique-action-type';
33

44
export const ActionTypes = {
5+
AUTHENTICATE: uniqueActionType('[Auth] Authenticate'),
6+
AUTH_SUCCESS: uniqueActionType('[Auth] Auth success'),
57
LOGOUT: uniqueActionType('[Auth] Logout'),
8+
LOAD_AND_PROCESS_TOKEN: uniqueActionType('[Auth] Load and process token'),
69
PROCESS_TOKEN: uniqueActionType('[Auth] Process token'),
710
CLEAR_AUTH_ERROR: uniqueActionType('[Auth] Clear auth error'),
11+
CLEAR_AUTH_STATE: uniqueActionType('[Auth] Clear auth state'),
812
AUTH_ERROR: uniqueActionType('[Auth] Auth error')
913
};
1014

15+
export class AuthenticateActionPayload {
16+
17+
constructor(public username: string, public password: string) {
18+
}
19+
20+
}
21+
22+
export class AuthenticateAction implements Action {
23+
type = ActionTypes.AUTHENTICATE;
24+
constructor(public payload: AuthenticateActionPayload) {
25+
}
26+
}
27+
1128
export class LogoutAction implements Action {
1229
type = ActionTypes.LOGOUT;
1330

1431
constructor() {
1532
}
1633
}
1734

35+
export class AuthSuccessAction implements Action {
36+
type = ActionTypes.AUTH_SUCCESS;
37+
38+
constructor(public payload: string) {
39+
}
40+
}
41+
42+
export class LoadAndProcessTokenAction implements Action {
43+
type = ActionTypes.LOAD_AND_PROCESS_TOKEN;
44+
45+
constructor() {
46+
}
47+
}
48+
49+
1850
export class ProcessTokenAction implements Action {
1951
type = ActionTypes.PROCESS_TOKEN;
2052

21-
constructor(public jwtToken: string) {
53+
constructor(public payload: string) {
2254
}
2355
}
2456

2557
export class AuthErrorAction implements Action {
2658
type = ActionTypes.AUTH_ERROR;
2759

28-
constructor(public error: string) {
60+
constructor(public payload: string) {
2961
}
3062
}
3163

3264
export class ClearAuthErrorAction implements Action {
33-
type = ActionTypes.CLEAR_AUTH_ERROR;
65+
type = ActionTypes.CLEAR_AUTH_STATE;
3466

3567
constructor() {
3668
}
3769
}
3870

3971

72+
export class ClearAuthStateAction implements Action {
73+
type = ActionTypes.CLEAR_AUTH_ERROR;
74+
75+
constructor() {
76+
}
77+
}
78+
4079

4180
export type Actions
42-
= LogoutAction
81+
= AuthenticateAction
82+
| AuthSuccessAction
83+
| LogoutAction
84+
| LoadAndProcessTokenAction
4385
| ProcessTokenAction
4486
| AuthErrorAction
87+
| ClearAuthStateAction
4588
| ClearAuthErrorAction;

src/main/typescript/app/core/store/auth/auth.service.spec.ts renamed to src/main/typescript/app/core/store/auth/auth.effects.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/* tslint:disable:no-unused-variable */
22
import {TestBed, inject} from '@angular/core/testing';
3-
import {AuthService} from './auth.service';
43
import {Http, BaseRequestOptions, ConnectionBackend, RequestOptions} from '@angular/http';
54
import {MockBackend} from '@angular/http/testing';
65
import {CoreModule} from '../../core.module';
6+
import {AuthEffects} from './auth.effects';
77

8-
describe('Service: Auth', () => {
8+
describe('Effects: Auth', () => {
99

1010
beforeEach(() => {
1111
TestBed.configureTestingModule({
@@ -22,12 +22,12 @@ describe('Service: Auth', () => {
2222
},
2323
deps: [MockBackend, BaseRequestOptions]
2424
},
25-
AuthService
25+
AuthEffects
2626
]
2727
});
2828
});
2929

30-
it('should exist', inject([AuthService], (service: AuthService) => {
30+
it('should exist', inject([AuthEffects], (service: AuthEffects) => {
3131
expect(service).toBeTruthy();
3232
}));
3333

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {Injectable} from '@angular/core';
2+
import {Http, Headers} from '@angular/http';
3+
import {
4+
ActionTypes,
5+
AuthenticateAction,
6+
ProcessTokenAction,
7+
AuthErrorAction,
8+
AuthSuccessAction,
9+
LogoutAction,
10+
ClearAuthStateAction,
11+
LoadAndProcessTokenAction
12+
} from './auth.actions';
13+
import {Effect, Actions} from '@ngrx/effects';
14+
import {Observable} from 'rxjs/Rx';
15+
import {go} from '@ngrx/router-store';
16+
import {LocalStorage} from 'ng2-webstorage';
17+
import {Action} from '@ngrx/store';
18+
19+
20+
@Injectable()
21+
export class AuthEffects {
22+
23+
@Effect() authenticate$: Observable<Action> = this.actions$
24+
.ofType(ActionTypes.AUTHENTICATE)
25+
.map(action => action as AuthenticateAction)
26+
.switchMap((action: AuthenticateAction) => {
27+
if (!action.payload.username.trim()) {
28+
return Observable.of(new AuthErrorAction('Username cannot be blank'));
29+
}
30+
if (!action.payload.password.trim()) {
31+
return Observable.of(new AuthErrorAction('Password cannot be blank'));
32+
}
33+
const headers = new Headers();
34+
headers.append('Content-Type', `application/x-www-form-urlencoded`);
35+
const payload = 'username=' + encodeURIComponent(action.payload.username)
36+
+ '&password=' + encodeURIComponent(action.payload.password);
37+
return this.http
38+
.post('/api/authentication', payload, {headers: headers})
39+
.map(res => new AuthSuccessAction(res.text()))
40+
.catch(() => Observable.of(new AuthErrorAction('Authentication failed')));
41+
});
42+
43+
@Effect() authSuccess$: Observable<Action> = this.actions$
44+
.ofType(ActionTypes.AUTH_SUCCESS)
45+
.map(action => action as AuthSuccessAction)
46+
.switchMap(action => {
47+
this.jwtToken = action.payload;
48+
return Observable.concat(
49+
Observable.of(go('/')),
50+
Observable.of(new ProcessTokenAction(action.payload))
51+
);
52+
});
53+
54+
@Effect() logout$: Observable<Action> = this.actions$
55+
.ofType(ActionTypes.LOGOUT)
56+
.map(action => action as LogoutAction)
57+
.map(() => {
58+
this.jwtToken = null;
59+
return new ClearAuthStateAction();
60+
});
61+
62+
@Effect() loadAndProcessToken$: Observable<Action> = this.actions$
63+
.ofType(ActionTypes.LOAD_AND_PROCESS_TOKEN)
64+
.map(action => action as LoadAndProcessTokenAction)
65+
.switchMap(() => {
66+
if (!!this.jwtToken) {
67+
return Observable.of(new ProcessTokenAction(this.jwtToken));
68+
} else {
69+
return Observable.empty();
70+
}
71+
});
72+
73+
@LocalStorage('token')
74+
private jwtToken: string;
75+
76+
constructor(private http: Http, private actions$: Actions) {
77+
}
78+
79+
}

src/main/typescript/app/core/store/auth/auth.reducer.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@ import 'rxjs/add/operator/map';
33
import 'rxjs/add/operator/let';
44
import {AuthState, initialState} from './auth.state';
55
import {Actions, ActionTypes, ProcessTokenAction, AuthErrorAction} from './auth.actions';
6-
import {AuthService} from './auth.service';
6+
7+
export function decodeAccessToken(access_token: string) {
8+
return JSON.parse(window.atob(access_token.split('.')[1]));
9+
}
710

811
export function authReducer(state = initialState, action: Actions): AuthState {
912
switch (action.type) {
1013

11-
case ActionTypes.LOGOUT:
14+
case ActionTypes.CLEAR_AUTH_STATE:
1215
return Object.assign({}, initialState);
1316

1417
case ActionTypes.PROCESS_TOKEN:
1518
const processTokenAction: ProcessTokenAction = action as ProcessTokenAction;
16-
const userData = AuthService.decodeAccessToken(processTokenAction.jwtToken);
19+
const userData = decodeAccessToken(processTokenAction.payload);
1720
return Object.assign({}, initialState, {
1821
authenticated: true,
1922
tokenExpirationDate: new Date(userData.exp * 1000),
2023
userData: userData,
21-
jwtToken: processTokenAction.jwtToken
24+
jwtToken: processTokenAction.payload
2225
});
2326

2427
case ActionTypes.AUTH_ERROR:
2528
const authErrorAction: AuthErrorAction = action as AuthErrorAction;
26-
return Object.assign({}, userData, {error: authErrorAction.error});
29+
return Object.assign({}, userData, {error: authErrorAction.payload});
2730

2831
case ActionTypes.CLEAR_AUTH_ERROR:
2932
return Object.assign({}, userData, {error: null});
@@ -33,4 +36,3 @@ export function authReducer(state = initialState, action: Actions): AuthState {
3336
}
3437
}
3538

36-
export const isAuthenticated = (state: AuthState) => state.authenticated;

0 commit comments

Comments
 (0)