Advanced

Route Guards

Introduzione

I route guard forniscono un middleware per la navigazione in Nylo Website. Intercettano le transizioni tra route e ti permettono di controllare se un utente puo' accedere a una pagina, reindirizzarlo altrove o modificare i dati passati a una route.

Casi d'uso comuni includono:

  • Controlli di autenticazione -- reindirizzare gli utenti non autenticati a una pagina di login
  • Accesso basato sui ruoli -- limitare le pagine agli utenti admin
  • Validazione dei dati -- assicurarsi che i dati richiesti esistano prima della navigazione
  • Arricchimento dei dati -- aggiungere dati supplementari a una route

I guard vengono eseguiti in ordine prima che avvenga la navigazione. Se un guard restituisce handled, la navigazione si interrompe (reindirizzando o annullando).

Creare un Route Guard

Crea un route guard usando la CLI Metro:

metro make:route_guard auth

Questo genera un file guard:

import 'package:nylo_framework/nylo_framework.dart';

class AuthRouteGuard extends NyRouteGuard {
  AuthRouteGuard();

  @override
  Future<GuardResult> onBefore(RouteContext context) async {
    // Add your guard logic here
    return next();
  }
}

Ciclo di Vita del Guard

Ogni route guard ha tre metodi del ciclo di vita:

onBefore

Chiamato prima che avvenga la navigazione. Qui verifichi le condizioni e decidi se consentire, reindirizzare o annullare la navigazione.

@override
Future<GuardResult> onBefore(RouteContext context) async {
  bool isLoggedIn = await Auth.isAuthenticated();

  if (!isLoggedIn) {
    return redirect(HomePage.path);
  }

  return next();
}

Valori di ritorno:

  • next() -- continua al guard successivo o naviga verso la route
  • redirect(path) -- reindirizza a una route diversa
  • abort() -- annulla completamente la navigazione

onAfter

Chiamato dopo una navigazione riuscita. Usalo per analytics, logging o effetti collaterali post-navigazione.

@override
Future<void> onAfter(RouteContext context) async {
  // Log page view
  Analytics.trackPageView(context.routeName);
}

onLeave

Chiamato quando l'utente sta lasciando una route. Restituisci false per impedire all'utente di uscire.

@override
Future<bool> onLeave(RouteContext context) async {
  if (hasUnsavedChanges) {
    // Show confirmation dialog
    return await showConfirmDialog();
  }
  return true; // Allow leaving
}

RouteContext

L'oggetto RouteContext viene passato a tutti i metodi del ciclo di vita del guard e contiene informazioni sulla navigazione:

Proprieta' Tipo Descrizione
context BuildContext? Build context corrente
data dynamic Dati passati alla route
queryParameters Map<String, String> Parametri di query dell'URL
routeName String Nome/percorso della route di destinazione
originalRouteName String? Nome originale della route prima delle trasformazioni
@override
Future<GuardResult> onBefore(RouteContext context) async {
  // Access route information
  String route = context.routeName;
  dynamic routeData = context.data;
  Map<String, String> params = context.queryParameters;

  return next();
}

Trasformare il RouteContext

Crea una copia con dati diversi:

// Change the data type
RouteContext<User> userContext = context.withData<User>(currentUser);

// Copy with modified fields
RouteContext updated = context.copyWith(
  data: enrichedData,
  queryParameters: {"tab": "settings"},
);

Azioni del Guard

next

Continua al guard successivo nella catena, o naviga verso la route se questo e' l'ultimo guard:

return next();

redirect

Reindirizza l'utente a una route diversa:

return redirect(LoginPage.path);

Con opzioni aggiuntive:

return redirect(
  LoginPage.path,
  data: {"returnTo": context.routeName},
  navigationType: NavigationType.pushReplace,
  queryParameters: {"source": "guard"},
);
Parametro Tipo Predefinito Descrizione
path Object obbligatorio Stringa del percorso della route o RouteView
data dynamic null Dati da passare alla route di reindirizzamento
queryParameters Map<String, dynamic>? null Parametri di query
navigationType NavigationType pushReplace Metodo di navigazione
result dynamic null Risultato da restituire
removeUntilPredicate Function? null Predicato di rimozione route
transitionType TransitionType? null Tipo di transizione della pagina
onPop Function(dynamic)? null Callback al pop

abort

Annulla la navigazione senza reindirizzare. L'utente rimane sulla pagina corrente:

return abort();

setData

Modifica i dati che verranno passati ai guard successivi e alla route di destinazione:

@override
Future<GuardResult> onBefore(RouteContext context) async {
  User user = await fetchUser();

  // Enrich the route data
  setData({"user": user, "originalData": context.data});

  return next();
}

Applicare i Guard alle Route

Aggiungi i guard alle singole route nel tuo file router:

appRouter() => nyRoutes((router) {
  router.route(
    HomePage.path,
    (_) => HomePage(),
  ).initialRoute();

  // Add a single guard
  router.route(
    ProfilePage.path,
    (_) => ProfilePage(),
    routeGuards: [AuthRouteGuard()],
  );

  // Add multiple guards (executed in order)
  router.route(
    AdminPage.path,
    (_) => AdminPage(),
    routeGuards: [AuthRouteGuard(), AdminRoleGuard()],
  );
});

Guard di Gruppo

Applica i guard a piu' route contemporaneamente usando i gruppi di route:

appRouter() => nyRoutes((router) {
  router.route(HomePage.path, (_) => HomePage()).initialRoute();

  // All routes in this group require authentication
  router.group(() {
    return {
      'prefix': '/dashboard',
      'route_guards': [AuthRouteGuard()],
    };
  }, (router) {
    router.route(DashboardPage.path, (_) => DashboardPage());
    router.route(SettingsPage.path, (_) => SettingsPage());
    router.route(ProfilePage.path, (_) => ProfilePage());
  });
});

Composizione dei Guard

Nylo Website fornisce strumenti per comporre i guard insieme in pattern riutilizzabili.

GuardStack

Combina piu' guard in un singolo guard riutilizzabile:

final protectedRoute = GuardStack([
  AuthRouteGuard(),
  VerifyEmailGuard(),
  TwoFactorGuard(),
]);

// Use the stack on a route
router.route(
  SecurePage.path,
  (_) => SecurePage(),
  routeGuards: [protectedRoute],
);

GuardStack esegue i guard in ordine. Se un guard restituisce handled, i guard rimanenti vengono saltati.

ConditionalGuard

Applica un guard solo quando una condizione e' vera:

router.route(
  BetaPage.path,
  (_) => BetaPage(),
  routeGuards: [
    ConditionalGuard(
      condition: (context) => context.queryParameters.containsKey("beta"),
      guard: BetaAccessGuard(),
    ),
  ],
);

Se la condizione restituisce false, il guard viene saltato e la navigazione continua.

ParameterizedGuard

Crea guard che accettano parametri di configurazione:

class RoleGuard extends ParameterizedGuard<List<String>> {
  RoleGuard(super.params); // params = allowed roles

  @override
  Future<GuardResult> onBefore(RouteContext context) async {
    User? user = await Auth.user<User>();

    if (user == null || !params.contains(user.role)) {
      return redirect(UnauthorizedPage.path);
    }

    return next();
  }
}

// Usage
router.route(
  AdminPage.path,
  (_) => AdminPage(),
  routeGuards: [RoleGuard(["admin", "super_admin"])],
);

Esempi

Guard di Autenticazione

class AuthRouteGuard extends NyRouteGuard {
  AuthRouteGuard();

  @override
  Future<GuardResult> onBefore(RouteContext context) async {
    bool isAuthenticated = await Auth.isAuthenticated();

    if (!isAuthenticated) {
      return redirect(HomePage.path);
    }

    return next();
  }
}

Guard di Abbonamento con Parametri

class SubscriptionGuard extends ParameterizedGuard<List<String>> {
  SubscriptionGuard(super.params);

  @override
  Future<GuardResult> onBefore(RouteContext context) async {
    User? user = await Auth.user<User>();
    bool hasAccess = params.any((plan) => user?.subscription == plan);

    if (!hasAccess) {
      return redirect(UpgradePage.path, data: {"plans": params});
    }

    setData({"user": user});
    return next();
  }
}

// Require premium or pro subscription
router.route(
  PremiumPage.path,
  (_) => PremiumPage(),
  routeGuards: [
    AuthRouteGuard(),
    SubscriptionGuard(["premium", "pro"]),
  ],
);

Guard di Logging

class LoggingGuard extends NyRouteGuard {
  LoggingGuard();

  @override
  Future<GuardResult> onBefore(RouteContext context) async {
    print("Navigating to: ${context.routeName}");
    return next();
  }

  @override
  Future<void> onAfter(RouteContext context) async {
    print("Arrived at: ${context.routeName}");
  }
}