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 @@ -7,7 +7,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
<ng-content />
`,
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,
})
Expand Down
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({
selector: 'app-client-dashboard',
imports: [RouterLink, ButtonComponent],
template: `
<p>dashboard for Client works!</p>
<button app-button routerLink="/">Logout</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClientDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -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: `
<p>dashboard for Manager works!</p>
<button app-button routerLink="/">Logout</button>
Expand Down
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({
selector: 'app-reader-dashboard',
imports: [RouterLink, ButtonComponent],
template: `
<p>dashboard for Reader works!</p>
<button app-button routerLink="/">Logout</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReaderDashboardComponent {}
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({
selector: 'app-writer-dashboard',
imports: [RouterLink, ButtonComponent],
template: `
<p>dashboard for Writer works!</p>
<button app-button routerLink="/">Logout</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WriterDashboardComponent {}
70 changes: 70 additions & 0 deletions apps/angular/6-structural-directive/src/app/has-role.directive.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>);
private readonly vcRef = inject(ViewContainerRef);
private readonly userStore = inject(UserStore);

hasRole = input<Role[], Role | Role[]>([], {
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<unknown>);
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);
}
}
}
18 changes: 18 additions & 0 deletions apps/angular/6-structural-directive/src/app/has-role.guard.ts
Original file line number Diff line number Diff line change
@@ -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);
};
};
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, IsSuperAdminDirective } from './has-role.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 *isSuperAdmin>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, IsSuperAdminDirective],
})
export class InformationComponent {
private readonly userStore = inject(UserStore);

user$ = this.userStore.user$;
}
export class InformationComponent {}
49 changes: 13 additions & 36 deletions apps/angular/6-structural-directive/src/app/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -19,13 +11,15 @@ import { UserStore } from './user.store';
template: `
<header class="flex items-center gap-3">
Log as :
<button app-button (click)="admin()">Admin</button>
<button app-button (click)="manager()">Manager</button>
<button app-button (click)="reader()">Reader</button>
<button app-button (click)="writer()">Writer</button>
<button app-button (click)="readerWriter()">Reader and Writer</button>
<button app-button (click)="client()">Client</button>
<button app-button (click)="everyone()">Everyone</button>
<button app-button (click)="loginAs('admin')">Admin</button>
<button app-button (click)="loginAs('manager')">Manager</button>
<button app-button (click)="loginAs('reader')">Reader</button>
<button app-button (click)="loginAs('writer')">Writer</button>
<button app-button (click)="loginAs('readerAndWriter')">
Reader and Writer
</button>
<button app-button (click)="loginAs('client')">Client</button>
<button app-button (click)="loginAs('everyone')">Everyone</button>
</header>

<app-information />
Expand All @@ -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);
}
}
38 changes: 38 additions & 0 deletions apps/angular/6-structural-directive/src/app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { hasRoleGuard, isAdminGuard } from './has-role.guard';

export const APP_ROUTES = [
{
path: '',
Expand All @@ -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,
),
},
];
31 changes: 31 additions & 0 deletions apps/angular/6-structural-directive/src/app/user-role.service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
16 changes: 14 additions & 2 deletions apps/angular/6-structural-directive/src/app/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const reader: User = {
};

export const readerAndWriter: User = {
name: 'reader',
name: 'reader and writer',
isAdmin: false,
roles: ['READER', 'WRITER'],
};
Expand All @@ -43,7 +43,19 @@ export const client: User = {
};

export const everyone: User = {
name: 'client',
name: 'everyone',
isAdmin: false,
roles: [],
};

export const USERS_MAP: Record<string, User> = {
admin,
manager,
reader,
writer,
readerAndWriter,
client,
everyone,
};

export type UserType = keyof typeof USERS_MAP;
14 changes: 8 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,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<User | undefined>(undefined);
user$ = this.user.asObservable();
private _user = signal<User | undefined>(undefined);

add(user: User) {
this.user.next(user);
getUser() {
return this._user.asReadonly();
}

setUser(user: User): void {
this._user.set(user);
}
}
Loading