From c7ed41a1a57b92dbd87d0f434e5cdb2945b2d5e2 Mon Sep 17 00:00:00 2001 From: Lucas Lara Date: Fri, 23 May 2025 13:55:58 +0200 Subject: [PATCH 1/6] 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']); + } +} From a26f2481fa0ed0735fd0e6306833099b874d766a Mon Sep 17 00:00:00 2001 From: Lucas Lara Date: Mon, 26 May 2025 13:10:51 +0200 Subject: [PATCH 2/6] fix: update BootSoPartition and RemoveCacheImage component tests with new dependencies and mock data --- .../boot-so-partition.component.spec.ts | 44 ++++++++------- .../boot-so-partition.component.ts | 14 ++--- .../remove-cache-image.component.spec.ts | 54 ++++++++++++------- 3 files changed, 65 insertions(+), 47 deletions(-) diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.spec.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.spec.ts index 8ee9576..ccd7bfe 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.spec.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.spec.ts @@ -1,24 +1,24 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { BootSoPartitionComponent } from './boot-so-partition.component'; -import {ExecuteCommandComponent} from "../execute-command.component"; -import {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms"; -import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog"; -import {MatFormFieldModule} from "@angular/material/form-field"; -import {MatInputModule} from "@angular/material/input"; -import {MatCheckboxModule} from "@angular/material/checkbox"; -import {MatButtonModule} from "@angular/material/button"; -import {MatMenuModule} from "@angular/material/menu"; -import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; -import {MatTableModule} from "@angular/material/table"; -import {MatSelectModule} from "@angular/material/select"; -import {MatIconModule} from "@angular/material/icon"; -import {ToastrModule, ToastrService} from "ngx-toastr"; -import {TranslateModule} from "@ngx-translate/core"; -import {DataService} from "../../data.service"; -import {provideHttpClient} from "@angular/common/http"; -import {provideHttpClientTesting} from "@angular/common/http/testing"; -import {ConfigService} from "@services/config.service"; +import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatButtonModule } from "@angular/material/button"; +import { MatMenuModule } from "@angular/material/menu"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { MatTableModule } from "@angular/material/table"; +import { MatSelectModule } from "@angular/material/select"; +import { MatIconModule } from "@angular/material/icon"; +import { ToastrModule, ToastrService } from "ngx-toastr"; +import { TranslateModule } from "@ngx-translate/core"; +import { DataService } from "../../data.service"; +import { provideHttpClient } from "@angular/common/http"; +import { provideHttpClientTesting } from "@angular/common/http/testing"; +import { ConfigService } from "@services/config.service"; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatDividerModule } from '@angular/material/divider'; describe('BootSoPartitionComponent', () => { let component: BootSoPartitionComponent; @@ -40,9 +40,11 @@ describe('BootSoPartitionComponent', () => { MatInputModule, MatCheckboxModule, MatButtonModule, + MatExpansionModule, MatMenuModule, BrowserAnimationsModule, MatTableModule, + MatDividerModule, MatSelectModule, MatIconModule, ToastrModule.forRoot(), @@ -60,7 +62,9 @@ describe('BootSoPartitionComponent', () => { }, { provide: MAT_DIALOG_DATA, - useValue: {} + useValue: { + clients: [] + } }, { provide: ConfigService, useValue: mockConfigService } ] diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.ts index e9bfaed..46d8290 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.ts @@ -1,16 +1,16 @@ -import {Component, Inject, OnInit} from '@angular/core'; -import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; -import {MatTableDataSource} from "@angular/material/table"; -import {ConfigService} from "@services/config.service"; -import {HttpClient} from "@angular/common/http"; -import {ToastrService} from "ngx-toastr"; +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { MatTableDataSource } from "@angular/material/table"; +import { ConfigService } from "@services/config.service"; +import { HttpClient } from "@angular/common/http"; +import { ToastrService } from "ngx-toastr"; @Component({ selector: 'app-boot-so-partition', templateUrl: './boot-so-partition.component.html', styleUrl: './boot-so-partition.component.css' }) -export class BootSoPartitionComponent implements OnInit{ +export class BootSoPartitionComponent implements OnInit { baseUrl: string; selectedPartition: any = null; dataSource = new MatTableDataSource(); diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component.spec.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component.spec.ts index 3d300a7..801a02a 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component.spec.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component.spec.ts @@ -1,24 +1,25 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { RemoveCacheImageComponent } from './remove-cache-image.component'; -import {BootSoPartitionComponent} from "../boot-so-partition/boot-so-partition.component"; -import {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms"; -import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog"; -import {MatFormFieldModule} from "@angular/material/form-field"; -import {MatInputModule} from "@angular/material/input"; -import {MatCheckboxModule} from "@angular/material/checkbox"; -import {MatButtonModule} from "@angular/material/button"; -import {MatMenuModule} from "@angular/material/menu"; -import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; -import {MatTableModule} from "@angular/material/table"; -import {MatSelectModule} from "@angular/material/select"; -import {MatIconModule} from "@angular/material/icon"; -import {ToastrModule, ToastrService} from "ngx-toastr"; -import {TranslateModule} from "@ngx-translate/core"; -import {DataService} from "../../data.service"; -import {provideHttpClient} from "@angular/common/http"; -import {provideHttpClientTesting} from "@angular/common/http/testing"; -import {ConfigService} from "@services/config.service"; +import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatButtonModule } from "@angular/material/button"; +import { MatMenuModule } from "@angular/material/menu"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { MatTableModule } from "@angular/material/table"; +import { MatSelectModule } from "@angular/material/select"; +import { MatIconModule } from "@angular/material/icon"; +import { ToastrModule, ToastrService } from "ngx-toastr"; +import { TranslateModule } from "@ngx-translate/core"; +import { DataService } from "../../data.service"; +import { provideHttpClient } from "@angular/common/http"; +import { provideHttpClientTesting } from "@angular/common/http/testing"; +import { ConfigService } from "@services/config.service"; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatRadioModule } from '@angular/material/radio'; describe('RemoveCacheImageComponent', () => { let component: RemoveCacheImageComponent; @@ -41,9 +42,12 @@ describe('RemoveCacheImageComponent', () => { MatCheckboxModule, MatButtonModule, MatMenuModule, + MatExpansionModule, BrowserAnimationsModule, MatTableModule, + MatDividerModule, MatSelectModule, + MatRadioModule, MatIconModule, ToastrModule.forRoot(), TranslateModule.forRoot() @@ -60,7 +64,17 @@ describe('RemoveCacheImageComponent', () => { }, { provide: MAT_DIALOG_DATA, - useValue: {} + useValue: { + clients: [ + { + '@id': '/clients/1', + uuid: 'client-uuid-1', + selected: false, + status: 'og-live', + state: 'og-live' + } + ] + } }, { provide: ConfigService, useValue: mockConfigService } ] From 84fd9d03352e76ddc64435f1c134185dfc36b3ed Mon Sep 17 00:00:00 2001 From: Lucas Lara Date: Tue, 27 May 2025 14:14:01 +0200 Subject: [PATCH 3/6] refs #2078 and #2079 Implement role-based command filtering and UI adjustments for user permissions --- .../execute-command.component.ts | 39 +++++++++++++++++-- .../components/groups/groups.component.html | 5 ++- .../app/components/groups/groups.component.ts | 4 +- .../app/layout/header/header.component.html | 2 +- .../app/layout/sidebar/sidebar.component.html | 27 ++++++------- .../app/layout/sidebar/sidebar.component.ts | 1 - 6 files changed, 56 insertions(+), 22 deletions(-) diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts index 8ac5030..0efcd55 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts @@ -3,9 +3,10 @@ import { HttpClient } from '@angular/common/http'; import { Router } from "@angular/router"; import { ToastrService } from "ngx-toastr"; import { ConfigService } from '@services/config.service'; -import {BootSoPartitionComponent} from "./boot-so-partition/boot-so-partition.component"; -import {MatDialog} from "@angular/material/dialog"; -import {RemoveCacheImageComponent} from "./remove-cache-image/remove-cache-image.component"; +import { BootSoPartitionComponent } from "./boot-so-partition/boot-so-partition.component"; +import { MatDialog } from "@angular/material/dialog"; +import { RemoveCacheImageComponent } from "./remove-cache-image/remove-cache-image.component"; +import { AuthService } from '@services/auth.service'; @Component({ selector: 'app-execute-command', @@ -44,6 +45,7 @@ export class ExecuteCommandComponent implements OnInit { private router: Router, private configService: ConfigService, private toastService: ToastrService, + public auth: AuthService, private dialog: MatDialog, ) { this.baseUrl = this.configService.apiUrl; @@ -51,6 +53,9 @@ export class ExecuteCommandComponent implements OnInit { ngOnInit(): void { this.clientData = this.clientData || []; + const allowed = this.getAllowedCommandsByRole(); + this.arrayCommands = this.arrayCommands.filter(c => allowed.includes(c.slug)); + this.updateCommandStates(); } @@ -58,6 +63,34 @@ export class ExecuteCommandComponent implements OnInit { this.updateCommandStates(); } + private getAllowedCommandsByRole(): string[] { + const role = this.auth.userCategory; + + const permissions: Record = { + 'super-admin': ['*'], + 'ou-admin': ['*'], + 'ou-operator': [ + 'power-on', + 'power-off', + 'reboot', + 'login', + 'deploy-image', + 'software-inventory', + 'hardware-inventory', + 'remove-cache-image', + 'partition' + ], + 'ou-minimal': [ + 'power-on', + 'power-off' + ] + }; + + const allowed = permissions[role] || []; + return allowed.includes('*') ? this.arrayCommands.map(c => c.slug) : allowed; + } + + private updateCommandStates(): void { let states: string[] = []; diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 1d7f19f..fde3d27 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -11,11 +11,12 @@
- - + - diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html index 1aab78b..3272552 100644 --- a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html @@ -1,9 +1,9 @@ @@ -18,7 +18,7 @@ + matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'"> playlist_play {{ 'actions' | translate }} @@ -28,7 +28,7 @@ + matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'"> chevron_right {{ 'commands' | translate }} @@ -44,7 +44,7 @@ --> + matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'"> chevron_right {{ 'tasks' | translate }} @@ -53,15 +53,15 @@ + matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'"> lan {{ 'subnets' | translate }} - + desktop_windows {{ 'boot' | translate }} @@ -94,7 +94,7 @@ + matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'"> calendar_month {{ 'calendars' | translate }} @@ -102,7 +102,7 @@ + matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'"> terminal {{ 'software' | translate }} @@ -135,7 +135,8 @@ + matTooltip="{{ 'TOOLTIP_REPOSITORIES' | translate }}" matTooltipShowDelay="1000" + *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'"> warehouse {{ 'repositories' | translate }} @@ -143,10 +144,10 @@ + matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'"> list {{ 'menus' | translate }} - + \ No newline at end of file diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts b/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts index ccb7ac9..7abcc59 100644 --- a/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.ts @@ -1,5 +1,4 @@ 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'; From 79188ffbc583403e66bd0d0908770788588cc3fb Mon Sep 17 00:00:00 2001 From: Lucas Lara Date: Wed, 28 May 2025 12:15:58 +0200 Subject: [PATCH 4/6] refs #2078 and #2079 Add user category checks for client management actions in groups component --- .../components/groups/groups.component.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index fde3d27..96ffd8c 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -181,23 +181,23 @@ map {{ 'roomMap' | translate }} - - - - - @@ -288,7 +288,7 @@ - @@ -305,7 +305,7 @@ list_alt {{ 'procedimientosCliente' | translate }} - @@ -423,7 +423,7 @@ [runScriptContext]="getRunScriptContext([client])"> - @@ -439,7 +439,7 @@ list_alt {{ 'procedimientosCliente' | translate }} - From 713776893981b68f786ace0f4f75803bdddadf40 Mon Sep 17 00:00:00 2001 From: Lucas Lara Date: Thu, 29 May 2025 11:51:20 +0200 Subject: [PATCH 5/6] refactor: remove Admin and Dashboard components; implement role-based routing and guards --- ogWebconsole/src/app/app-routing.module.ts | 74 +++++++++---------- ogWebconsole/src/app/app.module.ts | 2 - .../app/components/admin/admin.component.css | 48 ------------ .../app/components/admin/admin.component.html | 10 --- .../components/admin/admin.component.spec.ts | 48 ------------ .../app/components/admin/admin.component.ts | 13 ---- .../dashboard/dashboard.component.css | 0 .../dashboard/dashboard.component.html | 1 - .../dashboard/dashboard.component.spec.ts | 23 ------ .../dashboard/dashboard.component.ts | 10 --- .../src/app/guards/role.guard.spec.ts | 17 +++++ ogWebconsole/src/app/guards/role.guard.ts | 19 +++++ .../app/layout/header/header.component.html | 2 +- ogWebconsole/src/app/services/auth.service.ts | 11 +++ 14 files changed, 84 insertions(+), 194 deletions(-) delete mode 100644 ogWebconsole/src/app/components/admin/admin.component.css delete mode 100644 ogWebconsole/src/app/components/admin/admin.component.html delete mode 100644 ogWebconsole/src/app/components/admin/admin.component.spec.ts delete mode 100644 ogWebconsole/src/app/components/admin/admin.component.ts delete mode 100644 ogWebconsole/src/app/components/dashboard/dashboard.component.css delete mode 100644 ogWebconsole/src/app/components/dashboard/dashboard.component.html delete mode 100644 ogWebconsole/src/app/components/dashboard/dashboard.component.spec.ts delete mode 100644 ogWebconsole/src/app/components/dashboard/dashboard.component.ts create mode 100644 ogWebconsole/src/app/guards/role.guard.spec.ts create mode 100644 ogWebconsole/src/app/guards/role.guard.ts diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts index f901d43..501a3ad 100644 --- a/ogWebconsole/src/app/app-routing.module.ts +++ b/ogWebconsole/src/app/app-routing.module.ts @@ -3,28 +3,26 @@ import { RouterModule, Routes } from '@angular/router'; import { MainLayoutComponent } from './layout/main-layout/main-layout.component'; import { AuthLayoutComponent } from './layout/auth-layout/auth-layout.component'; import { LoginComponent } from './components/login/login.component'; -import { DashboardComponent } from './components/dashboard/dashboard.component'; import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component'; -import { AdminComponent } from './components/admin/admin.component'; import { UsersComponent } from './components/admin/users/users/users.component'; import { RolesComponent } from './components/admin/roles/roles/roles.component'; import { GroupsComponent } from './components/groups/groups.component'; import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component'; import { PxeComponent } from './components/ogboot/pxe/pxe.component'; import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component'; -import {OgbootStatusComponent} from "./components/ogboot/ogboot-status/ogboot-status.component"; +import { OgbootStatusComponent } from "./components/ogboot/ogboot-status/ogboot-status.component"; import { CalendarComponent } from "./components/calendar/calendar.component"; import { CommandsComponent } from './components/commands/main-commands/commands.component'; import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component'; import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component'; import { TaskLogsComponent } from './components/task-logs/task-logs.component'; -import {SoftwareComponent} from "./components/software/software.component"; -import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component"; -import {OperativeSystemComponent} from "./components/operative-system/operative-system.component"; +import { SoftwareComponent } from "./components/software/software.component"; +import { SoftwareProfileComponent } from "./components/software-profile/software-profile.component"; +import { OperativeSystemComponent } from "./components/operative-system/operative-system.component"; import { PartitionAssistantComponent } from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component"; -import {RepositoriesComponent} from "./components/repositories/repositories.component"; +import { RepositoriesComponent } from "./components/repositories/repositories.component"; import { CreateClientImageComponent } from "./components/groups/components/client-main-view/create-image/create-image.component"; @@ -34,44 +32,44 @@ import { import { MainRepositoryViewComponent } from "./components/repositories/main-repository-view/main-repository-view.component"; -import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component"; -import {MenusComponent} from "./components/menus/menus.component"; -import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component"; -import {StatusComponent} from "./components/ogdhcp/status/status.component"; +import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component"; +import { MenusComponent } from "./components/menus/menus.component"; +import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component"; +import { StatusComponent } from "./components/ogdhcp/status/status.component"; import { RunScriptAssistantComponent } from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component"; +import { roleGuard } from './guards/role.guard'; const routes: Routes = [ { path: '', redirectTo: 'auth/login', pathMatch: 'full' }, - { path: '', component: MainLayoutComponent, + { + path: '', component: MainLayoutComponent, children: [ - { path: 'dashboard', component: DashboardComponent }, - { path: 'admin', component: AdminComponent }, - { path: 'users', component: UsersComponent }, - { path: 'env-vars', component: EnvVarsComponent }, - { path: 'user-groups', component: RolesComponent }, + { path: 'users', component: UsersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } }, + { path: 'env-vars', component: EnvVarsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } }, + { path: 'roles', component: RolesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } }, { path: 'groups', component: GroupsComponent }, - { path: 'pxe-images', component: PXEimagesComponent }, - { path: 'pxe', component: PxeComponent }, - { path: 'pxe-boot-file', component: PxeBootFilesComponent }, - { path: 'ogboot-status', component: OgbootStatusComponent }, - { path: 'subnets', component: OgDhcpSubnetsComponent }, - { path: 'ogdhcp-status', component: StatusComponent }, - { path: 'commands', component: CommandsComponent }, - { path: 'commands-groups', component: CommandsGroupsComponent }, - { path: 'commands-task', component: CommandsTaskComponent }, - { path: 'commands-logs', component: TaskLogsComponent }, - { path: 'calendars', component: CalendarComponent }, - { path: 'clients/deploy-image', component: DeployImageComponent }, - { path: 'clients/partition-assistant', component: PartitionAssistantComponent }, - { path: 'clients/run-script', component: RunScriptAssistantComponent }, - { path: 'clients/:id/create-image', component: CreateClientImageComponent }, - { path: 'repositories', component: RepositoriesComponent }, - { path: 'repository/:id', component: MainRepositoryViewComponent }, - { path: 'software', component: SoftwareComponent }, - { path: 'software-profiles', component: SoftwareProfileComponent }, - { path: 'operative-systems', component: OperativeSystemComponent }, - { path: 'menus', component: MenusComponent }, + { path: 'pxe-images', component: PXEimagesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'pxe', component: PxeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'pxe-boot-file', component: PxeBootFilesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'ogdhcp-status', component: StatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'repositories', component: RepositoriesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'repository/:id', component: MainRepositoryViewComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'software', component: SoftwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'software-profiles', component: SoftwareProfileComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'operative-systems', component: OperativeSystemComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'menus', component: MenusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, ], }, { diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index c1c0e66..2e04ddc 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -17,7 +17,6 @@ import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatSidenavModule } from '@angular/material/sidenav'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { AdminComponent } from './components/admin/admin.component'; import { MatCardModule } from '@angular/material/card'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -172,7 +171,6 @@ registerLocaleData(localeEs, 'es-ES'); HeaderComponent, SidebarComponent, LoginComponent, - AdminComponent, MainLayoutComponent, UsersComponent, RolesComponent, diff --git a/ogWebconsole/src/app/components/admin/admin.component.css b/ogWebconsole/src/app/components/admin/admin.component.css deleted file mode 100644 index 8c90c1c..0000000 --- a/ogWebconsole/src/app/components/admin/admin.component.css +++ /dev/null @@ -1,48 +0,0 @@ -/* Estilos del contenedor para centrar los botones */ -.container { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; -} - -/* Estilos del contenedor de cada botón y texto */ -.button-container { - display: flex; - flex-direction: column; - align-items: center; - margin: 0 10px; -} - -/* Estilos del texto debajo de los botones */ -span{ - margin: 0; - font-size: 20px; - text-align: center; -} - -/* Media query para hacer los botones responsive */ -@media (max-width: 900px) { - button { - height: 120px; - width: 120px; - } -} - -@media (max-width: 600px) { - button { - height: 90px; - width: 90px; - } -} - -@media (max-width: 400px) { - button { - height: 70px; - width: 70px; - } - - span{ - font-size: 14px; - } -} diff --git a/ogWebconsole/src/app/components/admin/admin.component.html b/ogWebconsole/src/app/components/admin/admin.component.html deleted file mode 100644 index bf2ca78..0000000 --- a/ogWebconsole/src/app/components/admin/admin.component.html +++ /dev/null @@ -1,10 +0,0 @@ -
- - -
diff --git a/ogWebconsole/src/app/components/admin/admin.component.spec.ts b/ogWebconsole/src/app/components/admin/admin.component.spec.ts deleted file mode 100644 index 2ed2fec..0000000 --- a/ogWebconsole/src/app/components/admin/admin.component.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { AdminComponent } from './admin.component'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { TranslateModule } from '@ngx-translate/core'; -import { Router } from '@angular/router'; - -describe('AdminComponent', () => { - let component: AdminComponent; - let fixture: ComponentFixture; - let router: Router; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [AdminComponent], - imports: [ - RouterTestingModule, - MatButtonModule, - MatIconModule, - TranslateModule.forRoot() - ] - }).compileComponents(); - - router = TestBed.inject(Router); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(AdminComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('debería crear el componente', () => { - expect(component).toBeTruthy(); - }); - - it('debería renderizar dos botones', () => { - const buttons = fixture.nativeElement.querySelectorAll('button'); - expect(buttons.length).toBe(2); - }); - - it('debería tener un botón con routerLink a "/users"', () => { - const button = fixture.nativeElement.querySelector('button[routerLink="/users"]'); - expect(button).toBeTruthy(); - expect(button.querySelector('mat-icon').textContent.trim()).toBe('group'); - }); -}); diff --git a/ogWebconsole/src/app/components/admin/admin.component.ts b/ogWebconsole/src/app/components/admin/admin.component.ts deleted file mode 100644 index 256f80d..0000000 --- a/ogWebconsole/src/app/components/admin/admin.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component } from '@angular/core'; - - -@Component({ - selector: 'app-admin', - templateUrl: './admin.component.html', - styleUrl: './admin.component.css' -}) - -export class AdminComponent { - -} - diff --git a/ogWebconsole/src/app/components/dashboard/dashboard.component.css b/ogWebconsole/src/app/components/dashboard/dashboard.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/ogWebconsole/src/app/components/dashboard/dashboard.component.html b/ogWebconsole/src/app/components/dashboard/dashboard.component.html deleted file mode 100644 index c83a790..0000000 --- a/ogWebconsole/src/app/components/dashboard/dashboard.component.html +++ /dev/null @@ -1 +0,0 @@ -

dashboard works!

\ No newline at end of file diff --git a/ogWebconsole/src/app/components/dashboard/dashboard.component.spec.ts b/ogWebconsole/src/app/components/dashboard/dashboard.component.spec.ts deleted file mode 100644 index 5e8286f..0000000 --- a/ogWebconsole/src/app/components/dashboard/dashboard.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DashboardComponent } from './dashboard.component'; - -describe('DashboardComponent', () => { - let component: DashboardComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DashboardComponent] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create the component', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/ogWebconsole/src/app/components/dashboard/dashboard.component.ts b/ogWebconsole/src/app/components/dashboard/dashboard.component.ts deleted file mode 100644 index e32c2fc..0000000 --- a/ogWebconsole/src/app/components/dashboard/dashboard.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-dashboard', - templateUrl: './dashboard.component.html', - styleUrl: './dashboard.component.css' -}) -export class DashboardComponent { - -} diff --git a/ogWebconsole/src/app/guards/role.guard.spec.ts b/ogWebconsole/src/app/guards/role.guard.spec.ts new file mode 100644 index 0000000..30809a2 --- /dev/null +++ b/ogWebconsole/src/app/guards/role.guard.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { CanActivateFn } from '@angular/router'; + +import { roleGuard } from './role.guard'; + +describe('roleGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => roleGuard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeGuard).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/guards/role.guard.ts b/ogWebconsole/src/app/guards/role.guard.ts new file mode 100644 index 0000000..313f4f9 --- /dev/null +++ b/ogWebconsole/src/app/guards/role.guard.ts @@ -0,0 +1,19 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '@services/auth.service'; + +export const roleGuard: CanActivateFn = (route, state) => { + const auth = inject(AuthService); + const router = inject(Router); + + const currentRole = auth.getCurrentRole(); + + const allowedRoles = route.data?.['allowedRoles'] as string[]; + + if (allowedRoles?.includes(currentRole)) { + return true; + } else { + router.navigate(['/groups']); + return false; + } +}; diff --git a/ogWebconsole/src/app/layout/header/header.component.html b/ogWebconsole/src/app/layout/header/header.component.html index 1d311d9..1ad8f7d 100644 --- a/ogWebconsole/src/app/layout/header/header.component.html +++ b/ogWebconsole/src/app/layout/header/header.component.html @@ -65,7 +65,7 @@ matTooltipShowDelay="1000"> {{ 'labelUsers' | translate }} -