Skip to content
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

feat(auth-guard): Adding in modular auth guards #3001

Merged
merged 6 commits into from
Oct 8, 2021
Merged
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
6 changes: 6 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
"rules": "test/storage.rules"
},
"emulators": {
"auth": {
"port": 9099
},
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
Expand Down
2 changes: 1 addition & 1 deletion samples/advanced/src/app/app.module.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 9 additions & 25 deletions src/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
import { NgModule, Optional, NgZone, InjectionToken, ModuleWithProviders, APP_INITIALIZER, Injector } from '@angular/core';
import { Analytics as FirebaseAnalytics, isSupported } from 'firebase/analytics';
import { ɵgetDefaultInstanceOf, ɵAngularFireSchedulers, VERSION } from '@angular/fire';
import { Analytics as FirebaseAnalytics } from 'firebase/analytics';
import { ɵgetDefaultInstanceOf, ɵAngularFireSchedulers, VERSION, ɵisAnalyticsSupportedFactory } from '@angular/fire';
import { Analytics, ANALYTICS_PROVIDER_NAME, AnalyticsInstances } from './analytics';
import { FirebaseApps, FirebaseApp } from '@angular/fire/app';
import { registerVersion } from 'firebase/app';
import { ScreenTrackingService } from './screen-tracking.service';
import { UserTrackingService } from './user-tracking.service';

export const PROVIDED_ANALYTICS_INSTANCE_FACTORIES = new InjectionToken<Array<(injector: Injector) => Analytics>>('angularfire2.analytics-instances.factory');
export const PROVIDED_ANALYTICS_INSTANCES = new InjectionToken<Analytics[]>('angularfire2.analytics-instances');
const IS_SUPPORTED = new InjectionToken<boolean>('angularfire2.analytics.isSupported');

const isSupportedValueSymbol = Symbol('angularfire2.analytics.isSupported.value');
export const isSupportedPromiseSymbol = Symbol('angularfire2.analytics.isSupported');

globalThis[isSupportedPromiseSymbol] ||= isSupported().then(it => globalThis[isSupportedValueSymbol] = it);

export function defaultAnalyticsInstanceFactory(isSupported: boolean, provided: FirebaseAnalytics[]|undefined, defaultApp: FirebaseApp) {
if (!isSupported) { return null; }
export function defaultAnalyticsInstanceFactory(provided: FirebaseAnalytics[]|undefined, defaultApp: FirebaseApp) {
if (!ɵisAnalyticsSupportedFactory.sync()) { return null; }
const defaultAnalytics = ɵgetDefaultInstanceOf<FirebaseAnalytics>(ANALYTICS_PROVIDER_NAME, provided, defaultApp);
return defaultAnalytics && new Analytics(defaultAnalytics);
}

export function analyticsInstanceFactory(fn: (injector: Injector) => FirebaseAnalytics) {
return (zone: NgZone, isSupported: boolean, injector: Injector) => {
if (!isSupported) { return null; }
return (zone: NgZone, injector: Injector) => {
if (!ɵisAnalyticsSupportedFactory.sync()) { return null; }
const analytics = zone.runOutsideAngular(() => fn(injector));
return new Analytics(analytics);
};
Expand All @@ -41,7 +34,6 @@ const DEFAULT_ANALYTICS_INSTANCE_PROVIDER = {
provide: Analytics,
useFactory: defaultAnalyticsInstanceFactory,
deps: [
IS_SUPPORTED,
[new Optional(), PROVIDED_ANALYTICS_INSTANCES ],
FirebaseApp,
]
Expand All @@ -53,15 +45,15 @@ const DEFAULT_ANALYTICS_INSTANCE_PROVIDER = {
ANALYTICS_INSTANCES_PROVIDER,
{
provide: APP_INITIALIZER,
useValue: () => globalThis[isSupportedPromiseSymbol],
useValue: ɵisAnalyticsSupportedFactory.async,
multi: true,
}
]
})
export class AnalyticsModule {
constructor(
@Optional() _screenTracking: ScreenTrackingService,
@Optional() _userTracking: UserTrackingService,
@Optional() _screenTrackingService: ScreenTrackingService,
@Optional() _userTrackingService: UserTrackingService,
) {
registerVersion('angularfire', VERSION.full, 'analytics');
}
Expand All @@ -71,19 +63,11 @@ export function provideAnalytics(fn: (injector: Injector) => FirebaseAnalytics,
return {
ngModule: AnalyticsModule,
providers: [{
provide: IS_SUPPORTED,
useFactory: () => globalThis[isSupportedValueSymbol],
}, {
provide: PROVIDED_ANALYTICS_INSTANCE_FACTORIES,
useValue: fn,
multi: true,
}, {
provide: PROVIDED_ANALYTICS_INSTANCES,
useFactory: analyticsInstanceFactory(fn),
multi: true,
deps: [
NgZone,
IS_SUPPORTED,
Injector,
ɵAngularFireSchedulers,
FirebaseApps,
Expand Down
50 changes: 50 additions & 0 deletions src/analytics/analytics.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { TestBed } from '@angular/core/testing';
import { FirebaseApp, provideFirebaseApp, getApp, initializeApp, deleteApp } from '@angular/fire/app';
import { Analytics, provideAnalytics, getAnalytics, isSupported } from '@angular/fire/analytics';
import { COMMON_CONFIG } from '../test-config';
import { rando } from '../utils';

describe('Analytics', () => {
let app: FirebaseApp;
let analytics: Analytics;
let providedAnalytics: Analytics;
let appName: string;

beforeAll(done => {
// The APP_INITIALIZER that is making isSupported() sync for DI may not
// be done evaulating by the time we inject from the TestBed. We can
// ensure correct behavior by waiting for the (global) isSuppported() promise
// to resolve.
isSupported().then(() => done());
});

describe('single injection', () => {

beforeEach(() => {
appName = rando();
TestBed.configureTestingModule({
imports: [
provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)),
provideAnalytics(() => {
providedAnalytics = getAnalytics(getApp(appName));
return providedAnalytics;
}),
],
});
app = TestBed.inject(FirebaseApp);
analytics = TestBed.inject(Analytics);
});

afterEach(() => {
deleteApp(app).catch(() => undefined);
});

it('should be injectable', () => {
expect(providedAnalytics).toBeTruthy();
expect(analytics).toEqual(providedAnalytics);
expect(analytics.app).toEqual(app);
});

});

});
6 changes: 4 additions & 2 deletions src/analytics/firebase.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/analytics/overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ɵisAnalyticsSupportedFactory } from '@angular/fire';

export const isSupported = ɵisAnalyticsSupportedFactory.async;
39 changes: 16 additions & 23 deletions src/analytics/screen-tracking.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Inject, ComponentFactoryResolver, Injectable, NgZone, OnDestroy, Optional, Injector } from '@angular/core';
import { ComponentFactoryResolver, Injectable, NgZone, OnDestroy, Optional, Injector } from '@angular/core';
import { of, Subscription, Observable } from 'rxjs';
import { distinctUntilChanged, filter, groupBy, map, mergeMap, pairwise, startWith, switchMap } from 'rxjs/operators';
import { ActivationEnd, Router, ɵEmptyOutletComponent } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { VERSION } from '@angular/fire';
import { FirebaseApp } from '@angular/fire/app';
import { registerVersion } from 'firebase/app';

import { Analytics } from './analytics';
import { logEvent } from './firebase';
import { logEvent, isSupported } from './firebase';
import { UserTrackingService } from './user-tracking.service';
import { analyticsInstanceFactory, defaultAnalyticsInstanceFactory, isSupportedPromiseSymbol, PROVIDED_ANALYTICS_INSTANCE_FACTORIES } from './analytics.module';

const FIREBASE_EVENT_ORIGIN_KEY = 'firebase_event_origin';
const FIREBASE_PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class';
Expand Down Expand Up @@ -153,28 +151,23 @@ export class ScreenTrackingService implements OnDestroy {
componentFactoryResolver: ComponentFactoryResolver,
zone: NgZone,
@Optional() userTrackingService: UserTrackingService,
firebaseApp: FirebaseApp,
@Inject(PROVIDED_ANALYTICS_INSTANCE_FACTORIES) analyticsInstanceFactories: Array<(injector: Injector) => Analytics>,
injector: Injector,
) {
registerVersion('angularfire', VERSION.full, 'screen-tracking');
if (!router) { return this; }
// Analytics is not ready to be injected yet, as the APP_INITIALIZER hasn't evulated yet, do this the hard way
const analyticsInstance: Promise<Analytics|null> = globalThis[isSupportedPromiseSymbol].then((isSupported: boolean) => {
const analyticsInstances = analyticsInstanceFactories.map(fn => analyticsInstanceFactory(fn)(zone, isSupported, injector));
return defaultAnalyticsInstanceFactory(isSupported, analyticsInstances, firebaseApp);
});
zone.runOutsideAngular(() => {
this.disposable = ɵscreenViewEvent(router, title, componentFactoryResolver).pipe(
switchMap(async params => {
if (userTrackingService) {
await userTrackingService.initialized;
}
const analytics = await analyticsInstance;
if (!analytics) { return; }
return logEvent(analytics, SCREEN_VIEW_EVENT, params);
})
).subscribe();
// The APP_INITIALIZER that is making isSupported() sync for the sake of convenient DI
// may not be done when services are initialized. Guard the functionality by first ensuring
// that the (global) promise has resolved, then get Analytics from the injector.
isSupported().then(() => {
const analytics = injector.get(Analytics);
if (!router || !analytics) { return; }
zone.runOutsideAngular(() => {
this.disposable = ɵscreenViewEvent(router, title, componentFactoryResolver).pipe(
switchMap(async params => {
if (userTrackingService) { await userTrackingService.initialized; }
return logEvent(analytics, SCREEN_VIEW_EVENT, params);
})
).subscribe();
});
});
}

Expand Down
40 changes: 21 additions & 19 deletions src/analytics/user-tracking.service.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
import { Inject, Injectable, Injector, NgZone, OnDestroy } from '@angular/core';
import { Analytics } from './analytics';
import { Injectable, Injector, NgZone, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { VERSION } from '@angular/fire';
import { Auth, authState } from '@angular/fire/auth';
import { registerVersion } from 'firebase/app';
import { setUserId } from './firebase';
import { analyticsInstanceFactory, defaultAnalyticsInstanceFactory, isSupportedPromiseSymbol, PROVIDED_ANALYTICS_INSTANCE_FACTORIES } from './analytics.module';
import { FirebaseApp } from '@angular/fire/app';

import { Analytics } from './analytics';
import { setUserId, isSupported } from './firebase';

@Injectable()
export class UserTrackingService implements OnDestroy {

public readonly initialized: Promise<void>;
private readonly disposables: Array<Subscription> = [];
private disposables: Array<Subscription> = [];

constructor(
auth: Auth,
zone: NgZone,
@Inject(PROVIDED_ANALYTICS_INSTANCE_FACTORIES) analyticsInstanceFactories: Array<(injector: Injector) => Analytics>,
injector: Injector,
firebaseApp: FirebaseApp,
) {
registerVersion('angularfire', VERSION.full, 'user-tracking');
// Analytics is not ready to be injected yet, as the APP_INITIALIZER hasn't evulated yet, do this the hard way
const analyticsInstance: Promise<Analytics|null> = globalThis[isSupportedPromiseSymbol].then((isSupported: boolean) => {
const analyticsInstances = analyticsInstanceFactories.map(fn => analyticsInstanceFactory(fn)(zone, isSupported, injector));
return defaultAnalyticsInstanceFactory(isSupported, analyticsInstances, firebaseApp);
});
let resolveInitialized: () => void;
this.initialized = zone.runOutsideAngular(() => new Promise(resolve => { resolveInitialized = resolve; }));
this.disposables = [
// TODO add credential tracking back in
authState(auth).subscribe(user => {
analyticsInstance.then(analytics => analytics && setUserId(analytics, user?.uid));
// The APP_INITIALIZER that is making isSupported() sync for the sake of convenient DI
// may not be done when services are initialized. Guard the functionality by first ensuring
// that the (global) promise has resolved, then get Analytics from the injector.
isSupported().then(() => {
const analytics = injector.get(Analytics);
if (analytics) {
this.disposables = [
// TODO add credential tracking back in
authState(auth).subscribe(user => {
setUserId(analytics, user?.uid);
resolveInitialized();
}),
];
} else {
resolveInitialized();
}),
];
}
});
}

ngOnDestroy() {
Expand Down
43 changes: 43 additions & 0 deletions src/app-check/app-check.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TestBed } from '@angular/core/testing';
import { FirebaseApp, provideFirebaseApp, getApp, initializeApp, deleteApp } from '@angular/fire/app';
import { Auth, provideAuth, getAuth, connectAuthEmulator } from '@angular/fire/auth';
import { COMMON_CONFIG } from '../test-config';
import { rando } from '../utils';

describe('Auth', () => {
let app: FirebaseApp;
let auth: Auth;
let providedAuth: Auth;
let appName: string;

describe('single injection', () => {

beforeEach(() => {
appName = rando();
TestBed.configureTestingModule({
imports: [
provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)),
provideAuth(() => {
providedAuth = getAuth(getApp(appName));
connectAuthEmulator(providedAuth, 'http://localhost:9099');
return providedAuth;
}),
],
});
app = TestBed.inject(FirebaseApp);
auth = TestBed.inject(Auth);
});

afterEach(() => {
deleteApp(app).catch(() => undefined);
});

it('should be injectable', () => {
expect(auth).toBeTruthy();
expect(auth).toEqual(providedAuth);
expect(auth.app).toEqual(app);
});

});

});
37 changes: 37 additions & 0 deletions src/app/app.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TestBed } from '@angular/core/testing';
import { FirebaseApp, provideFirebaseApp, initializeApp, deleteApp } from '@angular/fire/app';
import { COMMON_CONFIG } from '../test-config';
import { rando } from '../utils';

describe('FirebaseApp', () => {
let app: FirebaseApp;
let providedApp: FirebaseApp;
let appName: string;

describe('single injection', () => {

beforeEach(() => {
appName = rando();
TestBed.configureTestingModule({
imports: [
provideFirebaseApp(() => {
providedApp = initializeApp(COMMON_CONFIG, appName);
return providedApp;
})
],
});
app = TestBed.inject(FirebaseApp);
});

afterEach(() => {
deleteApp(app).catch(() => undefined);
});

it('should be injectable', () => {
expect(app).toBeTruthy();
expect(app).toEqual(providedApp);
});

});

});
Loading