diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 2ffb389..763c832 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -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'); + } + }); + } } diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 5488cff..2608632 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -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' }, + ], + }, +]; diff --git a/frontend/src/app/pages/dashboard-root/dashboard-root.component.html b/frontend/src/app/pages/dashboard-root/dashboard-root.component.html new file mode 100644 index 0000000..f3e333e --- /dev/null +++ b/frontend/src/app/pages/dashboard-root/dashboard-root.component.html @@ -0,0 +1 @@ +

Hello World

diff --git a/frontend/src/app/pages/dashboard-root/dashboard-root.component.scss b/frontend/src/app/pages/dashboard-root/dashboard-root.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/pages/dashboard-root/dashboard-root.component.ts b/frontend/src/app/pages/dashboard-root/dashboard-root.component.ts new file mode 100644 index 0000000..6a160af --- /dev/null +++ b/frontend/src/app/pages/dashboard-root/dashboard-root.component.ts @@ -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() {} +} diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index ccf8788..f70ee29 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -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, diff --git a/frontend/src/app/shared/guard/auth.guard.ts b/frontend/src/app/shared/guard/auth.guard.ts new file mode 100644 index 0000000..36fb5f3 --- /dev/null +++ b/frontend/src/app/shared/guard/auth.guard.ts @@ -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 + | Promise + | boolean + | UrlTree => { + const authService: AuthService = inject(AuthService); + const router: Router = inject(Router); + + authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => { + if (!isAuthenticated) { + router.navigateByUrl('signup'); + } + }); + + return true; +}; diff --git a/frontend/src/app/shared/interceptors/auth.interceptor.ts b/frontend/src/app/shared/interceptors/auth.interceptor.ts index 7259b6c..00d0384 100644 --- a/frontend/src/app/shared/interceptors/auth.interceptor.ts +++ b/frontend/src/app/shared/interceptors/auth.interceptor.ts @@ -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, next: HttpHandlerFn ): Observable> => { - 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 + ): Observable> => { + const accessToken = authService.access_token; + + if (accessToken) { + req = addAuthHeader(req, accessToken); + } + return next(req); + }; + + const addAuthHeader = ( + req: HttpRequest, + token: string + ): HttpRequest => { + 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 + ): Observable> => { + 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 + ): Observable> => { + if (error.status === 401) { + return handle401Error(req); + } + return throwError(() => new Error('Unhandled error')); + }; + + return handleRequest(request).pipe( + catchError((error) => handleError(error, request)) ); }; diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts index c7a6ea3..381fed3 100644 --- a/frontend/src/app/shared/service/auth.service.ts +++ b/frontend/src/app/shared/service/auth.service.ts @@ -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 = + new BehaviorSubject(false); private _access_token: string | null = null; private _refresh_token: string | null = null; - private _isAuthenticated$: BehaviorSubject = - new BehaviorSubject(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('access_token'); + this._refresh_token = + this.sessionStorageService.getItem('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); } }