beamer icon indicating copy to clipboard operation
beamer copied to clipboard

How to work with guards?

Open bambinoua opened this issue 2 years ago • 5 comments

Hm... I don't understand why my guard is not working.

MaterialApp.router(
  ...
  routerDeletgate: BeamerDelagate(guards: BeamerRouter.guards),
  ...
)

class BeamerRouter {
  static final guards = <BeamGuard>[
    BeamGuard(
      pathPatterns: ['/alerts'],
      check: (context, location) => state.isSessionInitialized,
      beamToNamed: (origin, target) {
        return '/';
      }
    ),
    BeamGuard(
      pathPatterns: ['/'],
      check: (context, location) => state.isSessionInitialized,
      beamToNamed: (origin, target) {
        return target.state.routeInformation.location;
      }
    ),
  ];
}

My initial route is / and login route is also /. Then I type in browser 'http://127.0.0.1/alerts' after app initializing it tries to open /alerts and redirects to / route which shows login form. After successful authentication I execute

Beamer.of(context).update()

but it is not redirected to /alerts page and still in login / page. In both guards origin and target always return the same value. What is wrong or what I did wrong?

What I need is if I typed some secured (guarded) URL I need to be redirected to login route and after successful authentication to be redirected to requested URL.

And second one is, I need to check if user has permission to specific route it must be redirected there, if those route is denied he must be redirected to first from available routes and if there is no any permissions show user not found page.

bambinoua avatar May 03 '22 06:05 bambinoua

Schematically I need to implement some route dispatcher.

/alerts          |-----------------------|   yes
-------->        | is /alerts permitted? | ------> show alerts page
                 | --------  |  ---------|
                             | no
                 |-----------------------|
                 |  are there available  |   yes
                 |        routes?        | -------> show first    
                 |-----------------------|  
                             |no
                             |--------------------> show "page not found"

Is it possible to implement this using guards?

bambinoua avatar May 03 '22 06:05 bambinoua

Hey @bambinoua Thanks for creating a separate issue for this.

What I need is if I typed some secured (guarded) URL I need to be redirected to login route and after successful authentication to be redirected to requested URL.

The flow you are describing seems like a useful feature to have, but is not supported out of the box currently. Currently,

  • origin is from where you are beaming, i.e. the route that the user is on when beamTo* is called.
  • target is the route that the user tries to beam to, i.e. the input of beamTo*.

There is no concept of "where the user tried to beam previously, but failed". This is something you would have to store in your state and return that (if present) instead of return target.state.routeInformation.location;.

I'm interested in introducing that feature of "remembering failed routes" into Beamer. If you wish to work on it or have a good idea how it should look, please let me know.

and still in login / page

This seems correct from what I described above, but is the app still showing "login screen"? That shouldn't be the case and is probably an issue with state management. The page at / will not rebuild after calling update if its key is the same as it were during the last build. Maybe a better approach would be to have a separate /login route, but it is not impossible with "multipurpose /", but then it needs to be handled carefully with state management and/or corresponding BeamPage.key.

Schematically I need to implement some route dispatcher.

Generally, about the diagram you drew, I'm not sure about the "no" flow. I like to do the following:

BeamGuard(
      guardNonMatching: true,
      pathPatterns: ['/login', '/register'],
      check: (context, _) {
        final state = context.read<AuthenticationBloc>().state;
        return state.status == AuthenticationStatus.authenticated;
      },
      beamToNamed: (_, __) => '/login',
),

which is a guard that will guard every route except /login or /register from unauthenticated user. Basically, there doesn't exist a case where there are "no available routes", but you still can accomplish that "not found fallback" if you define guards that will sequentially fail and redirect to each other until the last one returns some "not found" route, e.g.:

guards: [
  BeamGuard(
      pathPatterns: ['/admin'],
      check: (_, __) => isAdmin(),
      beamToNamed: (_, __) => '/employee',
  ),
  BeamGuard(
      pathPatterns: ['/employee'],
      check: (_, __) => isEmployee(),
      beamToNamed: (_, __) => '/user',
  ),
  BeamGuard(
      pathPatterns: ['/user'],
      check: (_, __) => isUser(),
      beamToNamed: (_, __) => '/not-found',
  ),
],

slovnicki avatar May 03 '22 15:05 slovnicki

What I need is if I typed some secured (guarded) URL I need to be redirected to login route and after successful authentication to be redirected to requested URL.

I had the idea you suggested and below the way I implemented this:

class LoginRouteState {
  const LoginRouteState({this.redirectTo});
  final String redirectTo;
}
class LoginBeamLocation extends BeamLocation<BeamState> {
  LoginBeamLocation({
    RouteState? routeState,
    BeamParameters? beamParameters,
  }) : super(
    routeState != null ? 
      RouteInformation(location: '/login', state: routeState) : null, 
      beamParameters);

  ...
}

class SecureBeamLocation extends BeamLocation<BeamState> {
  SecureBeamLocation(
    String routeName, {
    RouteState? routeState,
    BeamParameters? beamParameters,
  }) : super(RouteInformation(location: routeName, state: routeState), beamParameters);

  ...
}

<BeamGuard>[
  BeamGuard(
    pathPatterns: ['/page1','/page2',...],
    check: (context, location) => AppState.isAuthenticated,
    beamTo: (context, origin, target) {
      final routeState = LoginRouteState(
        redirectTo: target.state.routeInformation.location!)
      return LoginBeamLocation(routeState: routeState);
    },
  ),
]

After successul login:

void onSuccess() {
  final beamerDelegate = Beamer.of(context);
  final routeState = beamerDelegate.configuration.state as LoginRouteState;
  final routeName = routeState.redirectTo.isNotEmpty ? routeState.redirectTo : '/dashboard';
  beamerDelegate.beamToReplacement(SecureBeamLocation(routeName: routeName));
}

The only problem I have is the title of page is not changed to title for beamed page and left as for Login BeamPage. It looks like there is a bug somewhere in switching routes.

bambinoua avatar May 04 '22 10:05 bambinoua

but you still can accomplish that "not found fallback" if you define guards that will sequentially fail and redirect to each other until the last one returns some "not found" route, e.g

The problem is how to initiate this guards re-execution?

For example the current route is /login. User clicks Login button.

LoginPage(onSuccess: () {
  // Here it is required to restart guards
  Beamer.of(context).update();
});

But you noticed that page is not refreshed because url /login is not changed. So how can I restart guards so?

The only way I see is:

LoginPage(onSuccess: () {
  final availableRoutes = securityBloc.getPermittedRoutes(userId);
  if (availableRoutes.isNotEmpty)
    Beamer.of(context).beamTo(SecureLocation(availableRoutes.first));
  else
    Beamer.of(context).beamTo(SecureLocation('/forbidden'));
});

But this approach does not use BeamGuards.

bambinoua avatar May 04 '22 13:05 bambinoua

@bambinoua Hm, the Beamer.of(context).update(); really should re-run the guards, regardless of any configuration :thinking:

slovnicki avatar May 08 '22 10:05 slovnicki