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

Refactor!: auth providers #2634

Merged
merged 11 commits into from
Dec 20, 2024
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
"@angular/platform-browser-dynamic": "~17.2.2",
"@angular/router": "~17.2.2",
"@capacitor-community/file-opener": "^6.0.0",
"@capacitor-firebase/authentication": "^6.1.0",
"@capacitor-firebase/crashlytics": "^6.1.0",
"@capacitor-firebase/performance": "^6.1.0",
"@capacitor-firebase/authentication": "^6.3.1",
"@capacitor-firebase/crashlytics": "^6.3.1",
"@capacitor-firebase/performance": "^6.3.1",
"@capacitor/android": "^6.0.0",
"@capacitor/app": "^6.0.0",
"@capacitor/clipboard": "^6.0.0",
Expand Down
1 change: 0 additions & 1 deletion packages/data-models/appConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ const APP_SIDEMENU_DEFAULTS = {
};

const APP_AUTHENTICATION_DEFAULTS = {
enforceLogin: false,
signInTemplate: "sign_in",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to move this to the auth namespace too?

};

Expand Down
15 changes: 9 additions & 6 deletions packages/data-models/deployment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { IGdriveEntry } from "../@idemsInternational/gdrive-tools";
import type { IAppConfig, IAppConfigOverride } from "./appConfig";

/** Update version to force recompile next time deployment set (e.g. after default config update) */
export const DEPLOYMENT_CONFIG_VERSION = 20241111.0;
export const DEPLOYMENT_CONFIG_VERSION = 20241215.1;

/** Configuration settings available to runtime application */
export interface IDeploymentRuntimeConfig {
Expand Down Expand Up @@ -36,6 +36,13 @@ export interface IDeploymentRuntimeConfig {
/** sentry/glitchtip logging dsn */
dsn: string;
};
/** Enable auth actions by specifying auth provider */
auth: {
/** provider to use with authentication actions. actions will be disabled if no provider specified */
provider?: "firebase" | "supabase";
/** prevent user accessing app pages without being logged in */
enforceLogin?: boolean;
};
/**
* Specify if using firebase for auth and crashlytics.
* Requires firebase config available through encrypted config */
Expand All @@ -51,10 +58,6 @@ export interface IDeploymentRuntimeConfig {
appId: string;
measurementId: string;
};
auth: {
/** Enables `auth` actions to allow user sign-in/out */
enabled: boolean;
};
crashlytics: {
/** Enables app crash reports to firebase crashlytics */
enabled: boolean;
Expand Down Expand Up @@ -200,9 +203,9 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = {
endpoint: "https://apps-server.idems.international/analytics",
},
app_config: {},
auth: {},
firebase: {
config: null,
auth: { enabled: false },
crashlytics: { enabled: true },
},
supabase: {
Expand Down
3 changes: 2 additions & 1 deletion packages/scripts/src/tasks/providers/appData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const copyDeploymentDataToApp = async () => {
const optimiseBuild = async () => new AppDataOptimiser(WorkflowRunner.config).run();

function generateRuntimeConfig(deploymentConfig: IDeploymentConfigJson): IDeploymentRuntimeConfig {
const { analytics, api, app_config, error_logging, firebase, git, name, supabase, web } =
const { analytics, api, app_config, auth, error_logging, firebase, git, name, supabase, web } =
deploymentConfig;

return {
Expand All @@ -77,6 +77,7 @@ function generateRuntimeConfig(deploymentConfig: IDeploymentConfigJson): IDeploy
analytics,
api,
app_config,
auth,
error_logging,
firebase,
name,
Expand Down
26 changes: 0 additions & 26 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export class AppComponent {
public templateTranslateService: TemplateTranslateService,
private crashlyticsService: CrashlyticsService,
private appDataService: AppDataService,
private authService: AuthService,
private seoService: SeoService,
private taskService: TaskService,
private feedbackService: FeedbackService,
Expand All @@ -134,7 +133,6 @@ export class AppComponent {
this.hackSetDeveloperOptions();
const isDeveloperMode = this.templateFieldService.getField("user_mode") === false;
const user = this.userMetaService.userMeta;
await this.loadAuthConfig();

if (!user.first_app_open) {
await this.userMetaService.setUserMeta({ first_app_open: new Date().toISOString() });
Expand Down Expand Up @@ -173,29 +171,6 @@ export class AppComponent {
}
}

/**
* Authentication requires verified domain and app ids populated to firebase console
* Currently only run on native where specified (but can comment out for testing locally)
*/
private async loadAuthConfig() {
const { firebase } = this.deploymentService.config;
const { enforceLogin, signInTemplate } =
this.appConfigService.appConfig().APP_AUTHENTICATION_DEFAULTS;
const ensureLogin = firebase.config && enforceLogin && Capacitor.isNativePlatform();
if (ensureLogin) {
this.authService.ready();
const authUser = await this.authService.getCurrentUser();
if (!authUser) {
const { modal } = await this.templateService.runStandaloneTemplate(signInTemplate, {
showCloseButton: false,
waitForDismiss: false,
});
await this.authService.waitForSignInComplete();
await modal.dismiss();
}
}
}

/**
* Various services set core app data which may be used in templates such as current app day,
* user id etc. Make sure these services have run their initialisation logic before proceeding.
Expand Down Expand Up @@ -240,7 +215,6 @@ export class AppComponent {
this.templateService,
this.templateProcessService,
this.appDataService,
this.authService,
this.serverService,
this.seoService,
this.feedbackService,
Expand Down
4 changes: 2 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ export function lottiePlayerFactory() {
BrowserAnimationsModule,
IonicModule.forRoot(),
AppRoutingModule,
TemplateComponentsModule,
DeploymentFeaturesModule,
HttpClientModule,
SharedModule,
FormsModule,
LottieModule.forRoot({ player: lottiePlayerFactory }),
// NOTE CC 2021-11-04 not sure if cache causes issues or not https://github.com/ngx-lottie/ngx-lottie/issues/115
// LottieCacheModule.forRoot(),
TemplateComponentsModule,
TourModule,
ContextMenuModule,
DeploymentFeaturesModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
Expand Down
3 changes: 2 additions & 1 deletion src/app/deployment-features.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NgModule } from "@angular/core";

import { AnalyticsModule } from "./shared/services/analytics";
import { NavStackModule } from "./feature/nav-stack/nav-stack.module";
import { AuthModule } from "./shared/services/auth/auth.module";

/**
* Module imports required for specific deployment features
Expand All @@ -14,5 +15,5 @@ import { NavStackModule } from "./feature/nav-stack/nav-stack.module";
*
* This is a feature marked for future implementation
*/
@NgModule({ imports: [AnalyticsModule, NavStackModule] })
@NgModule({ imports: [AuthModule, AnalyticsModule, NavStackModule] })
export class DeploymentFeaturesModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { TemplateService } from "../template.service";
import { TemplateTranslateService } from "../template-translate.service";
import { EventService } from "src/app/shared/services/event/event.service";
import { DBSyncService } from "src/app/shared/services/db/db-sync.service";
import { AuthService } from "src/app/shared/services/auth/auth.service";
import { SkinService } from "src/app/shared/services/skin/skin.service";
import { ThemeService } from "src/app/feature/theme/services/theme.service";
import { getGlobalService } from "src/app/shared/services/global.service";
Expand Down Expand Up @@ -65,9 +64,6 @@ export class TemplateActionService extends SyncServiceBase {
private get dbSyncService() {
return getGlobalService(this.injector, DBSyncService);
}
private get authService() {
return getGlobalService(this.injector, AuthService);
}
private get skinService() {
return getGlobalService(this.injector, SkinService);
}
Expand All @@ -93,7 +89,6 @@ export class TemplateActionService extends SyncServiceBase {
this.analyticsService,
this.templateService,
this.eventService,
this.authService,
this.skinService,
]);
}
Expand Down
15 changes: 15 additions & 0 deletions src/app/shared/services/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NgModule } from "@angular/core";

import { AuthService } from "./auth.service";

@NgModule({
imports: [],
exports: [],
providers: [],
})
export class AuthModule {
constructor(private service: AuthService) {
// include service to initialise and register handlers
service.ready();
}
}
97 changes: 53 additions & 44 deletions src/app/shared/services/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,80 @@
import { Injectable } from "@angular/core";
import { FirebaseAuthentication, User } from "@capacitor-firebase/authentication";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { filter } from "rxjs/operators";
import { SyncServiceBase } from "../syncService.base";
import { effect, Injectable, Injector, signal } from "@angular/core";
import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry";
import { FirebaseService } from "../firebase/firebase.service";
import { LocalStorageService } from "../local-storage/local-storage.service";
import { DeploymentService } from "../deployment/deployment.service";
import { AuthProviderBase } from "./providers/base.auth";
import { AsyncServiceBase } from "../asyncService.base";
import { getAuthProvider } from "./providers";
import { IAuthUser } from "./types";
import { filter, firstValueFrom, tap } from "rxjs";
import { TemplateService } from "../../components/template/services/template.service";
import { toObservable } from "@angular/core/rxjs-interop";

@Injectable({
providedIn: "root",
})
export class AuthService extends SyncServiceBase {
private authUser$ = new BehaviorSubject<User | null>(null);
export class AuthService extends AsyncServiceBase {
/** Auth provider used */
private provider: AuthProviderBase;

// include auth import to ensure app registered
constructor(
private templateActionRegistry: TemplateActionRegistry,
private firebaseService: FirebaseService,
private localStorageService: LocalStorageService,
private deploymentService: DeploymentService
private deploymentService: DeploymentService,
private injector: Injector,
private templateService: TemplateService
) {
super("Auth");
this.initialise();
}
private initialise() {
const { firebase } = this.deploymentService.config;
if (firebase?.auth?.enabled && this.firebaseService.app) {
this.addAuthListeners();
this.registerTemplateActionHandlers();
}
}

/** Return a promise that resolves after a signed in user defined */
public async waitForSignInComplete() {
return firstValueFrom(this.authUser$.pipe(filter((value?: User | null) => !!value)));
this.provider = getAuthProvider(this.config.provider);
this.registerInitFunction(this.initialise);
effect(async () => {
const authUser = this.provider.authUser();
this.addStorageEntry(authUser);
});
}

public async signInWithGoogle() {
return FirebaseAuthentication.signInWithGoogle();
private get config() {
return this.deploymentService.config.auth || {};
}

public async signOut() {
return FirebaseAuthentication.signOut();
private async initialise() {
await this.provider.initialise(this.injector);
this.registerTemplateActionHandlers();
if (this.config.enforceLogin) {
// NOTE - Do not await the enforce login to allow other services to initialise in background
this.enforceLogin();
}
}

public async getCurrentUser() {
const { user } = await FirebaseAuthentication.getCurrentUser();
return user;
private async enforceLogin() {
// If user already logged in simply return. If providers auto-login during then waiting to verify
// should be included during the provide init method
if (this.provider.authUser()) {
return;
}
const { signInTemplate } = this.deploymentService.config.app_config.APP_AUTHENTICATION_DEFAULTS;
const { modal } = await this.templateService.runStandaloneTemplate(signInTemplate, {
showCloseButton: false,
waitForDismiss: false,
});
// wait for user signal to update with a signed in user before dismissing modal
const authUser$ = toObservable(this.provider.authUser, { injector: this.injector });
await firstValueFrom(
authUser$.pipe(
tap((authUser) => console.log("auth user", authUser)),
filter((value: IAuthUser | null) => !!value)
)
);
await modal.dismiss();
}

private registerTemplateActionHandlers() {
this.templateActionRegistry.register({
auth: async ({ args }) => {
const [actionId] = args;
const childActions = {
sign_in_google: async () => await this.signInWithGoogle(),
sign_out: async () => await this.signOut(),
sign_in_google: async () => await this.provider.signInWithGoogle(),
sign_out: async () => await this.provider.signOut(),
};
if (!(actionId in childActions)) {
console.error(`[AUTH] - No action, "${actionId}"`);
Expand All @@ -69,22 +87,13 @@ export class AuthService extends SyncServiceBase {
* Use `auth: sign_in_google` instead
* */
google_auth: async () => {
return await this.signInWithGoogle();
return await this.provider.signInWithGoogle();
},
});
}

/** Listen to auth state changes and update local subject accordingly */
private addAuthListeners() {
FirebaseAuthentication.addListener("authStateChange", ({ user }) => {
// console.log("[User] updated", user);
this.addStorageEntry(user);
this.authUser$.next(user);
});
}

/** Keep a subset of auth user info in contact fields for db lookup*/
private addStorageEntry(user?: User) {
private addStorageEntry(user?: IAuthUser) {
if (user) {
const { uid } = user;
this.localStorageService.setProtected("APP_AUTH_USER", JSON.stringify({ uid }));
Expand Down
18 changes: 18 additions & 0 deletions src/app/shared/services/auth/providers/base.auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injector, signal } from "@angular/core";
import { IAuthUser } from "../types";

export class AuthProviderBase {
public authUser = signal<IAuthUser | null>(null);

public async initialise(injector: Injector) {}

public async signInWithGoogle() {
throw new Error("Google sign in not enabled");
return this.authUser();
}

public async signOut() {
this.authUser.set(undefined);
return this.authUser();
}
}
Loading
Loading