Skip to content

Commit c0c0cf6

Browse files
committed
feat(challenge 6): implement role-based access control with directives and guards
1 parent 543770b commit c0c0cf6

13 files changed

Lines changed: 248 additions & 58 deletions

apps/angular/6-structural-directive/src/app/button.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
77
<ng-content />
88
`,
99
host: {
10-
class: 'border border-blue-700 bg-blue-400 p-2 rounded-sm text-white',
10+
class:
11+
'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',
1112
},
1213
changeDetection: ChangeDetectionStrategy.OnPush,
1314
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { RouterLink } from '@angular/router';
3+
import { ButtonComponent } from '../button.component';
4+
5+
@Component({
6+
selector: 'app-client-dashboard',
7+
imports: [RouterLink, ButtonComponent],
8+
template: `
9+
<p>dashboard for Client works!</p>
10+
<button app-button routerLink="/">Logout</button>
11+
`,
12+
changeDetection: ChangeDetectionStrategy.OnPush,
13+
})
14+
export class ClientDashboardComponent {}

apps/angular/6-structural-directive/src/app/dashboard/manager.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { RouterLink } from '@angular/router';
3+
import { ButtonComponent } from '../button.component';
24

35
@Component({
46
selector: 'app-dashboard',
5-
imports: [],
7+
imports: [RouterLink, ButtonComponent],
68
template: `
79
<p>dashboard for Manager works!</p>
810
<button app-button routerLink="/">Logout</button>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { RouterLink } from '@angular/router';
3+
import { ButtonComponent } from '../button.component';
4+
5+
@Component({
6+
selector: 'app-reader-dashboard',
7+
imports: [RouterLink, ButtonComponent],
8+
template: `
9+
<p>dashboard for Reader works!</p>
10+
<button app-button routerLink="/">Logout</button>
11+
`,
12+
changeDetection: ChangeDetectionStrategy.OnPush,
13+
})
14+
export class ReaderDashboardComponent {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { RouterLink } from '@angular/router';
3+
import { ButtonComponent } from '../button.component';
4+
5+
@Component({
6+
selector: 'app-writer-dashboard',
7+
imports: [RouterLink, ButtonComponent],
8+
template: `
9+
<p>dashboard for Writer works!</p>
10+
<button app-button routerLink="/">Logout</button>
11+
`,
12+
changeDetection: ChangeDetectionStrategy.OnPush,
13+
})
14+
export class WriterDashboardComponent {}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
Directive,
3+
effect,
4+
inject,
5+
input,
6+
TemplateRef,
7+
ViewContainerRef,
8+
} from '@angular/core';
9+
import { Role } from './user.model';
10+
import { UserStore } from './user.store';
11+
12+
@Directive({ selector: '[hasRole]' })
13+
export class HasRoleDirective {
14+
private readonly templateRef = inject(TemplateRef<unknown>);
15+
private readonly vcRef = inject(ViewContainerRef);
16+
private readonly userStore = inject(UserStore);
17+
18+
hasRole = input<Role[], Role | Role[]>([], {
19+
alias: 'hasRole',
20+
transform: (v: Role | Role[]) => {
21+
return typeof v === 'string' ? [v] : v;
22+
},
23+
});
24+
25+
effect = effect(() => {
26+
const user = this.userStore.getUser();
27+
const userRoles = user()?.roles ?? [];
28+
const requiredRoles = this.hasRole() ?? [];
29+
30+
this.updateView(userRoles, requiredRoles);
31+
});
32+
33+
private updateView(userRoles: Role[], requiredRoles: Role[]) {
34+
this.vcRef.clear();
35+
36+
if (requiredRoles.length === 0) {
37+
this.vcRef.createEmbeddedView(this.templateRef);
38+
return;
39+
}
40+
41+
const canAccess = requiredRoles.some((role) =>
42+
userRoles.includes(role as any),
43+
);
44+
45+
if (canAccess) {
46+
this.vcRef.createEmbeddedView(this.templateRef);
47+
}
48+
}
49+
}
50+
51+
@Directive({ selector: '[isSuperAdmin]' })
52+
export class IsSuperAdminDirective {
53+
private readonly templateRef = inject(TemplateRef<unknown>);
54+
private readonly vcRef = inject(ViewContainerRef);
55+
private readonly userStore = inject(UserStore);
56+
57+
effect = effect(() => {
58+
const user = this.userStore.getUser();
59+
60+
this.updateView(user()?.isAdmin);
61+
});
62+
63+
private updateView(isAdminUser: boolean | undefined) {
64+
this.vcRef.clear();
65+
66+
if (isAdminUser) {
67+
this.vcRef.createEmbeddedView(this.templateRef);
68+
}
69+
}
70+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { inject } from '@angular/core';
2+
import { CanMatchFn } from '@angular/router';
3+
import { UserRoleService } from './user-role.service';
4+
import { Role } from './user.model';
5+
6+
export const isAdminGuard: CanMatchFn = () => {
7+
const userRoleService = inject(UserRoleService);
8+
9+
return userRoleService.isSuperAdminUser();
10+
};
11+
12+
export const hasRoleGuard = (roles: Role[]): CanMatchFn => {
13+
return () => {
14+
const userRoleService = inject(UserRoleService);
15+
16+
return userRoleService.hasAnyRole(roles);
17+
};
18+
};
Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1-
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
2-
import { UserStore } from './user.store';
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { HasRoleDirective, IsSuperAdminDirective } from './has-role.directive';
33

44
@Component({
55
selector: 'app-information',
66
template: `
77
<h2 class="mt-10 text-xl">Information Panel</h2>
88
<!-- admin can see everything -->
9-
<div>visible only for super admin</div>
10-
<div>visible if manager</div>
11-
<div>visible if manager and/or reader</div>
12-
<div>visible if manager and/or writer</div>
13-
<div>visible if client</div>
9+
<div *isSuperAdmin>visible only for super admin</div>
10+
<div *hasRole="'MANAGER'">visible if manager</div>
11+
<div *hasRole="['MANAGER', 'READER']">visible if manager and/or reader</div>
12+
<div *hasRole="['MANAGER', 'WRITER']">visible if manager and/or writer</div>
13+
<div *hasRole="'CLIENT'">visible if client</div>
1414
<div>visible for everyone</div>
1515
`,
1616
changeDetection: ChangeDetectionStrategy.OnPush,
17+
imports: [HasRoleDirective, IsSuperAdminDirective],
1718
})
18-
export class InformationComponent {
19-
private readonly userStore = inject(UserStore);
20-
21-
user$ = this.userStore.user$;
22-
}
19+
export class InformationComponent {}

apps/angular/6-structural-directive/src/app/login.component.ts

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,7 @@ import { Component, inject } from '@angular/core';
22
import { RouterLink } from '@angular/router';
33
import { ButtonComponent } from './button.component';
44
import { InformationComponent } from './information.component';
5-
import {
6-
admin,
7-
client,
8-
everyone,
9-
manager,
10-
reader,
11-
readerAndWriter,
12-
writer,
13-
} from './user.model';
5+
import { USERS_MAP, UserType } from './user.model';
146
import { UserStore } from './user.store';
157

168
@Component({
@@ -19,13 +11,15 @@ import { UserStore } from './user.store';
1911
template: `
2012
<header class="flex items-center gap-3">
2113
Log as :
22-
<button app-button (click)="admin()">Admin</button>
23-
<button app-button (click)="manager()">Manager</button>
24-
<button app-button (click)="reader()">Reader</button>
25-
<button app-button (click)="writer()">Writer</button>
26-
<button app-button (click)="readerWriter()">Reader and Writer</button>
27-
<button app-button (click)="client()">Client</button>
28-
<button app-button (click)="everyone()">Everyone</button>
14+
<button app-button (click)="loginAs('admin')">Admin</button>
15+
<button app-button (click)="loginAs('manager')">Manager</button>
16+
<button app-button (click)="loginAs('reader')">Reader</button>
17+
<button app-button (click)="loginAs('writer')">Writer</button>
18+
<button app-button (click)="loginAs('readerAndWriter')">
19+
Reader and Writer
20+
</button>
21+
<button app-button (click)="loginAs('client')">Client</button>
22+
<button app-button (click)="loginAs('everyone')">Everyone</button>
2923
</header>
3024
3125
<app-information />
@@ -38,25 +32,8 @@ import { UserStore } from './user.store';
3832
export class LoginComponent {
3933
private readonly userStore = inject(UserStore);
4034

41-
admin() {
42-
this.userStore.add(admin);
43-
}
44-
manager() {
45-
this.userStore.add(manager);
46-
}
47-
reader() {
48-
this.userStore.add(reader);
49-
}
50-
writer() {
51-
this.userStore.add(writer);
52-
}
53-
readerWriter() {
54-
this.userStore.add(readerAndWriter);
55-
}
56-
client() {
57-
this.userStore.add(client);
58-
}
59-
everyone() {
60-
this.userStore.add(everyone);
35+
loginAs(userType: UserType) {
36+
const user = USERS_MAP[userType];
37+
this.userStore.setUser(user);
6138
}
6239
}

apps/angular/6-structural-directive/src/app/routes.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { hasRoleGuard, isAdminGuard } from './has-role.guard';
2+
13
export const APP_ROUTES = [
24
{
35
path: '',
@@ -6,9 +8,45 @@ export const APP_ROUTES = [
68
},
79
{
810
path: 'enter',
11+
canMatch: [isAdminGuard],
912
loadComponent: () =>
1013
import('./dashboard/admin.component').then(
1114
(m) => m.AdminDashboardComponent,
1215
),
1316
},
17+
{
18+
path: 'enter',
19+
canMatch: [hasRoleGuard(['MANAGER'])],
20+
data: { role: 'MANAGER' },
21+
loadComponent: () =>
22+
import('./dashboard/manager.component').then(
23+
(m) => m.ManagerDashboardComponent,
24+
),
25+
},
26+
{
27+
path: 'enter',
28+
canMatch: [hasRoleGuard(['READER'])],
29+
data: { role: 'READER' },
30+
loadComponent: () =>
31+
import('./dashboard/reader.component').then(
32+
(m) => m.ReaderDashboardComponent,
33+
),
34+
},
35+
{
36+
path: 'enter',
37+
canMatch: [hasRoleGuard(['WRITER'])],
38+
data: { role: 'WRITER' },
39+
loadComponent: () =>
40+
import('./dashboard/writer.component').then(
41+
(m) => m.WriterDashboardComponent,
42+
),
43+
},
44+
{
45+
path: 'enter',
46+
canMatch: [hasRoleGuard(['CLIENT'])],
47+
loadComponent: () =>
48+
import('./dashboard/client.component').then(
49+
(m) => m.ClientDashboardComponent,
50+
),
51+
},
1452
];

0 commit comments

Comments
 (0)