DevGang
Авторизоваться

Создайте Route Guard для управления разрешениями

Многим приложениям приходится обрабатывать различные типы разрешений пользователей, таких как администраторы, менеджеры… В этом задании мы узнаем, как использовать защиту в Angular для ограничения доступа к определенным страницам на основе разрешений пользователя.

Мы начинаем вызов со следующего приложения. У нас есть список кнопок для входа в качестве разных пользователей, у каждого из которых есть свой набор разрешений. У нас также есть кнопка для входа в приложение. В зависимости от разрешений пользователя мы будем отображать соответствующую панель инструментов.

Angular предоставляет список встроенных охранников для защиты наших маршрутов: canLoad, canActivate, canDeactivate, canActivateChild и canMatch.

Для решения этой задачи мы будем использовать защиту canMatch, которая идеально подходит для нашего варианта использования.

Мы можем написать один и тот же маршрут несколько раз. Если первый маршрут в файле совпадает, router перейдет к этому маршруту. В противном случае он проверит следующий. Давайте посмотрим, как работает canMatch на очень простом примере:

  {
    path: 'enter',
    canMatch: [() => false],
    loadComponent: () => import('./dashboard/writer-reader.component'),
  },
  {
    path: 'enter',
    canMatch: [() => true],
    loadComponent: () => import('./dashboard/client.component'),
  },
  {
    path: 'enter',
    loadComponent: () => import('./dashboard/everyone.component'),
  },

В этом примере, если пользователь переходит в enter, router попытается сопоставить первый маршрут. Однако защита canMatch возвращает false, поэтому router попытается использовать второй маршрут, который возвращает true. Наконец, router перейдет к ClientComponent и не будет выполнять последующие маршруты.

Теперь давайте применим наши знания к нашим потребностям. Сначала нам нужно создать внедряемый сервис для реализации нашей логики защиты для управления разрешениями пользователей.

Вот реализация:

@Injectable({ providedIn: 'root' })
export class HasPermissionGuard implements CanMatch {
  private router = inject(Router);
  private userStore = inject(UserStore);

  canMatch(route: Route): Observable<boolean | UrlTree> {
    const accessRolesList: Role[] = route.data?.['roles'] ?? [];
    const isAdmin: boolean = route.data?.['isAdmin'] ?? false;
    return this.hasPermission$(isAdmin, accessRolesList);
  }

  private hasPermission$(isAdmin: boolean, accessRolesList: Role[]) {
    return this.userStore.isUserLoggedIn$.pipe(
      mergeMap((hasUser) => {
        if (hasUser) {
          if (isAdmin) {
            return this.userStore.isAdmin$.pipe(map(Boolean));
          } else if (accessRolesList.length > 0) {
            return this.userStore
              .hasAnyRole(accessRolesList)
              .pipe(map(Boolean));
          }
          return of(false);
        } else {
          return of(this.router.parseUrl('no-user'));
        }
      })
    );
  }
}

Мы извлекаем roles и свойства isAdmin из атрибута данных маршрута.

Затем мы сначала проверяем, вошел ли пользователь в систему. Если нет, мы переходим на страницу no-user.

Если пользователь вошел в систему, мы сравниваем роли пользователя с ролями, установленными на маршруте, чтобы определить, должен ли маршрутизатор перейти к этому маршруту или проверить следующий.

Как только наша защита как услуга будет завершена, мы можем создать наши маршруты следующим образом:

  {
    path: 'enter',
    canMatch: [HasPermissionGuard],
    data: {
      isAdmin: true,
    },
    loadComponent: () => import('./dashboard/admin.component'),
  },
  {
    path: 'enter',
    canMatch: [HasPermissionGuard],
    data: {
      roles: ['MANAGER'],
    },
    loadComponent: () => import('./dashboard/manager.component'),
  },
  {
    path: 'enter',
    canMatch: [HasPermissionGuard],
    data: {
      roles: ['WRITER', 'READER'],
    },
    loadComponent: () => import('./dashboard/writer-reader.component'),
  },
  {
    path: 'enter',
    canMatch: [HasPermissionGuard],
    data: {
      roles: ['CLIENT'],
    },
    loadComponent: () => import('./dashboard/client.component'),
  },
  {
    path: 'enter',
    loadComponent: () => import('./dashboard/everyone.component'),
  },

Начиная с Angular v.14.2 мы можем использовать функциональное программирование. Кроме того, защита как услуга будет объявлена устаревшей в версии 15.2.

Это дает нам следующую функцию. Чтобы внедрить наш сервис с помощью системы внедрения зависимостей Angular, теперь мы можем воспользоваться функцией inject. (Примечание: функция inject работает только внутри контекста зависимости)

export const hasAdminPermission = (isAdmin: boolean, accessRolesList: Role[]) => {
  const userStore = inject(UserStore);
  const router = inject(Router);
  return userStore.isUserLoggedIn$.pipe(
    mergeMap((hasUser) => {
      if (hasUser) {
        if (isAdmin) {
          return userStore.isAdmin$.pipe(map(Boolean));
        } else if (accessRolesList.length > 0) {
          return userStore.hasAnyRole(accessRolesList).pipe(map(Boolean));
        }
        return of(false);
      } else {
        return of(router.parseUrl('no-user'));
      }
    })
  );
};

Поскольку мы используем функцию, мы можем разделить нашу логику на две отдельные функции.

export const isAdmin = () => {
  const userStore = inject(UserStore);
  const router = inject(Router);
  return userStore.isUserLoggedIn$.pipe(
    mergeMap((hasUser) =>
      iif(
        () => hasUser,
        userStore.isAdmin$.pipe(map(Boolean)),
        of(router.parseUrl('no-user'))
      )
    )
  );
};

export const hasRole = (accessRolesList: Role[]) => {
  const userStore = inject(UserStore);
  const router = inject(Router);
  return userStore.isUserLoggedIn$.pipe(
    mergeMap((hasUser) =>
      iif(
        () => hasUser,
        userStore.hasAnyRole(accessRolesList).pipe(map(Boolean)),
        of(router.parseUrl('no-user'))
      )
    )
  );
};

Таким образом, код легче понять, легче поддерживать и меньше подвержен ошибкам. Если мы хотим использовать обе функции для защиты одного маршрута, мы можем это сделать, так как защита принимает массив в качестве входных данных.

Теперь мы можем реорганизовать наше определение route.

  {
    path: 'enter',
    canMatch: [() => isAdmin()],
    loadComponent: () => import('./dashboard/admin.component'),
  },
  {
    path: 'enter',
    canMatch: [() => hasRole(['MANAGER'])],
    loadComponent: () => import('./dashboard/manager.component'),
  },
  {
    path: 'enter',
    canMatch: [() => hasRole(['WRITER', 'READER'])],
    loadComponent: () => import('./dashboard/writer-reader.component'),
  },
  {
    path: 'enter',
    canMatch: [() => hasRole(['CLIENT'])],
    loadComponent: () => import('./dashboard/client.component'),
  },
  {
    path: 'enter',
    loadComponent: () => import('./dashboard/everyone.component'),
  },
  1. Лучший разработчик eXperience
  2. Менее подвержен ошибкам (больше нет необходимости в свойстве data типа any)
  3. Меньше стандартного кода
  4. Легче читать
  5. Более ремонтопригодный

Еще одно улучшение

Использование функции inject внутри функции может привести к ошибкам, если кто-то захочет повторно использовать функцию вне контекста зависимости. Чтобы избежать этого, мы можем установить две внедренные зависимости в качестве необязательных параметров:

export const hasRole = (accessRolesList: Role[], userStore = inject(UserStore), router = inject(Router)) => {
  return userStore.isUserLoggedIn$.pipe(
    mergeMap((hasUser) =>
      iif(
        () => hasUser,
        userStore.hasAnyRole(accessRolesList).pipe(map(Boolean)),
        of(router.parseUrl('no-user'))
      )
    )
  );
};

Это дает еще одно преимущество: теперь вы можете легко протестировать свою функцию без использования TestBed.

#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу