authentication

This commit is contained in:
Bastian Wagner
2024-09-13 21:14:09 +02:00
parent c00aad559d
commit b4a5f04505
65 changed files with 1140 additions and 77 deletions

View File

@@ -0,0 +1,36 @@
import { inject, Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, Router } from "@angular/router";
import { HotToastService } from "@ngxpert/hot-toast";
import { AuthService } from "./auth.service";
@Injectable({
providedIn: 'root'
})
export class AuthenticatedGuard {
public isLoading = false;
private router = inject(Router);
private toast = inject(HotToastService);
private authService = inject(AuthService);
async canActivate(route: ActivatedRouteSnapshot):
Promise<boolean> {
this.isLoading = true;
if (this.authService.authenticated) { return true; }
const s = await this.authService.getMe();
if (s) {
return true;
}
const authCode = route.queryParams["code"];
if (authCode) {
const success = await this.authService.authenticateWithCode(authCode);
if (success) { return true; }
}
this.router.navigateByUrl('/login');
return false;
}
}

View File

@@ -0,0 +1,108 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, Observable, tap, of, catchError } from 'rxjs';
import { IUser } from '../../model/interface/user.interface';
import { environment } from '../../../environments/environment.development';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private accessTokenSubject = new BehaviorSubject<string | null>(null);
private refreshToken: string | null = null;
private http: HttpClient = inject(HttpClient);
private route: ActivatedRoute = inject(ActivatedRoute);
private user: IUser | null = null;
constructor() {
const token = localStorage.getItem('accessToken_vault');
const refresh = localStorage.getItem('refreshToken_vault');
this.accessTokenSubject.next(token);
this.refreshToken = refresh;
}
getMe() {
if (!this.getAccessToken()) {
return false;
}
return new Promise(resolve => {
this.http.get<IUser>('/api/auth/me').subscribe({
next: user => {
this.user = user;
resolve(true)
},
error: () => {
resolve(false)
}
})
})
}
authenticateWithCode(authcode: string) {
return new Promise(resolve => {
this.http.post<IUser>('/api/auth/auth-code', { code: authcode }).subscribe(user => {
this.setTokens({ accessToken: user.accessToken, refreshToken: user.refreshToken});
this.user = user;
return resolve(true)
})
})
}
get authenticated(): boolean {
return this.user != null;
}
login(credentials: { username: string; password: string }): Observable<any> {
return this.http.post<any>('/api/auth/login', credentials).pipe(
tap(tokens => {
this.setTokens(tokens);
})
);
}
private setTokens(tokens: { accessToken: string; refreshToken: string }) {
this.accessTokenSubject.next(tokens.accessToken);
this.refreshToken = tokens.refreshToken;
localStorage.setItem('accessToken_vault', tokens.accessToken);
localStorage.setItem('refreshToken_vault', tokens.refreshToken);
}
getAccessToken(): string | null {
return this.accessTokenSubject.value;
}
refreshAccessToken(): Observable<any> {
if (!this.refreshToken) {
return of(null);
}
return this.http.post<any>('/api/auth/refresh', { refreshToken: this.refreshToken }).pipe(
tap(tokens => {
this.setTokens(tokens);
}),
catchError(() => {
this.logout();
return of(null);
})
);
}
logout() {
this.accessTokenSubject.next(null);
this.refreshToken = null;
localStorage.removeItem('accessToken_vault');
localStorage.removeItem('refreshToken_vault');
}
public routeToLogin() {
const url = `https://sso.beantastic.de?client_id=ffc46841-26f8-4946-a57a-5a9f8f21bc13&redirect_uri=${environment.location}`;
location.href = url;
}
}

View File

@@ -0,0 +1,52 @@
import { inject } from '@angular/core';
import {
HttpEvent,
HttpRequest,
HttpErrorResponse,
HttpInterceptorFn,
HttpHandlerFn
} from '@angular/common/http';
import { Observable, catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { HotToastService } from '@ngxpert/hot-toast';
export const tokenInterceptor: HttpInterceptorFn = (
req: HttpRequest<any>,
next: HttpHandlerFn
): Observable<HttpEvent<any>> => {
const authService: AuthService = inject(AuthService);
const toast: HotToastService = inject(HotToastService);
const accessToken = authService.getAccessToken();
let authReq = req;
if (accessToken) {
authReq = req.clone({
setHeaders: { Authorization: `Bearer ${accessToken}` }
});
}
return next(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
return authService.refreshAccessToken().pipe(
switchMap(() => {
const newAccessToken = authService.getAccessToken();
const newAuthReq = req.clone({
setHeaders: { Authorization: `Bearer ${newAccessToken}` }
});
return next(newAuthReq);
}),
catchError(err => {
authService.logout();
toast.error(err.error.message)
return throwError(() => err);
})
);
} else {
return throwError(() => error);
}
})
);
}

View File

@@ -0,0 +1,29 @@
<mat-toolbar>
<button mat-icon-button (click)="drawer.toggle()">
<mat-icon>menu</mat-icon>
</button>
<span>Keyvault</span>
<span class="example-spacer"></span>
<button mat-icon-button class="example-icon favorite-icon" aria-label="Example icon-button with heart icon">
<mat-icon>favorite</mat-icon>
</button>
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon">
<mat-icon>share</mat-icon>
</button>
</mat-toolbar>
<mat-drawer-container class="example-container" autosize>
<mat-drawer #drawer class="main_sidenav" mode="side" opened="true">
sdf
</mat-drawer>
<router-outlet></router-outlet>
<!-- <div class="example-sidenav-content">
<button type="button" mat-button (click)="drawer.toggle()">
Toggle sidenav
</button>
</div> -->
</mat-drawer-container>

View File

@@ -0,0 +1,24 @@
:host {
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
mat-drawer-container {
flex: 1 1 auto;
}
.example-spacer {
flex: 1 1 auto;
}
mat-drawer, mat-toolbar {
background-color: #fff;
border-radius: 0;
}
.main_sidenav{
width: 200px;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LayoutComponent } from './layout.component';
describe('LayoutComponent', () => {
let component: LayoutComponent;
let fixture: ComponentFixture<LayoutComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LayoutComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,17 @@
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-layout',
standalone: true,
imports: [MatButtonModule, MatIconModule, MatSidenavModule, RouterModule, MatToolbarModule],
templateUrl: './layout.component.html',
styleUrl: './layout.component.scss'
})
export class LayoutComponent {
}