Angular
While role-baker doesn’t provide specific utilities for frameworks like Angular, it delivers the most important functionality through the hasPermission
function. This function checks whether a user has the necessary permissions for a particular resource and action, and it can be easily integrated into various frameworks.
In Angular, we can leverage this function by creating custom utilities like directives and guards. These utilities enable developers to implement permission-based rendering and navigation, ensuring that users can only access certain content or routes based on their roles.
1. Permission Directive for Access Control
A directive in Angular allows you to conditionally render a portion of your template based on the user’s permissions. The directive can be used to show or hide parts of the page, making it very flexible.
Here’s how we can create a directive to check permissions in an Angular component:
import {
Directive,
Input,
OnDestroy,
OnInit,
TemplateRef,
ViewContainerRef,
} from "@angular/core";
import { Store } from "@ngrx/store";
import { AppState } from "../../app-state/app-reducers/app.reducers";
import { Subscription } from "rxjs";
import { AuthSelectors } from "../user/auth-state/selectors";
import { hasPermission, MyResourceConfig } from ".";
interface PermissionDirectiveInput<
R extends keyof MyResourceConfig["resources"]
> {
resource: R;
action: MyResourceConfig["resources"][R]["action"];
data?: MyResourceConfig["resources"][R]["dataType"];
}
@Directive({
selector: "[checkPermission]",
})
export class PermissionRolesDirective<
Resource extends keyof MyResourceConfig["resources"]
> implements OnInit, OnDestroy
{
@Input() checkPermissionInput!: PermissionDirectiveInput<Resource>;
private userSubscription!: Subscription;
private hasView = false;
constructor(
private readonly store: Store<AppState>,
private readonly templateRef: TemplateRef<
PermissionRolesDirective<Resource>
>,
private readonly viewContainerRef: ViewContainerRef
) {}
ngOnInit(): void {
this.userSubscription = this.store
.select(AuthSelectors.selectUser)
.subscribe((user) => {
if (!user) {
this.clearView();
return;
}
const { resource, action, data } = this.checkPermissionInput;
const permissionGranted = hasPermission(user, resource, action, data);
if (permissionGranted && !this.hasView) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (!permissionGranted && this.hasView) {
this.clearView();
}
});
}
private clearView(): void {
this.viewContainerRef.clear();
this.hasView = false;
}
ngOnDestroy(): void {
this.userSubscription.unsubscribe();
}
}
Explanation:
- Directive Setup: This directive listens to changes in the user’s roles and checks if the user has the appropriate permission for the specified resource and action.
hasPermission
Check: It uses thehasPermission
function from role-baker to determine whether the current user should be able to access the content represented by the directive.- View Management: The directive either creates or clears the view depending on whether the user has permission.
Example Usage in a Template:
<div
*checkPermission="{
resource: 'todos',
action: 'read',
data: todoData
}"
>
<!-- This content will only be shown if the user has permission to read todos -->
<p>{{ todoData.title }}</p>
</div>
2. Permission Guard for Route Protection
A guard ensures that a user cannot navigate to a route unless they have the required permissions. This is especially useful for protecting routes based on user roles.
Here’s how we can create a route guard using hasPermission
:
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { filter, map, Observable } from "rxjs";
import { Store } from "@ngrx/store";
import { AppState } from "../../app-state/app-reducers/app.reducers";
import { AuthSelectors } from "../user/auth-state/selectors";
import { hasPermission, MyResourceConfig } from "../access-controll"; // adjust the path for your project
export function checkPermissionGuard<
R extends keyof MyResourceConfig["resources"]
>(
resource: R,
action: MyResourceConfig["resources"][R]["action"],
data?: MyResourceConfig["resources"][R]["dataType"]
): CanActivateFn {
return (): Observable<boolean> => {
const store = inject(Store<AppState>);
const router = inject(Router);
return store.select(AuthSelectors.selectLoggedUser).pipe(
filter((user) => !!user),
map((user) => {
const hasAccess = hasPermission(user, resource, action, data);
if (!hasAccess) {
router.navigate(["403"]); // Redirect to a 403 error page if permission is denied
return false;
}
return true;
})
);
};
}
Explanation:
- Guard Setup: This guard function checks the user’s permissions before allowing navigation to a route. It ensures that the user has the necessary access rights.
hasPermission
Check: The guard uses thehasPermission
function to evaluate whether the user has the permission required for a specific resource and action.- Navigation on Failure: If the user does not have the required permission, they are redirected to a 403 Forbidden page.
Example Usage in Routes:
import { Routes } from "@angular/router";
import { checkPermissionGuard } from "./guards/permission.guard";
const routes: Routes = [
{
path: "todo",
component: TodoComponent,
canActivate: [checkPermissionGuard("todos", "read")],
},
];