implement role support for angular authguard

This commit is contained in:
Roland Schneider 2025-11-20 12:14:41 +01:00
parent b08663fb28
commit d635ba0986
11 changed files with 223 additions and 30 deletions

View File

@ -16,6 +16,7 @@
"@angular/router": "^20.3.0",
"@tailwindcss/postcss": "^4.1.17",
"daisyui": "^5.4.5",
"jwt-decode": "^4.0.0",
"postcss": "^8.5.6",
"rxjs": "~7.8.0",
"tailwindcss": "^4.1.17",
@ -6498,6 +6499,15 @@
],
"license": "MIT"
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/karma": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",

View File

@ -30,6 +30,7 @@
"@angular/router": "^20.3.0",
"@tailwindcss/postcss": "^4.1.17",
"daisyui": "^5.4.5",
"jwt-decode": "^4.0.0",
"postcss": "^8.5.6",
"rxjs": "~7.8.0",
"tailwindcss": "^4.1.17",

View File

@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
import { LoginComponent } from './components/login/login.component';
import { AuthGuard } from './auth/auth.guard';
import { HomeComponent } from './components/home/home.component';
import { Welcome } from './page/welcome/welcome';
import { EventTypeFormComponent } from "./features/event-type/components/event-type-form/event-type-form.component";
import { EventTypeDetailsComponent } from "./features/event-type/components/event-type-details/event-type-details.component";
import { EventTypeTableComponent } from "./features/event-type/components/event-type-table/event-type-table.component";
@ -12,17 +13,89 @@ import { ProductTableComponent } from "./features/products/components/product-ta
import { ProductListComponent } from "./features/products/components/product-list/product-list.component";
export const routes: Routes = [
{ path: 'products', component: ProductListComponent },
{ path: 'products/table', component: ProductTableComponent },
{ path: 'products/:id', component: ProductDetailsComponent },
{ path: 'products/:id/edit', component: ProductFormComponent },
{ path: 'products/new', component: ProductFormComponent },
{ path: 'event-type', component: EventTypeListComponent },
{ path: 'event-type/table', component: EventTypeTableComponent },
{ path: 'event-type/new', component: EventTypeFormComponent },
{ path: 'event-type/:id', component: EventTypeDetailsComponent },
{ path: 'event-type/:id/edit', component: EventTypeFormComponent },
{
path: 'products',
component: ProductListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products/table',
component: ProductTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products/:id',
component: ProductDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products/:id/edit',
component: ProductFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'products/new',
component: ProductFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type',
component: EventTypeListComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type/table',
component: EventTypeTableComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type/:id',
component: EventTypeDetailsComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type/:id/edit',
component: EventTypeFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{
path: 'event-type/new',
component: EventTypeFormComponent,
canActivate: [AuthGuard],
data: {
roles: ['admin'],
},
},
{ path: 'login', component: LoginComponent },
{ path: '', component: HomeComponent, canActivate: [AuthGuard] },
{ path: '**', redirectTo: '' } // Redirect to home for any other route
{ path: 'welcome', component: Welcome },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
{ path: '', component: Welcome },
{ path: '**', redirectTo: '' }, // Redirect to home for any other route
];

View File

@ -13,11 +13,32 @@ export class AuthGuard implements CanActivate {
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if (this.authService.isLoggedIn()) {
const requiredRoles = route.data['roles'] as string[];
console.info("auth guard started", requiredRoles)
// 1. Check if the route requires any specific roles
if (!requiredRoles || requiredRoles.length === 0) {
// If no roles are required, only check if the user is logged in
return this.authService.isLoggedIn() ? true : this.router.parseUrl('/login');
}
// 2. Check if the user is logged in
if (!this.authService.isLoggedIn()) {
// If not logged in, redirect to the login page
return this.router.parseUrl('/login');
}
// 3. Check if the user has the required role
if (this.authService.hasRole(requiredRoles)) {
// If the user has the role, allow access
return true;
} else {
// Redirect to the login page
return this.router.createUrlTree(['/login']);
}
// If the user does not have the role, redirect to an unauthorized page
console.warn(`User does not have one of the required roles: ${requiredRoles}`);
return this.router.parseUrl('/welcome');
}
}
}

View File

@ -1,8 +1,23 @@
import { Injectable } from '@angular/core';
import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Router } from '@angular/router';
import { jwtDecode } from 'jwt-decode';
interface User {
name: string;
roles: string[];
}
export interface DecodedToken {
name: string; // Or 'username', 'given_name', etc.
roles: string[]; // The claim can be a single string or an array
exp: number; // Expiration time (Unix timestamp)
// Add other claims you need, like 'sub' for user ID
sub: string;
}
@Injectable({
providedIn: 'root',
@ -12,11 +27,21 @@ export class AuthService {
private readonly REFRESH_TOKEN_KEY = 'refreshToken';
private apiUrl = 'http://localhost:4200/api/auth'; // Adjust if your server URL is different
constructor(private http: HttpClient, private router: Router) {}
currentUser = signal<User | null>(null);
constructor(private http: HttpClient, private router: Router) {
const accessToken = this.getAccessToken();
if (accessToken) {
this.decodeAndSetUser(accessToken);
}
}
login(credentials: { username: string; password: string }): Observable<any> {
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/login`, credentials).pipe(
tap((response) => this.setTokens(response.accessToken, response.refreshToken))
tap((response) => {
this.setTokens(response.accessToken, response.refreshToken);
this.decodeAndSetUser(response.accessToken);
})
);
}
@ -32,8 +57,9 @@ export class AuthService {
* This is the definitive logout action from the user's perspective.
*/
clientSideLogout(): void {
console.info("clientSideLogout")
console.info("clientSideLogout");
this.removeTokens();
this.currentUser.set(null);
this.router.navigate(['/login']);
}
@ -64,6 +90,15 @@ export class AuthService {
return this.getAccessToken() !== null;
}
hasRole(requiredRoles: string[]): boolean {
const user = this.currentUser();
if (!user) {
return false; // Not logged in, so no roles
}
// Check if any of the user's roles match any of the required roles
return requiredRoles.some(requiredRole => user.roles.includes(requiredRole));
}
private setTokens(accessToken: string, refreshToken: string): void {
localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
@ -73,4 +108,28 @@ export class AuthService {
localStorage.removeItem(this.ACCESS_TOKEN_KEY);
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
}
private decodeAndSetUser(token: string): void {
try {
const decodedToken: DecodedToken = jwtDecode(token);
// Check if the token is expired. exp is in seconds, Date.now() is in ms.
// if (decodedToken.exp * 1000 < Date.now()) {
// console.warn('Attempted to use an expired token.');
// this.logout(); // The token is expired, so log the user out
// return;
// }
this.currentUser.set({
name: decodedToken.name,
roles: decodedToken.roles
});
} catch (error) {
console.error('Failed to decode JWT:', error);
this.currentUser.set(null);
}
}
}

View File

View File

@ -0,0 +1 @@
<h1>Welcome on the DV Booking</h1>

View File

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

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
imports: [],
templateUrl: './welcome.html',
styleUrl: './welcome.css',
})
export class Welcome {
}

View File

@ -52,10 +52,7 @@ export class EventTypesController {
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateEventTypeDto: UpdateEventTypeDto,
) {
update(@Param('id', ParseIntPipe) id: number, @Body() updateEventTypeDto: UpdateEventTypeDto) {
return this.eventTypesService.update(id, updateEventTypeDto);
}

View File

@ -52,10 +52,7 @@ export class ProductsController {
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateProductDto: UpdateProductDto,
) {
update(@Param('id', ParseIntPipe) id: number, @Body() updateProductDto: UpdateProductDto) {
return this.productsService.update(id, updateProductDto);
}