Skip to content

Commit

Permalink
[CARD-75] Add protected routes
Browse files Browse the repository at this point in the history
  • Loading branch information
jrsmth-tier2 committed Apr 17, 2024
1 parent e664564 commit 6064193
Show file tree
Hide file tree
Showing 20 changed files with 267 additions and 81 deletions.
69 changes: 69 additions & 0 deletions cypress/e2e/auth.spec.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

describe('Authentication', () => {
const urlRegistration = 'http://localhost:8010/auth/register';
const urlLogin = 'http://localhost:8010/auth/login';

beforeEach(() => {

});

describe('Protected Routes', () => {

it('should grant access to protected routes when attempt made with a valid token', () => {
/** Given */

/** When */

/** Then */

})

it('should redirect user to /home when attempt made to access protected routes without valid token', () => {
/** Given */

/** When */

/** Then */

})

it('should allow access to unprotected routes without valid token', () => {
/** Given */

/** When */

/** Then */

})
})

describe('Registration', () => {

})

describe('Login', () => {

})

describe('Logout', () => {

})

})

/** What authentication rules should the webapp have? */
/**
* Protected routes
* All routes (except /home ; /login ; /registration), should require a valid JWT to have access
* If no JWT, should be routed to /home
* Login
* Should sign user in, then route to /home
* Failed login should present the reason and give user another chance (unlimited)
* Registration
* Should create new account, then login
* Failed registration should present the reason and give user another chance (unlimited)
* Logout
* Should clear JWT and route back to /home
*
* Note :: reset password not to be considered as part of 0.2.x Authentication
*/
10 changes: 4 additions & 6 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { CommonModule } from '@angular/common';
import { Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { HomeComponent } from "./presentation/home/home.component";
import {
faCog, faHouse, faPersonRunning, faQuestion, faSearch, faBell, faPencil, faCoins, faList, faHammer, faCameraRetro
} from "@fortawesome/free-solid-svg-icons";
import { AuthenticationService } from "./core/service/auth/authentication-service";
import { faCog, faHouse, faPersonRunning, faQuestion, faSearch, faBell, faPencil, faCoins, faList, faHammer, faCameraRetro } from "@fortawesome/free-solid-svg-icons";
import { AuthenticationService } from "./core/service/auth/authentication.service";

@Component({
selector: 'app-root',
Expand All @@ -32,8 +30,8 @@ export class AppComponent {
constructor(public router: Router, private authService: AuthenticationService) {}

logout(): void {
this.authService.removeToken();
this.router.navigate(['/auth']);
this.authService.logout();
this.router.navigate(['/home']).then();
}

}
22 changes: 17 additions & 5 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { Routes} from '@angular/router';
import { Routes } from '@angular/router';
import { FitTrackComponent } from "./presentation/fit-track/fit-track.component";
import { HomeComponent } from "./presentation/home/home.component";
import { XComponent } from "./presentation/x/x.component";
import { YComponent } from "./presentation/y/y.component";
import {LoginComponent} from "./presentation/login/login.component";
import {AuthGuard} from "./core/guard/auth-guard";
import { LoginComponent } from "./presentation/login/login.component";
import { AuthGuard } from "./core/guard/auth-guard";

export const routes: Routes = [
{ path: 'auth', component: LoginComponent },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
/** Open Routes */
{
path: 'auth',
children: [
{ path: 'login', component: LoginComponent },
{ path: 'register', component: LoginComponent },
],
},
{ path: 'home', component: HomeComponent },

/** Protected Routes */
{ path: 'x', component: XComponent, canActivate: [AuthGuard] },
{ path: 'y', component: YComponent, canActivate: [AuthGuard] },
{ path: 'fit-track', component: FitTrackComponent, canActivate: [AuthGuard] },


/** Default */
{ path: '**', redirectTo: 'home' }
];
6 changes: 6 additions & 0 deletions src/app/app.uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@ export const APP_URI = {
addWeek: '/weeks',
getWeek: '/weeks/{id}',
getWeeks: '/weeks'
},
security: {
auth: {
login: '/auth/login',
register: '/auth/register'
}
}
}
15 changes: 6 additions & 9 deletions src/app/core/guard/auth-guard.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { Injectable } from '@angular/core';
import { Router, RouterStateSnapshot } from "@angular/router";
import { AuthenticationService } from "../service/auth/authentication-service";
import { Router } from "@angular/router";
import { TokenService } from "../service/auth/token.service";

@Injectable({providedIn: 'root'})
export class AuthGuard {

constructor (private authService: AuthenticationService, private router: Router) {}
constructor (private tokenService: TokenService, private router: Router) { }

canActivate(): boolean {
const userAuthenticated = this.authService.hasValidToken();
const userAuthenticated = this.tokenService.hasValidToken();

if (!userAuthenticated) {
this.router.navigate(
['/auth']
);
}
if (!userAuthenticated)
this.router.navigate(['/home']).then();

return userAuthenticated;
}
Expand Down
14 changes: 14 additions & 0 deletions src/app/core/model/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Entity } from "./entity";

/**
* Authorisation Token Class
*
* @author J. R. Smith
* @since 17th April 2024
*/
export class Token extends Entity {
sub: string = '';
exp: number = 0;
iat: number = 0;
userId: number = 0;
}
File renamed without changes.
20 changes: 0 additions & 20 deletions src/app/core/service/auth/authentication-service.ts

This file was deleted.

39 changes: 39 additions & 0 deletions src/app/core/service/auth/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Injectable } from "@angular/core";
import { TokenService } from "./token.service";
import { HttpService } from "../http/http.service";
import { environment } from "../../../../environments/environment";

const BASE_URL = environment.services.security.baseUrl;
const URI = environment.services.security.uri;

@Injectable({
providedIn: 'root'
})
export class AuthenticationService {

constructor(private http: HttpService, private tokenService: TokenService) { }

/** Attempt to sign in a user to Cardinal with given credentials */
async login() {
this.http.post(BASE_URL, URI.auth.login, '');

this.tokenService.set('a_token')

// Note :: in login component, redirect to /home
}

/** Attempt to register a new Cardinal user */
async register() {
this.http.post(BASE_URL, URI.auth.register, '');

// Note :: in registration component, call login and redirect to /home
}

/** End the user session */
logout() {
this.tokenService.removeToken();

// Note :: in component, redirect to /home
}

}
58 changes: 58 additions & 0 deletions src/app/core/service/auth/token.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { computed, Injectable, Signal, signal, WritableSignal } from '@angular/core';
import { StorageUtil } from "../../util/storage/storage.util";
import { Token } from "../../model/token";

@Injectable({
providedIn: 'root'
})
export class TokenService {

private token: WritableSignal<null | string> = signal(null);
private decoded: Signal<null | Token> = computed(() => this.decode(this.token()));

constructor() { }

/** Set authentication token in local storage */
set(token: string): void {
StorageUtil.set('token', token);
}

/** Determine if user is signed in by presence of valid token */
hasValidToken(): boolean {
this.token.update(token => StorageUtil.get('token'));

const tokenEmpty = !this.decoded();
if (tokenEmpty) return false;

return this.hasValidSignature() && this.hasValidExpiry();
}

/** Logout user by removing token from local storage */
removeToken(): void {
StorageUtil.remove('token');
}

/** Determine if a token has a valid signature */
private hasValidSignature(): boolean {
return true
}

/** Determine if a token is yet to expire */
private hasValidExpiry(): boolean {
const token = this.decoded()!
const expiry = new Date(1000 * token.exp);
return expiry.getTime() > new Date().getTime();
}

/** Decode an authorisation token */
private decode(token: string | null): Token | null {
if (!token) return null

const decoded: any = {};
/** TODO :: how to verify... using secret (bad idea)
* is the model to soft force a login but require any actions to be authenticated on the backend (dumb client?)
*/
return Token.convert(decoded);
}

}
16 changes: 6 additions & 10 deletions src/app/core/service/fit-track/fit-track.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,19 @@ import { HttpService } from "../http/http.service";
import { Week } from "../../model/fit-track/week";
import { environment } from "../../../../environments/environment";

const BASE_URL = environment.services.fitTrack.baseUrl;
const URI = environment.services.fitTrack.uri;

@Injectable({
providedIn: 'root'
})
export class FitTrackService {

private baseUrl: string;
private uri: any;

constructor(private httpService: HttpService) {
this.baseUrl = environment.services.fitTrack.baseUrl;
this.uri = environment.services.fitTrack.uri;

}
constructor(private httpService: HttpService) { }

getHistoricalWeeks(): Promise<Week[]> {
return lastValueFrom(
this.httpService.get(this.baseUrl, this.uri.getWeeks)
this.httpService.get(BASE_URL, URI.getWeeks)
.pipe(map((response: any) => {
const weeks = response["_embedded"].weeks;
return weeks.map((week: any) => {
Expand All @@ -32,7 +28,7 @@ export class FitTrackService {

addWeek(newWeek: Week): Promise<Week> {
return lastValueFrom(
this.httpService.post(this.baseUrl, this.uri.addWeek, newWeek)
this.httpService.post(BASE_URL, URI.addWeek, newWeek)
.pipe(map(response => {
console.log(response);
return response;
Expand Down
24 changes: 0 additions & 24 deletions src/app/core/service/storage/local-storage-service.ts

This file was deleted.

Empty file.
File renamed without changes.
Empty file.
31 changes: 31 additions & 0 deletions src/app/core/util/storage/storage.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

/**
* Utility to interact with LocalStorage
*
* @author J. R. Smith
* @since 17th April 2024
*/
export class StorageUtil {

/** Get the value from LocalStorage for a given key */
static get(key: string): string | null {
if (typeof window !== 'undefined') {
return localStorage.getItem(key);
}

return null;
}

/** Set the value in LocalStorage for a given key */
static set(key: string, value: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem(key, value);
}
}

/** Delete the value in LocalStorage for a given key */
static remove(key: string): void {
localStorage.removeItem(key);
}

}
Loading

0 comments on commit 6064193

Please sign in to comment.