From b68403762f2b6c4834a093851ba5fe4efec951eb Mon Sep 17 00:00:00 2001 From: thomas Date: Thu, 22 Dec 2022 11:39:19 +0100 Subject: [PATCH] feat(challenge6): add hasrole directive --- apps/permissions/project.json | 3 +- .../src/app/dashboard/admin.component.ts | 3 +- .../src/app/dashboard/client.component.ts | 14 ++++ .../src/app/dashboard/everyone.component.ts | 14 ++++ .../src/app/dashboard/manager.component.ts | 3 +- .../src/app/dashboard/no-user.component.ts | 14 ++++ .../app/dashboard/writer-reader.component.ts | 14 ++++ .../app/directive/has-role.cs.directive.ts | 57 ++++++++++++++++ .../src/app/directive/has-role.directive.ts | 60 ++++++++++++++++ .../directive/has-role.rx.v15.directive.ts | 42 ++++++++++++ .../app/directive/has-role.rxjs.directive.ts | 65 ++++++++++++++++++ .../app/directive/has-role.v15.directive.ts | 36 ++++++++++ .../src/app/has-permission.guard.ts | 36 ++++++++++ .../permissions/src/app/has-role.directive.ts | 68 ------------------- .../src/app/information.component.ts | 20 +++--- apps/permissions/src/app/login.component.ts | 2 +- apps/permissions/src/app/routes.ts | 54 +++++++++++++-- apps/permissions/src/app/user.store.ts | 17 ++++- 18 files changed, 430 insertions(+), 92 deletions(-) create mode 100644 apps/permissions/src/app/dashboard/client.component.ts create mode 100644 apps/permissions/src/app/dashboard/everyone.component.ts create mode 100644 apps/permissions/src/app/dashboard/no-user.component.ts create mode 100644 apps/permissions/src/app/dashboard/writer-reader.component.ts create mode 100644 apps/permissions/src/app/directive/has-role.cs.directive.ts create mode 100644 apps/permissions/src/app/directive/has-role.directive.ts create mode 100644 apps/permissions/src/app/directive/has-role.rx.v15.directive.ts create mode 100644 apps/permissions/src/app/directive/has-role.rxjs.directive.ts create mode 100644 apps/permissions/src/app/directive/has-role.v15.directive.ts create mode 100644 apps/permissions/src/app/has-permission.guard.ts delete mode 100644 apps/permissions/src/app/has-role.directive.ts diff --git a/apps/permissions/project.json b/apps/permissions/project.json index d8575e6c0..d395f7327 100644 --- a/apps/permissions/project.json +++ b/apps/permissions/project.json @@ -20,7 +20,8 @@ "apps/permissions/src/assets" ], "styles": ["apps/permissions/src/styles.scss"], - "scripts": [] + "scripts": [], + "allowedCommonJsDependencies": ["seedrandom"] }, "configurations": { "production": { diff --git a/apps/permissions/src/app/dashboard/admin.component.ts b/apps/permissions/src/app/dashboard/admin.component.ts index 72e4b3bef..3bbf4e9f3 100644 --- a/apps/permissions/src/app/dashboard/admin.component.ts +++ b/apps/permissions/src/app/dashboard/admin.component.ts @@ -3,7 +3,6 @@ import { RouterLink } from '@angular/router'; import { ButtonComponent } from '../button.component'; @Component({ - selector: 'app-dashboard', standalone: true, imports: [RouterLink, ButtonComponent], template: ` @@ -12,4 +11,4 @@ import { ButtonComponent } from '../button.component'; `, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AdminDashboardComponent {} +export default class AdminDashboardComponent {} diff --git a/apps/permissions/src/app/dashboard/client.component.ts b/apps/permissions/src/app/dashboard/client.component.ts new file mode 100644 index 000000000..56d055b5a --- /dev/null +++ b/apps/permissions/src/app/dashboard/client.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { ButtonComponent } from '../button.component'; + +@Component({ + standalone: true, + imports: [RouterLink, ButtonComponent], + template: ` +

dashboard for Client works!

+ + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class ClientDashboardComponent {} diff --git a/apps/permissions/src/app/dashboard/everyone.component.ts b/apps/permissions/src/app/dashboard/everyone.component.ts new file mode 100644 index 000000000..e778e04f9 --- /dev/null +++ b/apps/permissions/src/app/dashboard/everyone.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { ButtonComponent } from '../button.component'; + +@Component({ + standalone: true, + imports: [RouterLink, ButtonComponent], + template: ` +

dashboard for Everyone!

+ + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class EveryoneDashboardComponent {} diff --git a/apps/permissions/src/app/dashboard/manager.component.ts b/apps/permissions/src/app/dashboard/manager.component.ts index b92fc7925..cff903fbf 100644 --- a/apps/permissions/src/app/dashboard/manager.component.ts +++ b/apps/permissions/src/app/dashboard/manager.component.ts @@ -3,7 +3,6 @@ import { RouterLink } from '@angular/router'; import { ButtonComponent } from '../button.component'; @Component({ - selector: 'app-dashboard', standalone: true, imports: [RouterLink, ButtonComponent], template: ` @@ -12,4 +11,4 @@ import { ButtonComponent } from '../button.component'; `, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ManagerDashboardComponent {} +export default class ManagerDashboardComponent {} diff --git a/apps/permissions/src/app/dashboard/no-user.component.ts b/apps/permissions/src/app/dashboard/no-user.component.ts new file mode 100644 index 000000000..88b965de3 --- /dev/null +++ b/apps/permissions/src/app/dashboard/no-user.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { ButtonComponent } from '../button.component'; + +@Component({ + standalone: true, + imports: [RouterLink, ButtonComponent], + template: ` +

User is not Logged In

+ + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class NoUserDashboardComponent {} diff --git a/apps/permissions/src/app/dashboard/writer-reader.component.ts b/apps/permissions/src/app/dashboard/writer-reader.component.ts new file mode 100644 index 000000000..f9a2f6145 --- /dev/null +++ b/apps/permissions/src/app/dashboard/writer-reader.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { ButtonComponent } from '../button.component'; + +@Component({ + standalone: true, + imports: [RouterLink, ButtonComponent], + template: ` +

dashboard for Writer or Reader works!

+ + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class WriterReaderDashboardComponent {} diff --git a/apps/permissions/src/app/directive/has-role.cs.directive.ts b/apps/permissions/src/app/directive/has-role.cs.directive.ts new file mode 100644 index 000000000..ada64e43f --- /dev/null +++ b/apps/permissions/src/app/directive/has-role.cs.directive.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/member-ordering */ +/* eslint-disable @angular-eslint/directive-selector */ +import { + Directive, + inject, + Input, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import { ComponentStore } from '@ngrx/component-store'; +import { pipe, tap } from 'rxjs'; +import { Role } from '../user.model'; +import { UserStore } from '../user.store'; + +@Directive({ + selector: '[hasRole], [hasRoleIsAdmin]', + standalone: true, + providers: [ComponentStore], +}) +export class HasRoleDirective { + private templateRef = inject(TemplateRef); + private viewContainer = inject(ViewContainerRef); + private componentStore = inject(ComponentStore); + private store = inject(UserStore); + + @Input('hasRole') set role(role: Role | Role[] | undefined) { + if (role) { + this.showTemplate(this.store.hasAnyRole(role)); + } + } + + @Input('hasRoleIsAdmin') set isAdmin(isAdmin: boolean) { + if (isAdmin) { + this.showTemplate(this.store.isAdmin$); + } + } + + private readonly showTemplate = this.componentStore.effect< + boolean | undefined + >( + pipe( + tap((showTemplate) => + showTemplate ? this.addTemplate() : this.clearTemplate() + ) + ) + ); + + private addTemplate() { + this.viewContainer.clear(); + this.viewContainer.createEmbeddedView(this.templateRef); + } + + private clearTemplate() { + this.viewContainer.clear(); + } +} diff --git a/apps/permissions/src/app/directive/has-role.directive.ts b/apps/permissions/src/app/directive/has-role.directive.ts new file mode 100644 index 000000000..8c09ad3ab --- /dev/null +++ b/apps/permissions/src/app/directive/has-role.directive.ts @@ -0,0 +1,60 @@ +/* eslint-disable @angular-eslint/directive-selector */ +import { + injectDestroyService, + provideDestroyService, +} from '@angular-challenges/shared/utils'; +import { + Directive, + inject, + Input, + OnInit, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import { takeUntil } from 'rxjs'; +import { Role } from '../user.model'; +import { UserStore } from '../user.store'; + +@Directive({ + selector: '[hasRole], [hasRoleIsAdmin]', + standalone: true, + providers: [provideDestroyService()], +}) +export class HasRoleDirective implements OnInit { + private destroy$ = injectDestroyService(); + private templateRef = inject(TemplateRef); + private viewContainer = inject(ViewContainerRef); + private store = inject(UserStore); + + @Input('hasRole') role: Role | Role[] | undefined = undefined; + + @Input('hasRoleIsAdmin') isAdmin = false; + + ngOnInit(): void { + if (this.isAdmin) { + this.store.isAdmin$ + .pipe(takeUntil(this.destroy$)) + .subscribe((isAdmin) => + isAdmin ? this.addTemplate() : this.clearTemplate() + ); + } else if (this.role) { + this.store + .hasAnyRole(this.role) + .pipe(takeUntil(this.destroy$)) + .subscribe((hasPermission) => + hasPermission ? this.addTemplate() : this.clearTemplate() + ); + } else { + this.addTemplate(); + } + } + + private addTemplate() { + this.viewContainer.clear(); + this.viewContainer.createEmbeddedView(this.templateRef); + } + + private clearTemplate() { + this.viewContainer.clear(); + } +} diff --git a/apps/permissions/src/app/directive/has-role.rx.v15.directive.ts b/apps/permissions/src/app/directive/has-role.rx.v15.directive.ts new file mode 100644 index 000000000..2ea5bdc74 --- /dev/null +++ b/apps/permissions/src/app/directive/has-role.rx.v15.directive.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @angular-eslint/directive-selector */ +import { NgIf } from '@angular/common'; +import { Directive, inject, Input } from '@angular/core'; +import { RxEffects } from '@rx-angular/state/effects'; +import { mergeMap, Observable, Subject } from 'rxjs'; +import { Role } from '../user.model'; +import { UserStore } from '../user.store'; + +@Directive({ + selector: '[hasRole], [hasRoleIsAdmin]', + standalone: true, + hostDirectives: [NgIf], + providers: [RxEffects], +}) +export class HasRoleDirective { + private store = inject(UserStore); + private rxEffect = inject(RxEffects); + private ngIf = inject(NgIf, { host: true }); + + private show = new Subject>(); + private show$ = this.show.asObservable().pipe(mergeMap((b) => b)); + + @Input('hasRole') set role(role: Role | Role[] | undefined) { + if (role) { + this.show.next(this.store.hasAnyRole(role)); + } + } + + @Input('hasRoleIsAdmin') set isAdmin(isAdmin: boolean) { + if (isAdmin) { + this.show.next(this.store.isAdmin$); + } + } + + constructor() { + this.rxEffect.register(this.show$, this.showTemplate); + } + + private showTemplate = (showTemplate: boolean | undefined) => + (this.ngIf.ngIf = showTemplate); +} diff --git a/apps/permissions/src/app/directive/has-role.rxjs.directive.ts b/apps/permissions/src/app/directive/has-role.rxjs.directive.ts new file mode 100644 index 000000000..b61e6e6f2 --- /dev/null +++ b/apps/permissions/src/app/directive/has-role.rxjs.directive.ts @@ -0,0 +1,65 @@ +/* eslint-disable @angular-eslint/directive-selector */ + +import { + injectDestroyService, + provideDestroyService, +} from '@angular-challenges/shared/utils'; +import { + Directive, + inject, + Input, + OnInit, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import { BehaviorSubject, mergeMap, Observable, of, takeUntil } from 'rxjs'; +import { Role } from '../user.model'; +import { UserStore } from '../user.store'; + +@Directive({ + selector: '[hasRole], [hasRoleIsAdmin]', + standalone: true, + providers: [provideDestroyService()], +}) +export class HasRoleDirective implements OnInit { + private destroy$ = injectDestroyService(); + private templateRef = inject(TemplateRef); + private viewContainer = inject(ViewContainerRef); + private store = inject(UserStore); + + private show = new BehaviorSubject>( + of(undefined) + ); + + @Input('hasRole') set role(role: Role | Role[] | undefined) { + if (role) { + this.show.next(this.store.hasAnyRole(role)); + } + } + + @Input('hasRoleIsAdmin') set isAdmin(isAdmin: boolean) { + if (isAdmin) { + this.show.next(this.store.isAdmin$); + } + } + + ngOnInit(): void { + this.show + .pipe( + mergeMap((s) => s), + takeUntil(this.destroy$) + ) + .subscribe((showTemplate) => + showTemplate ? this.addTemplate() : this.clearTemplate() + ); + } + + private addTemplate() { + this.viewContainer.clear(); + this.viewContainer.createEmbeddedView(this.templateRef); + } + + private clearTemplate() { + this.viewContainer.clear(); + } +} diff --git a/apps/permissions/src/app/directive/has-role.v15.directive.ts b/apps/permissions/src/app/directive/has-role.v15.directive.ts new file mode 100644 index 000000000..61e7409ec --- /dev/null +++ b/apps/permissions/src/app/directive/has-role.v15.directive.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @angular-eslint/directive-selector */ +import { NgIf } from '@angular/common'; +import { Directive, inject, Input } from '@angular/core'; +import { ComponentStore } from '@ngrx/component-store'; +import { pipe, tap } from 'rxjs'; +import { Role } from '../user.model'; +import { UserStore } from '../user.store'; + +@Directive({ + selector: '[hasRole], [hasRoleIsAdmin]', + standalone: true, + hostDirectives: [NgIf], + providers: [ComponentStore], +}) +export class HasRoleDirective { + private store = inject(UserStore); + private componentStore = inject(ComponentStore); + private ngIf = inject(NgIf, { host: true }); + + @Input('hasRole') set role(role: Role | Role[] | undefined) { + if (role) { + this.showTemplate(this.store.hasAnyRole(role)); + } + } + + @Input('hasRoleIsAdmin') set isAdmin(isAdmin: boolean) { + if (isAdmin) { + this.showTemplate(this.store.isAdmin$); + } + } + + private readonly showTemplate = this.componentStore.effect< + boolean | undefined + >(pipe(tap((showTemplate) => (this.ngIf.ngIf = showTemplate)))); +} diff --git a/apps/permissions/src/app/has-permission.guard.ts b/apps/permissions/src/app/has-permission.guard.ts new file mode 100644 index 000000000..5226f85b7 --- /dev/null +++ b/apps/permissions/src/app/has-permission.guard.ts @@ -0,0 +1,36 @@ +import { inject, Injectable } from '@angular/core'; +import { CanMatch, Route, Router, UrlTree } from '@angular/router'; +import { map, mergeMap, Observable, of } from 'rxjs'; +import { Role } from './user.model'; +import { UserStore } from './user.store'; + +@Injectable({ providedIn: 'root' }) +export class HasPermissionGuard implements CanMatch { + private router = inject(Router); + private userStore = inject(UserStore); + + canMatch(route: Route): Observable { + const accessRolesList: Role[] = route.data?.['roles'] ?? []; + const isAdmin: boolean = route.data?.['isAdmin'] ?? false; + return this.hasPermission$(isAdmin, accessRolesList); + } + + private hasPermission$(isAdmin: boolean, accessRolesList: Role[]) { + return this.userStore.isUserLoggedIn$.pipe( + mergeMap((hasUser) => { + if (hasUser) { + if (isAdmin) { + return this.userStore.isAdmin$.pipe(map(Boolean)); + } else if (accessRolesList.length > 0) { + return this.userStore + .hasAnyRole(accessRolesList) + .pipe(map(Boolean)); + } + return of(false); + } else { + return of(this.router.parseUrl('no-user')); + } + }) + ); + } +} diff --git a/apps/permissions/src/app/has-role.directive.ts b/apps/permissions/src/app/has-role.directive.ts deleted file mode 100644 index 807e8b604..000000000 --- a/apps/permissions/src/app/has-role.directive.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable @angular-eslint/directive-selector */ -import { NgIfContext } from '@angular/common'; -import { - Directive, - Input, - OnDestroy, - OnInit, - TemplateRef, - ViewContainerRef, -} from '@angular/core'; -import { Role } from './user.model'; -import { UserStore } from './user.store'; - -@Directive({ - selector: '[hasRole], [hasRoleIsAdmin]', - standalone: true, - providers: [provideDestroyService()], -}) -export class HasRoleDirective implements OnInit, OnDestroy { - @Input('hasRole') role: Role | Role[] | undefined = undefined; - - @Input('hasRoleIsAdmin') isAdmin = false; - - @Input('hasRoleIsAdminElseTemplate') - elseTemplate?: TemplateRef | null; - - constructor( - private templateRef: TemplateRef, - private viewContainer: ViewContainerRef, - private store: UserStore - ) {} - - ngOnDestroy(): void { - console.log('on destroy'); - } - - ngOnInit(): void { - console.log(this.role, this.isAdmin, this.elseTemplate); - if (this.isAdmin) { - this.store.isAdmin$.subscribe((isAdmin) => { - console.log(isAdmin); - isAdmin ? this.addTemplate() : this.addElseTemplate(); - }); - } - // else if (this.role) { - // this.store - // .hasAnyRole(this.role) - // .subscribe((hasPermission) => - // hasPermission ? this.addTemplate() : this.addElseTemplate() - // ); - // } else { - // this.addTemplate(); - // } - } - - private addTemplate() { - console.log('Add'); - this.viewContainer.clear(); - this.viewContainer.createEmbeddedView(this.templateRef); - } - - private addElseTemplate() { - console.log('ici'); - this.viewContainer.clear(); - this.elseTemplate && - this.viewContainer.createEmbeddedView(this.elseTemplate); - } -} diff --git a/apps/permissions/src/app/information.component.ts b/apps/permissions/src/app/information.component.ts index e4adeb1b9..605c9c210 100644 --- a/apps/permissions/src/app/information.component.ts +++ b/apps/permissions/src/app/information.component.ts @@ -1,24 +1,22 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { UserStore } from './user.store'; +import { HasRoleDirective } from './directive/has-role.rx.v15.directive'; @Component({ selector: 'app-information', standalone: true, - imports: [CommonModule], + imports: [CommonModule, HasRoleDirective], template: `

Information Panel

-
visible only for super admin
-
visible if manager
-
visible if manager and/or reader
-
visible if manager and/or writer
-
visible if client
+
visible only for super admin
+ +
visible if manager
+
visible if manager and/or reader
+
visible if manager and/or writer
+
visible if client
visible for everyone
`, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class InformationComponent { - user$ = this.userStore.user$; - constructor(private userStore: UserStore) {} -} +export class InformationComponent {} diff --git a/apps/permissions/src/app/login.component.ts b/apps/permissions/src/app/login.component.ts index ab27b11ec..5c7207a70 100644 --- a/apps/permissions/src/app/login.component.ts +++ b/apps/permissions/src/app/login.component.ts @@ -26,7 +26,7 @@ import { UserStore } from './user.store'; - + diff --git a/apps/permissions/src/app/routes.ts b/apps/permissions/src/app/routes.ts index 79a3eccfa..ee1229e10 100644 --- a/apps/permissions/src/app/routes.ts +++ b/apps/permissions/src/app/routes.ts @@ -1,14 +1,58 @@ -export const APP_ROUTES = [ +import { Route } from '@angular/router'; +import { HasPermissionGuard } from './has-permission.guard'; +import { Role } from './user.model'; + +interface TypedRoute extends Route { + data?: { + isAdmin?: boolean; + roles?: Role[]; + }; +} + +export const APP_ROUTES: TypedRoute[] = [ { path: '', loadComponent: () => import('./login.component').then((m) => m.LoginComponent), }, + { + path: 'no-user', + loadComponent: () => import('./dashboard/no-user.component'), + }, { path: 'enter', - loadComponent: () => - import('./dashboard/admin.component').then( - (m) => m.AdminDashboardComponent - ), + canMatch: [HasPermissionGuard], + data: { + isAdmin: true, + }, + loadComponent: () => import('./dashboard/admin.component'), + }, + { + path: 'enter', + canMatch: [HasPermissionGuard], + data: { + roles: ['MANAGER'], + }, + loadComponent: () => import('./dashboard/manager.component'), + }, + { + path: 'enter', + canMatch: [HasPermissionGuard], + data: { + roles: ['WRITER', 'READER'], + }, + loadComponent: () => import('./dashboard/writer-reader.component'), + }, + { + path: 'enter', + canMatch: [HasPermissionGuard], + data: { + roles: ['CLIENT'], + }, + loadComponent: () => import('./dashboard/client.component'), + }, + { + path: 'enter', + loadComponent: () => import('./dashboard/everyone.component'), }, ]; diff --git a/apps/permissions/src/app/user.store.ts b/apps/permissions/src/app/user.store.ts index 1b00288b7..7a5c6a98d 100644 --- a/apps/permissions/src/app/user.store.ts +++ b/apps/permissions/src/app/user.store.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; -import { User } from './user.model'; +import { BehaviorSubject, map } from 'rxjs'; +import { Role, User } from './user.model'; @Injectable({ providedIn: 'root', @@ -9,6 +9,19 @@ export class UserStore { private user = new BehaviorSubject(undefined); user$ = this.user.asObservable(); + isUserLoggedIn$ = this.user$.pipe(map(Boolean)); + isAdmin$ = this.user$.pipe(map((user) => user?.isAdmin)); + + hasAnyRole = (role: Role | Role[]) => + this.user$.pipe( + map((user) => { + if (user?.isAdmin) return true; + + const roles: Role[] = Array.isArray(role) ? role : [role]; + return roles.length === 0 || user?.roles.some((r) => roles.includes(r)); + }) + ); + add(user: User) { this.user.next(user); }