refs #2089 and #2091 Implement AuthService for user authentication and role management
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/24/head
Lucas Lara García 2025-05-23 13:55:58 +02:00
parent 4e6dbde59c
commit c7ed41a1a5
6 changed files with 137 additions and 53 deletions

View File

@ -29,6 +29,7 @@ import { MatMenuTrigger } from '@angular/material/menu';
import { ClientDetailsComponent } from './shared/client-details/client-details.component'; import { ClientDetailsComponent } from './shared/client-details/client-details.component';
import { PartitionTypeOrganizatorComponent } from './shared/partition-type-organizator/partition-type-organizator.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 { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-task-logs.component';
import { AuthService } from '@services/auth.service';
enum NodeType { enum NodeType {
OrganizationalUnit = 'organizational-unit', OrganizationalUnit = 'organizational-unit',
@ -114,6 +115,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
private joyrideService: JoyrideService, private joyrideService: JoyrideService,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
private toastr: ToastrService, private toastr: ToastrService,
private auth: AuthService,
private configService: ConfigService, private configService: ConfigService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
) { ) {
@ -132,7 +134,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
); );
this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
this.currentView = localStorage.getItem('groupsView') || 'list'; this.currentView = this.auth.groupsView || 'list';
} }

View File

@ -3,10 +3,10 @@ import { Component, signal } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ToastrService } from "ngx-toastr"; import { ToastrService } from "ngx-toastr";
import { jwtDecode } from "jwt-decode";
import { ConfigService } from '@services/config.service'; import { ConfigService } from '@services/config.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { GlobalStatusComponent } from '../global-status/global-status.component' import { GlobalStatusComponent } from '../global-status/global-status.component'
import { AuthService } from '@services/auth.service';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@ -20,8 +20,6 @@ export class LoginComponent {
}; };
errorMessage: string = ''; errorMessage: string = '';
isLoading: boolean = false; isLoading: boolean = false;
decodedToken: any;
baseUrl: string; baseUrl: string;
constructor( constructor(
@ -29,6 +27,7 @@ export class LoginComponent {
private router: Router, private router: Router,
private configService: ConfigService, private configService: ConfigService,
private toastService: ToastrService, private toastService: ToastrService,
private auth: AuthService,
private translateService: TranslateService, private translateService: TranslateService,
private dialog: MatDialog private dialog: MatDialog
) { ) {
@ -62,13 +61,8 @@ export class LoginComponent {
next: (res: any) => { next: (res: any) => {
if (res.token) { if (res.token) {
localStorage.setItem('loginToken', res.token); localStorage.setItem('loginToken', res.token);
localStorage.setItem('refreshToken', res.refreshToken); this.auth.refresh();
localStorage.setItem('username', this.loginObj.username); this.openSnackBar(false, 'Bienvenido ' + this.auth.username);
this.decodedToken = jwtDecode(res.token);
localStorage.setItem('groupsView', this.decodedToken.groupsView);
this.openSnackBar(false, 'Bienvenido ' + this.loginObj.username);
this.router.navigateByUrl('/groups'); this.router.navigateByUrl('/groups');
this.dialog.open(GlobalStatusComponent, { this.dialog.open(GlobalStatusComponent, {
width: '45vw', width: '45vw',

View File

@ -18,17 +18,17 @@
{{ 'GlobalStatus' | translate }} {{ 'GlobalStatus' | translate }}
</button> </button>
<button class="ordinary-button" *ngIf="isSuperAdmin" [matMenuTriggerFor]="menu" <button class="ordinary-button" *ngIf="auth.userCategory === 'super-admin'" [matMenuTriggerFor]="menu"
matTooltip="Gestión de usuarios y roles de la aplicación" matTooltipShowDelay="1000"> matTooltip="Gestión de usuarios y roles de la aplicación" matTooltipShowDelay="1000">
{{ 'Administration' | translate }} {{ 'Administration' | translate }}
</button> </button>
<button class="ordinary-button" *ngIf="!isSuperAdmin" (click)="editUser()" <button class="ordinary-button" *ngIf="auth.userCategory === 'super-admin'" (click)="editUser()"
matTooltip="Editar tu información de usuario" matTooltipShowDelay="1000"> matTooltip="Editar tu información de usuario" matTooltipShowDelay="1000">
{{ 'changePassword' | translate }} {{ 'changePassword' | translate }}
</button> </button>
<button class="logout-button" routerLink="/auth/login" matTooltip="Cerrar sesión y salir de la aplicación" <button class="logout-button" (click)="logOut()" matTooltip="Cerrar sesión y salir de la aplicación"
matTooltipShowDelay="1000"> matTooltipShowDelay="1000">
{{ 'logout' | translate }} {{ 'logout' | translate }}
</button> </button>
@ -47,10 +47,10 @@
<button mat-menu-item (click)="showGlobalStatus()"> <button mat-menu-item (click)="showGlobalStatus()">
{{ 'GlobalStatus' | translate }} {{ 'GlobalStatus' | translate }}
</button> </button>
<button mat-menu-item *ngIf="isSuperAdmin" [matMenuTriggerFor]="menu"> <button mat-menu-item *ngIf="auth.userCategory === 'super-admin'" [matMenuTriggerFor]="menu">
{{ 'Administration' | translate }} {{ 'Administration' | translate }}
</button> </button>
<button mat-menu-item *ngIf="!isSuperAdmin" (click)="editUser()"> <button mat-menu-item *ngIf="auth.userCategory === 'super-admin'" (click)="editUser()">
{{ 'changePassword' | translate }} {{ 'changePassword' | translate }}
</button> </button>
</mat-menu> </mat-menu>

View File

@ -1,9 +1,9 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core'; 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 { ChangePasswordModalComponent } from '../../components/admin/users/users/change-password-modal/change-password-modal.component';
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { GlobalStatusComponent } from 'src/app/components/global-status/global-status.component'; 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({ @Component({
selector: 'app-header', selector: 'app-header',
@ -16,39 +16,26 @@ export class HeaderComponent implements OnInit {
isSmallScreen: boolean = false; isSmallScreen: boolean = false;
@Output() toggleSidebar = new EventEmitter<void>(); @Output() toggleSidebar = new EventEmitter<void>();
private decodedToken: any;
private username: any;
onToggleSidebar() { onToggleSidebar() {
this.toggleSidebar.emit(); this.toggleSidebar.emit();
} }
constructor(public dialog: MatDialog, private breakpointObserver: BreakpointObserver) { } constructor(
public dialog: MatDialog,
public auth: AuthService,
private breakpointObserver: BreakpointObserver
) { }
ngOnInit(): void { 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.breakpointObserver.observe(['(max-width: 576px)']).subscribe((result) => {
this.isSmallScreen = result.matches; this.isSmallScreen = result.matches;
}) })
} }
ngDoCheck(): void {
this.isSuperAdmin = localStorage.getItem('isSuperAdmin') === 'true';
}
editUser() { editUser() {
const dialogRef = this.dialog.open(ChangePasswordModalComponent, { 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', width: '400px',
}); });
} }
@ -56,7 +43,11 @@ export class HeaderComponent implements OnInit {
showGlobalStatus() { showGlobalStatus() {
this.dialog.open(GlobalStatusComponent, { this.dialog.open(GlobalStatusComponent, {
width: '45vw', width: '45vw',
height: '80vh', height: '80vh',
}) })
} }
logOut() {
this.auth.logout();
}
} }

View File

@ -1,6 +1,7 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import { jwtDecode } from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AuthService } from '@services/auth.service';
@Component({ @Component({
selector: 'app-sidebar', selector: 'app-sidebar',
@ -12,9 +13,7 @@ export class SidebarComponent {
@Input() sidebarMode: 'side' | 'over' = 'side'; @Input() sidebarMode: 'side' | 'over' = 'side';
@Output() closeSidebar: EventEmitter<void> = new EventEmitter<void>(); @Output() closeSidebar: EventEmitter<void> = new EventEmitter<void>();
isSuperAdmin: boolean = false; username: string | null = "";
username: string = "";
decodedToken: any = "";
showOgBootSub: boolean = false; showOgBootSub: boolean = false;
showOgDhcpSub: boolean = false; showOgDhcpSub: boolean = false;
showCommandSub: 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 { ngOnInit(): void {
const token = localStorage.getItem('loginToken'); this.username = this.auth.username
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);
}
}
} }
} }

View File

@ -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<JwtPayload>(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']);
}
}