diff --git a/package-lock.json b/package-lock.json index d28f638..d86861d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -336,6 +336,26 @@ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.4.tgz", "integrity": "sha512-jEpglcwMlwdXc/JgvJaJtCSkPMktnFeI0gAZxPrmbJxKVzMZJ2zM582NbW/r6M22pSdNWjcWeg1I2LRg3jQGQA==" }, + "@ngrx/effects": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-4.1.1.tgz", + "integrity": "sha1-y3WLhSeWSyWOpBlR9ZqhROPvn64=" + }, + "@ngrx/router-store": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-4.1.1.tgz", + "integrity": "sha1-F/rHwPX/3e+LdemnTtLLCQdPO8o=" + }, + "@ngrx/store": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-4.1.1.tgz", + "integrity": "sha1-aA403yd16IUnVO13f/rJW9gbfeA=" + }, + "@ngrx/store-devtools": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-4.1.1.tgz", + "integrity": "sha1-IHRcOcdWD9wF+k8iY4RCp+x91nY=" + }, "@ngtools/json-schema": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz", diff --git a/package.json b/package.json index 5cea932..b8b46e8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,10 @@ "@angular/platform-browser": "^5.0.0", "@angular/platform-browser-dynamic": "^5.0.0", "@angular/router": "^5.0.0", + "@ngrx/effects": "^4.1.1", + "@ngrx/router-store": "^4.1.1", + "@ngrx/store": "^4.1.1", + "@ngrx/store-devtools": "^4.1.1", "bootstrap": "^3.3.7", "core-js": "^2.4.1", "firebase": "^4.6.2", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9e779e7..0b0b23e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,11 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreRouterConnectingModule } from '@ngrx/router-store'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { environment } from '../environments/environment'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; @@ -8,6 +13,8 @@ import { SharedModule } from './shared/shared.module'; import { ShoppingListModule } from './shopping-list/shopping-list.module'; import { AuthModule } from './auth/auth.module'; import { CoreModule } from './core/core.module'; +import { appReducers } from './ngrx/app.reducers'; +import { AuthEffects } from './auth/ngrx/auth.effects'; @NgModule({ declarations: [ @@ -20,7 +27,11 @@ import { CoreModule } from './core/core.module'; SharedModule, ShoppingListModule, AuthModule, - CoreModule + CoreModule, + StoreModule.forRoot(appReducers), + EffectsModule.forRoot([AuthEffects]), + StoreRouterConnectingModule, + !environment.production ? StoreDevtoolsModule.instrument() : [] ], bootstrap: [AppComponent] }) diff --git a/src/app/auth/auth-guard.service.ts b/src/app/auth/auth-guard.service.ts index 82a26ed..6674d29 100644 --- a/src/app/auth/auth-guard.service.ts +++ b/src/app/auth/auth-guard.service.ts @@ -1,14 +1,22 @@ import { CanActivate } from "@angular/router"; import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router/src/router_state"; import { Injectable } from "@angular/core"; -import { AuthService } from "./auth.service"; +import { Store } from "@ngrx/store"; +import "rxjs/add/operator/map"; + +import * as fromApp from '../ngrx/app.reducers'; +import * as fromAuth from './ngrx/auth.reducers'; @Injectable() export class AuthGuard implements CanActivate { - constructor(private authService: AuthService) {} + constructor(private store: Store) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - return this.authService.isAuthenticated(); + return this.store.select('auth') + .take(1) + .map((authState: fromAuth.State) => { + return authState.authenticated; + }); } } \ No newline at end of file diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts deleted file mode 100644 index f264911..0000000 --- a/src/app/auth/auth.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as firebase from 'firebase'; -import { Router } from '@angular/router'; -import { Injectable } from '@angular/core'; - -@Injectable() -export class AuthService { - token: string; - - constructor(private router: Router) {} - - signupUser(email: string, password: string) { - firebase.auth().createUserWithEmailAndPassword(email, password).catch( - error => console.log(error) - ); - } - - signinUser(email: string, password: string) { - firebase.auth().signInWithEmailAndPassword(email, password) - .then( - response => { - this.router.navigate(['/']); - firebase.auth().currentUser.getIdToken() - .then( - (token: string) => this.token = token - ); - } - ) - .catch( - error => console.log(error) - ); - } - - logout() { - firebase.auth().signOut(); - this.token = null; - this.router.navigate(['/']); - } - - getToken() { - firebase.auth().currentUser.getIdToken() - .then( - (token: string) => this.token = token - ); - return this.token; - } - - isAuthenticated() { - return this.token != null; - } -} \ No newline at end of file diff --git a/src/app/auth/ngrx/auth.actions.ts b/src/app/auth/ngrx/auth.actions.ts new file mode 100644 index 0000000..7aac427 --- /dev/null +++ b/src/app/auth/ngrx/auth.actions.ts @@ -0,0 +1,40 @@ +import { Action } from '@ngrx/store'; + +export const TRY_SIGNUP = 'TRY_SIGNUP'; +export const SIGNUP = 'SIGNUP'; +export const SIGNIN = 'SIGNIN'; +export const TRY_SIGNIN = 'TRY_SIGNIN'; +export const LOGOUT = 'LOGOUT'; +export const SET_TOKEN = 'SET_TOKEN'; + +export class TrySignup implements Action { + readonly type = TRY_SIGNUP; + + constructor(public payload: {username: string, password: string}) {} +} + +export class Signup implements Action { + readonly type = SIGNUP; +} + +export class TrySignin implements Action { + readonly type = TRY_SIGNIN; + + constructor(public payload: {username: string, password: string}) {} +} + +export class Signin implements Action { + readonly type = SIGNIN; +} + +export class Logout implements Action { + readonly type = LOGOUT; +} + +export class SetToken implements Action { + readonly type = SET_TOKEN; + + constructor(public payload: string) {} +} + +export type AuthActions = TrySignup | Signup | TrySignin | Signin | Logout | SetToken; \ No newline at end of file diff --git a/src/app/auth/ngrx/auth.effects.ts b/src/app/auth/ngrx/auth.effects.ts new file mode 100644 index 0000000..7352e99 --- /dev/null +++ b/src/app/auth/ngrx/auth.effects.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { Actions, Effect } from '@ngrx/effects'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/mergeMap'; +import { fromPromise } from 'rxjs/observable/fromPromise'; +import * as firebase from 'firebase'; + +import * as AuthActions from './auth.actions'; +import * as fromAuth from './auth.reducers'; + +@Injectable() +export class AuthEffects { + @Effect() + authSignup = this.actions$.ofType(AuthActions.TRY_SIGNUP) + .map((action: AuthActions.TrySignup) => { + return action.payload; + }) + .switchMap((authData: {username: string, password: string}) => { + return fromPromise(firebase.auth().createUserWithEmailAndPassword(authData.username, authData.password)); + }) + .switchMap(() => { + return fromPromise(firebase.auth().currentUser.getIdToken()); + }) + .mergeMap((token: string) => { + this.router.navigate(['/']); + return [ + { type: AuthActions.SIGNUP }, + { type: AuthActions.SET_TOKEN, payload: token } + ]; + }); + + @Effect() + authSignin = this.actions$.ofType(AuthActions.TRY_SIGNIN) + .map((action: AuthActions.TrySignup) => { + return action.payload; + }) + .switchMap((authData: {username: string, password: string}) => { + return fromPromise(firebase.auth().signInWithEmailAndPassword(authData.username, authData.password)); + }) + .switchMap(() => { + return fromPromise(firebase.auth().currentUser.getIdToken()); + }) + .mergeMap((token: string) => { + this.router.navigate(['/']); + return [ + { type: AuthActions.SIGNIN }, + { type: AuthActions.SET_TOKEN, payload: token } + ] + }); + + @Effect({dispatch: false}) + authLogout = this.actions$.ofType(AuthActions.LOGOUT) + .do(() => { + this.router.navigate(['/']); + }); + + // variable with dollar sign at the end marks an observable + constructor(private router: Router, private actions$: Actions) {} +} \ No newline at end of file diff --git a/src/app/auth/ngrx/auth.reducers.ts b/src/app/auth/ngrx/auth.reducers.ts new file mode 100644 index 0000000..67ad145 --- /dev/null +++ b/src/app/auth/ngrx/auth.reducers.ts @@ -0,0 +1,35 @@ +import * as AuthActions from './auth.actions'; + +export interface State { + token: string; + authenticated: boolean; +} + +const initialState: State = { + token: null, + authenticated: false +}; + +export function authReducers(state = initialState, action: AuthActions.AuthActions) { + switch (action.type) { + case AuthActions.SIGNUP: + case AuthActions.SIGNIN: + return { + ...state, + authenticated: true + }; + case AuthActions.LOGOUT: + return { + ...state, + token: null, + authenticated: false + }; + case AuthActions.SET_TOKEN: + return { + ...state, + token: action.payload + }; + default: + return state; + } +} \ No newline at end of file diff --git a/src/app/auth/signin/signin.component.ts b/src/app/auth/signin/signin.component.ts index 2ee4dc9..79e0c27 100644 --- a/src/app/auth/signin/signin.component.ts +++ b/src/app/auth/signin/signin.component.ts @@ -1,6 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { NgForm } from '@angular/forms'; -import { AuthService } from '../auth.service'; +import { Store } from '@ngrx/store'; + +import * as fromApp from '../../ngrx/app.reducers'; +import * as AuthActions from '../ngrx/auth.actions'; @Component({ selector: 'app-signin', @@ -9,7 +12,7 @@ import { AuthService } from '../auth.service'; }) export class SigninComponent implements OnInit { - constructor(private authService: AuthService) { } + constructor(private store: Store) { } ngOnInit() { } @@ -17,7 +20,7 @@ export class SigninComponent implements OnInit { onSignin(form: NgForm) { const email = form.value.email; const password = form.value.password; - this.authService.signinUser(email, password); + this.store.dispatch(new AuthActions.TrySignin({username: email, password: password})); } } diff --git a/src/app/auth/signup/signup.component.ts b/src/app/auth/signup/signup.component.ts index d33e109..7a6c0fe 100644 --- a/src/app/auth/signup/signup.component.ts +++ b/src/app/auth/signup/signup.component.ts @@ -1,6 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { NgForm } from '@angular/forms'; -import { AuthService } from '../auth.service'; +import { Store } from '@ngrx/store'; + +import * as fromApp from '../../ngrx/app.reducers'; +import * as AuthActions from '../ngrx/auth.actions'; @Component({ selector: 'app-signup', @@ -9,7 +12,7 @@ import { AuthService } from '../auth.service'; }) export class SignupComponent implements OnInit { - constructor(private authService: AuthService) { } + constructor(private store: Store) { } ngOnInit() { } @@ -17,7 +20,7 @@ export class SignupComponent implements OnInit { onSignup(form: NgForm) { const email = form.value.email; const password = form.value.password; - this.authService.signupUser(email, password); + this.store.dispatch(new AuthActions.TrySignup({username: email, password: password})); } } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 285be48..8aa1ffe 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -5,10 +5,6 @@ import { HeaderComponent } from "./header/header.component"; import { HomeComponent } from "./home/home.component"; import { SharedModule } from "../shared/shared.module"; import { AppRoutingModule } from "../app-routing.module"; -import { ShoppingListService } from "../shopping-list/shopping-list.service"; -import { RecipeService } from "../recipes/recipe.service"; -import { DataStorageService } from "../shared/data-storage.service"; -import { AuthService } from "../auth/auth.service"; import { AuthInterceptor } from "../shared/auth.interceptor"; import { LoggingInterceptor } from "../shared/logging.interceptor"; @@ -26,10 +22,6 @@ import { LoggingInterceptor } from "../shared/logging.interceptor"; HeaderComponent ], providers: [ - ShoppingListService, - RecipeService, - DataStorageService, - AuthService, {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true} ], diff --git a/src/app/core/header/header.component.html b/src/app/core/header/header.component.html index a83d6b9..cd84455 100644 --- a/src/app/core/header/header.component.html +++ b/src/app/core/header/header.component.html @@ -10,12 +10,12 @@
  • Shopping List