marcglasberg / async_redux

Flutter Package: A Redux version tailored for Flutter, which is easy to learn, to use, to test, and has no boilerplate. Allows for both sync and async reducers.
Other
230 stars 41 forks source link

What is the recommended way to implement route guarding? #134

Closed point-source closed 2 years ago

point-source commented 2 years ago

What is the best way to protect some routes behind an authenticated state? Specifically, I want to be able to use the NavigateAction like usual but just have it redirect to the Login page, regardless of the called route, if the user is not authenticated.

marcglasberg commented 2 years ago

There are a few ways to do that.

1) Create your own NavigateAction that extends the original NavigateAction (or just copy the NavigateAction code and change it). Then you check if the user is authenticated or not. If it's not, you actually do a pushAndRemoveAll(LoginScreen()).

2) When you access your backend, it will throw an error if the user is not authenticated. You can catch this error in the global wrapError (not the one inside the action) and then do a pushAndRemoveAll(LoginScreen()).

3) Use some interceptor from your http package (it depends on the package you use for the requests). When the user is not authenticated, do a pushAndRemoveAll(LoginScreen()).

4) Create a timer that checks if the user is authenticated (for example, each 30 secs). When the user is not authenticated, do a pushAndRemoveAll(LoginScreen()).

point-source commented 2 years ago

My concern is that when using the app on platforms that allow the user to specify path (primarily web), it is possible that the user attempts to go directly to a route that they could not have otherwise reached via typical interaction with the app. Especially if I have different user roles. Sure, the backend should prevent them from actually doing anything they shouldn't do but I would rather throw a permission error dialog/notification rather than just booting them out of the page suddenly.

Ultimately, I did come up with a "solution" so I'm chiming in here to document it for future devs and on the off-chance you might want to add it to the docs.

It relies on MaterialApp's onGenerateRoute so that the guarding occurs no matter what Navigation method is employed:

onGenerateRoute: (RouteSettings routeSettings) {
            return MaterialPageRoute<void>(
              settings: routeSettings,
              builder: (BuildContext context) =>
                  StoreConnector<AppState, String>(
                converter: (store) => store.state.loginState.user.id,
                builder: (context, userId) {
                  // Public / Unprotected views
                  switch (routeSettings.name) {
                    case Splash.routeName:
                      return const Splash();
                    case LoginView.routeName:
                      return const LoginView();
                    case CreateAccountInfoView.routeName:
                      return const CreateAccountInfoView();
                    case CreateAccountEmailConfirmView.routeName:
                      return const CreateAccountEmailConfirmView();
                    default:
                      break;
                  }
                  // Authenticated-only views
                  if (userId.isEmpty) return const LoginView();
                  switch (routeSettings.name) {
                    case AddAssetView.routeName:
                      return const AddAssetView();
                    case HomeView.routeName:
                    default:
                      return const HomeView();
                  }
                },
              ),
            );
          },

It felt somewhat like overkill to be adding a StoreConnector right there between the StoreProvider and the route generator but since it (apparently) only rebuilds when userId actually changes, this seems okay.

Fwiw, I also took it an unecessary step further and just removed the use of NavigateAction in favor of putting call to Navigator inside the functions I was passing to my view models. So instead of navigating inside a reducer or in the after{} method of a reducer, I am awaiting completion of the dispatched action and then navigating via flutter's built-in navigator. I've found this to be more flexible for me since dispatching an action doesn't automatically cause navigation and I can use the actions differently in different places.