diff --git a/apps/front/src/app/libs/auth/directives/README.md b/apps/front/src/app/libs/auth/directives/README.md index a3eaf3c..fc099d6 100644 --- a/apps/front/src/app/libs/auth/directives/README.md +++ b/apps/front/src/app/libs/auth/directives/README.md @@ -1,211 +1,50 @@ -# Directivas de Permisos Angular +# Permission rendering — control flow nativo de Angular 21 -Este directorio contiene directivas estructurales de Angular para mostrar/ocultar elementos basado en los permisos del usuario. +> Las cuatro directivas estructurales que vivían aquí +> (`*appHasPermission`, `*appHasAnyPermission`, `*appHasAllPermissions`, +> `*appIfLoggedIn`) **se han eliminado**. El barrel `index.ts` ya no +> existe. Migra a `@if` siguiendo la tabla de `USAGE.md`. -## Directivas Disponibles +Angular 17+ trae `@if`, `@for` y `@switch` integrados en el lenguaje de +plantillas. Son type-safe, no requieren `import`, no necesitan +`TemplateRef + ViewContainerRef` y eliminan de raíz la familia de bugs +por nombre-de-input ≠ selector estructural (caso real: un `@Input()` +llamado `hasPermission` no recibía el valor que el azúcar +`*appHasPermission="X"` desugaring intentaba pasar via +`[appHasPermission]="X"`). -### 1. `*hasPermission` - -Muestra el elemento solo si el usuario tiene el permiso específico. - -```html - - - - -
Panel de Administración
-``` - -### 2. `*hasAnyPermission` - -Muestra el elemento si el usuario tiene cualquiera de los permisos especificados. - -```html - - - - -
Panel de Edición
-``` - -### 3. `*hasAllPermissions` - -Muestra el elemento solo si el usuario tiene todos los permisos especificados. +Consumí los helpers que ya expone `AuthService`: ```html - -
Panel Avanzado
- - - Admin +@if (auth.hasPermission(Permission.ADMIN)) { + +} @if (auth.hasAnyPermission([Permission.WRITE_SOME_ENTITY, Permission.ADMIN])) { + +} @if (auth.hasAllPermissions([Permission.READ_SOME_ENTITY, Permission.WRITE_SOME_ENTITY])) { + +} @if (auth.isLoggedIn()) { + +} @else { + +} ``` -## Uso en Componentes - -### Importar las Directivas - -```typescript -import { Component } from '@angular/core'; -import { Permission } from '@dto'; -import { HasPermissionDirective, HasAnyPermissionDirective, HasAllPermissionsDirective } from '@front/app/libs/auth/directives'; +`auth.isLoggedIn()`, `auth.hasPermission()`, `auth.hasAnyPermission()` y +`auth.hasAllPermissions()` son métodos públicos de `AuthService`. En tu +componente standalone: +```ts @Component({ - selector: 'app-example', + selector: 'app-foo', standalone: true, - imports: [HasPermissionDirective, HasAnyPermissionDirective, HasAllPermissionsDirective], - template: ` -
- -
-

Panel de Administración

- -
- - -
-

Herramientas de Edición

- - -
- - -
-

Funciones Avanzadas

- -
-
- `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TranslocoModule], + templateUrl: './foo.component.html', }) -export class ExampleComponent { - Permission = Permission; // Para usar en el template +export class FooComponent { + readonly auth = inject(AuthService); + readonly Permission = Permission; // exponer enum para usar en template } ``` -### Uso con NgModule - -Si estás usando NgModule en lugar de componentes standalone: - -```typescript -import { NgModule } from '@angular/core'; -import { AuthModule } from '@front/app/libs/auth'; - -@NgModule({ - imports: [AuthModule], // Las directivas se exportan automáticamente - // ... -}) -export class YourModule {} -``` - -## Ejemplos Prácticos - -### Barra de Navegación Condicional - -```html - -``` - -### Botones de Acción Condicionales - -```html -
-

Usuario: {{ user.name }}

- - - - - - - - - -
-``` - -### Formularios Condicionales - -```html -
-
- - -
- - -
- - -
- - - -
-``` - -## Características Técnicas - -### Reactividad - -Las directivas son reactivas y se actualizan automáticamente cuando: - -- El usuario inicia sesión -- El usuario cierra sesión -- Los permisos del usuario cambian - -### Performance - -- Las directivas solo se evalúan cuando es necesario -- No hay suscripciones innecesarias a observables -- El DOM se actualiza eficientemente - -### Type Safety - -- Soporte completo para TypeScript -- IntelliSense en IDEs -- Validación en tiempo de compilación - -## Notas Importantes - -1. **Autenticación Requerida**: Las directivas asumen que el usuario está autenticado. Para contenido que requiere autenticación, combina con `*ngIf="authService.isLoggedIn$ | async"`. - -2. **Permisos Vacíos**: Si se pasa un array vacío a `*hasAnyPermission` o `*hasAllPermissions`, el elemento no se mostrará. - -3. **Valores Null/Undefined**: Si se pasa `null` o `undefined`, el elemento no se mostrará. - -4. **Compatibilidad**: Las directivas son compatibles con Angular 17+ y componentes standalone. - -## Troubleshooting - -### El elemento no se muestra cuando debería - -1. Verifica que el usuario esté autenticado -2. Confirma que el usuario tenga los permisos correctos -3. Revisa la consola para errores de JavaScript - -### El elemento se muestra cuando no debería - -1. Verifica que los permisos se estén pasando correctamente -2. Confirma que el servicio de autenticación esté funcionando -3. Revisa la lógica de permisos en el backend +Ver `USAGE.md` para la tabla antes → después de cada directiva. diff --git a/apps/front/src/app/libs/auth/directives/USAGE.md b/apps/front/src/app/libs/auth/directives/USAGE.md index 3f795b1..ff3c5be 100644 --- a/apps/front/src/app/libs/auth/directives/USAGE.md +++ b/apps/front/src/app/libs/auth/directives/USAGE.md @@ -1,194 +1,120 @@ -# Directivas de Permisos Angular - Guía de Uso +# Guía de migración — directivas → control flow nativo -## Directivas Disponibles +Esta guía explica cómo migrar consumidores que usaban las antiguas +directivas estructurales `*appHasPermission`, `*appHasAnyPermission`, +`*appHasAllPermissions` y `*appIfLoggedIn` al control flow nativo de +Angular (`@if`). -### 1. `*appHasPermission` +## Tabla 1-a-1 -Muestra el elemento solo si el usuario tiene el permiso específico. +| Antes (directiva estructural) | Después (`@if` nativo) | +| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | +| `` | `@if (auth.hasPermission(Permission.ADMIN)) { }` | +| `` | `@if (auth.hasAnyPermission([Permission.ADMIN, Permission.WRITE_SOME_ENTITY])) { }` | +| `` | `@if (auth.hasAllPermissions([Permission.READ_SOME_ENTITY, Permission.WRITE_SOME_ENTITY])) { }` | +| `` | `@if (auth.isLoggedIn()) { }` | +| `` | `@if (!auth.isLoggedIn()) { }` | +| Combinado: positivo + negativo | `@if (auth.isLoggedIn()) { … } @else { … }` | -```html - - - - -
Panel de Administración
-``` - -### 2. `*appHasAnyPermission` - -Muestra el elemento si el usuario tiene cualquiera de los permisos especificados. - -```html - - - - -
Panel de Edición
-``` - -### 3. `*appHasAllPermissions` - -Muestra el elemento solo si el usuario tiene todos los permisos especificados. - -```html - -
Panel Avanzado
- - - Admin -``` - -## Uso en Componentes - -### Importar las Directivas +## Componente standalone -```typescript -import { Component } from '@angular/core'; +```ts +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { Permission } from '@dto'; -import { HasPermissionDirective, HasAnyPermissionDirective, HasAllPermissionsDirective } from '@front/app/libs/auth/directives'; +import { AuthService } from '@front/app/libs/auth/services/auth.service'; @Component({ selector: 'app-example', standalone: true, - imports: [HasPermissionDirective, HasAnyPermissionDirective, HasAllPermissionsDirective], - template: ` -
- -
-

Panel de Administración

- -
- - -
-

Herramientas de Edición

- - -
- - -
-

Funciones Avanzadas

- -
-
- `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [], + templateUrl: './example.component.html', }) export class ExampleComponent { - Permission = Permission; // Para usar en el template + readonly auth = inject(AuthService); + readonly Permission = Permission; // exponer para que el template lo lea } ``` -## Ejemplos Prácticos +Ya no hace falta importar ninguna directiva — `@if` está en el lenguaje +de plantillas — y `CommonModule` deja de ser necesario si era lo único +que usabas para `*ngIf` / `*ngFor`. -### Barra de Navegación Condicional +## Ejemplos prácticos migrados + +### Navbar condicional ```html ``` -### Botones de Acción Condicionales +### Botones de acción por permiso ```html

Usuario: {{ user.name }}

- - - - - - - - + @if (auth.hasAnyPermission([Permission.ADMIN, Permission.WRITE_SOME_ENTITY])) { + + } @if (auth.hasPermission(Permission.ADMIN)) { + + }
``` -### Formularios Condicionales +### Cabecera con bloque logueado / no logueado ```html -
-
- - -
- - -
- - -
- - - -
+@if (auth.isLoggedIn()) { + +} @else { + +} ``` -## Características Técnicas - -### Reactividad - -Las directivas son reactivas y se actualizan automáticamente cuando: - -- El usuario inicia sesión -- El usuario cierra sesión -- Los permisos del usuario cambian - -### Performance +## Por qué se eliminaron las directivas -- Las directivas solo se evalúan cuando es necesario -- No hay suscripciones innecesarias a observables -- El DOM se actualiza eficientemente +El patrón `TemplateRef + ViewContainerRef + @Input()` sufría un bug +recurrente: el azúcar sintáctico del selector estructural -### Type Safety - -- Soporte completo para TypeScript -- IntelliSense en IDEs -- Validación en tiempo de compilación - -## Notas Importantes - -1. **Autenticación Requerida**: Las directivas asumen que el usuario está autenticado. Para contenido que requiere autenticación, combina con `*ngIf="authService.isLoggedIn$ | async"`. - -2. **Permisos Vacíos**: Si se pasa un array vacío a `*appHasAnyPermission` o `*appHasAllPermissions`, el elemento no se mostrará. +```html + +``` -3. **Valores Null/Undefined**: Si se pasa `null` o `undefined`, el elemento no se mostrará. +se desugarea internamente en -4. **Compatibilidad**: Las directivas son compatibles con Angular 17+ y componentes standalone. +```html + +``` -## Troubleshooting +es decir, Angular busca un `@Input()` que **coincida exactamente** con +el nombre del selector. Si el campo interno se llama `hasPermission` +(sin prefijo `app`), el valor nunca llega y la directiva ve `undefined`, +con lo que el bloque desaparece silenciosamente. El alias +`@Input('appHasPermission')` es la mitigación correcta, pero el control +flow nativo cierra la categoría entera. -### El elemento no se muestra cuando debería +Además, `@if` es: -1. Verifica que el usuario esté autenticado -2. Confirma que el usuario tenga los permisos correctos -3. Revisa la consola para errores de JavaScript +- **Type-safe**: errores del expression en tiempo de build. +- **Más eficiente**: cero overhead de directiva, sin clases nuevas. +- **Más legible**: la condición vive junto al bloque, no en un atributo. +- **Nativo**: no requiere `import`, no rompe si el componente olvidó + declarar la directiva en `imports: [...]`. -### El elemento se muestra cuando no debería +## Reactividad -1. Verifica que los permisos se estén pasando correctamente -2. Confirma que el servicio de autenticación esté funcionando -3. Revisa la lógica de permisos en el backend +`auth.isLoggedIn()`, `auth.hasPermission(...)`, etc. leen los signals +internos del `AuthService` (`token`, `tokenDecoded`). Angular re-evalúa +el `@if` automáticamente cuando el usuario inicia/cierra sesión o el +gateway rota el access token, sin código adicional. diff --git a/apps/front/src/app/libs/auth/directives/has-all-permissions.directive.spec.ts b/apps/front/src/app/libs/auth/directives/has-all-permissions.directive.spec.ts deleted file mode 100644 index 5ebf5f4..0000000 --- a/apps/front/src/app/libs/auth/directives/has-all-permissions.directive.spec.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { TemplateRef, ViewContainerRef, signal } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { Permission } from '@dto'; -import { AuthService } from '../services/auth.service'; -import { HasAllPermissionsDirective } from './has-all-permissions.directive'; - -describe('HasAllPermissionsDirective', () => { - let mockAuthService: { - hasAllPermissions: ReturnType; - token: ReturnType>; - }; - let directive: HasAllPermissionsDirective; - let mockTemplateRef: TemplateRef; - let mockViewContainer: ViewContainerRef; - - beforeEach(() => { - mockAuthService = { - hasAllPermissions: vi.fn(), - token: signal(''), - }; - - mockTemplateRef = {} as TemplateRef; - mockViewContainer = { - createEmbeddedView: vi.fn(), - clear: vi.fn(), - } as unknown as ViewContainerRef; - - TestBed.configureTestingModule({ - providers: [ - HasAllPermissionsDirective, - { provide: AuthService, useValue: mockAuthService }, - { provide: TemplateRef, useValue: mockTemplateRef }, - { provide: ViewContainerRef, useValue: mockViewContainer }, - ], - }); - - directive = TestBed.inject(HasAllPermissionsDirective); - }); - - it('should create directive', () => { - expect(directive).toBeTruthy(); - }); - - it('should show content when user has all required permissions', () => { - mockAuthService.hasAllPermissions.mockReturnValue(true); - - directive.hasAllPermissions = [ - Permission.ADMIN, - Permission.WRITE_SOME_ENTITY, - ]; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - expect(mockAuthService.hasAllPermissions).toHaveBeenCalledWith([ - Permission.ADMIN, - Permission.WRITE_SOME_ENTITY, - ]); - }); - - it('should hide content when user does not have all required permissions', () => { - mockAuthService.hasAllPermissions.mockReturnValue(false); - - directive.hasAllPermissions = [ - Permission.ADMIN, - Permission.WRITE_SOME_ENTITY, - ]; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - }); - - it('should handle single permission (not array)', () => { - mockAuthService.hasAllPermissions.mockReturnValue(true); - - directive.hasAllPermissions = Permission.ADMIN; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - expect(mockAuthService.hasAllPermissions).toHaveBeenCalledWith([ - Permission.ADMIN, - ]); - }); - - it('should hide content when permissions array is empty', () => { - mockAuthService.hasAllPermissions.mockReturnValue(false); - - directive.hasAllPermissions = []; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - }); - - it('should update view when permissions change', () => { - // First show content - mockAuthService.hasAllPermissions.mockReturnValue(true); - directive.hasAllPermissions = [Permission.ADMIN]; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalled(); - - // Then hide content - mockAuthService.hasAllPermissions.mockReturnValue(false); - directive.hasAllPermissions = [Permission.READ_SOME_ENTITY]; - - expect(mockViewContainer.clear).toHaveBeenCalled(); - expect(mockAuthService.hasAllPermissions).toHaveBeenCalledWith([ - Permission.READ_SOME_ENTITY, - ]); - }); - - it('should work with all permission types', () => { - mockAuthService.hasAllPermissions.mockReturnValue(true); - - directive.hasAllPermissions = [ - Permission.ADMIN, - Permission.READ_SOME_ENTITY, - Permission.WRITE_SOME_ENTITY, - ]; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - expect(mockAuthService.hasAllPermissions).toHaveBeenCalledWith([ - Permission.ADMIN, - Permission.READ_SOME_ENTITY, - Permission.WRITE_SOME_ENTITY, - ]); - }); - - it('should differentiate between hasAny and hasAll behavior', () => { - mockAuthService.hasAllPermissions.mockReturnValue(false); - - directive.hasAllPermissions = [ - Permission.ADMIN, - Permission.WRITE_SOME_ENTITY, - ]; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - expect(mockAuthService.hasAllPermissions).toHaveBeenCalledWith([ - Permission.ADMIN, - Permission.WRITE_SOME_ENTITY, - ]); - }); -}); diff --git a/apps/front/src/app/libs/auth/directives/has-all-permissions.directive.ts b/apps/front/src/app/libs/auth/directives/has-all-permissions.directive.ts deleted file mode 100644 index 6481846..0000000 --- a/apps/front/src/app/libs/auth/directives/has-all-permissions.directive.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - Directive, - Input, - TemplateRef, - ViewContainerRef, - inject, -} from '@angular/core'; -import { Permission } from '@dto'; -import { AuthService } from '../services/auth.service'; - -@Directive({ - selector: '[appHasAllPermissions]', - standalone: true, -}) -export class HasAllPermissionsDirective { - private templateRef = inject(TemplateRef); - private viewContainer = inject(ViewContainerRef); - private authService = inject(AuthService); - - private hasView = false; - private currentPermissions: Permission[] = []; - - @Input() set hasAllPermissions(permissions: Permission[] | Permission) { - this.currentPermissions = Array.isArray(permissions) - ? permissions - : [permissions]; - this.updateView(); - } - - private updateView() { - if ( - this.currentPermissions.length > 0 && - this.authService.hasAllPermissions(this.currentPermissions) - ) { - if (!this.hasView) { - this.viewContainer.createEmbeddedView(this.templateRef); - this.hasView = true; - } - } else { - if (this.hasView) { - this.viewContainer.clear(); - this.hasView = false; - } - } - } -} diff --git a/apps/front/src/app/libs/auth/directives/has-any-permission.directive.spec.ts b/apps/front/src/app/libs/auth/directives/has-any-permission.directive.spec.ts deleted file mode 100644 index 595d41f..0000000 --- a/apps/front/src/app/libs/auth/directives/has-any-permission.directive.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { TemplateRef, ViewContainerRef, signal } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { Permission } from '@dto'; -import { AuthService } from '../services/auth.service'; -import { HasAnyPermissionDirective } from './has-any-permission.directive'; - -describe('HasAnyPermissionDirective', () => { - let mockAuthService: { - hasAnyPermission: ReturnType; - token: ReturnType>; - }; - let directive: HasAnyPermissionDirective; - let mockTemplateRef: TemplateRef; - let mockViewContainer: ViewContainerRef; - - beforeEach(() => { - mockAuthService = { - hasAnyPermission: vi.fn(), - token: signal(''), - }; - - mockTemplateRef = {} as TemplateRef; - mockViewContainer = { - createEmbeddedView: vi.fn(), - clear: vi.fn(), - } as unknown as ViewContainerRef; - - TestBed.configureTestingModule({ - providers: [ - HasAnyPermissionDirective, - { provide: AuthService, useValue: mockAuthService }, - { provide: TemplateRef, useValue: mockTemplateRef }, - { provide: ViewContainerRef, useValue: mockViewContainer }, - ], - }); - - directive = TestBed.inject(HasAnyPermissionDirective); - }); - - it('should create directive', () => { - expect(directive).toBeTruthy(); - }); - - it('should show content when user has any of the required permissions', () => { - mockAuthService.hasAnyPermission.mockReturnValue(true); - - directive.hasAnyPermission = [ - Permission.ADMIN, - Permission.WRITE_SOME_ENTITY, - ]; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - expect(mockAuthService.hasAnyPermission).toHaveBeenCalledWith([ - Permission.ADMIN, - Permission.WRITE_SOME_ENTITY, - ]); - }); - - it('should hide content when user does not have any required permission', () => { - mockAuthService.hasAnyPermission.mockReturnValue(false); - - directive.hasAnyPermission = [ - Permission.ADMIN, - Permission.WRITE_SOME_ENTITY, - ]; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - }); - - it('should handle single permission (not array)', () => { - mockAuthService.hasAnyPermission.mockReturnValue(true); - - directive.hasAnyPermission = Permission.ADMIN; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - expect(mockAuthService.hasAnyPermission).toHaveBeenCalledWith([ - Permission.ADMIN, - ]); - }); - - it('should hide content when permissions array is empty', () => { - mockAuthService.hasAnyPermission.mockReturnValue(false); - - directive.hasAnyPermission = []; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - }); - - it('should update view when permissions change', () => { - // First show content - mockAuthService.hasAnyPermission.mockReturnValue(true); - directive.hasAnyPermission = [Permission.ADMIN]; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalled(); - - // Then hide content - mockAuthService.hasAnyPermission.mockReturnValue(false); - directive.hasAnyPermission = [Permission.READ_SOME_ENTITY]; - - expect(mockViewContainer.clear).toHaveBeenCalled(); - expect(mockAuthService.hasAnyPermission).toHaveBeenCalledWith([ - Permission.READ_SOME_ENTITY, - ]); - }); - - it('should work with all permission types', () => { - mockAuthService.hasAnyPermission.mockReturnValue(true); - - directive.hasAnyPermission = [ - Permission.ADMIN, - Permission.READ_SOME_ENTITY, - Permission.WRITE_SOME_ENTITY, - ]; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - expect(mockAuthService.hasAnyPermission).toHaveBeenCalledWith([ - Permission.ADMIN, - Permission.READ_SOME_ENTITY, - Permission.WRITE_SOME_ENTITY, - ]); - }); -}); diff --git a/apps/front/src/app/libs/auth/directives/has-any-permission.directive.ts b/apps/front/src/app/libs/auth/directives/has-any-permission.directive.ts deleted file mode 100644 index 3544117..0000000 --- a/apps/front/src/app/libs/auth/directives/has-any-permission.directive.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - Directive, - Input, - TemplateRef, - ViewContainerRef, - inject, -} from '@angular/core'; -import { Permission } from '@dto'; -import { AuthService } from '../services/auth.service'; - -@Directive({ - selector: '[appHasAnyPermission]', - standalone: true, -}) -export class HasAnyPermissionDirective { - private templateRef = inject(TemplateRef); - private viewContainer = inject(ViewContainerRef); - private authService = inject(AuthService); - - private hasView = false; - private currentPermissions: Permission[] = []; - - @Input() set hasAnyPermission(permissions: Permission[] | Permission) { - this.currentPermissions = Array.isArray(permissions) - ? permissions - : [permissions]; - this.updateView(); - } - - private updateView() { - if ( - this.currentPermissions.length > 0 && - this.authService.hasAnyPermission(this.currentPermissions) - ) { - if (!this.hasView) { - this.viewContainer.createEmbeddedView(this.templateRef); - this.hasView = true; - } - } else { - if (this.hasView) { - this.viewContainer.clear(); - this.hasView = false; - } - } - } -} diff --git a/apps/front/src/app/libs/auth/directives/has-permission.directive.spec.ts b/apps/front/src/app/libs/auth/directives/has-permission.directive.spec.ts deleted file mode 100644 index 9006e8c..0000000 --- a/apps/front/src/app/libs/auth/directives/has-permission.directive.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { TemplateRef, ViewContainerRef, signal } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { Permission } from '@dto'; -import { AuthService } from '../services/auth.service'; -import { HasPermissionDirective } from './has-permission.directive'; - -describe('HasPermissionDirective', () => { - let mockAuthService: { - hasPermission: ReturnType; - token: ReturnType>; - }; - let directive: HasPermissionDirective; - let mockTemplateRef: TemplateRef; - let mockViewContainer: ViewContainerRef; - - beforeEach(() => { - mockAuthService = { - hasPermission: vi.fn(), - token: signal(''), - }; - - mockTemplateRef = {} as TemplateRef; - mockViewContainer = { - createEmbeddedView: vi.fn(), - clear: vi.fn(), - } as unknown as ViewContainerRef; - - TestBed.configureTestingModule({ - providers: [ - HasPermissionDirective, - { provide: AuthService, useValue: mockAuthService }, - { provide: TemplateRef, useValue: mockTemplateRef }, - { provide: ViewContainerRef, useValue: mockViewContainer }, - ], - }); - - directive = TestBed.inject(HasPermissionDirective); - }); - - it('should create directive', () => { - expect(directive).toBeTruthy(); - }); - - it('should show content when user has the required permission', () => { - mockAuthService.hasPermission.mockReturnValue(true); - - directive.hasPermission = Permission.ADMIN; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - expect(mockAuthService.hasPermission).toHaveBeenCalledWith( - Permission.ADMIN, - ); - }); - - it('should hide content when user does not have the required permission', () => { - mockAuthService.hasPermission.mockReturnValue(false); - - directive.hasPermission = Permission.ADMIN; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - expect(mockAuthService.hasPermission).toHaveBeenCalledWith( - Permission.ADMIN, - ); - }); - - it('should hide content when permission is null', () => { - mockAuthService.hasPermission.mockReturnValue(false); - - directive.hasPermission = null; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - }); - - it('should update view when permission changes', () => { - // First show content - mockAuthService.hasPermission.mockReturnValue(true); - directive.hasPermission = Permission.ADMIN; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - - // Then hide content by changing permission - mockAuthService.hasPermission.mockReturnValue(false); - directive.hasPermission = Permission.READ_SOME_ENTITY; - - expect(mockViewContainer.clear).toHaveBeenCalled(); - expect(mockAuthService.hasPermission).toHaveBeenCalledWith( - Permission.READ_SOME_ENTITY, - ); - }); - - it('should work with different permission types', () => { - mockAuthService.hasPermission.mockReturnValue(true); - - directive.hasPermission = Permission.WRITE_SOME_ENTITY; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - expect(mockAuthService.hasPermission).toHaveBeenCalledWith( - Permission.WRITE_SOME_ENTITY, - ); - }); -}); diff --git a/apps/front/src/app/libs/auth/directives/has-permission.directive.ts b/apps/front/src/app/libs/auth/directives/has-permission.directive.ts deleted file mode 100644 index 3e035ed..0000000 --- a/apps/front/src/app/libs/auth/directives/has-permission.directive.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - Directive, - Input, - TemplateRef, - ViewContainerRef, - inject, -} from '@angular/core'; -import { Permission } from '@dto'; -import { AuthService } from '../services/auth.service'; - -@Directive({ - selector: '[appHasPermission]', - standalone: true, -}) -export class HasPermissionDirective { - private templateRef = inject(TemplateRef); - private viewContainer = inject(ViewContainerRef); - private authService = inject(AuthService); - - private hasView = false; - private currentPermission: Permission | null = null; - - @Input() set hasPermission(permission: Permission | null) { - this.currentPermission = permission; - this.updateView(); - } - - private updateView() { - if ( - this.currentPermission && - this.authService.hasPermission(this.currentPermission) - ) { - if (!this.hasView) { - this.viewContainer.createEmbeddedView(this.templateRef); - this.hasView = true; - } - } else { - if (this.hasView) { - this.viewContainer.clear(); - this.hasView = false; - } - } - } -} diff --git a/apps/front/src/app/libs/auth/directives/if-logged-in.directive.spec.ts b/apps/front/src/app/libs/auth/directives/if-logged-in.directive.spec.ts deleted file mode 100644 index 6a73a03..0000000 --- a/apps/front/src/app/libs/auth/directives/if-logged-in.directive.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { TemplateRef, ViewContainerRef, signal } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { BehaviorSubject, Subscription } from 'rxjs'; -import { AuthService } from '../services/auth.service'; -import { IfLoggedInDirective } from './if-logged-in.directive'; - -describe('IfLoggedInDirective', () => { - let mockAuthService: { - token: ReturnType>; - isLoggedIn$: BehaviorSubject; - }; - let directive: IfLoggedInDirective; - let mockTemplateRef: TemplateRef; - let mockViewContainer: ViewContainerRef; - - beforeEach(() => { - mockAuthService = { - token: signal(''), - isLoggedIn$: new BehaviorSubject(false), - }; - - mockTemplateRef = {} as TemplateRef; - mockViewContainer = { - createEmbeddedView: vi.fn(), - clear: vi.fn(), - } as unknown as ViewContainerRef; - - TestBed.configureTestingModule({ - providers: [ - IfLoggedInDirective, - { provide: AuthService, useValue: mockAuthService }, - { provide: TemplateRef, useValue: mockTemplateRef }, - { provide: ViewContainerRef, useValue: mockViewContainer }, - ], - }); - - directive = TestBed.inject(IfLoggedInDirective); - }); - - it('should create directive', () => { - expect(directive).toBeTruthy(); - }); - - describe('when condition is true (default)', () => { - it('should show content when user is logged in', () => { - mockAuthService.token.set('valid-token'); - - directive.ngOnInit(); - directive.appIfLoggedIn = true; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - }); - - it('should hide content when user is not logged in', () => { - mockAuthService.token.set(''); - - directive.ngOnInit(); - directive.appIfLoggedIn = true; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - }); - }); - - describe('when condition is false', () => { - it('should show content when user is NOT logged in', () => { - mockAuthService.token.set(''); - - directive.appIfLoggedIn = false; - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalledWith( - mockTemplateRef, - ); - }); - - it('should hide content when user IS logged in', () => { - mockAuthService.token.set('valid-token'); - - directive.appIfLoggedIn = false; - - expect(mockViewContainer.createEmbeddedView).not.toHaveBeenCalled(); - }); - }); - - describe('reactive behavior', () => { - it('should show content when user logs in (reactive)', () => { - mockAuthService.token.set(''); - directive.ngOnInit(); - directive.appIfLoggedIn = true; - - // Simulate user login - mockAuthService.token.set('valid-token'); - mockAuthService.isLoggedIn$.next(true); - - expect(mockViewContainer.createEmbeddedView).toHaveBeenCalled(); - }); - - it('should hide content when user logs out (reactive)', () => { - mockAuthService.token.set('valid-token'); - directive.ngOnInit(); - directive.appIfLoggedIn = true; - - // Simulate user logout - mockAuthService.token.set(''); - mockAuthService.isLoggedIn$.next(false); - - expect(mockViewContainer.clear).toHaveBeenCalled(); - }); - }); - - describe('condition changes', () => { - it('should update view when condition input changes from true to false', () => { - mockAuthService.token.set('valid-token'); - - directive.appIfLoggedIn = true; - - directive.appIfLoggedIn = false; - - expect(mockViewContainer.clear).toHaveBeenCalled(); - }); - - it('should update view when condition input changes from false to true', () => { - mockAuthService.token.set(''); - - directive.appIfLoggedIn = false; - - directive.appIfLoggedIn = true; - - expect(mockViewContainer.clear).toHaveBeenCalled(); - }); - }); - - it('should unsubscribe from isLoggedIn$ on destroy', () => { - directive.ngOnInit(); - - const subscription = ( - directive as unknown as { subscription?: Subscription } - ).subscription as Subscription; - const spy = vi.spyOn(subscription, 'unsubscribe'); - - directive.ngOnDestroy(); - - expect(spy).toHaveBeenCalled(); - }); -}); diff --git a/apps/front/src/app/libs/auth/directives/if-logged-in.directive.ts b/apps/front/src/app/libs/auth/directives/if-logged-in.directive.ts deleted file mode 100644 index c5b9f8a..0000000 --- a/apps/front/src/app/libs/auth/directives/if-logged-in.directive.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - Directive, - inject, - Input, - OnDestroy, - OnInit, - TemplateRef, - ViewContainerRef, -} from '@angular/core'; -import { Subscription } from 'rxjs'; -import { AuthService } from '../services/auth.service'; - -@Directive({ - selector: '[appIfLoggedIn]', - standalone: true, -}) -export class IfLoggedInDirective implements OnInit, OnDestroy { - private templateRef = inject(TemplateRef); - private viewContainer = inject(ViewContainerRef); - private authService = inject(AuthService); - - private hasView = false; - private currentCondition = true; - private subscription?: Subscription; - - @Input() set appIfLoggedIn(condition: boolean) { - this.currentCondition = condition !== false; // Default to true if not specified - this.updateView(); - } - - ngOnInit() { - // Subscribe to login status changes - this.subscription = this.authService.isLoggedIn$.subscribe(() => { - this.updateView(); - }); - } - - ngOnDestroy() { - this.subscription?.unsubscribe(); - } - - private updateView() { - const userIsLoggedIn = this.authService.token() !== ''; - - // Show element if: - // - condition is true AND user is logged in, OR - // - condition is false AND user is NOT logged in - const shouldShow = - (this.currentCondition && userIsLoggedIn) || - (!this.currentCondition && !userIsLoggedIn); - - if (shouldShow) { - if (!this.hasView) { - this.viewContainer.createEmbeddedView(this.templateRef); - this.hasView = true; - } - } else { - if (this.hasView) { - this.viewContainer.clear(); - this.hasView = false; - } - } - } -} diff --git a/apps/front/src/app/libs/auth/directives/index.ts b/apps/front/src/app/libs/auth/directives/index.ts deleted file mode 100644 index 73bf10e..0000000 --- a/apps/front/src/app/libs/auth/directives/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { HasAllPermissionsDirective } from './has-all-permissions.directive'; -export { HasAnyPermissionDirective } from './has-any-permission.directive'; -export { HasPermissionDirective } from './has-permission.directive'; -export { IfLoggedInDirective } from './if-logged-in.directive'; diff --git a/apps/front/src/app/libs/auth/index.ts b/apps/front/src/app/libs/auth/index.ts index 3a2e146..a186ef5 100644 --- a/apps/front/src/app/libs/auth/index.ts +++ b/apps/front/src/app/libs/auth/index.ts @@ -9,14 +9,6 @@ export { } from './guards/auth-permission.guard'; export { canActivateFn } from './guards/auth.guard'; -// Directives -export { - HasAllPermissionsDirective, - HasAnyPermissionDirective, - HasPermissionDirective, - IfLoggedInDirective, -} from './directives'; - // Interfaces export { AuthConfig, Login, UserTokenData } from './auth.interface'; diff --git a/apps/front/src/app/libs/auth/services/auth.service.ts b/apps/front/src/app/libs/auth/services/auth.service.ts index 57de67a..8f55daa 100644 --- a/apps/front/src/app/libs/auth/services/auth.service.ts +++ b/apps/front/src/app/libs/auth/services/auth.service.ts @@ -141,6 +141,15 @@ export class AuthService { this.isInitialized.set(false); } + /** + * Template-friendly helper. Pair with the native control flow: + * @if (auth.isLoggedIn()) { ... } @else { ... } + * Re-evaluates automatically because it reads the `token` signal. + */ + isLoggedIn(): boolean { + return this.token() !== ''; + } + hasPermission(requiredPermission: Permission): boolean { const userPermissions = this.tokenDecoded.permissions || []; return userPermissions.includes(requiredPermission); diff --git a/apps/front/src/app/pages/home/home.component.html b/apps/front/src/app/pages/home/home.component.html index 380bc1e..2eb427f 100644 --- a/apps/front/src/app/pages/home/home.component.html +++ b/apps/front/src/app/pages/home/home.component.html @@ -9,65 +9,67 @@
- -
-
-

- {{ 'welcome.title' | transloco }} -

+ @if (auth.isLoggedIn()) { + +
+
+
+ +
-

- {{ 'welcome.subtitle' | transloco }} -

+

+ {{ 'welcome.loggedIn.title' | transloco }} +

-

- {{ 'welcome.description' | transloco }} -

+

+ {{ 'welcome.loggedIn.subtitle' | transloco }} +

- -
-
+

+ {{ 'welcome.loggedIn.description' | transloco }} +

- -
-
-
- +
+ +
+
+ } @else { + +
+
+

+ {{ 'welcome.title' | transloco }} +

-

- {{ 'welcome.loggedIn.title' | transloco }} -

- -

- {{ 'welcome.loggedIn.subtitle' | transloco }} -

+

+ {{ 'welcome.subtitle' | transloco }} +

-

- {{ 'welcome.loggedIn.description' | transloco }} -

+

+ {{ 'welcome.description' | transloco }} +

-
-
+ }
diff --git a/apps/front/src/app/pages/home/home.component.ts b/apps/front/src/app/pages/home/home.component.ts index 9e00906..1833b9f 100644 --- a/apps/front/src/app/pages/home/home.component.ts +++ b/apps/front/src/app/pages/home/home.component.ts @@ -1,33 +1,27 @@ -import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { Router } from '@angular/router'; import { LanguageSwitcherComponent } from '@front/app/components/language-switcher/language-switcher.component'; -import { IfLoggedInDirective } from '@front/app/libs/auth/directives'; import { AuthService } from '@front/app/libs/auth/services/auth.service'; import { TranslocoModule } from '@jsverse/transloco'; @Component({ selector: 'app-home', standalone: true, - imports: [ - CommonModule, - TranslocoModule, - LanguageSwitcherComponent, - IfLoggedInDirective, - ], + imports: [TranslocoModule, LanguageSwitcherComponent], templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export default class HomeComponent { - #router = inject(Router); - #authService = inject(AuthService); + readonly auth = inject(AuthService); + readonly #router = inject(Router); goToLogin() { this.#router.navigate(['login']); } logout() { - this.#authService.logout(); + this.auth.logout(); this.#router.navigate(['/']); } }