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);
}