diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6b9fc55..7e65103 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -6,9 +6,9 @@ import { OverviewComponent } from './components/dashboard/overview/overview.comp import { FundersComponent } from './components/funders/funders.component'; import { SmesComponent } from './components/smes/smes.component'; import { UserModule } from './components/user/user.module'; -import { SignInComponent } from './sign-in/sign-in.component'; +import { SignInComponent } from './components/sign-in/sign-in.component'; import { SmesDashboardComponent } from './components/smes-dashboard/smes-dashboard.component'; - +import { AuthGuardService as AuthGuard } from './services/auth-guard.service'; const routes: Routes = [ { @@ -17,7 +17,8 @@ const routes: Routes = [ }, { path: 'smes-dashboard', - component: SmesDashboardComponent + component: SmesDashboardComponent, + canActivate: [AuthGuard] }, { path: 'user', loadChildren: () => UserModule }, { @@ -26,9 +27,11 @@ const routes: Routes = [ { path: 'Overview', component: OverviewComponent}, { path: 'Funders', component: FundersComponent}, - { path: 'Smes', component: SmesComponent,}] + { path: 'Smes', component: SmesComponent}, + ] }, - { path: 'sign-in', component: SignInComponent} + { path: 'sign-in', component: SignInComponent}, + { path: '**', redirectTo: '/' } ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index aeb03a5..ec994a6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,3 +1,5 @@ +import { TokenInterceptor, ErrorInterceptor } from './services/token.interceptor'; +import { SideNavService } from './shared/services/side-nav.service'; import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -16,11 +18,18 @@ import { OverviewComponent } from './components/dashboard/overview/overview.comp import { FundersComponent } from './components/funders/funders.component'; import { SmesComponent } from './components/smes/smes.component'; import { FooterComponent } from './components/footer/footer.component'; -import { SignInComponent } from './sign-in/sign-in.component'; +import { SignInComponent } from './components/sign-in/sign-in.component'; +import { StoreModule } from '@ngrx/store'; +import { AuthService } from './services/auth.service'; +import { authReducers } from './store/state/user.state'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from './store/effects/auth.effects'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { environment } from '../environments/environment'; import { SideNavTogglerComponent } from './shared/components/side-nav-toggler/side-nav-toggler.component'; import { SmesDashboardComponent } from './components/smes-dashboard/smes-dashboard.component'; import { NavbarComponent } from './shared/components/navbar/navbar.component'; -import { SideNavService } from './shared/services/side-nav.service'; import { SideNavComponent } from './shared/components/side-nav/side-nav.component'; @@ -49,10 +58,26 @@ import { SideNavComponent } from './shared/components/side-nav/side-nav.componen MatSidenavModule, MatToolbarModule, MatButtonModule, - MatIconModule + MatIconModule, + HttpClientModule, + StoreModule.forRoot(authReducers, {}), + EffectsModule.forRoot([AuthEffects]), + StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }), ], - providers: [SideNavService], + providers: [ + AuthService, SideNavService, + { + provide: HTTP_INTERCEPTORS, + useClass: TokenInterceptor, + multi: true + }, + { + provide: HTTP_INTERCEPTORS, + useClass: ErrorInterceptor, + multi: true + } + ], bootstrap: [AppComponent] }) diff --git a/src/app/components/home-page/home-page.component.html b/src/app/components/home-page/home-page.component.html index cb188f0..6a74a6b 100644 --- a/src/app/components/home-page/home-page.component.html +++ b/src/app/components/home-page/home-page.component.html @@ -1,35 +1,35 @@
- +
+
+ U Business + Connecting Businesses to Opportunity
+ +
diff --git a/src/app/components/sign-in/sign-in.component.html b/src/app/components/sign-in/sign-in.component.html new file mode 100644 index 0000000..948511e --- /dev/null +++ b/src/app/components/sign-in/sign-in.component.html @@ -0,0 +1,56 @@ +
+
+
+ + + +
+ +
diff --git a/src/app/sign-in/sign-in.component.scss b/src/app/components/sign-in/sign-in.component.scss similarity index 100% rename from src/app/sign-in/sign-in.component.scss rename to src/app/components/sign-in/sign-in.component.scss diff --git a/src/app/sign-in/sign-in.component.spec.ts b/src/app/components/sign-in/sign-in.component.spec.ts similarity index 100% rename from src/app/sign-in/sign-in.component.spec.ts rename to src/app/components/sign-in/sign-in.component.spec.ts diff --git a/src/app/components/sign-in/sign-in.component.ts b/src/app/components/sign-in/sign-in.component.ts new file mode 100644 index 0000000..ce08209 --- /dev/null +++ b/src/app/components/sign-in/sign-in.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import {Router} from '@angular/router'; +import { Store } from '@ngrx/store'; +import { UserState } from '../../store/state/user.state'; +import { selectAuthState } from './../../store/state/user.state'; + +import { LogIn } from './../../store/actions/auth.actions'; +import { User } from './../user/user.model'; +import { Observable } from 'rxjs'; + + +@Component({ + selector: 'app-sign-in', + templateUrl: './sign-in.component.html', + styleUrls: ['./sign-in.component.scss'] +}) +export class SignInComponent implements OnInit { + loginForm: FormGroup; + login = false; + getState: Observable; + errorMessage: string | null; + + + + constructor( + private router: Router, + private store: Store, + private formBuilder: FormBuilder + ) { + this.getState = this.store.select(selectAuthState); + } + + ngOnInit(): void { + this.loginForm = this.formBuilder.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required] + }); + this.getState.subscribe(state => { + this.errorMessage = state.errorMessage; + }); + } + + // convenient getter for easy access to form fields + get f() { return this.loginForm.controls; } + + onLogin(): void { + this.login = true; + + if (this.loginForm.invalid) { return; } + const rawLogin = this.loginForm.getRawValue(); + const payload = { + email: rawLogin.email, + password: rawLogin.password + }; + console.log('payload', payload); + + this.store.dispatch(new LogIn(payload)); + } + +} diff --git a/src/app/components/smes-dashboard/smes-dashboard.component.html b/src/app/components/smes-dashboard/smes-dashboard.component.html index 6de1dc4..b73d6ab 100644 --- a/src/app/components/smes-dashboard/smes-dashboard.component.html +++ b/src/app/components/smes-dashboard/smes-dashboard.component.html @@ -1,4 +1,8 @@
- +
+ +
+ + diff --git a/src/app/components/smes-dashboard/smes-dashboard.component.ts b/src/app/components/smes-dashboard/smes-dashboard.component.ts index f07b23d..f420b1d 100644 --- a/src/app/components/smes-dashboard/smes-dashboard.component.ts +++ b/src/app/components/smes-dashboard/smes-dashboard.component.ts @@ -1,4 +1,10 @@ +import { Router } from '@angular/router'; +import { selectAuthState } from './../../store/state/user.state'; +import { Observable } from 'rxjs'; import { Component, OnInit } from '@angular/core'; +import { Logout } from './../../store/actions/auth.actions'; +import { UserState } from './../../store/reducers/auth.reducers'; +import { Store } from '@ngrx/store'; @Component({ selector: 'app-smes-dashboard', @@ -7,9 +13,26 @@ import { Component, OnInit } from '@angular/core'; }) export class SmesDashboardComponent implements OnInit { - constructor() { } + getState: Observable; + isAuthenticated: false; + user = null; + errorMessage = null; + + constructor(private store: Store, public router: Router) { + this.getState = this.store.select(selectAuthState); + } ngOnInit(): void { + this.getState.subscribe(state => { + this.isAuthenticated = state.isAuthenticated; + this.user = state.user; + this.errorMessage = state.errorMessage; + }); + } + + logout(): void { + this.store.dispatch(new Logout()); + this.router.navigateByUrl('/'); } } diff --git a/src/app/components/user/user.model.ts b/src/app/components/user/user.model.ts index 765ff23..46c1c3e 100644 --- a/src/app/components/user/user.model.ts +++ b/src/app/components/user/user.model.ts @@ -20,3 +20,21 @@ export interface IUser { username: string; fullname: string; } + +export class User { + token?: string; + fullName?: string; + physicalAddress?: string; + country?: string; + phone?: string; + email?: string; + businessName?: string; + businessNumber?: string; + dateOfReg?: Date; + regAddress?: string; + businessEmail?: string; + businessTel?: string; + website?: string; + businessNature?: string; + newPassword?: string; +} diff --git a/src/app/services/auth-guard.service.spec.ts b/src/app/services/auth-guard.service.spec.ts new file mode 100644 index 0000000..35afd37 --- /dev/null +++ b/src/app/services/auth-guard.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthGuardService } from './auth-guard.service'; + +describe('AuthGuardService', () => { + let service: AuthGuardService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthGuardService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/auth-guard.service.ts b/src/app/services/auth-guard.service.ts new file mode 100644 index 0000000..45d2138 --- /dev/null +++ b/src/app/services/auth-guard.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate } from '@angular/router'; +import { AuthService } from './auth.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuardService implements CanActivate{ + + constructor( + public auth: AuthService, + public router: Router + ) { } + + canActivate(): boolean { + if (!this.auth.getToken()) { + this.router.navigateByUrl('/sign-in'); + return false; + } + return true; + } +} diff --git a/src/app/services/auth.service.spec.ts b/src/app/services/auth.service.spec.ts new file mode 100644 index 0000000..f1251ca --- /dev/null +++ b/src/app/services/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 0000000..5807c55 --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,23 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { User } from '../components/user/user.model'; +import { environment } from './../../environments/environment'; + + + +@Injectable() +export class AuthService { + private BASE_URL = environment.BASE_URL; + + constructor(private http: HttpClient) {} + + getToken(): string { + return localStorage.getItem('token'); + } + + logIn(email: string, password: string): Observable { + const url = `${this.BASE_URL}/login`; + return this.http.post(url, {email, password}); + } +} diff --git a/src/app/services/token.interceptor.ts b/src/app/services/token.interceptor.ts new file mode 100644 index 0000000..a3094f3 --- /dev/null +++ b/src/app/services/token.interceptor.ts @@ -0,0 +1,39 @@ +import { catchError } from 'rxjs/operators'; +import { Injectable, Injector } from '@angular/core'; +import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http'; +import { Router } from '@angular/router'; +import { Observable, throwError } from 'rxjs'; +import { AuthService } from './auth.service'; + +@Injectable() +export class TokenInterceptor implements HttpInterceptor { + private authService: AuthService; + + constructor(private injector: Injector) { } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + this.authService = this.injector.get(AuthService); + const token = this.authService.getToken(); + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + return next.handle(request); + } +} + +@Injectable() +export class ErrorInterceptor implements HttpInterceptor { + constructor(private router: Router) {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + catchError(response => { + if (response instanceof HttpErrorResponse && response.status === 401) {} + return throwError(response); + }) + ); + } +} diff --git a/src/app/shared/components/navbar/navbar.component.html b/src/app/shared/components/navbar/navbar.component.html index 199d4f9..afec515 100644 --- a/src/app/shared/components/navbar/navbar.component.html +++ b/src/app/shared/components/navbar/navbar.component.html @@ -1,4 +1,14 @@ diff --git a/src/app/shared/components/navbar/navbar.component.ts b/src/app/shared/components/navbar/navbar.component.ts index 6a9bec8..58f4a90 100644 --- a/src/app/shared/components/navbar/navbar.component.ts +++ b/src/app/shared/components/navbar/navbar.component.ts @@ -1,4 +1,11 @@ +import { Router } from '@angular/router'; import { Component, OnInit } from '@angular/core'; +import { Logout } from './../../../store/actions/auth.actions'; +import { selectAuthState, UserState } from './../../../store/state/user.state'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + + @Component({ selector: 'app-navbar', @@ -6,10 +13,26 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./navbar.component.scss'] }) export class NavbarComponent implements OnInit { + getState: Observable; + isAuthenticated: false; + user = null; + errorMessage = null; - constructor() { } + constructor(private store: Store, public router: Router) { + this.getState = this.store.select(selectAuthState); + } ngOnInit(): void { + this.getState.subscribe(state => { + this.isAuthenticated = state.isAuthenticated; + this.user = state.user; + this.errorMessage = state.errorMessage; + }); + } + + logout(): void { + this.store.dispatch(new Logout()); + this.router.navigateByUrl('/'); } } diff --git a/src/app/sign-in/sign-in.component.html b/src/app/sign-in/sign-in.component.html deleted file mode 100644 index a12e091..0000000 --- a/src/app/sign-in/sign-in.component.html +++ /dev/null @@ -1,44 +0,0 @@ -
-
-
- - - -
- -
\ No newline at end of file diff --git a/src/app/sign-in/sign-in.component.ts b/src/app/sign-in/sign-in.component.ts deleted file mode 100644 index 7a341b0..0000000 --- a/src/app/sign-in/sign-in.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import {Router} from '@angular/router'; - -@Component({ - selector: 'app-sign-in', - templateUrl: './sign-in.component.html', - styleUrls: ['./sign-in.component.scss'] -}) -export class SignInComponent implements OnInit { - - constructor( private router: Router) { } - - ngOnInit(): void { - } - -} diff --git a/src/app/store/actions/auth.actions.ts b/src/app/store/actions/auth.actions.ts new file mode 100644 index 0000000..86019ca --- /dev/null +++ b/src/app/store/actions/auth.actions.ts @@ -0,0 +1,30 @@ +import { Action } from '@ngrx/store'; + + +export enum AuthActionTypes { + LOGIN = '[Auth] Login', + LOGIN_SUCCESS = '[Auth] Login Success', + LOGIN_FAILURE = '[Auth] Login Failure', + LOGOUT = '[Auth] Logout' +} + +export class LogIn implements Action { + readonly type = AuthActionTypes.LOGIN; + constructor(public payload: any) {} +} + +export class LogInSuccess implements Action { + readonly type = AuthActionTypes.LOGIN_SUCCESS; + constructor(public payload: any) {} +} + +export class LogInFailure implements Action { + readonly type = AuthActionTypes.LOGIN_FAILURE; + constructor(public payload: any) {} +} + +export class Logout implements Action { + readonly type = AuthActionTypes.LOGOUT; +} + +export type All = LogIn | LogInSuccess | LogInFailure | Logout; diff --git a/src/app/store/effects/auth.effects.ts b/src/app/store/effects/auth.effects.ts new file mode 100644 index 0000000..d38fd03 --- /dev/null +++ b/src/app/store/effects/auth.effects.ts @@ -0,0 +1,62 @@ +import { AuthService } from './../../services/auth.service'; +import { Observable, of } from 'rxjs'; +import { Router } from '@angular/router'; +import { Action } from '@ngrx/store'; +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { map, switchMap, mergeMap, catchError, tap } from 'rxjs/operators'; +import { AuthActionTypes, LogIn, LogInSuccess, LogInFailure } from '../actions/auth.actions'; + +@Injectable() +export class AuthEffects { + + constructor( + private actions: Actions, + private authService: AuthService, + private router: Router, + ) { } + + // effects go here + @Effect() + LogIn: Observable = this.actions.pipe( + ofType(AuthActionTypes.LOGIN), + map((action: LogIn) => action.payload) + ).pipe( + switchMap(payload => { + return this.authService.logIn(payload.email, payload.password) + .pipe( + map(user => { + console.log(user); + return new LogInSuccess({ token: user.token, email: user.email }); + }), + catchError(error => { + console.error(error); + return of(new LogInFailure({ error })); + }) + ); + }) + ); + + @Effect({ dispatch: false }) + LogInSuccess: Observable = this.actions.pipe( + ofType(AuthActionTypes.LOGIN_SUCCESS), + tap(user => { + localStorage.setItem('token', user.payload.token); + this.router.navigateByUrl('/smes-dashboard'); + }) + ); + + @Effect({ dispatch: false }) + LogInFailure: Observable = this.actions.pipe( + ofType(AuthActionTypes.LOGIN_FAILURE) + ); + + @Effect({ dispatch: false }) + public LogOut: Observable = this.actions.pipe( + ofType(AuthActionTypes.LOGOUT), + tap((user) => { + localStorage.removeItem('token'); + }) + ); + +} diff --git a/src/app/store/index.ts b/src/app/store/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/store/reducers/auth.reducers.ts b/src/app/store/reducers/auth.reducers.ts new file mode 100644 index 0000000..da1f64a --- /dev/null +++ b/src/app/store/reducers/auth.reducers.ts @@ -0,0 +1,45 @@ +import { User } from 'src/app/components/user/user.model'; +import { AuthActionTypes, All } from '../actions/auth.actions'; + +export interface UserState { + // is a user authenticated? + isAuthenticated: boolean; + // if authenticated, there should be a user object + user: User | null; + // error message + errorMessage: string | null; +} + +export const initialUserState: UserState = { + isAuthenticated: false, + user: null, + errorMessage: null +}; + +export function reducer(state = initialUserState, action: All): UserState { + switch (action.type) { + case AuthActionTypes.LOGIN_SUCCESS: { + return { + ...state, + isAuthenticated: true, + user: { + token: action.payload.token, + email: action.payload.email + }, + errorMessage: null + }; + } + case AuthActionTypes.LOGIN_FAILURE: { + return { + ...state, + errorMessage: 'Incorrect email and/or password.' + }; + } + case AuthActionTypes.LOGOUT: { + return initialUserState; + } + default: { + return state; + } + } +} diff --git a/src/app/store/router.ts b/src/app/store/router.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/store/state/user.state.ts b/src/app/store/state/user.state.ts new file mode 100644 index 0000000..5888b6f --- /dev/null +++ b/src/app/store/state/user.state.ts @@ -0,0 +1,12 @@ +import * as auth from '../reducers/auth.reducers'; +import { createFeatureSelector } from '@ngrx/store'; + +export interface UserState { + authState: auth.UserState; +} + +export const authReducers = { + auth: auth.reducer +}; + +export const selectAuthState = createFeatureSelector('auth'); diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 7b4f817..9956246 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + BASE_URL: 'http://localhost:1337' }; /*