Skip to content

Commit 3eec8c5

Browse files
committed
feat: route guards
1 parent 4605cbd commit 3eec8c5

File tree

7 files changed

+935
-0
lines changed

7 files changed

+935
-0
lines changed

docs/ROUTE_GUARDS.md

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# Route Guards
2+
3+
Route guards for integrating ng-ability permissions with Angular routing.
4+
5+
## Available Guards
6+
7+
- **`canActivateAbility`** - Guard for route activation (`canActivate`)
8+
- **`canActivateChildAbility`** - Guard for child route activation (`canActivateChild`)
9+
- **`canMatchAbility`** - Guard for route matching (`canMatch`)
10+
11+
## Basic Usage
12+
13+
### canActivate - Route Activation
14+
15+
Protects individual routes from unauthorized access:
16+
17+
```typescript
18+
import { Routes } from '@angular/router';
19+
import { canActivateAbility } from 'ng-ability';
20+
21+
const routes: Routes = [
22+
{
23+
path: 'articles',
24+
component: ArticlesComponent,
25+
canActivate: [canActivateAbility('Article', 'view')]
26+
},
27+
{
28+
path: 'articles/create',
29+
component: CreateArticleComponent,
30+
canActivate: [canActivateAbility('Article', 'create')]
31+
},
32+
{
33+
path: 'articles/:id/edit',
34+
component: EditArticleComponent,
35+
canActivate: [canActivateAbility('Article', 'edit')]
36+
}
37+
];
38+
```
39+
40+
### canActivateChild - Child Routes
41+
42+
Protects all child routes with a single guard:
43+
44+
```typescript
45+
import { canActivateChildAbility } from 'ng-ability';
46+
47+
const routes: Routes = [
48+
{
49+
path: 'admin',
50+
component: AdminLayoutComponent,
51+
canActivateChild: [canActivateChildAbility('Admin', 'access')],
52+
children: [
53+
{ path: 'users', component: UsersComponent },
54+
{ path: 'settings', component: SettingsComponent },
55+
{ path: 'reports', component: ReportsComponent }
56+
]
57+
}
58+
];
59+
```
60+
61+
### canMatch - Route Matching
62+
63+
Prevents routes from matching entirely based on permissions. Runs before route parameters are extracted:
64+
65+
```typescript
66+
import { canMatchAbility } from 'ng-ability';
67+
68+
const routes: Routes = [
69+
{
70+
path: 'admin',
71+
component: AdminDashboardComponent,
72+
canMatch: [canMatchAbility('Admin', 'access')]
73+
},
74+
{
75+
// Fallback route if user doesn't have admin access
76+
path: 'admin',
77+
component: AccessDeniedComponent
78+
}
79+
];
80+
```
81+
82+
## Advanced Usage
83+
84+
### Thing Resolver - Accessing Route Data
85+
86+
All guards support an optional `thing` parameter - a callback that receives the route and state to resolve what to check permissions against:
87+
88+
```typescript
89+
import { canActivateAbility } from 'ng-ability';
90+
91+
const routes: Routes = [
92+
{
93+
path: 'posts/:id',
94+
component: PostDetailComponent,
95+
resolve: { post: PostResolver },
96+
canActivate: [
97+
// Check permissions against the resolved post
98+
canActivateAbility('Post', 'view', (route) => route.data['post'])
99+
]
100+
}
101+
];
102+
```
103+
104+
#### Using Route Parameters
105+
106+
```typescript
107+
const routes: Routes = [
108+
{
109+
path: 'users/:userId/profile',
110+
component: UserProfileComponent,
111+
canActivate: [
112+
canActivateAbility('User', 'view', (route) => ({
113+
id: route.params['userId']
114+
}))
115+
]
116+
}
117+
];
118+
```
119+
120+
#### Accessing Parent Route Data
121+
122+
```typescript
123+
const routes: Routes = [
124+
{
125+
path: 'projects/:projectId',
126+
resolve: { project: ProjectResolver },
127+
children: [
128+
{
129+
path: 'tasks/:taskId',
130+
component: TaskComponent,
131+
canActivate: [
132+
// Access parent route's resolved data
133+
canActivateAbility('Task', 'view', (route) => {
134+
const project = route.parent?.data['project'];
135+
const taskId = route.params['taskId'];
136+
return { projectId: project?.id, id: taskId };
137+
})
138+
]
139+
}
140+
]
141+
}
142+
];
143+
```
144+
145+
## Type Safety
146+
147+
All guards are fully type-safe and work with declared ability actions:
148+
149+
```typescript
150+
declare module 'ng-ability' {
151+
interface AbilityActions {
152+
Article: 'view' | 'edit' | 'delete' | 'create';
153+
Post: 'read' | 'write';
154+
}
155+
}
156+
157+
// ✅ Type-safe: 'view' is a valid action for 'Article'
158+
canActivate: [canActivateAbility('Article', 'view')]
159+
160+
// ❌ Type error: 'publish' is not declared for 'Article'
161+
canActivate: [canActivateAbility('Article', 'publish')]
162+
163+
// ✅ Works: unregistered matchers accept any action
164+
canActivate: [canActivateAbility('CustomMatcher', 'any-action')]
165+
```
166+
167+
## Using with Class-Based Matchers
168+
169+
Guards also work with class-based matchers:
170+
171+
```typescript
172+
import { Routes } from '@angular/router';
173+
import { canActivateAbility } from 'ng-ability';
174+
175+
class Article {
176+
id: string;
177+
title: string;
178+
}
179+
180+
const routes: Routes = [
181+
{
182+
path: 'articles',
183+
component: ArticlesComponent,
184+
canActivate: [canActivateAbility(Article, 'view')]
185+
}
186+
];
187+
```
188+
189+
## Complete Example
190+
191+
```typescript
192+
import { Routes } from '@angular/router';
193+
import {
194+
canActivateAbility,
195+
canActivateChildAbility,
196+
canMatchAbility,
197+
} from 'ng-ability';
198+
199+
// Type declarations
200+
declare module 'ng-ability' {
201+
interface AbilityActions {
202+
Article: 'list' | 'view' | 'create' | 'edit' | 'delete';
203+
Admin: 'access';
204+
}
205+
}
206+
207+
export const routes: Routes = [
208+
{
209+
path: 'articles',
210+
component: ArticlesComponent,
211+
canActivate: [canActivateAbility('Article', 'list')]
212+
},
213+
{
214+
path: 'articles/create',
215+
component: CreateArticleComponent,
216+
canActivate: [canActivateAbility('Article', 'create')]
217+
},
218+
{
219+
path: 'articles/:id',
220+
component: ArticleDetailComponent,
221+
resolve: { article: ArticleResolver },
222+
canActivate: [
223+
canActivateAbility('Article', 'view', (route) => route.data['article'])
224+
]
225+
},
226+
{
227+
path: 'admin',
228+
component: AdminLayoutComponent,
229+
canMatch: [canMatchAbility('Admin', 'access')],
230+
canActivateChild: [canActivateChildAbility('Admin', 'access')],
231+
children: [
232+
{ path: 'users', component: UsersComponent },
233+
{ path: 'settings', component: SettingsComponent }
234+
]
235+
}
236+
];
237+
```
238+
239+
## Example with Ability Implementation
240+
241+
```typescript
242+
import { AbilityFor, Ability, provideAbilities } from 'ng-ability';
243+
244+
interface User {
245+
id: string;
246+
role: 'admin' | 'editor' | 'viewer';
247+
}
248+
249+
@AbilityFor('Article')
250+
export class ArticleAbility implements Ability<User> {
251+
can(currentUser: User | null, action: string): boolean {
252+
if (!currentUser) return false;
253+
254+
switch (action) {
255+
case 'view':
256+
case 'list':
257+
return true; // All authenticated users can view
258+
case 'edit':
259+
case 'create':
260+
return currentUser.role === 'editor' || currentUser.role === 'admin';
261+
case 'delete':
262+
return currentUser.role === 'admin';
263+
default:
264+
return false;
265+
}
266+
}
267+
}
268+
269+
// Register the ability
270+
import { ApplicationConfig } from '@angular/core';
271+
272+
export const appConfig: ApplicationConfig = {
273+
providers: [
274+
provideAbilities([ArticleAbility]),
275+
// ... other providers
276+
]
277+
};
278+
```

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@angular/build": "^21.1.3",
2525
"@angular/cli": "^21.1.3",
2626
"@angular/compiler-cli": "^21.1.0",
27+
"@angular/router": "21.1.3",
2728
"@vitest/coverage-v8": "4.0.18",
2829
"commit-and-tag-version": "^12.0.0",
2930
"jsdom": "^27.1.0",

projects/ng-ability/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"peerDependencies": {
1010
"@angular/common": "^21.0.0",
1111
"@angular/core": "^21.0.0",
12+
"@angular/router": "^21.0.0",
1213
"rxjs": "^7.0.0"
1314
},
1415
"dependencies": {

0 commit comments

Comments
 (0)