diff --git a/apps/angular/6-structural-directive/src/app/button.component.ts b/apps/angular/6-structural-directive/src/app/button.component.ts
index 5d1323605..8bad2cf46 100644
--- a/apps/angular/6-structural-directive/src/app/button.component.ts
+++ b/apps/angular/6-structural-directive/src/app/button.component.ts
@@ -7,7 +7,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
`,
host: {
- class: 'border border-blue-700 bg-blue-400 p-2 rounded-sm text-white',
+ class:
+ 'rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 cursor-pointer',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
diff --git a/apps/angular/6-structural-directive/src/app/dashboard/client.component.ts b/apps/angular/6-structural-directive/src/app/dashboard/client.component.ts
new file mode 100644
index 000000000..d78f8d6e1
--- /dev/null
+++ b/apps/angular/6-structural-directive/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({
+ selector: 'app-client-dashboard',
+ imports: [RouterLink, ButtonComponent],
+ template: `
+
dashboard for Client works!
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ClientDashboardComponent {}
diff --git a/apps/angular/6-structural-directive/src/app/dashboard/manager.component.ts b/apps/angular/6-structural-directive/src/app/dashboard/manager.component.ts
index 60ea7695b..4bb0977be 100644
--- a/apps/angular/6-structural-directive/src/app/dashboard/manager.component.ts
+++ b/apps/angular/6-structural-directive/src/app/dashboard/manager.component.ts
@@ -1,8 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { ButtonComponent } from '../button.component';
@Component({
selector: 'app-dashboard',
- imports: [],
+ imports: [RouterLink, ButtonComponent],
template: `
dashboard for Manager works!
diff --git a/apps/angular/6-structural-directive/src/app/dashboard/reader.component.ts b/apps/angular/6-structural-directive/src/app/dashboard/reader.component.ts
new file mode 100644
index 000000000..90a054b6f
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/dashboard/reader.component.ts
@@ -0,0 +1,14 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { ButtonComponent } from '../button.component';
+
+@Component({
+ selector: 'app-reader-dashboard',
+ imports: [RouterLink, ButtonComponent],
+ template: `
+ dashboard for Reader works!
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ReaderDashboardComponent {}
diff --git a/apps/angular/6-structural-directive/src/app/dashboard/writer.component.ts b/apps/angular/6-structural-directive/src/app/dashboard/writer.component.ts
new file mode 100644
index 000000000..ef47efa92
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/dashboard/writer.component.ts
@@ -0,0 +1,14 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { ButtonComponent } from '../button.component';
+
+@Component({
+ selector: 'app-writer-dashboard',
+ imports: [RouterLink, ButtonComponent],
+ template: `
+ dashboard for Writer works!
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class WriterDashboardComponent {}
diff --git a/apps/angular/6-structural-directive/src/app/has-role.directive.ts b/apps/angular/6-structural-directive/src/app/has-role.directive.ts
new file mode 100644
index 000000000..9cfc8c504
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/has-role.directive.ts
@@ -0,0 +1,70 @@
+import {
+ Directive,
+ effect,
+ inject,
+ input,
+ TemplateRef,
+ ViewContainerRef,
+} from '@angular/core';
+import { Role } from './user.model';
+import { UserStore } from './user.store';
+
+@Directive({ selector: '[hasRole]' })
+export class HasRoleDirective {
+ private readonly templateRef = inject(TemplateRef);
+ private readonly vcRef = inject(ViewContainerRef);
+ private readonly userStore = inject(UserStore);
+
+ hasRole = input([], {
+ alias: 'hasRole',
+ transform: (v: Role | Role[]) => {
+ return typeof v === 'string' ? [v] : v;
+ },
+ });
+
+ effect = effect(() => {
+ const user = this.userStore.getUser();
+ const userRoles = user()?.roles ?? [];
+ const requiredRoles = this.hasRole() ?? [];
+
+ this.updateView(userRoles, requiredRoles);
+ });
+
+ private updateView(userRoles: Role[], requiredRoles: Role[]) {
+ this.vcRef.clear();
+
+ if (requiredRoles.length === 0) {
+ this.vcRef.createEmbeddedView(this.templateRef);
+ return;
+ }
+
+ const canAccess = requiredRoles.some((role) =>
+ userRoles.includes(role as any),
+ );
+
+ if (canAccess) {
+ this.vcRef.createEmbeddedView(this.templateRef);
+ }
+ }
+}
+
+@Directive({ selector: '[isSuperAdmin]' })
+export class IsSuperAdminDirective {
+ private readonly templateRef = inject(TemplateRef);
+ private readonly vcRef = inject(ViewContainerRef);
+ private readonly userStore = inject(UserStore);
+
+ effect = effect(() => {
+ const user = this.userStore.getUser();
+
+ this.updateView(user()?.isAdmin);
+ });
+
+ private updateView(isAdminUser: boolean | undefined) {
+ this.vcRef.clear();
+
+ if (isAdminUser) {
+ this.vcRef.createEmbeddedView(this.templateRef);
+ }
+ }
+}
diff --git a/apps/angular/6-structural-directive/src/app/has-role.guard.ts b/apps/angular/6-structural-directive/src/app/has-role.guard.ts
new file mode 100644
index 000000000..abdd749f7
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/has-role.guard.ts
@@ -0,0 +1,18 @@
+import { inject } from '@angular/core';
+import { CanMatchFn } from '@angular/router';
+import { UserRoleService } from './user-role.service';
+import { Role } from './user.model';
+
+export const isAdminGuard: CanMatchFn = () => {
+ const userRoleService = inject(UserRoleService);
+
+ return userRoleService.isSuperAdminUser();
+};
+
+export const hasRoleGuard = (roles: Role[]): CanMatchFn => {
+ return () => {
+ const userRoleService = inject(UserRoleService);
+
+ return userRoleService.hasAnyRole(roles);
+ };
+};
diff --git a/apps/angular/6-structural-directive/src/app/information.component.ts b/apps/angular/6-structural-directive/src/app/information.component.ts
index ecf937efc..e1ac22b6b 100644
--- a/apps/angular/6-structural-directive/src/app/information.component.ts
+++ b/apps/angular/6-structural-directive/src/app/information.component.ts
@@ -1,22 +1,19 @@
-import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
-import { UserStore } from './user.store';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { HasRoleDirective, IsSuperAdminDirective } from './has-role.directive';
@Component({
selector: 'app-information',
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,
+ imports: [HasRoleDirective, IsSuperAdminDirective],
})
-export class InformationComponent {
- private readonly userStore = inject(UserStore);
-
- user$ = this.userStore.user$;
-}
+export class InformationComponent {}
diff --git a/apps/angular/6-structural-directive/src/app/login.component.ts b/apps/angular/6-structural-directive/src/app/login.component.ts
index f38e5e5ca..996ef7535 100644
--- a/apps/angular/6-structural-directive/src/app/login.component.ts
+++ b/apps/angular/6-structural-directive/src/app/login.component.ts
@@ -2,15 +2,7 @@ import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonComponent } from './button.component';
import { InformationComponent } from './information.component';
-import {
- admin,
- client,
- everyone,
- manager,
- reader,
- readerAndWriter,
- writer,
-} from './user.model';
+import { USERS_MAP, UserType } from './user.model';
import { UserStore } from './user.store';
@Component({
@@ -19,13 +11,15 @@ import { UserStore } from './user.store';
template: `
Log as :
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -38,25 +32,8 @@ import { UserStore } from './user.store';
export class LoginComponent {
private readonly userStore = inject(UserStore);
- admin() {
- this.userStore.add(admin);
- }
- manager() {
- this.userStore.add(manager);
- }
- reader() {
- this.userStore.add(reader);
- }
- writer() {
- this.userStore.add(writer);
- }
- readerWriter() {
- this.userStore.add(readerAndWriter);
- }
- client() {
- this.userStore.add(client);
- }
- everyone() {
- this.userStore.add(everyone);
+ loginAs(userType: UserType) {
+ const user = USERS_MAP[userType];
+ this.userStore.setUser(user);
}
}
diff --git a/apps/angular/6-structural-directive/src/app/routes.ts b/apps/angular/6-structural-directive/src/app/routes.ts
index 4db203f3b..f20aa1ba4 100644
--- a/apps/angular/6-structural-directive/src/app/routes.ts
+++ b/apps/angular/6-structural-directive/src/app/routes.ts
@@ -1,3 +1,5 @@
+import { hasRoleGuard, isAdminGuard } from './has-role.guard';
+
export const APP_ROUTES = [
{
path: '',
@@ -6,9 +8,45 @@ export const APP_ROUTES = [
},
{
path: 'enter',
+ canMatch: [isAdminGuard],
loadComponent: () =>
import('./dashboard/admin.component').then(
(m) => m.AdminDashboardComponent,
),
},
+ {
+ path: 'enter',
+ canMatch: [hasRoleGuard(['MANAGER'])],
+ data: { role: 'MANAGER' },
+ loadComponent: () =>
+ import('./dashboard/manager.component').then(
+ (m) => m.ManagerDashboardComponent,
+ ),
+ },
+ {
+ path: 'enter',
+ canMatch: [hasRoleGuard(['READER'])],
+ data: { role: 'READER' },
+ loadComponent: () =>
+ import('./dashboard/reader.component').then(
+ (m) => m.ReaderDashboardComponent,
+ ),
+ },
+ {
+ path: 'enter',
+ canMatch: [hasRoleGuard(['WRITER'])],
+ data: { role: 'WRITER' },
+ loadComponent: () =>
+ import('./dashboard/writer.component').then(
+ (m) => m.WriterDashboardComponent,
+ ),
+ },
+ {
+ path: 'enter',
+ canMatch: [hasRoleGuard(['CLIENT'])],
+ loadComponent: () =>
+ import('./dashboard/client.component').then(
+ (m) => m.ClientDashboardComponent,
+ ),
+ },
];
diff --git a/apps/angular/6-structural-directive/src/app/user-role.service.ts b/apps/angular/6-structural-directive/src/app/user-role.service.ts
new file mode 100644
index 000000000..32ea50fcf
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/user-role.service.ts
@@ -0,0 +1,31 @@
+import { inject, Injectable } from '@angular/core';
+import { Role } from './user.model';
+import { UserStore } from './user.store';
+
+@Injectable({ providedIn: 'root' })
+export class UserRoleService {
+ private readonly userStore = inject(UserStore);
+
+ hasAnyRole(roles: Role[]): boolean {
+ const user = this.userStore.getUser();
+ const userRoles = user()?.roles ?? [];
+
+ return roles.some((role) => userRoles.includes(role));
+ }
+
+ hasAllRoles(roles: Role[]): boolean {
+ const user = this.userStore.getUser();
+ const userRoles = user()?.roles ?? [];
+
+ return roles.every((role) => userRoles.includes(role));
+ }
+
+ hasRole(role: Role): boolean {
+ return this.hasAnyRole([role]);
+ }
+
+ isSuperAdminUser(): boolean {
+ const user = this.userStore.getUser();
+ return user()?.isAdmin ?? false;
+ }
+}
diff --git a/apps/angular/6-structural-directive/src/app/user.model.ts b/apps/angular/6-structural-directive/src/app/user.model.ts
index 353a5e214..07d7eb508 100644
--- a/apps/angular/6-structural-directive/src/app/user.model.ts
+++ b/apps/angular/6-structural-directive/src/app/user.model.ts
@@ -31,7 +31,7 @@ export const reader: User = {
};
export const readerAndWriter: User = {
- name: 'reader',
+ name: 'reader and writer',
isAdmin: false,
roles: ['READER', 'WRITER'],
};
@@ -43,7 +43,19 @@ export const client: User = {
};
export const everyone: User = {
- name: 'client',
+ name: 'everyone',
isAdmin: false,
roles: [],
};
+
+export const USERS_MAP: Record = {
+ admin,
+ manager,
+ reader,
+ writer,
+ readerAndWriter,
+ client,
+ everyone,
+};
+
+export type UserType = keyof typeof USERS_MAP;
diff --git a/apps/angular/6-structural-directive/src/app/user.store.ts b/apps/angular/6-structural-directive/src/app/user.store.ts
index 1b00288b7..7ba6ac1b5 100644
--- a/apps/angular/6-structural-directive/src/app/user.store.ts
+++ b/apps/angular/6-structural-directive/src/app/user.store.ts
@@ -1,15 +1,17 @@
-import { Injectable } from '@angular/core';
-import { BehaviorSubject } from 'rxjs';
+import { Injectable, signal } from '@angular/core';
import { User } from './user.model';
@Injectable({
providedIn: 'root',
})
export class UserStore {
- private user = new BehaviorSubject(undefined);
- user$ = this.user.asObservable();
+ private _user = signal(undefined);
- add(user: User) {
- this.user.next(user);
+ getUser() {
+ return this._user.asReadonly();
+ }
+
+ setUser(user: User): void {
+ this._user.set(user);
}
}