Skip to content

Latest commit

 

History

History
352 lines (261 loc) · 7.77 KB

File metadata and controls

352 lines (261 loc) · 7.77 KB
title Guards Overview
description Access control system for routes and components using guard functions

Guards Overview

Guards are functions that control access to routes and UI elements in AppShell. They provide a flexible, composable way to implement permission-based and role-based access control.

How Guards Work

Guards are executed sequentially and can return one of three results:

  • pass() - Allow access, continue to next guard
  • hidden() - Deny access, return 404
  • redirectTo(path) - Redirect to another page

If any guard returns hidden() or redirectTo(), execution stops immediately.

Guard Function Signature

type Guard = (ctx: GuardContext) => GuardResult | Promise<GuardResult>;

type GuardContext = {
  context: ContextData; // Your custom context from AppShell
};

type GuardResult = { type: "pass" } | { type: "hidden" } | { type: "redirect"; to: string };

Guard Functions

pass()

Allows access and continues to the next guard.

import { pass } from "@tailor-platform/app-shell";

const allowAll: Guard = () => pass();

Full Reference →

hidden()

Denies access and shows 404 Not Found.

import { hidden } from "@tailor-platform/app-shell";

const denyAll: Guard = () => hidden();

Full Reference →

redirectTo(path)

Redirects to another page.

import { redirectTo } from "@tailor-platform/app-shell";

const redirectToLogin: Guard = () => redirectTo("/login");

Full Reference →

Usage Locations

Route-Level Guards

Applied to modules and resources:

import { defineModule, pass, hidden } from "@tailor-platform/app-shell";

const adminModule = defineModule({
  path: "admin",
  component: AdminPage,
  guards: [
    ({ context }) => {
      return context.currentUser?.role === "admin" ? pass() : hidden();
    },
  ],
});

Component-Level Guards

Applied to UI elements with WithGuard:

import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";

const isAdmin = ({ context }) =>
  context.currentUser?.role === "admin" ? pass() : hidden();

<WithGuard guards={[isAdmin]}>
  <Button>Delete All</Button>
</WithGuard>

Context Data

Guards receive context data from AppShell:

// Define your context type
declare module "@tailor-platform/app-shell" {
  interface AppShellRegister {
    contextData: {
      currentUser: User | null;
      permissions: string[];
      featureFlags: FeatureFlags;
    };
  }
}

// Pass context to AppShell
<AppShell
  modules={modules}
  contextData={{
    currentUser,
    permissions,
    featureFlags,
  }}
>
  {/* ... */}
</AppShell>

// Access in guards
const guard: Guard = ({ context }) => {
  // context is fully typed!
  if (context.currentUser) {
    return pass();
  }
  return hidden();
};

Common Patterns

Authentication Check

const requireAuth: Guard = ({ context }) => {
  if (!context.currentUser) {
    return redirectTo("/login");
  }
  return pass();
};

Role-Based Access

const requireAdmin: Guard = ({ context }) => {
  if (context.currentUser?.role !== "admin") {
    return hidden();
  }
  return pass();
};

Permission-Based Access

const requirePermission =
  (permission: string): Guard =>
  ({ context }) => {
    if (!context.permissions.includes(permission)) {
      return hidden();
    }
    return pass();
  };

// Usage
guards: [requirePermission("users:delete")];

Feature Flags

const requireFeature =
  (flag: string): Guard =>
  ({ context }) => {
    if (!context.featureFlags[flag]) {
      return hidden();
    }
    return pass();
  };

Async Permission Check

const checkApiPermission: Guard = async ({ context }) => {
  const response = await fetch("/api/permissions/admin");
  const { allowed } = await response.json();
  return allowed ? pass() : hidden();
};

Multiple Guards (AND logic)

const guards = [requireAuth, requireAdmin, requireFeature("beta")];

// All must pass() for access to be granted
defineModule({
  path: "admin",
  guards: guards,
});

Reusable Guard Library

// guards.ts
export const requireAuth: Guard = ({ context }) => {
  return context.currentUser ? pass() : redirectTo("/login");
};

export const requireRole =
  (role: string): Guard =>
  ({ context }) => {
    return context.currentUser?.role === role ? pass() : hidden();
  };

export const requirePermission =
  (perm: string): Guard =>
  ({ context }) => {
    return context.permissions.includes(perm) ? pass() : hidden();
  };

// Usage across application
import { requireAuth, requireRole } from "./guards";

defineModule({
  path: "admin",
  guards: [requireAuth, requireRole("admin")],
});

Guard Execution Order

Guards are executed sequentially in the order they're defined:

guards: [guardA, guardB, guardC];

// Execution:
// 1. guardA runs → if not pass(), stop
// 2. guardB runs → if not pass(), stop
// 3. guardC runs → if not pass(), stop
// 4. All passed → grant access

Navigation Behavior

When guards return hidden():

  • Route returns 404 Not Found
  • Item hidden from sidebar navigation
  • Item hidden from CommandPalette
  • Breadcrumbs don't show the page

When guards return redirectTo():

  • User is redirected immediately
  • Original URL is not accessible
  • Useful for login flows

Best Practices

Do:

  • ✅ Keep guards simple and focused
  • ✅ Reuse guards across routes and components
  • ✅ Use descriptive names (requireAuth, not guard1)
  • ✅ Return quickly for better performance
  • ✅ Cache expensive checks when possible

Don't:

  • ❌ Make guards too complex (extract logic)
  • ❌ Have side effects in guards (logging OK)
  • ❌ Duplicate guard logic (DRY principle)
  • ❌ Forget to handle async errors
  • ❌ Use redirectTo() in WithGuard (not supported)

TypeScript

Full type safety:

import { type Guard, type GuardContext, type GuardResult } from "@tailor-platform/app-shell";

const myGuard: Guard = (ctx: GuardContext): GuardResult => {
  // Fully typed context
  return pass();
};

// Async guard
const asyncGuard: Guard = async (ctx): Promise<GuardResult> => {
  await someAsyncCheck();
  return pass();
};

Performance

Sync Guards (Fast)

const isAdmin: Guard = ({ context }) => {
  return context.currentUser?.role === "admin" ? pass() : hidden();
};
// ~0ms - Instant

Async Guards (Slower)

const checkPermission: Guard = async ({ context }) => {
  const allowed = await fetch("/api/check");
  return allowed ? pass() : hidden();
};
// ~50-200ms - Network request

Tip: Prefer sync guards when possible. Cache API results in context data.

Comparison: Route vs Component Guards

Aspect Route Guards Component Guards (WithGuard)
Location defineModule/defineResource WithGuard component
Supports pass() ✅ Yes ✅ Yes
Supports hidden() ✅ Yes ✅ Yes
Supports redirectTo() ✅ Yes ❌ No
Execution Before route loads During render
Use Case Page access control UI element visibility

Related