-
Couldn't load subscription status.
- Fork 2.2k
feat(): Angularfire auth guards #2016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
bb720d7
fd99984
67389a1
da76d0a
90bda62
31118b8
e7c21a9
3b93c79
bcff88e
5def10b
dffefa0
fe2a9f7
1335cb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Route users with AngularFire guards | ||
|
|
||
| ## Basic example | ||
|
|
||
| ```ts | ||
| import { AngularFireAuthGuard } from '@angular/fire/auth-guard'; | ||
|
|
||
| export const routes: Routes = [ | ||
| { path: '', component: AppComponent }, | ||
| { path: 'items', component: ItemListComponent, canActivate: [AngularFireAuthGuard] }, | ||
| ] | ||
| ``` | ||
|
|
||
| ## Use our pre-built pipes for common tests | ||
jamesdaniels marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```ts | ||
| import { AngularFireAuthGuard, hasCustomClaim, redirectUnauthorizedTo, redirectLoggedInTo } from '@angular/fire/auth-guard'; | ||
|
|
||
| const adminOnly = hasCustomClaim('admin'); | ||
| const redirectUnauthorizedToLogin = redirectUnauthorizedTo(['login']); | ||
| const redirectLoggedInToItems = redirectLoggedInTo(['items']); | ||
| const belongsToAccount = (next) => hasCustomClaim(`account-${next.params.id}`); | ||
|
|
||
| export const routes: Routes = [ | ||
| { path: '', component: AppComponent }, | ||
| { path: 'login', component: LoginComponent, canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectLoggedInToItems }}, | ||
| { path: 'items', component: ItemListComponent, canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectUnauthorizedToLogin }, | ||
| { path: 'admin', component: AdminComponent, canActivate: [AngularFireAuthGuard], data: { authGuardPipe: adminOnly }}, | ||
| { path: 'accounts/:id', component: AdminComponent, canActivate: [AngularFireAuthGuard], data: { authGuardPipe: belongsToAccount }} | ||
| ]; | ||
| ``` | ||
|
|
||
| ## Increase readability with our `canActivate` helper | ||
|
|
||
| ```ts | ||
| import { canActivate } from '@angular/fire/auth-guard'; | ||
|
|
||
| export const routes: Routes = [ | ||
| { path: '', component: AppComponent }, | ||
| { path: 'login', component: LoginComponent, ...canActivate(redirectLoggedInToItems) }, | ||
| { path: 'items', component: ItemListComponent, ...canActivate(redirectUnauthorizedToLogin) }, | ||
| { path: 'admin', component: AdminComponent, ...canActivate(adminOnly) }, | ||
| { path: 'accounts/:id', component: AdminComponent, ...canActivate(belongsToAccount) } | ||
| ]; | ||
| ``` | ||
|
|
||
| ## Compose your own pipes | ||
|
|
||
| ```ts | ||
| import { pipe, of } from 'rxjs'; | ||
| import { map, switchMap } from 'rxjs/operators'; | ||
| import { customClaims } from '@angular/fire/auth-guard'; | ||
|
|
||
| const editorOnly = pipe(customClaims, map(claims => claims.role === "editor")); | ||
| const redirectToProfileEditOrLogin = map(user => user ? ['profiles', user.uid, 'edit'] : ['login']); | ||
| const onlyAllowSelf = (next) => map(user => !!user && next.params.userId === user.uid); | ||
| const accountAdmin = (next) => pipe(customClaims, map(claims => claims[`account-${next.params.accountId}-role`] === "admin")); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { NgModule } from '@angular/core'; | ||
| import { AngularFireAuthGuard } from './auth-guard'; | ||
|
|
||
| @NgModule({ | ||
| providers: [ AngularFireAuthGuard ] | ||
| }) | ||
| export class AngularFireAuthGuardModule { } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { TestBed, inject } from '@angular/core/testing'; | ||
| import { FirebaseApp, AngularFireModule } from '@angular/fire'; | ||
| import { COMMON_CONFIG } from './test-config'; | ||
| import { AngularFireAuthModule } from '@angular/fire/auth'; | ||
| import { AngularFireAuthGuardModule, AngularFireAuthGuard } from '@angular/fire/auth-guard'; | ||
| import { RouterModule, Router } from '@angular/router'; | ||
| import { APP_BASE_HREF } from '@angular/common'; | ||
|
|
||
| describe('AngularFireAuthGuard', () => { | ||
| let app: FirebaseApp; | ||
| let router: Router; | ||
|
|
||
| beforeEach(() => { | ||
| TestBed.configureTestingModule({ | ||
| imports: [ | ||
| AngularFireModule.initializeApp(COMMON_CONFIG), | ||
| AngularFireAuthModule, | ||
| AngularFireAuthGuardModule, | ||
| RouterModule.forRoot([ | ||
| { path: 'a', redirectTo: '/', canActivate: [AngularFireAuthGuard] } | ||
| ]) | ||
| ], | ||
| providers: [ | ||
| { provide: APP_BASE_HREF, useValue: 'http://localhost:4200/' } | ||
| ] | ||
| }); | ||
| inject([FirebaseApp, Router], (app_: FirebaseApp, router_: Router) => { | ||
| app = app_; | ||
| router = router_; | ||
| })(); | ||
| }); | ||
|
|
||
| afterEach(done => { | ||
| app.delete().then(done, done.fail); | ||
| }); | ||
|
|
||
| it('should be injectable', () => { | ||
| expect(router).toBeTruthy(); | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here's a StackOverflow answer with suggestions on ways to test auth guards. But maybe that's overkill and we just have a dummy user object and quick happy path tests for the different built-in pipes for now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe also spy on the AngularFireAuthGuard pipe to make sure it calls the pipe provided to it with the data provided to it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'll have to put together a proper E2E for this, something I'm working on on another branch. Think I'll punt on building more robust tests for now & take it as an action item. |
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { Injectable, InjectionToken } from '@angular/core'; | ||
| import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; | ||
| import { Observable, of, pipe, UnaryFunction } from 'rxjs'; | ||
| import { map, switchMap, take } from 'rxjs/operators' | ||
| import { User, auth } from 'firebase/app'; | ||
| import { AngularFireAuth } from '@angular/fire/auth'; | ||
|
|
||
| export const EnableRouterGuardListeners = new InjectionToken<boolean>('angularfire2.enableRouterGuardListeners'); | ||
|
|
||
| export type AuthPipeGenerator = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => AuthPipe; | ||
| export type AuthPipe = UnaryFunction<Observable<User|null>, Observable<boolean|any[]>>; | ||
|
|
||
| @Injectable() | ||
| export class AngularFireAuthGuard implements CanActivate { | ||
|
|
||
| constructor(private afAuth: AngularFireAuth, private router: Router) {} | ||
|
|
||
| canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | ||
| const authPipeFactory: AuthPipeGenerator = next.data.authGuardPipe || (() => loggedIn); | ||
| return this.afAuth.user.pipe( | ||
| take(1), | ||
| authPipeFactory(next, state), | ||
| map(canActivate => typeof canActivate == "boolean" ? canActivate : this.router.createUrlTree(canActivate)) | ||
| ); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| export const canActivate = (pipe: AuthPipe|AuthPipeGenerator) => ({ | ||
| canActivate: [ AngularFireAuthGuard ], data: { authGuardPipe: pipe.name === "" ? pipe : () => pipe} | ||
| }); | ||
|
|
||
| export const loggedIn: AuthPipe = map(user => !!user); | ||
| export const isNotAnonymous: AuthPipe = map(user => !!user && !user.isAnonymous); | ||
| export const idTokenResult = switchMap((user: User|null) => user ? user.getIdTokenResult() : of(null)); | ||
jamesdaniels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| export const emailVerified: AuthPipe = map(user => !!user && user.emailVerified); | ||
| export const customClaims = pipe(idTokenResult, map(idTokenResult => idTokenResult ? idTokenResult.claims : [])); | ||
| export const hasCustomClaim = (claim:string) => pipe(customClaims, map(claims => claims.hasOwnProperty(claim))); | ||
| export const redirectUnauthorizedTo = (redirect: any[]) => pipe(loggedIn, map(loggedIn => loggedIn || redirect)); | ||
| export const redirectLoggedInTo = (redirect: any[]) => pipe(loggedIn, map(loggedIn => loggedIn && redirect || true)); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| import './auth-guard.spec'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './public_api'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| { | ||
| "name": "@angular/fire/auth-guard", | ||
| "version": "ANGULARFIRE2_VERSION", | ||
| "description": "The auth guard module", | ||
| "main": "../bundles/auth-guard.umd.js", | ||
| "module": "index.js", | ||
| "es2015": "./es2015/index.js", | ||
| "keywords": [ | ||
| "angular", | ||
| "firebase", | ||
| "rxjs" | ||
| ], | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/angular/angularfire2.git" | ||
| }, | ||
| "author": "angular,firebase", | ||
| "license": "MIT", | ||
| "peerDependencies": { | ||
| "@angular/fire": "ANGULARFIRE2_VERSION", | ||
| "@angular/common": "ANGULAR_VERSION", | ||
| "@angular/core": "ANGULAR_VERSION", | ||
| "@angular/platform-browser": "ANGULAR_VERSION", | ||
| "@angular/platform-browser-dynamic": "ANGULAR_VERSION", | ||
| "@angular/router": "ANGULAR_VERSION", | ||
| "firebase": "FIREBASE_VERSION", | ||
| "rxjs": "RXJS_VERSION", | ||
| "zone.js": "ZONEJS_VERSION" | ||
| }, | ||
| "typings": "index.d.ts" | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './auth-guard'; | ||
| export * from './auth-guard.module'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
|
|
||
| export const COMMON_CONFIG = { | ||
| apiKey: "AIzaSyBVSy3YpkVGiKXbbxeK0qBnu3-MNZ9UIjA", | ||
| authDomain: "angularfire2-test.firebaseapp.com", | ||
| databaseURL: "https://angularfire2-test.firebaseio.com", | ||
| storageBucket: "angularfire2-test.appspot.com", | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "baseUrl": ".", | ||
| "experimentalDecorators": true, | ||
| "emitDecoratorMetadata": true, | ||
| "module": "es2015", | ||
| "target": "es2015", | ||
| "noImplicitAny": false, | ||
| "outDir": "../../dist/packages-dist/auth-guard/es2015", | ||
| "rootDir": ".", | ||
| "sourceMap": true, | ||
| "inlineSources": true, | ||
| "declaration": false, | ||
| "removeComments": true, | ||
| "strictNullChecks": true, | ||
| "lib": ["es2015", "dom", "es2015.promise", "es2015.collection", "es2015.iterable"], | ||
| "skipLibCheck": true, | ||
| "moduleResolution": "node", | ||
| "paths": { | ||
| "@angular/fire": ["../../dist/packages-dist"], | ||
| "@angular/fire/auth": ["../../dist/packages-dist/auth"] | ||
| } | ||
| }, | ||
| "files": [ | ||
| "index.ts", | ||
| "../../node_modules/zone.js/dist/zone.js.d.ts" | ||
| ], | ||
| "angularCompilerOptions": { | ||
| "skipTemplateCodegen": true, | ||
| "strictMetadataEmit": true, | ||
| "enableSummariesForJit": false | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "extends": "./tsconfig-build.json", | ||
| "compilerOptions": { | ||
| "target": "es5", | ||
| "outDir": "../../dist/packages-dist/auth-guard", | ||
| "declaration": true | ||
| }, | ||
| "files": [ | ||
| "public_api.ts", | ||
| "../../node_modules/zone.js/dist/zone.js.d.ts" | ||
| ], | ||
| "angularCompilerOptions": { | ||
| "skipTemplateCodegen": true, | ||
| "strictMetadataEmit": true, | ||
| "enableSummariesForJit": false, | ||
| "flatModuleOutFile": "index.js", | ||
| "flatModuleId": "@angular/fire/auth-guard" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "extends": "./tsconfig-esm.json", | ||
| "compilerOptions": { | ||
| "baseUrl": ".", | ||
| "paths": { | ||
| "@angular/fire": ["../../dist/packages-dist"], | ||
| "@angular/fire/auth": ["../../dist/packages-dist/auth"], | ||
| "@angular/fire/auth-guard": ["../../dist/packages-dist/auth-guard"] | ||
| } | ||
| }, | ||
| "files": [ | ||
| "index.spec.ts", | ||
| "../../node_modules/zone.js/dist/zone.js.d.ts" | ||
| ] | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.