Skip to content

Commit

Permalink
Implement auth
Browse files Browse the repository at this point in the history
  • Loading branch information
yevhenchmykhun committed Apr 6, 2023
1 parent 1545743 commit a36a5bc
Show file tree
Hide file tree
Showing 28 changed files with 448 additions and 26 deletions.
5 changes: 4 additions & 1 deletion configs/mocked-backend/app.conf.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"name": "FinServe",
"env": "Mocked Backend",
"api": "http://localhost:3000/api"
"api": "http://localhost:3000/api",
"auth": {
"api": "http://localhost:3000/api/auth/token"
}
}
41 changes: 41 additions & 0 deletions mocked-backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,52 @@ const delayMiddleware = (req, res, next) => {
}, 500) // Add a 0.5 second delay
}

const authMiddleware = (req, res, next) => {
if (req.originalUrl !== "/api/auth/token") {

// check if 'Authorization' header is present
const header = req.headers.authorization;
if (!header) {
return res.status(401).json({ message: "Authorization header is missing" });
}

// check if token is present
const token = header.substring(header.indexOf(" ") + 1);
if (!token) {
return res.status(401).json({ message: "Token is missing" });
}

try {

// check if token is not expired
const decodedToken = JSON.parse(token);
if (decodedToken.expiresAt < Date.now()) {
return res.status(401).json({ message: "Token expired" });
}
} catch (error) {
console.log(error);

// provided token is not valid
return res.status(401).json({ message: "Token is not valid" });
}
}
next();
}

const server = jsonServer.create();
server.use(jsonServer.defaults());
server.use(delayMiddleware);
server.use(authMiddleware);
// server.use(jsonServer.bodyParser);

server.get('/api/auth/token', (req, res) => {
const token = {
name: 'John Doe',
roles: ['User'/* , 'Administrator' */],
expiresAt: Date.now() + 1000 * 60 * 15 // now + 15 minutes
}
res.status(200).send(token);
});

server.get('/api/business-dates', (req, res) => {
const data = require('./data/business-dates.json');
Expand Down
5 changes: 4 additions & 1 deletion src/app.conf.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"name": "FinServe",
"env": "$env$",
"api": "$api$"
"api": "$api$",
"auth": {
"api": "$auth-api$"
}
}
2 changes: 1 addition & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p-toast></p-toast>
<app-splash-screen *ngIf="showSplashScreen"></app-splash-screen>
<app-splash-screen *ngIf="showSplashScreen$ | async"></app-splash-screen>
<div class="layout-wrapper">
<app-sidebar></app-sidebar>
<div class="layout-main">
Expand Down
10 changes: 8 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { AppService } from './core/services/app.service';
import { delay } from 'rxjs';

@Component({
selector: 'app-root',
Expand All @@ -7,6 +9,10 @@ import { Component } from '@angular/core';
})
export class AppComponent {

showSplashScreen = false;
private readonly appService = inject(AppService);

showSplashScreen$ = this.appService.showSplashScreen$.pipe(delay(0));

showSidebar$ = this.appService.showSidebar$.pipe(delay(0));

}
8 changes: 7 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { MessageModule } from 'primeng/message';
import { ToastModule } from 'primeng/toast';
import { HttpClientModule } from '@angular/common/http';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { RoutingModule } from './routing.module';
import { AuthInterceptor } from './core/interceptors/auth.interceptor';

@NgModule({
declarations: [
Expand All @@ -28,6 +29,11 @@ import { RoutingModule } from './routing.module';
ToastModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
MessageService
],
bootstrap: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</ng-template>
<div *ngIf="expanded" @inOut>
<ng-container *ngFor="let child of item.items">
<app-sidebar-menu-item [item]="child"></app-sidebar-menu-item>
<app-sidebar-menu-item *appHasRoles="item.state?.['roles']" [item]="child"></app-sidebar-menu-item>
</ng-container>
</div>
</div>
2 changes: 1 addition & 1 deletion src/app/core/components/sidebar/sidebar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<div class="layout-menu-container">
<div class="layout-menu">
<ng-container *ngFor="let item of items">
<app-sidebar-menu-item [item]="item"></app-sidebar-menu-item>
<app-sidebar-menu-item *appHasRoles="item.state?.['roles']" [item]="item"></app-sidebar-menu-item>
</ng-container>
</div>
</div>
Expand Down
35 changes: 22 additions & 13 deletions src/app/core/components/sidebar/sidebar.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { MenuItem, PrimeIcons } from 'primeng/api';
import { UserRole } from '../../model/user-role.enum';

@Component({
selector: 'app-sidebar',
Expand All @@ -16,20 +17,28 @@ export class SidebarComponent {
{
label: 'Home',
routerLink: '/home',
icon: PrimeIcons.HOME

icon: PrimeIcons.HOME,
state: {
roles: [UserRole.user]
}
},
// {
// label: 'Admin',
// routerLink: '/admin',
// items: [
// {
// label: 'ATS',
// routerLink: '/admin/ats',
// icon: 'pi pi-fw pi-cog'
// }
// ]
// }
{
label: 'Admin',
routerLink: '/admin',
state: {
roles: [UserRole.administrator]
},
items: [
{
label: 'ATS',
routerLink: '/admin/ats',
icon: 'pi pi-fw pi-cog',
state: {
roles: [UserRole.administrator]
},
}
]
}
]

onLockChange(): void {
Expand Down
4 changes: 3 additions & 1 deletion src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { SidebarComponent } from './components/sidebar/sidebar.component';
import { RouterModule } from '@angular/router';
import { SidebarMenuItemComponent } from './components/sidebar-menu-item/sidebar-menu-item.component';
import { SplashScreenComponent } from './components/splash-screen/splash-screen.component';
import { SharedModule } from '../shared/shared.module';

@NgModule({
imports: [
CommonModule,
RouterModule
RouterModule,
SharedModule
],
declarations: [
HeaderComponent,
Expand Down
16 changes: 16 additions & 0 deletions src/app/core/guards/auth.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AuthGuard } from './auth.guard';

describe('AuthGuard', () => {
let guard: AuthGuard;

beforeEach(() => {
TestBed.configureTestingModule({});
guard = TestBed.inject(AuthGuard);
});

it('should be created', () => {
expect(guard).toBeTruthy();
});
});
39 changes: 39 additions & 0 deletions src/app/core/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable, tap } from 'rxjs';
import { AuthService } from '../services/auth.service';

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

private readonly router = inject(Router);

private readonly authService = inject(AuthService);

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | boolean {
if (route.data?.['roles']) {
return this.authService.hasRoles(route.data['roles'])
.pipe(
tap(value => {
if (!value) {
this.error('Access denied');
}
})
);
}

return true;
}

private error(message: string): void {
this.router.navigate(['/error'], {
skipLocationChange: true,
state: {
message
}
});
}

}
16 changes: 16 additions & 0 deletions src/app/core/interceptors/auth.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AuthInterceptor } from './auth.interceptor';

describe('AuthInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
AuthInterceptor
]
}));

it('should be created', () => {
const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor);
expect(interceptor).toBeTruthy();
});
});
53 changes: 53 additions & 0 deletions src/app/core/interceptors/auth.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Injectable, inject } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable, switchMap, take, tap } from 'rxjs';
import { AuthService } from '../services/auth.service';
import { APP_CONFIG } from '../model/app-config';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

private readonly appConfig = inject(APP_CONFIG);

private readonly authService = inject(AuthService);

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (request.url === this.appConfig.auth.api) {

return next.handle(request);
} else {

if (!this.authService.isTokenExpired()) {
return next.handle(this.addAuthHeader(request));
}

if (this.authService.isTokenRefreshing) {
return this.authService.tokenRefreshed$
.pipe(
take(1),
switchMap(() => next.handle(this.addAuthHeader(request)))
);
} else {
return this.authService.exchangeToken()
.pipe(
switchMap(() => next.handle(this.addAuthHeader(request)))
);
}
}

}

private addAuthHeader(request: HttpRequest<unknown>): HttpRequest<unknown> {
return request.clone({
setHeaders: {
Authorization: 'Bearer ' + JSON.stringify(this.authService.token)
}
});
}

}
3 changes: 3 additions & 0 deletions src/app/core/model/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export interface AppConfig {
name: string;
env: string;
api: string;
auth: {
api: string;
}
}
7 changes: 7 additions & 0 deletions src/app/core/model/auth-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { UserRole } from "./user-role.enum";

export interface AuthToken {
name?: string;
roles?: UserRole[];
expiresAt?: number;
}
4 changes: 4 additions & 0 deletions src/app/core/model/user-role.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum UserRole {
user = 'User',
administrator = 'Administrator'
}
16 changes: 16 additions & 0 deletions src/app/core/services/app.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AppService } from './app.service';

describe('AppService', () => {
let service: AppService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AppService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
Loading

0 comments on commit a36a5bc

Please sign in to comment.