implement role support for angular authguard
This commit is contained in:
parent
b08663fb28
commit
d635ba0986
10
admin/package-lock.json
generated
10
admin/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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: 'login', component: LoginComponent },
|
||||
{ path: '', component: HomeComponent, canActivate: [AuthGuard] },
|
||||
{ path: '**', redirectTo: '' } // Redirect to home for any other route
|
||||
{
|
||||
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: 'welcome', component: Welcome },
|
||||
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
|
||||
{ path: '', component: Welcome },
|
||||
{ path: '**', redirectTo: '' }, // Redirect to home for any other route
|
||||
];
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
0
admin/src/app/page/welcome/welcome.css
Normal file
0
admin/src/app/page/welcome/welcome.css
Normal file
1
admin/src/app/page/welcome/welcome.html
Normal file
1
admin/src/app/page/welcome/welcome.html
Normal file
@ -0,0 +1 @@
|
||||
<h1>Welcome on the DV Booking</h1>
|
||||
23
admin/src/app/page/welcome/welcome.spec.ts
Normal file
23
admin/src/app/page/welcome/welcome.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
11
admin/src/app/page/welcome/welcome.ts
Normal file
11
admin/src/app/page/welcome/welcome.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-welcome',
|
||||
imports: [],
|
||||
templateUrl: './welcome.html',
|
||||
styleUrl: './welcome.css',
|
||||
})
|
||||
export class Welcome {
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user