From c7ed41a1a57b92dbd87d0f434e5cdb2945b2d5e2 Mon Sep 17 00:00:00 2001 From: Lucas Lara Date: Fri, 23 May 2025 13:55:58 +0200 Subject: [PATCH] refs #2089 and #2091 Implement AuthService for user authentication and role management --- .../app/components/groups/groups.component.ts | 4 +- .../app/components/login/login.component.ts | 14 +-- .../app/layout/header/header.component.html | 10 +- .../src/app/layout/header/header.component.ts | 35 +++--- .../app/layout/sidebar/sidebar.component.ts | 19 +-- ogWebconsole/src/app/services/auth.service.ts | 108 ++++++++++++++++++ 6 files changed, 137 insertions(+), 53 deletions(-) create mode 100644 ogWebconsole/src/app/services/auth.service.ts diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index c75f2fd..535ad29 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -29,6 +29,7 @@ import { MatMenuTrigger } from '@angular/material/menu'; import { ClientDetailsComponent } from './shared/client-details/client-details.component'; import { PartitionTypeOrganizatorComponent } from './shared/partition-type-organizator/partition-type-organizator.component'; import { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-task-logs.component'; +import { AuthService } from '@services/auth.service'; enum NodeType { OrganizationalUnit = 'organizational-unit', @@ -114,6 +115,7 @@ export class GroupsComponent implements OnInit, OnDestroy { private joyrideService: JoyrideService, private breakpointObserver: BreakpointObserver, private toastr: ToastrService, + private auth: AuthService, private configService: ConfigService, private cd: ChangeDetectorRef, ) { @@ -132,7 +134,7 @@ export class GroupsComponent implements OnInit, OnDestroy { ); this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); - this.currentView = localStorage.getItem('groupsView') || 'list'; + this.currentView = this.auth.groupsView || 'list'; } diff --git a/ogWebconsole/src/app/components/login/login.component.ts b/ogWebconsole/src/app/components/login/login.component.ts index 2a55593..4a58364 100644 --- a/ogWebconsole/src/app/components/login/login.component.ts +++ b/ogWebconsole/src/app/components/login/login.component.ts @@ -3,10 +3,10 @@ import { Component, signal } from '@angular/core'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { ToastrService } from "ngx-toastr"; -import { jwtDecode } from "jwt-decode"; import { ConfigService } from '@services/config.service'; import { MatDialog } from '@angular/material/dialog'; import { GlobalStatusComponent } from '../global-status/global-status.component' +import { AuthService } from '@services/auth.service'; @Component({ selector: 'app-login', @@ -20,8 +20,6 @@ export class LoginComponent { }; errorMessage: string = ''; isLoading: boolean = false; - decodedToken: any; - baseUrl: string; constructor( @@ -29,6 +27,7 @@ export class LoginComponent { private router: Router, private configService: ConfigService, private toastService: ToastrService, + private auth: AuthService, private translateService: TranslateService, private dialog: MatDialog ) { @@ -62,13 +61,8 @@ export class LoginComponent { next: (res: any) => { if (res.token) { localStorage.setItem('loginToken', res.token); - localStorage.setItem('refreshToken', res.refreshToken); - localStorage.setItem('username', this.loginObj.username); - - this.decodedToken = jwtDecode(res.token); - localStorage.setItem('groupsView', this.decodedToken.groupsView); - - this.openSnackBar(false, 'Bienvenido ' + this.loginObj.username); + this.auth.refresh(); + this.openSnackBar(false, 'Bienvenido ' + this.auth.username); this.router.navigateByUrl('/groups'); this.dialog.open(GlobalStatusComponent, { width: '45vw', diff --git a/ogWebconsole/src/app/layout/header/header.component.html b/ogWebconsole/src/app/layout/header/header.component.html index e9c1a50..7a73a8a 100644 --- a/ogWebconsole/src/app/layout/header/header.component.html +++ b/ogWebconsole/src/app/layout/header/header.component.html @@ -18,17 +18,17 @@ {{ 'GlobalStatus' | translate }} - - - @@ -47,10 +47,10 @@ - - diff --git a/ogWebconsole/src/app/layout/header/header.component.ts b/ogWebconsole/src/app/layout/header/header.component.ts index 212ef50..c009d8a 100644 --- a/ogWebconsole/src/app/layout/header/header.component.ts +++ b/ogWebconsole/src/app/layout/header/header.component.ts @@ -1,9 +1,9 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { jwtDecode } from 'jwt-decode'; import { ChangePasswordModalComponent } from '../../components/admin/users/users/change-password-modal/change-password-modal.component'; import { MatDialog } from "@angular/material/dialog"; import { GlobalStatusComponent } from 'src/app/components/global-status/global-status.component'; -import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { AuthService } from '@services/auth.service'; @Component({ selector: 'app-header', @@ -16,39 +16,26 @@ export class HeaderComponent implements OnInit { isSmallScreen: boolean = false; @Output() toggleSidebar = new EventEmitter(); - private decodedToken: any; - private username: any; onToggleSidebar() { this.toggleSidebar.emit(); } - constructor(public dialog: MatDialog, private breakpointObserver: BreakpointObserver) { } + constructor( + public dialog: MatDialog, + public auth: AuthService, + private breakpointObserver: BreakpointObserver + ) { } ngOnInit(): void { - const token = localStorage.getItem('loginToken'); - if (token) { - try { - this.decodedToken = jwtDecode(token); - this.isSuperAdmin = this.decodedToken.roles.includes('ROLE_SUPER_ADMIN'); - localStorage.setItem('isSuperAdmin', String(this.isSuperAdmin)); - this.username = this.decodedToken.username; - } catch (error) { - console.error('Error decoding JWT:', error); - } - } this.breakpointObserver.observe(['(max-width: 576px)']).subscribe((result) => { this.isSmallScreen = result.matches; }) } - ngDoCheck(): void { - this.isSuperAdmin = localStorage.getItem('isSuperAdmin') === 'true'; - } - editUser() { const dialogRef = this.dialog.open(ChangePasswordModalComponent, { - data: { user: this.decodedToken.username, uuid: this.decodedToken.uuid }, + data: { user: this.auth.username, uuid: this.auth.uuid }, width: '400px', }); } @@ -56,7 +43,11 @@ export class HeaderComponent implements OnInit { showGlobalStatus() { this.dialog.open(GlobalStatusComponent, { width: '45vw', - height: '80vh', + height: '80vh', }) } + + logOut() { + this.auth.logout(); + } } diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts b/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts index 83d250f..ccb7ac9 100644 --- a/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts @@ -1,6 +1,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { jwtDecode } from 'jwt-decode'; import { MatDialog } from '@angular/material/dialog'; +import { AuthService } from '@services/auth.service'; @Component({ selector: 'app-sidebar', @@ -12,9 +13,7 @@ export class SidebarComponent { @Input() sidebarMode: 'side' | 'over' = 'side'; @Output() closeSidebar: EventEmitter = new EventEmitter(); - isSuperAdmin: boolean = false; - username: string = ""; - decodedToken: any = ""; + username: string | null = ""; showOgBootSub: boolean = false; showOgDhcpSub: boolean = false; showCommandSub: boolean = false; @@ -39,19 +38,9 @@ export class SidebarComponent { } } - constructor(public dialog: MatDialog) {} + constructor(public dialog: MatDialog, public auth: AuthService,) {} ngOnInit(): void { - const token = localStorage.getItem('loginToken'); - if (token) { - try { - this.decodedToken = jwtDecode(token); - this.isSuperAdmin = this.decodedToken.roles.includes('ROLE_SUPER_ADMIN'); - localStorage.setItem('isSuperAdmin', String(this.isSuperAdmin)); - this.username = this.decodedToken.username; - } catch (error) { - console.error('Error decoding JWT:', error); - } - } + this.username = this.auth.username } } \ No newline at end of file diff --git a/ogWebconsole/src/app/services/auth.service.ts b/ogWebconsole/src/app/services/auth.service.ts new file mode 100644 index 0000000..fa5736b --- /dev/null +++ b/ogWebconsole/src/app/services/auth.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { jwtDecode } from 'jwt-decode'; + +export type UserCategory = + | 'super-admin' + | 'ou-admin' + | 'ou-operator' + | 'ou-minimal' + | 'user'; + +interface JwtPayload { + roles: string[]; + username: string; + id: number; + uuid: string; + groupsView: string; +} + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private tokenPayload: JwtPayload | null = null; + + constructor(private router: Router) { + this.loadToken(); + } + + /** Intenta leer y decodificar el token de localStorage */ + private loadToken() { + const token = localStorage.getItem('loginToken'); + if (!token) return; + try { + this.tokenPayload = jwtDecode(token); + } catch (e) { + console.error('Error decoding JWT', e); + this.tokenPayload = null; + } + } + + /** Fuerza recarga del token (por si cambió en localStorage) */ + public refresh() { + this.loadToken(); + } + + /** Roles básicos que aparecen en el token */ + private get roles(): string[] { + return this.tokenPayload?.roles || []; + } + + get isSuperAdmin(): boolean { + return this.roles.includes('ROLE_SUPER_ADMIN'); + } + + get isOUAdmin(): boolean { + return this.roles.includes('ROLE_ORGANIZATIONAL_UNIT_ADMIN'); + } + + get isOUOperator(): boolean { + return this.roles.includes('ROLE_ORGANIZATIONAL_UNIT_OPERATOR'); + } + + get isOUMinimal(): boolean { + return this.roles.includes('ROLE_ORGANIZATIONAL_UNIT_MINIMAL'); + } + + /** + * Categoría única de usuario, según prioridades: + * - super-admin + * - ou-admin + * - ou-operator + * - ou-minimal + * - user (fallback) + */ + get userCategory(): UserCategory { + if (this.isSuperAdmin) return 'super-admin'; + if (this.isOUAdmin) return 'ou-admin'; + if (this.isOUOperator) return 'ou-operator'; + if (this.isOUMinimal) return 'ou-minimal'; + return 'user'; + } + + /** Nombre de usuario */ + get username(): string | null { + return this.tokenPayload?.username || null; + } + + /** Uuid del usuario*/ + get uuid(): any | null { + return this.tokenPayload?.uuid || null; + } + + /** groupsView del usuario */ + get groupsView(): string | null { + return this.tokenPayload?.groupsView || null; + } + + /** Logout: limpia tokens y redirige al login */ + logout(): void { + localStorage.removeItem('loginToken'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('isSuperAdmin'); + localStorage.removeItem('username'); + localStorage.removeItem('groupsView'); + localStorage.removeItem('language'); + this.tokenPayload = null; + this.router.navigate(['/auth/login']); + } +}