diff --git a/apps/angular/66-functional-auth-guard/.eslintrc.json b/apps/angular/66-functional-auth-guard/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "app",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "app",
+ "style": "kebab-case"
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/angular/66-functional-auth-guard/README.md b/apps/angular/66-functional-auth-guard/README.md
new file mode 100644
index 000000000..247e3e054
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/README.md
@@ -0,0 +1,13 @@
+# functional-auth-guard
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-functional-auth-guard
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/66-functional-auth-guard/).
diff --git a/apps/angular/66-functional-auth-guard/project.json b/apps/angular/66-functional-auth-guard/project.json
new file mode 100644
index 000000000..d4c024a74
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/project.json
@@ -0,0 +1,87 @@
+{
+ "name": "angular-functional-auth-guard",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/66-functional-auth-guard/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular/build:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/66-functional-auth-guard",
+ "browser": "apps/angular/66-functional-auth-guard/src/main.ts",
+ "tsConfig": "apps/angular/66-functional-auth-guard/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "apps/angular/66-functional-auth-guard/public"
+ }
+ ],
+ "styles": ["apps/angular/66-functional-auth-guard/src/styles.scss"]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "4kb",
+ "maximumError": "8kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "continuous": true,
+ "executor": "@angular/build:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-functional-auth-guard:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-functional-auth-guard:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "executor": "@angular/build:extract-i18n",
+ "options": {
+ "buildTarget": "angular-functional-auth-guard:build"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ },
+ "test": {
+ "executor": "@angular/build:unit-test",
+ "options": {
+ "runnerConfig": "apps/angular/66-functional-auth-guard/vitest-base.config.ts"
+ }
+ },
+ "serve-static": {
+ "continuous": true,
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "angular-functional-auth-guard:build",
+ "staticFilePath": "dist/apps/angular/66-functional-auth-guard/browser",
+ "spa": true
+ }
+ }
+ }
+}
diff --git a/apps/angular/66-functional-auth-guard/public/favicon.ico b/apps/angular/66-functional-auth-guard/public/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/angular/66-functional-auth-guard/public/favicon.ico differ
diff --git a/apps/angular/66-functional-auth-guard/src/app/admin.component.ts b/apps/angular/66-functional-auth-guard/src/app/admin.component.ts
new file mode 100644
index 000000000..e44150aea
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/admin.component.ts
@@ -0,0 +1,15 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
+
+@Component({
+ selector: 'app-admin',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [RouterLink],
+})
+export class AdminComponent {}
diff --git a/apps/angular/66-functional-auth-guard/src/app/admin.guard.ts b/apps/angular/66-functional-auth-guard/src/app/admin.guard.ts
new file mode 100644
index 000000000..6bbfbd2d1
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/admin.guard.ts
@@ -0,0 +1,10 @@
+import { inject } from '@angular/core';
+import { CanActivateFn, Router } from '@angular/router';
+import { AuthService } from './auth.service';
+
+export const adminGuard: CanActivateFn = () => {
+ const authService = inject(AuthService);
+ const router = inject(Router);
+
+ return authService.isAdmin() || router.createUrlTree(['/']);
+};
diff --git a/apps/angular/66-functional-auth-guard/src/app/app.component.spec.ts b/apps/angular/66-functional-auth-guard/src/app/app.component.spec.ts
new file mode 100644
index 000000000..4b8632966
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/app.component.spec.ts
@@ -0,0 +1,54 @@
+import { TestBed } from '@angular/core/testing';
+import { Router, provideRouter } from '@angular/router';
+import { AppComponent } from './app.component';
+import { AuthService } from './auth.service';
+import { routes } from './app.routes';
+
+describe('Functional Auth Guards', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [provideRouter(routes)],
+ });
+ TestBed.createComponent(AppComponent);
+ });
+
+ describe('authGuard', () => {
+ it('should redirect to / when not logged in', async () => {
+ const router = TestBed.inject(Router);
+ await router.navigate(['/dashboard']);
+ expect(router.url).toBe('/');
+ });
+
+ it('should allow navigation when logged in', async () => {
+ const authService = TestBed.inject(AuthService);
+ const router = TestBed.inject(Router);
+ authService.login('user');
+ await router.navigate(['/dashboard']);
+ expect(router.url).toBe('/dashboard');
+ });
+ });
+
+ describe('adminGuard', () => {
+ it('should redirect to / when not logged in', async () => {
+ const router = TestBed.inject(Router);
+ await router.navigate(['/admin']);
+ expect(router.url).toBe('/');
+ });
+
+ it('should redirect to / when logged in as user', async () => {
+ const authService = TestBed.inject(AuthService);
+ const router = TestBed.inject(Router);
+ authService.login('user');
+ await router.navigate(['/admin']);
+ expect(router.url).toBe('/');
+ });
+
+ it('should allow navigation when logged in as admin', async () => {
+ const authService = TestBed.inject(AuthService);
+ const router = TestBed.inject(Router);
+ authService.login('admin');
+ await router.navigate(['/admin']);
+ expect(router.url).toBe('/admin');
+ });
+ });
+});
diff --git a/apps/angular/66-functional-auth-guard/src/app/app.component.ts b/apps/angular/66-functional-auth-guard/src/app/app.component.ts
new file mode 100644
index 000000000..6f48a095f
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/app.component.ts
@@ -0,0 +1,10 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ selector: 'app-root',
+ template: ``,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [RouterOutlet],
+})
+export class AppComponent {}
diff --git a/apps/angular/66-functional-auth-guard/src/app/app.config.ts b/apps/angular/66-functional-auth-guard/src/app/app.config.ts
new file mode 100644
index 000000000..414eeaeb8
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/app.config.ts
@@ -0,0 +1,16 @@
+import {
+ ApplicationConfig,
+ provideBrowserGlobalErrorListeners,
+ provideZoneChangeDetection,
+} from '@angular/core';
+import { provideRouter } from '@angular/router';
+
+import { routes } from './app.routes';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideBrowserGlobalErrorListeners(),
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(routes),
+ ],
+};
diff --git a/apps/angular/66-functional-auth-guard/src/app/app.routes.ts b/apps/angular/66-functional-auth-guard/src/app/app.routes.ts
new file mode 100644
index 000000000..0a43617e9
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/app.routes.ts
@@ -0,0 +1,12 @@
+import { Routes } from '@angular/router';
+import { adminGuard } from './admin.guard';
+import { AdminComponent } from './admin.component';
+import { authGuard } from './auth.guard';
+import { DashboardComponent } from './dashboard.component';
+import { HomeComponent } from './home.component';
+
+export const routes: Routes = [
+ { path: '', component: HomeComponent },
+ { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
+ { path: 'admin', component: AdminComponent, canActivate: [authGuard, adminGuard] },
+];
diff --git a/apps/angular/66-functional-auth-guard/src/app/auth.guard.ts b/apps/angular/66-functional-auth-guard/src/app/auth.guard.ts
new file mode 100644
index 000000000..abb444031
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/auth.guard.ts
@@ -0,0 +1,10 @@
+import { inject } from '@angular/core';
+import { CanActivateFn, Router } from '@angular/router';
+import { AuthService } from './auth.service';
+
+export const authGuard: CanActivateFn = () => {
+ const authService = inject(AuthService);
+ const router = inject(Router);
+
+ return authService.isLoggedIn() || router.createUrlTree(['/']);
+};
diff --git a/apps/angular/66-functional-auth-guard/src/app/auth.service.ts b/apps/angular/66-functional-auth-guard/src/app/auth.service.ts
new file mode 100644
index 000000000..012ffb44c
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/auth.service.ts
@@ -0,0 +1,21 @@
+import { computed, Injectable, signal } from '@angular/core';
+
+@Injectable({ providedIn: 'root' })
+export class AuthService {
+ private readonly _isLoggedIn = signal(false);
+ private readonly _role = signal<'user' | 'admin'>('user');
+
+ readonly isLoggedIn = this._isLoggedIn.asReadonly();
+ readonly role = this._role.asReadonly();
+ readonly isAdmin = computed(() => this._isLoggedIn() && this._role() === 'admin');
+
+ login(role: 'user' | 'admin'): void {
+ this._isLoggedIn.set(true);
+ this._role.set(role);
+ }
+
+ logout(): void {
+ this._isLoggedIn.set(false);
+ this._role.set('user');
+ }
+}
diff --git a/apps/angular/66-functional-auth-guard/src/app/dashboard.component.ts b/apps/angular/66-functional-auth-guard/src/app/dashboard.component.ts
new file mode 100644
index 000000000..bf5db8a90
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/dashboard.component.ts
@@ -0,0 +1,15 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
+
+@Component({
+ selector: 'app-dashboard',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [RouterLink],
+})
+export class DashboardComponent {}
diff --git a/apps/angular/66-functional-auth-guard/src/app/home.component.ts b/apps/angular/66-functional-auth-guard/src/app/home.component.ts
new file mode 100644
index 000000000..5b56ed2f4
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/app/home.component.ts
@@ -0,0 +1,47 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { AuthService } from './auth.service';
+
+@Component({
+ selector: 'app-home',
+ template: `
+
+
Home
+
+
+ Status:
+
+ {{ authService.isLoggedIn() ? 'Logged in as ' + authService.role() : 'Not logged in' }}
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [RouterLink],
+})
+export class HomeComponent {
+ protected authService = inject(AuthService);
+}
diff --git a/apps/angular/66-functional-auth-guard/src/index.html b/apps/angular/66-functional-auth-guard/src/index.html
new file mode 100644
index 000000000..592c9264b
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-functional-auth-guard
+
+
+
+
+
+
+
+
diff --git a/apps/angular/66-functional-auth-guard/src/main.ts b/apps/angular/66-functional-auth-guard/src/main.ts
new file mode 100644
index 000000000..7205a13df
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/main.ts
@@ -0,0 +1,5 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
diff --git a/apps/angular/66-functional-auth-guard/src/styles.scss b/apps/angular/66-functional-auth-guard/src/styles.scss
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/styles.scss
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/apps/angular/66-functional-auth-guard/src/test-setup/no-teardown.ts b/apps/angular/66-functional-auth-guard/src/test-setup/no-teardown.ts
new file mode 100644
index 000000000..655736368
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/src/test-setup/no-teardown.ts
@@ -0,0 +1,25 @@
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserTestingModule,
+ platformBrowserTesting,
+} from '@angular/platform-browser/testing';
+import { beforeEach } from 'vitest';
+
+/**
+ * @see https://github.com/angular/angular-cli/issues/31733
+ */
+const symbol = Symbol.for('@angular-challenge/testbed-setup');
+const g = globalThis as unknown as { [symbol]: boolean };
+
+if (!g[symbol]) {
+ g[symbol] = true;
+
+ getTestBed().resetTestEnvironment();
+ getTestBed().initTestEnvironment(
+ BrowserTestingModule,
+ platformBrowserTesting(),
+ { teardown: { destroyAfterEach: false } },
+ );
+}
+
+beforeEach(() => getTestBed().resetTestingModule());
diff --git a/apps/angular/66-functional-auth-guard/tailwind.config.js b/apps/angular/66-functional-auth-guard/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/tailwind.config.js
@@ -0,0 +1,14 @@
+const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
+const { join } = require('path');
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
+ ...createGlobPatternsForDependencies(__dirname),
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/apps/angular/66-functional-auth-guard/tsconfig.app.json b/apps/angular/66-functional-auth-guard/tsconfig.app.json
new file mode 100644
index 000000000..5d5af52c7
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": []
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": [
+ "jest.config.ts",
+ "src/test-setup.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts"
+ ]
+}
diff --git a/apps/angular/66-functional-auth-guard/tsconfig.json b/apps/angular/66-functional-auth-guard/tsconfig.json
new file mode 100644
index 000000000..1ae47abeb
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "target": "es2022",
+ "moduleResolution": "bundler",
+ "isolatedModules": true,
+ "emitDecoratorMetadata": false,
+ "module": "preserve",
+ "lib": ["dom", "es2022"]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "typeCheckHostBindings": true,
+ "strictTemplates": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/apps/angular/66-functional-auth-guard/tsconfig.spec.json b/apps/angular/66-functional-auth-guard/tsconfig.spec.json
new file mode 100644
index 000000000..55718726e
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/tsconfig.spec.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": ["vitest/globals"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts"]
+}
diff --git a/apps/angular/66-functional-auth-guard/vitest-base.config.ts b/apps/angular/66-functional-auth-guard/vitest-base.config.ts
new file mode 100644
index 000000000..d5747f9a9
--- /dev/null
+++ b/apps/angular/66-functional-auth-guard/vitest-base.config.ts
@@ -0,0 +1,20 @@
+import { playwright } from '@vitest/browser-playwright';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ setupFiles: [
+ 'apps/angular/66-functional-auth-guard/src/test-setup/no-teardown.ts',
+ ],
+ testTimeout: 3_000,
+ browser: {
+ enabled: true,
+ provider: playwright({
+ launchOptions: {
+ args: ['--remote-debugging-port=9222'],
+ },
+ }),
+ instances: [{ browser: 'chromium' }],
+ },
+ },
+});
diff --git a/challenge-number.json b/challenge-number.json
index c828ee7e3..f8ba4a130 100644
--- a/challenge-number.json
+++ b/challenge-number.json
@@ -1,6 +1,6 @@
{
- "total": 62,
- "🟢": 25,
+ "total": 63,
+ "🟢": 26,
"🟠": 124,
"🔴": 212
}