From 3494c9abf50536ad8a876edb32bc137c5e63f0e1 Mon Sep 17 00:00:00 2001 From: cipchk Date: Fri, 21 Jul 2023 17:42:55 +0800 Subject: [PATCH] refactor(*): refactor `CanActivate` to `CanActivateFn` - refactor `ACLGuard` to `aclCanMatch`, `aclCanActivate`, `aclCanActivateChild` --- docs/how-to-start.zh-CN.md | 4 +- packages/acl/docs/guard.en-US.md | 20 +- packages/acl/docs/guard.zh-CN.md | 24 +-- packages/acl/src/acl-guard.spec.ts | 295 +++++++++++++---------------- packages/acl/src/acl-guard.ts | 88 +++++---- packages/acl/src/acl.module.ts | 3 +- packages/acl/src/acl.type.ts | 5 + 7 files changed, 212 insertions(+), 227 deletions(-) diff --git a/docs/how-to-start.zh-CN.md b/docs/how-to-start.zh-CN.md index 607fe066e..b8991baf2 100644 --- a/docs/how-to-start.zh-CN.md +++ b/docs/how-to-start.zh-CN.md @@ -159,13 +159,13 @@ const routes: Routes = [ #### 用户授权 -接者用户访问的页面还需要取决于授权程度,例如系统配置页普通用户肯定无法进入。在初始化项目数据小节里会根据当前用户的 Token 来获得授权的数据,并将数据交给 `@delon/acl`,同时它也提供一组路由守卫的具体实现 `ACLGuard` 类,例如希望整个系统配置模块都必须是 `admin` 角色才能访问,则: +接者用户访问的页面还需要取决于授权程度,例如系统配置页普通用户肯定无法进入。在初始化项目数据小节里会根据当前用户的 Token 来获得授权的数据,并将数据交给 `@delon/acl`,同时它也提供一组路由守卫的具体实现 `aclCanActivate` 方法,例如希望整个系统配置模块都必须是 `admin` 角色才能访问,则: ```ts const routes: Routes = [ { path: 'sys', - canActivate: [ACLGuard], + canActivate: [aclCanActivate], data: { guard: 'admin' }, children: [ { path: 'config', component: ConfigComponent }, diff --git a/packages/acl/docs/guard.en-US.md b/packages/acl/docs/guard.en-US.md index 8f25851bd..e889fd398 100644 --- a/packages/acl/docs/guard.en-US.md +++ b/packages/acl/docs/guard.en-US.md @@ -8,7 +8,7 @@ type: Documents Routing guard prevent unauthorized users visit the page. -`@delon/acl` implements the generic guard class `ACLGuard`, which allows for complex operations through simple configuration in route registration, and supports the `Observable` type. +`@delon/acl` implements the generic guard functions `aclCanMatch`, `aclCanActivate`, `aclCanActivateChild`, which allows for complex operations through simple configuration in route registration, and supports the `Observable` type. Use the fixed attribute `guard` to specify the `ACLCanType` parameter value, for example: @@ -16,12 +16,12 @@ Use the fixed attribute `guard` to specify the `ACLCanType` parameter value, for const routes: Routes = [ { path: 'auth', - canActivate: [ ACLGuard ], + canActivate: [ aclCanActivate ], data: { guard: 'user1' as ACLGuardType } }, { path: 'auth', - canActivate: [ ACLGuard ], + canActivate: [ aclCanActivate ], data: { guard: { role: [ 'user1' ], @@ -33,7 +33,7 @@ const routes: Routes = [ }, { path: 'obs', - canActivate: [ ACLGuard ], + canActivate: [ aclCanActivate ], data: { guard: ((_srv, _injector) => { return of('user'); @@ -50,19 +50,19 @@ const routes: Routes = [ ```ts import { of } from 'rxjs'; -import { ACLGuard } from '@delon/acl'; +import { aclCanActivate, aclCanActivateChild, aclCanMatch } from '@delon/acl'; const routes: Routes = [ { path: 'guard', component: GuardComponent, children: [ - { path: 'auth', component: GuardAuthComponent, canActivate: [ ACLGuard ], data: { guard: 'user1' } }, - { path: 'admin', component: GuardAdminComponent, canActivate: [ ACLGuard ], data: { guard: 'admin' } } + { path: 'auth', component: GuardAuthComponent, canActivate: [ aclCanActivate ], data: { guard: 'user1' } }, + { path: 'admin', component: GuardAdminComponent, canActivate: [ aclCanActivate ], data: { guard: 'admin' } } ], - canActivateChild: [ ACLGuard ], + canActivateChild: [ aclCanActivateChild ], data: { guard: { role: [ 'user1' ], ability: [ 10, 'USER-EDIT' ], mode: 'allOf' } } }, - { path: 'pro', loadChildren: './pro/pro.module#ProModule', canMatch: [ ACLGuard ], data: { guard: 1 } }, - { path: 'pro', loadChildren: './pro/pro.module#ProModule', canMatch: [ ACLGuard ], data: { guard: of(false).pipe(map(v => 'admin')) } } + { path: 'pro', loadChildren: './pro/pro.module#ProModule', canMatch: [ aclCanMatch ], data: { guard: 1 } }, + { path: 'pro', loadChildren: './pro/pro.module#ProModule', canMatch: [ aclCanMatch ], data: { guard: of(false).pipe(map(v => 'admin')) } } ]; ``` diff --git a/packages/acl/docs/guard.zh-CN.md b/packages/acl/docs/guard.zh-CN.md index 47253a69a..6df6e61c3 100644 --- a/packages/acl/docs/guard.zh-CN.md +++ b/packages/acl/docs/guard.zh-CN.md @@ -8,7 +8,7 @@ type: Documents 路由守卫可以防止未授权用户访问页面。 -路由守卫需要单独对每一个路由进行设置,很多时候这看起来很繁琐,`@delon/acl` 实现了通用守卫类 `ACLGuard`,可以在路由注册时透过简单的配置完成一些复杂的操作,甚至支持 `Observable` 类型。 +路由守卫需要单独对每一个路由进行设置,很多时候这看起来很繁琐,`@delon/acl` 实现了通用守卫函数 `aclCanMatch`, `aclCanActivate`, `aclCanActivateChild`,可以在路由注册时透过简单的配置完成一些复杂的操作,甚至支持 `Observable` 类型。 使用固定属性 `guard` 来指定 `ACLCanType` 参数,例如: @@ -16,12 +16,12 @@ type: Documents const routes: Routes = [ { path: 'auth', - canActivate: [ ACLGuard ], + canActivate: [ aclCanActivate ], data: { guard: 'user1' as ACLGuardType } }, { path: 'auth', - canActivate: [ ACLGuard ], + canActivate: [ aclCanActivate ], data: { guard: { role: [ 'user1' ], @@ -33,7 +33,7 @@ const routes: Routes = [ }, { path: 'obs', - canActivate: [ ACLGuard ], + canActivate: [ aclCanActivate ], data: { guard: ((_srv, _injector) => { return of('user'); @@ -50,23 +50,19 @@ const routes: Routes = [ ```ts import { of } from 'rxjs'; -import { ACLGuard } from '@delon/acl'; +import { aclCanActivate, aclCanActivateChild, aclCanMatch } from '@delon/acl'; const routes: Routes = [ { path: 'guard', component: GuardComponent, children: [ - // 角色限定 - { path: 'auth', component: GuardAuthComponent, canActivate: [ ACLGuard ], data: { guard: 'user1' } }, - { path: 'admin', component: GuardAdminComponent, canActivate: [ ACLGuard ], data: { guard: 'admin' } } + { path: 'auth', component: GuardAuthComponent, canActivate: [ aclCanActivate ], data: { guard: 'user1' } }, + { path: 'admin', component: GuardAdminComponent, canActivate: [ aclCanActivate ], data: { guard: 'admin' } } ], - // 所有子路由有效 - canActivateChild: [ ACLGuard ], + canActivateChild: [ aclCanActivateChild ], data: { guard: { role: [ 'user1' ], ability: [ 10, 'USER-EDIT' ], mode: 'allOf' } } }, - // 权限点限定 - { path: 'pro', loadChildren: './pro/pro.module#ProModule', canMatch: [ ACLGuard ], data: { guard: 1 } }, - // 或使用Observable实现更复杂的行为 - { path: 'pro', loadChildren: './pro/pro.module#ProModule', canMatch: [ ACLGuard ], data: { guard: of(false).pipe(map(v => 'admin')) } } + { path: 'pro', loadChildren: './pro/pro.module#ProModule', canMatch: [ aclCanMatch ], data: { guard: 1 } }, + { path: 'pro', loadChildren: './pro/pro.module#ProModule', canMatch: [ aclCanMatch ], data: { guard: of(false).pipe(map(v => 'admin')) } } ]; ``` diff --git a/packages/acl/src/acl-guard.spec.ts b/packages/acl/src/acl-guard.spec.ts index 5122bd301..ede2957ee 100644 --- a/packages/acl/src/acl-guard.spec.ts +++ b/packages/acl/src/acl-guard.spec.ts @@ -1,211 +1,188 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Component } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { of } from 'rxjs'; -import { ACLGuard } from './acl-guard'; +import { ACLGuardService, aclCanActivate, aclCanActivateChild, aclCanMatch } from './acl-guard'; import { DelonACLModule } from './acl.module'; import { ACLService } from './acl.service'; -import { ACLGuardFunctionType, ACLGuardType, ACLType } from './acl.type'; +import { ACLGuardData } from './acl.type'; describe('acl: guard', () => { - let srv: ACLGuard; + let srv: ACLGuardService; let acl: ACLService; + let router: Router; let routerSpy: jasmine.Spy; beforeEach(() => { TestBed.configureTestingModule({ - imports: [RouterTestingModule.withRoutes([]), DelonACLModule.forRoot()] + declarations: [TestComponent], + imports: [ + RouterTestingModule.withRoutes([ + { path: '403', component: TestComponent }, + { + path: 'canActivate', + component: TestComponent, + canActivate: [aclCanActivate], + data: { guard: { role: ['admin'] } } as ACLGuardData + }, + { + path: 'canActivateChild', + canActivateChild: [aclCanActivateChild], + data: { guard: { role: ['admin'] } } as ACLGuardData, + children: [{ path: '1', component: TestComponent }] + }, + { + path: 'canMatch', + component: TestComponent, + canMatch: [aclCanMatch], + data: { guard: { role: ['admin'] } } as ACLGuardData + } + ]), + DelonACLModule.forRoot() + ] }); - srv = TestBed.inject(ACLGuard); + srv = TestBed.inject(ACLGuardService); acl = TestBed.inject(ACLService); acl.set({ role: ['user'], ability: [1, 2, 3] - } as ACLType); - routerSpy = spyOn(TestBed.inject(Router), 'navigateByUrl'); - }); - - it(`should load route when no-specify permission`, (done: () => void) => { - srv.canActivate({} as any, null).subscribe(res => { - expect(res).toBeTruthy(); - done(); }); + router = TestBed.inject(Router); }); - it(`should load route when specify permission`, (done: () => void) => { - srv - .canActivate( - { - data: { - guard: 'user' - } - } as any, - null - ) - .subscribe(res => { - expect(res).toBeTruthy(); - done(); - }); - }); + describe('', () => { + beforeEach(() => (routerSpy = spyOn(router, 'navigateByUrl'))); - it(`should unable load route if no-permission`, (done: () => void) => { - srv - .canActivate( - { - data: { - guard: 'admin' - } - } as any, - null - ) - .subscribe(res => { - expect(res).toBeFalsy(); - done(); - }); - }); - - it(`should load route via function`, (done: () => void) => { - srv - .canActivate( - { - data: { - guard: ((_srv, _injector) => { - return of('user'); - }) as ACLGuardFunctionType - } - } as any, - null - ) - .subscribe(res => { + it(`should load route when no-specify permission`, (done: () => void) => { + srv.process({}).subscribe(res => { expect(res).toBeTruthy(); done(); }); - }); - - it(`should load route via Observable`, (done: () => void) => { - srv - .canActivate( - { - data: { - guard: of('user') as ACLGuardType - } - } as any, - null - ) - .subscribe(res => { - expect(res).toBeTruthy(); - done(); - }); - }); + }); - it(`should load route using ability`, (done: () => void) => { - srv - .canActivate( - { - data: { - guard: of(1) - } - } as any, - null - ) - .subscribe(res => { - expect(res).toBeTruthy(); - done(); - }); - }); + it(`should load route when specify permission`, (done: () => void) => { + srv + .process({ + guard: 'user' + }) + .subscribe(res => { + expect(res).toBeTruthy(); + done(); + }); + }); - it(`should unable load route using ability`, (done: () => void) => { - srv - .canActivate( - { - data: { - guard: of(10) - } - } as any, - null - ) - .subscribe(res => { - expect(res).toBeFalsy(); - done(); - }); - }); + it(`should unable load route if no-permission`, (done: () => void) => { + srv + .process({ + guard: 'admin' + }) + .subscribe(res => { + expect(res).toBeFalsy(); + done(); + }); + }); - describe(`#canMatch`, () => { - it(`should be can load when has [user] role`, (done: () => void) => { + it(`should load route via function`, (done: () => void) => { srv - .canMatch({ - data: { - guard: of('user') - } - } as any) + .process({ + guard: (_srv, _injector) => of('user') + }) .subscribe(res => { expect(res).toBeTruthy(); done(); }); }); - it(`should be can load when is null`, (done: () => void) => { + + it(`should load route via Observable`, (done: () => void) => { srv - .canMatch({ - data: { - guard: null - } - } as any) + .process({ + guard: of('user') + }) .subscribe(res => { expect(res).toBeTruthy(); done(); }); }); - }); - - it(`#canActivateChild`, (done: () => void) => { - srv - .canActivateChild( - { - data: { - guard: of('user') - } - } as any, - null! - ) - .subscribe(res => { - expect(res).toBeTruthy(); - done(); - }); - }); - describe('#guard_url', () => { - it(`should be rediect to default url: /403`, (done: () => void) => { + it(`should load route using ability`, (done: () => void) => { srv - .canActivate( - { - data: { - guard: 'admin' - } - } as any, - null - ) - .subscribe(() => { - expect(routerSpy.calls.first().args[0]).toBe(`/403`); + .process({ + guard: of(1) + }) + .subscribe(res => { + expect(res).toBeTruthy(); done(); }); }); - it(`should be specify rediect url`, (done: () => void) => { + + it(`should unable load route using ability`, (done: () => void) => { srv - .canActivate( - { - data: { - guard: 'admin', - guard_url: '/no' - } - } as any, - null - ) - .subscribe(() => { - expect(routerSpy.calls.first().args[0]).toBe(`/no`); + .process({ + guard: of(10) + }) + .subscribe(res => { + expect(res).toBeFalsy(); done(); }); }); + + describe('#guard_url', () => { + it(`should be rediect to default url: /403`, (done: () => void) => { + srv + .process({ + guard: 'admin' + }) + .subscribe(() => { + expect(routerSpy.calls.first().args[0]).toBe(`/403`); + done(); + }); + }); + it(`should be specify rediect url`, (done: () => void) => { + srv + .process({ + guard: 'admin', + guard_url: '/no' + }) + .subscribe(() => { + expect(routerSpy.calls.first().args[0]).toBe(`/no`); + done(); + }); + }); + }); + }); + + describe('#router', () => { + it('canMatch', async () => { + acl.set({ role: ['user'] }); + const targetUrl = '/canMatch'; + await router.navigateByUrl(targetUrl); + expect(router.url).toBe('/403'); + acl.set({ role: ['admin'] }); + await router.navigateByUrl(targetUrl); + expect(router.url).toBe(targetUrl); + }); + it('canActivate', async () => { + acl.set({ role: ['user'] }); + const targetUrl = '/canActivate'; + await router.navigateByUrl(targetUrl); + expect(router.url).toBe('/403'); + acl.set({ role: ['admin'] }); + await router.navigateByUrl(targetUrl); + expect(router.url).toBe(targetUrl); + }); + it('canActivateChild', async () => { + acl.set({ role: ['user'] }); + const targetUrl = '/canActivateChild/1'; + await router.navigateByUrl(targetUrl); + expect(router.url).toBe('/403'); + acl.set({ role: ['admin'] }); + await router.navigateByUrl(targetUrl); + expect(router.url).toBe(targetUrl); + }); }); }); + +@Component({ template: `` }) +class TestComponent {} diff --git a/packages/acl/src/acl-guard.ts b/packages/acl/src/acl-guard.ts index 990779f8f..1b1339cd3 100644 --- a/packages/acl/src/acl-guard.ts +++ b/packages/acl/src/acl-guard.ts @@ -1,65 +1,71 @@ -import { Injectable, Injector } from '@angular/core'; -import { - ActivatedRouteSnapshot, - CanActivate, - CanActivateChild, - CanMatch, - Data, - Route, - Router, - RouterStateSnapshot -} from '@angular/router'; +import { Injectable, Injector, inject } from '@angular/core'; +import { CanActivateChildFn, CanActivateFn, CanMatchFn, Router } from '@angular/router'; import { Observable, of, map, tap } from 'rxjs'; import { ACLService } from './acl.service'; -import { ACLCanType, ACLGuardType } from './acl.type'; +import type { ACLCanType, ACLGuardData } from './acl.type'; -/** - * Routing guard prevent unauthorized users visit the page, [ACL Document](https://ng-alain.com/acl). - * - * ```ts - * data: { - * path: 'home', - * canActivate: [ ACLGuard ], - * data: { guard: 'user1' } - * } - * ``` - */ -@Injectable({ providedIn: 'root' }) -export class ACLGuard implements CanActivate, CanActivateChild, CanMatch { +@Injectable() +export class ACLGuardService { constructor( private srv: ACLService, private router: Router, private injector: Injector ) {} - private process(data: Data): Observable { + process(data?: ACLGuardData): Observable { data = { guard: null, guard_url: this.srv.guard_url, ...data }; - let guard: ACLGuardType = data.guard; + let guard = data.guard; if (typeof guard === 'function') guard = guard(this.srv, this.injector); return (guard && guard instanceof Observable ? guard : of(guard != null ? (guard as ACLCanType) : null)).pipe( map(v => this.srv.can(v)), tap(v => { if (v) return; - this.router.navigateByUrl(data.guard_url); + this.router.navigateByUrl(data!!.guard_url!!); }) ); } - - // lazy loading - canMatch(route: Route): Observable { - return this.process(route.data!); - } - // all children route - canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.canActivate(childRoute, state); - } - // route - canActivate(route: ActivatedRouteSnapshot, _state: RouterStateSnapshot | null): Observable { - return this.process(route.data); - } } + +/** + * Routing guard prevent unauthorized users visit the page, [ACL Document](https://ng-alain.com/acl). + * + * ```ts + * data: { + * path: 'home', + * canActivate: [ aclCanActivate ], + * data: { guard: 'user1' } + * } + * ``` + */ +export const aclCanActivate: CanActivateFn = route => inject(ACLGuardService).process(route.data); + +/** + * Routing guard prevent unauthorized users visit the page, [ACL Document](https://ng-alain.com/acl). + * + * ```ts + * data: { + * path: 'home', + * canActivateChild: [ aclCanActivateChild ], + * data: { guard: 'user1' } + * } + * ``` + */ +export const aclCanActivateChild: CanActivateChildFn = route => inject(ACLGuardService).process(route.data); + +/** + * Routing guard prevent unauthorized users visit the page, [ACL Document](https://ng-alain.com/acl). + * + * ```ts + * data: { + * path: 'home', + * canMatch: [ aclCanMatch ], + * data: { guard: 'user1' } + * } + * ``` + */ +export const aclCanMatch: CanMatchFn = route => inject(ACLGuardService).process(route.data); diff --git a/packages/acl/src/acl.module.ts b/packages/acl/src/acl.module.ts index 91f56da5b..64e366cc9 100644 --- a/packages/acl/src/acl.module.ts +++ b/packages/acl/src/acl.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { ModuleWithProviders, NgModule } from '@angular/core'; +import { ACLGuardService } from './acl-guard'; import { ACLIfDirective } from './acl-if.directive'; import { ACLDirective } from './acl.directive'; import { ACLService } from './acl.service'; @@ -16,7 +17,7 @@ export class DelonACLModule { static forRoot(): ModuleWithProviders { return { ngModule: DelonACLModule, - providers: [ACLService] + providers: [ACLService, ACLGuardService] }; } } diff --git a/packages/acl/src/acl.type.ts b/packages/acl/src/acl.type.ts index 409f33b99..4b0ad5c5e 100644 --- a/packages/acl/src/acl.type.ts +++ b/packages/acl/src/acl.type.ts @@ -39,3 +39,8 @@ export type ACLCanType = number | number[] | string | string[] | ACLType; export type ACLGuardFunctionType = (srv: ACLService, injector: Injector) => Observable; export type ACLGuardType = ACLCanType | Observable | ACLGuardFunctionType; + +export interface ACLGuardData { + guard?: ACLGuardType | null; + guard_url?: string | null; +}