Feature: Added Login / Register Feature #3

Merged
igorpropisnov merged 26 commits from feature/register-view into main 2024-05-21 21:24:11 +02:00
5 changed files with 106 additions and 25 deletions
Showing only changes of commit e308a7bace - Show all commits

View File

@ -1,11 +1,15 @@
import { Component } from '@angular/core'; import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { AuthService } from './shared/service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
providers: [], providers: [AuthService],
imports: [RouterOutlet], imports: [RouterOutlet],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
}) })
export class AppComponent {} export class AppComponent {
private readonly authService: AuthService = inject(AuthService);
}

View File

@ -1,11 +1,14 @@
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core'; import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations'; import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, withComponentInputBinding } from '@angular/router'; import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { AuthInterceptor } from './shared/interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideHttpClient(withInterceptors([AuthInterceptor])),
provideRouter(routes, withComponentInputBinding()), provideRouter(routes, withComponentInputBinding()),
provideAnimations(), provideAnimations(),
], ],

View File

@ -0,0 +1,51 @@
import {
HttpErrorResponse,
HttpEvent,
HttpHandlerFn,
HttpInterceptorFn,
HttpRequest,
} from '@angular/common/http';
import { inject } from '@angular/core';
import { Observable, catchError, switchMap, throwError } from 'rxjs';
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;
if (accessToken) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`,
},
});
}
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());
})
);
};

View File

@ -1,7 +1,7 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject, Observable, tap } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { LoginCredentials, Tokens } from '../types'; import { LoginCredentials, Tokens } from '../types';
@ -13,21 +13,27 @@ import { SessionStorageService } from './session-storage.service';
providedIn: 'root', providedIn: 'root',
}) })
export class AuthService { export class AuthService {
private readonly path: string = '/api/auth'; private readonly _path: string = '/api/auth';
private access_token: string | null = null; private _access_token: string | null = null;
private refresh_token: string | null = null; private _refresh_token: string | null = null;
private isAuthenticated$: BehaviorSubject<boolean> = private _isAuthenticated$: BehaviorSubject<boolean> =
new BehaviorSubject<boolean>(false); new BehaviorSubject<boolean>(false);
public get access_token(): string | null {
return this._access_token;
}
public constructor( public constructor(
private readonly httpClient: HttpClient, private readonly httpClient: HttpClient,
private readonly localStorageService: LocalStorageService, private readonly localStorageService: LocalStorageService,
private readonly sessionStorageService: SessionStorageService private readonly sessionStorageService: SessionStorageService
) {} ) {
this.autoLogin();
}
public signin(credentials: LoginCredentials): void { public signin(credentials: LoginCredentials): void {
this.httpClient this.httpClient
.post<Tokens>(environment.api.base + `${this.path}/signin`, credentials) .post<Tokens>(environment.api.base + `${this._path}/signin`, credentials)
.subscribe((response: Tokens) => { .subscribe((response: Tokens) => {
this.handleSuccess(response); this.handleSuccess(response);
}); });
@ -35,43 +41,60 @@ export class AuthService {
public signup(credentials: LoginCredentials): void { public signup(credentials: LoginCredentials): void {
this.httpClient this.httpClient
.post<Tokens>(environment.api.base + `${this.path}/signup`, credentials) .post<Tokens>(environment.api.base + `${this._path}/signup`, credentials)
.subscribe((response: Tokens) => { .subscribe((response: Tokens) => {
// The checked accept terms should be saved with a timestamp in the db //TODO The checked accept terms should be saved with a timestamp in the db
this.handleSuccess(response); this.handleSuccess(response);
}); });
} }
public signout(): void { public signout(): void {
this.access_token = null; this._access_token = null;
this.refresh_token = null; this._refresh_token = null;
this.localStorageService.removeItem('access_token'); this.localStorageService.removeItem('access_token');
this.sessionStorageService.removeItem('refresh_token'); this.sessionStorageService.removeItem('refresh_token');
this.isAuthenticated$.next(false); this._isAuthenticated$.next(false);
} }
public refreshToken(): void { 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();
}
}
public refreshToken(): Observable<Tokens> {
const headers = new HttpHeaders().set( const headers = new HttpHeaders().set(
'Authorization', 'Authorization',
'Bearer ' + this.refresh_token 'Bearer ' + this._refresh_token
); );
this.httpClient return this.httpClient
.post<Tokens>( .post<Tokens>(
environment.api.base + `${this.path}/refresh`, environment.api.base + `${this._path}/refresh`,
{}, {},
{ headers: headers } { headers: headers }
) )
.subscribe((response: Tokens) => { .pipe(
this.handleSuccess(response); tap((response: Tokens) => {
}); this.handleSuccess(response);
})
);
} }
private handleSuccess(tokens: Tokens): void { private handleSuccess(tokens: Tokens): void {
this.access_token = tokens.access_token; this._access_token = tokens.access_token;
this.refresh_token = tokens.refresh_token; this._refresh_token = tokens.refresh_token;
this.localStorageService.setItem('access_token', tokens.access_token); this.localStorageService.setItem('access_token', tokens.access_token);
this.sessionStorageService.setItem('refresh_token', tokens.refresh_token); this.sessionStorageService.setItem('refresh_token', tokens.refresh_token);
this.isAuthenticated$.next(true); this._isAuthenticated$.next(true);
} }
} }