Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/angular/66-functional-auth-guard/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
]
}
13 changes: 13 additions & 0 deletions apps/angular/66-functional-auth-guard/README.md
Original file line number Diff line number Diff line change
@@ -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/).
87 changes: 87 additions & 0 deletions apps/angular/66-functional-auth-guard/project.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Binary file not shown.
15 changes: 15 additions & 0 deletions apps/angular/66-functional-auth-guard/src/app/admin.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
selector: 'app-admin',
template: `
<div class="flex min-h-screen flex-col items-center justify-center gap-4 p-8">
<h1 class="text-3xl font-bold">Admin</h1>
<a class="text-blue-600 underline" routerLink="/">Back to Home</a>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink],
})
export class AdminComponent {}
10 changes: 10 additions & 0 deletions apps/angular/66-functional-auth-guard/src/app/admin.guard.ts
Original file line number Diff line number Diff line change
@@ -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(['/']);
};
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
10 changes: 10 additions & 0 deletions apps/angular/66-functional-auth-guard/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
selector: 'app-root',
template: `<router-outlet />`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet],
})
export class AppComponent {}
16 changes: 16 additions & 0 deletions apps/angular/66-functional-auth-guard/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -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),
],
};
12 changes: 12 additions & 0 deletions apps/angular/66-functional-auth-guard/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -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] },
];
10 changes: 10 additions & 0 deletions apps/angular/66-functional-auth-guard/src/app/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -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(['/']);
};
21 changes: 21 additions & 0 deletions apps/angular/66-functional-auth-guard/src/app/auth.service.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
selector: 'app-dashboard',
template: `
<div class="flex min-h-screen flex-col items-center justify-center gap-4 p-8">
<h1 class="text-3xl font-bold">Dashboard</h1>
<a class="text-blue-600 underline" routerLink="/">Back to Home</a>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink],
})
export class DashboardComponent {}
47 changes: 47 additions & 0 deletions apps/angular/66-functional-auth-guard/src/app/home.component.ts
Original file line number Diff line number Diff line change
@@ -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: `
<div class="flex min-h-screen flex-col items-center justify-center gap-6 p-8">
<h1 class="text-3xl font-bold">Home</h1>

<p class="text-gray-600">
Status:
<span class="font-semibold">
{{ authService.isLoggedIn() ? 'Logged in as ' + authService.role() : 'Not logged in' }}
</span>
</p>

<div class="flex gap-3">
<button
class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
(click)="authService.login('user')">
Login as User
</button>
<button
class="rounded bg-purple-600 px-4 py-2 text-white hover:bg-purple-700"
(click)="authService.login('admin')">
Login as Admin
</button>
<button
class="rounded bg-gray-300 px-4 py-2 text-gray-800 hover:bg-gray-400"
(click)="authService.logout()">
Logout
</button>
</div>

<div class="flex gap-4">
<a class="text-blue-600 underline" routerLink="/dashboard">Go to Dashboard</a>
<a class="text-blue-600 underline" routerLink="/admin">Go to Admin</a>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink],
})
export class HomeComponent {
protected authService = inject(AuthService);
}
13 changes: 13 additions & 0 deletions apps/angular/66-functional-auth-guard/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>angular-functional-auth-guard</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>
5 changes: 5 additions & 0 deletions apps/angular/66-functional-auth-guard/src/main.ts
Original file line number Diff line number Diff line change
@@ -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));
3 changes: 3 additions & 0 deletions apps/angular/66-functional-auth-guard/src/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
Loading
Loading