Feature / Refactor to session based auth in backend #9

Merged
igorpropisnov merged 7 commits from feature/frontend-dashboard into main 2024-06-02 13:09:16 +02:00
9 changed files with 171 additions and 61 deletions
Showing only changes of commit 9aec010316 - Show all commits

View File

@ -1,15 +1,33 @@
import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { RouterOutlet, Router } from '@angular/router';
import { AuthService } from './shared/service';
@Component({
selector: 'app-root',
standalone: true,
providers: [AuthService],
providers: [],
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
private readonly authService: AuthService = inject(AuthService);
export class AppComponent implements OnInit {
public constructor(
private readonly authService: AuthService,
private readonly router: Router
) {}
public ngOnInit(): void {
this.checkAuthentication();
}
private checkAuthentication(): void {
this.authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => {
if (isAuthenticated) {
console.log('User is authenticated');
this.router.navigateByUrl('dashboard');
} else {
this.router.navigateByUrl('signup');
}
});
}
}

View File

@ -1,7 +1,12 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: '' },
import { AuthGuard } from './shared/guard/auth.guard';
const publicRoutes: Routes = [
{
path: '',
loadComponent: () => import('./app.component').then((m) => m.AppComponent),
},
{
path: 'signup',
loadComponent: () =>
@ -17,3 +22,25 @@ export const routes: Routes = [
),
},
];
const protectedRoutes: Routes = [
{
path: 'dashboard',
loadComponent: () =>
import('./pages/dashboard-root/dashboard-root.component').then(
(m) => m.DashboardRootComponent
),
canActivate: [AuthGuard],
},
];
export const routes: Routes = [
{
path: '',
children: [
...publicRoutes,
...protectedRoutes,
{ path: '', redirectTo: '', pathMatch: 'full' },
],
},
];

View File

@ -0,0 +1 @@
<h1>Hello World</h1>

View File

@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-dashboard-root',
standalone: true,
imports: [],
providers: [],
templateUrl: './dashboard-root.component.html',
styleUrl: './dashboard-root.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DashboardRootComponent {
public constructor() {}
}

View File

@ -47,7 +47,7 @@ type AuthAction = 'register' | 'signup';
PasswordModule,
HttpClientModule,
],
providers: [AuthService],
providers: [],
templateUrl: './register-root.component.html',
styleUrl: './register-root.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@ -0,0 +1,32 @@
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../service';
export const AuthGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree => {
const authService: AuthService = inject(AuthService);
const router: Router = inject(Router);
authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => {
if (!isAuthenticated) {
router.navigateByUrl('signup');
}
});
return true;
};

View File

@ -1,51 +1,79 @@
import {
HttpErrorResponse,
HttpEvent,
HttpHandlerFn,
HttpInterceptorFn,
HttpRequest,
HttpHandlerFn,
HttpEvent,
HttpErrorResponse,
} from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, catchError, switchMap, throwError } from 'rxjs';
import { Observable, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { AuthService } from '../service';
import { Tokens } from '../types';
export const AuthInterceptor: HttpInterceptorFn = (
request: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
const authService: AuthService = inject(AuthService);
const accessToken: string | null = authService.access_token;
const router = inject(Router);
const authService = inject(AuthService);
if (accessToken) {
request = request.clone({
const handleRequest = (
req: HttpRequest<unknown>
): Observable<HttpEvent<unknown>> => {
const accessToken = authService.access_token;
if (accessToken) {
req = addAuthHeader(req, accessToken);
}
return next(req);
};
const addAuthHeader = (
req: HttpRequest<unknown>,
token: string
): HttpRequest<unknown> => {
return req.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`,
Authorization: `Bearer ${token}`,
},
});
}
return next(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
return authService.refreshToken().pipe(
switchMap((tokens: Tokens) => {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${tokens.access_token}`,
},
});
return next(request);
}),
catchError((refreshError) => {
authService.signout();
return throwError(() => new Error(refreshError));
})
);
}
};
return throwError(() => new Error());
})
const handle401Error = (
req: HttpRequest<unknown>
): Observable<HttpEvent<unknown>> => {
console.log(authService.refresh_token);
if (!authService.refresh_token) {
router.navigateByUrl('signup');
return throwError(() => new Error('Authentication required'));
}
return authService.refreshToken().pipe(
switchMap((tokens) => {
req = addAuthHeader(req, tokens.access_token);
return next(req);
}),
catchError((refreshError) => {
router.navigateByUrl('signup');
return throwError(() => new Error(refreshError));
})
);
};
const handleError = (
error: HttpErrorResponse,
req: HttpRequest<unknown>
): Observable<HttpEvent<unknown>> => {
if (error.status === 401) {
return handle401Error(req);
}
return throwError(() => new Error('Unhandled error'));
};
return handleRequest(request).pipe(
catchError((error) => handleError(error, request))
);
};

View File

@ -1,4 +1,3 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, tap } from 'rxjs';
@ -13,22 +12,28 @@ import { SessionStorageService } from './session-storage.service';
providedIn: 'root',
})
export class AuthService {
public isAuthenticated$: BehaviorSubject<boolean> =
new BehaviorSubject<boolean>(false);
private _access_token: string | null = null;
private _refresh_token: string | null = null;
private _isAuthenticated$: BehaviorSubject<boolean> =
new BehaviorSubject<boolean>(false);
public get access_token(): string | null {
return this._access_token;
}
public get refresh_token(): string | null {
return this._refresh_token;
}
public constructor(
private readonly httpClient: HttpClient,
private readonly localStorageService: LocalStorageService,
private readonly sessionStorageService: SessionStorageService,
private readonly authenticationApiService: AuthenticationApiService
) {
//this.autoLogin();
this._access_token =
this.localStorageService.getItem<string>('access_token');
this._refresh_token =
this.sessionStorageService.getItem<string>('refresh_token');
}
public signin(credentials: LoginCredentials): void {
@ -66,31 +71,16 @@ export class AuthService {
this._refresh_token = null;
this.localStorageService.removeItem('access_token');
this.sessionStorageService.removeItem('refresh_token');
this._isAuthenticated$.next(false);
this.isAuthenticated$.next(false);
}
});
}
public autoLogin(): void {
const storedAccessToken: string | null =
this.localStorageService.getItem('access_token');
const storedRefreshToken: string | null =
this.sessionStorageService.getItem('refresh_token');
if (storedAccessToken && storedRefreshToken) {
this._refresh_token = storedRefreshToken;
this._isAuthenticated$.next(true);
//TODO Validate tokens with backend or decode JWT to check expiration
} else {
this.signout();
}
}
private handleSuccess(tokens: Tokens): void {
this._access_token = tokens.access_token;
this._refresh_token = tokens.refresh_token;
this.localStorageService.setItem('access_token', tokens.access_token);
this.sessionStorageService.setItem('refresh_token', tokens.refresh_token);
this._isAuthenticated$.next(true);
this.isAuthenticated$.next(true);
}
}