Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { RouterLink } from '@angular/router';
import { ButtonComponent } from '../button.component';

@Component({
selector: 'app-dashboard',
imports: [RouterLink, ButtonComponent],
template: `
<p>dashboard for Admin works!</p>
<button app-button routerLink="/">Logout</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { hostID: crypto.randomUUID().toString() },
})
export class AdminDashboardComponent {}
export default class AdminDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonComponent } from '../button.component';

@Component({
imports: [RouterLink, ButtonComponent],
template: `
<p>dashboard for Client works!</p>
<button app-button routerLink="/">Logout</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { hostID: crypto.randomUUID().toString() },
})
export default class ClientDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonComponent } from '../button.component';

@Component({
imports: [RouterLink, ButtonComponent],
template: `
<p>dashboard for Everyone works!</p>
<button app-button routerLink="/">Logout</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { hostID: crypto.randomUUID().toString() },
})
export default class EveryoneDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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: `
<p>dashboard for Manager works!</p>
<button app-button routerLink="/">Logout</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { hostID: crypto.randomUUID().toString() },
})
export class ManagerDashboardComponent {}
export default class ManagerDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonComponent } from '../button.component';

@Component({
imports: [RouterLink, ButtonComponent],
template: `
<p>dashboard for Reader/Writer works!</p>
<button app-button routerLink="/">Logout</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { hostID: crypto.randomUUID().toString() },
})
export default class ReaderWriterDashboardComponent {}
60 changes: 60 additions & 0 deletions apps/angular/6-structural-directive/src/app/hasRole.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
booleanAttribute,
computed,
Directive,
effect,
inject,
input,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { Role } from './user.model';
import { UserStore } from './user.store';

@Directive({
selector: '[hasRole], [hasRoleSuperAdmin]',
standalone: true,
})
export class HasRoleDirective {
private templateRef = inject(TemplateRef<unknown>);
private viewContainer = inject(ViewContainerRef);
private userStore = inject(UserStore);

role = input<Role | Role[] | null>(null, { alias: 'hasRole' });
isAdmin = input(false, {
alias: 'hasRoleSuperAdmin',
transform: booleanAttribute,
});

private hasAccess = computed(() => {
const isAdminFlag = this.isAdmin();
const role = normalizeRoles(this.role());

if (isAdminFlag) {
return this.userStore.isAdmin();
}

if (role) {
return this.userStore.hasAnyRole(role)();
}
return false;
});

private _effect = effect(() =>
this.hasAccess() ? this.addTemplate() : this.clearTemplate(),
);

private addTemplate() {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef);
}

private clearTemplate() {
this.viewContainer.clear();
}
}

function normalizeRoles(role: Role | Role[] | null): Role[] | null {
if (!role) return null;
return Array.isArray(role) ? role : [role];
}
8 changes: 8 additions & 0 deletions apps/angular/6-structural-directive/src/app/hasRole.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { inject } from '@angular/core';
import { Role } from './user.model';
import { UserStore } from './user.store';

export const hasRoleGuard = (role: Role[]) => {
const userStore = inject(UserStore);
return userStore.hasAnyRole(role)();
};
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { UserStore } from './user.store';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { HasRoleDirective } from './hasRole.directive';

@Component({
selector: 'app-information',
template: `
<h2 class="mt-10 text-xl">Information Panel</h2>
<!-- admin can see everything -->
<div>visible only for super admin</div>
<div>visible if manager</div>
<div>visible if manager and/or reader</div>
<div>visible if manager and/or writer</div>
<div>visible if client</div>
<div *hasRoleSuperAdmin="true">visible only for super admin</div>
<div *hasRole="'MANAGER'">visible if manager</div>
<div *hasRole="['MANAGER', 'READER']">visible if manager and/or reader</div>
<div *hasRole="['MANAGER', 'WRITER']">visible if manager and/or writer</div>
<div *hasRole="'CLIENT'">visible if client</div>
<div>visible for everyone</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [HasRoleDirective],
})
export class InformationComponent {
private readonly userStore = inject(UserStore);

user$ = this.userStore.user$;
}
export class InformationComponent {}
8 changes: 8 additions & 0 deletions apps/angular/6-structural-directive/src/app/isAdmin.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { inject } from '@angular/core';
import { CanMatchFn } from '@angular/router';
import { UserStore } from './user.store';

export const isAdminGuard: CanMatchFn = () => {
const userStore = inject(UserStore);
return userStore.isAdmin();
};
28 changes: 24 additions & 4 deletions apps/angular/6-structural-directive/src/app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { hasRoleGuard } from './hasRole.guard';
import { isAdminGuard } from './isAdmin.guard';

export const APP_ROUTES = [
{
path: '',
Expand All @@ -6,9 +9,26 @@ export const APP_ROUTES = [
},
{
path: 'enter',
loadComponent: () =>
import('./dashboard/admin.component').then(
(m) => m.AdminDashboardComponent,
),
canMatch: [isAdminGuard],
loadComponent: () => import('./dashboard/admin.component'),
},
{
path: 'enter',
canMatch: [() => hasRoleGuard(['MANAGER'])],
loadComponent: () => import('./dashboard/manager.component'),
},
{
path: 'enter',
canMatch: [() => hasRoleGuard(['CLIENT'])],
loadComponent: () => import('./dashboard/client.component'),
},
{
path: 'enter',
canMatch: [() => hasRoleGuard(['READER', 'WRITER'])],
loadComponent: () => import('./dashboard/reader-writer.component'),
},
{
path: 'enter',
loadComponent: () => import('./dashboard/everyone.component'),
},
];
22 changes: 16 additions & 6 deletions apps/angular/6-structural-directive/src/app/user.store.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { User } from './user.model';
import { computed, Injectable, signal } from '@angular/core';
import { Role, User } from './user.model';

@Injectable({
providedIn: 'root',
})
export class UserStore {
private user = new BehaviorSubject<User | undefined>(undefined);
user$ = this.user.asObservable();
private _user = signal<User | undefined>(undefined);
readonly user = this._user.asReadonly();

add(user: User) {
this.user.next(user);
this._user.set(user);
}

isAdmin = computed(() => !!this.user()?.isAdmin);

hasAnyRole = (role: Role | Role[]) => {
return computed(() => {
const user = this.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));
});
};
}