Skip to main content

Overview

BarberApp uses functional Angular route guards to protect routes. All guards are located in src/app/core/guards/ and use dependency injection to access facades and services.
All guards are implemented as async functions to properly wait for authentication state checks.

Authentication Guards

authGuard

Protects routes that require user authentication. Location: src/app/core/guards/auth.guard.ts:5 Type: CanActivateFn
export const authGuard = async () => {
  const authFacade = inject(AuthFacade);
  const router = inject(Router);

  while (authFacade.isCheckingAuth()) {
    await new Promise((resolve) => setTimeout(resolve, 10));
  }

  const isAuthenticated = authFacade.isAuthenticated();
  if (isAuthenticated) {
    return true;
  } else {
    router.navigate(['/auth/login']);
    return false;
  }
};

Behavior

1

Wait for Auth Check

Polls authFacade.isCheckingAuth() until authentication state is determined
2

Verify Authentication

Checks if user is authenticated via authFacade.isAuthenticated()
3

Allow or Redirect

  • Authenticated: Returns true, allows access
  • Not Authenticated: Redirects to /auth/login, returns false

Usage

routes.ts
{
  path: 'dashboard',
  canActivate: [authGuard],
  loadChildren: () => import('./dashboard/routes')
}
This guard waits for the auth check to complete, which may cause a brief delay on initial page load.

publicGuard

Protects public routes (login, register) from already authenticated users. Location: src/app/core/guards/public.guard.ts:6 Type: CanActivateFn
export const publicGuard = async () => {
  const authFacade = inject(AuthFacade);
  const router = inject(Router);

  while (authFacade.isCheckingAuth()) {
    await new Promise(resolve => setTimeout(resolve, 10));
  }

  const user = authFacade.user();
  if (!user) {
    return true; // Allow access to public pages if not authenticated
  }

  // If already authenticated, redirect to their dashboard
  switch (user.role) {
    case UserRoles.CLIENT:
      router.navigate(['/dashboard/client']);
      break;
    case UserRoles.SPECIALIST:
      router.navigate(['/dashboard/specialist']);
      break;
    case UserRoles.ADMIN:
      router.navigate(['/dashboard/specialist']);
      break;
    default:
      router.navigate(['/dashboard/client']);
  }
  return false;
};

Behavior

1

Wait for Auth Check

Waits until authentication state is determined
2

Check User

  • No user: Returns true, allows access to public page
  • User exists: Redirects to appropriate dashboard based on role

Usage

auth.routes.ts
{
  path: 'login',
  canActivate: [publicGuard],
  component: LoginPageComponent
},
{
  path: 'register',
  canActivate: [publicGuard],
  component: RegisterPageComponent
}
This prevents authenticated users from seeing login/register pages unnecessarily.

Authorization Guards

roleGuard

Protects routes based on user role, with automatic redirection to the correct dashboard. Location: src/app/core/guards/role.guard.ts:6 Type: Factory function returning CanActivateFn
export const roleGuard = (allowedRoles: UserRoles[]) => {
  return async () => {
    const authFacade = inject(AuthFacade);
    const router = inject(Router);

    while (authFacade.isCheckingAuth()) {
      await new Promise(resolve => setTimeout(resolve, 10));
    }

    const user = authFacade.user();
    if (!user) {
      router.navigate(['/auth/login']);
      return false;
    }

    if (allowedRoles.includes(user.role)) {
      return true;
    } else {
      // Redirect to the corresponding dashboard according to the user's role
      switch (user.role) {
        case UserRoles.CLIENT:
          router.navigate(['/dashboard/client']);
          break;
        case UserRoles.SPECIALIST:
          router.navigate(['/dashboard/specialist']);
          break;
        case UserRoles.ADMIN:
          router.navigate(['/dashboard/specialist']);
          break;
        default:
          router.navigate(['/dashboard/client']);
      }
      return false;
    }
  };
};

Parameters

allowedRoles
UserRoles[]
required
Array of roles permitted to access the route

Behavior

1

Wait for Auth Check

Waits for authentication state to be determined
2

Verify User Exists

Redirects to login if no user is authenticated
3

Check Role

  • Role allowed: Returns true, grants access
  • Role not allowed: Redirects to user’s appropriate dashboard

Usage

{
  path: 'request-appointment',
  canActivate: [roleGuard([UserRoles.CLIENT])],
  component: RequestAppointmentComponent
}
Admin users are currently redirected to the specialist dashboard. Adjust this behavior if you create an admin-specific dashboard.

Resource-Specific Guards

idGuard

Protects appointment detail routes, ensuring users can only access their own appointments. Location: src/app/core/guards/id.guard.ts:6 Type: CanActivateFn with route parameter
export const idGuard = async (
  route: ActivatedRouteSnapshot
): Promise<boolean> => {
  const authFacade = inject(AuthFacade);
  const appointmentFacade = inject(AppointmentFacade);
  const router = inject(Router);

  const appointmentId = route.paramMap.get('id');
  const user = authFacade.user();

  if (!appointmentId || !user) {
    router.navigate(['/']);
    return false;
  }

  await appointmentFacade.loadAppointmentById(appointmentId);
  const appointment = appointmentFacade.selectedAppointment();

  if (
    appointment &&
    (user.id === appointment.clientId || user.id === appointment.specialistId)
  ) {
    return true;
  }

  router.navigate(['/dashboard/appointments-list']);
  return false;
};

Behavior

1

Extract Route Parameters

Gets appointment ID from route and current user
2

Load Appointment

Fetches appointment details via AppointmentFacade
3

Verify Ownership

  • User is client or specialist: Returns true, grants access
  • User is neither: Redirects to appointments list

Usage

appointment.routes.ts
{
  path: 'appointment/:id',
  canActivate: [idGuard],
  component: AppointmentDetailComponent
}
This guard loads appointment data during route activation, which may cause a delay if the appointment isn’t cached.

recordGuard

Protects medical record routes, ensuring only the client or specialists can view records. Location: src/app/core/guards/record.guard.ts:6 Type: CanActivateFn with route parameter
export const recordGuard = (
  route: ActivatedRouteSnapshot
): boolean => {
  const authFacade = inject(AuthFacade);
  const router = inject(Router);
  const user = authFacade.user();

  const clientIdFromRoute = route.paramMap.get('id');

  if (!user || !clientIdFromRoute) {
    router.navigate(['/']);
    return false;
  }

  // A specialist can see any client's record.
  if (user.role === UserRoles.SPECIALIST) {
    return true;
  }

  // A client can only see their own record.
  if (user.role === UserRoles.CLIENT) {
    if (user.id === clientIdFromRoute) {
      return true;
    }
  }

  router.navigate(['/dashboard/client']);
  return false;
};

Behavior

1

Extract Parameters

Gets client ID from route and current user
2

Check Role

  • Specialist: Always allowed
  • Client: Only allowed if viewing own record
  • Other: Denied and redirected

Usage

record.routes.ts
{
  path: 'record/:id',
  canActivate: [recordGuard],
  component: MedicalRecordComponent
}
This implements a simple authorization model: specialists can view all records, clients can only view their own.

Guard Composition

You can combine multiple guards on a single route:
{
  path: 'specialist/appointments',
  canActivate: [
    authGuard,
    roleGuard([UserRoles.SPECIALIST])
  ],
  component: SpecialistAppointmentsComponent
}
Guards are executed in order. If any guard returns false, the route is blocked and subsequent guards are not executed.

Testing Guards

Example test for authGuard:
auth.guard.spec.ts
describe('authGuard', () => {
  it('should allow authenticated users', async () => {
    const authFacade = TestBed.inject(AuthFacade);
    authFacade.setUser(mockUser);
    
    const result = await TestBed.runInInjectionContext(() => authGuard());
    
    expect(result).toBe(true);
  });

  it('should redirect unauthenticated users to login', async () => {
    const router = TestBed.inject(Router);
    const navigateSpy = spyOn(router, 'navigate');
    
    const result = await TestBed.runInInjectionContext(() => authGuard());
    
    expect(result).toBe(false);
    expect(navigateSpy).toHaveBeenCalledWith(['/auth/login']);
  });
});
All guards are functional (not class-based), so they should be tested using TestBed.runInInjectionContext().

Build docs developers (and LLMs) love