From eda093188945f37ef1079d5364441219914a7697 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 15 Dec 2024 12:58:18 -0800 Subject: [PATCH 1/7] refactor: auth service and firebase auth provider --- packages/data-models/deployment.model.ts | 13 ++-- .../scripts/src/tasks/providers/appData.ts | 3 +- src/app/deployment-features.module.ts | 3 +- .../instance/template-action.service.ts | 5 -- src/app/shared/services/auth/auth.module.ts | 14 ++++ src/app/shared/services/auth/auth.service.ts | 78 ++++++++----------- .../services/auth/providers/base.auth.ts | 16 ++++ .../services/auth/providers/firebase.auth.ts | 45 +++++++++++ .../shared/services/auth/providers/index.ts | 17 ++++ src/app/shared/services/auth/types.ts | 7 ++ 10 files changed, 142 insertions(+), 59 deletions(-) create mode 100644 src/app/shared/services/auth/auth.module.ts create mode 100644 src/app/shared/services/auth/providers/base.auth.ts create mode 100644 src/app/shared/services/auth/providers/firebase.auth.ts create mode 100644 src/app/shared/services/auth/providers/index.ts create mode 100644 src/app/shared/services/auth/types.ts diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index f884873b8b..2e518e0de8 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -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 { @@ -36,6 +36,11 @@ 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"; + }; /** * Specify if using firebase for auth and crashlytics. * Requires firebase config available through encrypted config */ @@ -51,10 +56,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; @@ -200,9 +201,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: { diff --git a/packages/scripts/src/tasks/providers/appData.ts b/packages/scripts/src/tasks/providers/appData.ts index 94e9277c1c..3e4ba0b09c 100644 --- a/packages/scripts/src/tasks/providers/appData.ts +++ b/packages/scripts/src/tasks/providers/appData.ts @@ -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 { @@ -77,6 +77,7 @@ function generateRuntimeConfig(deploymentConfig: IDeploymentConfigJson): IDeploy analytics, api, app_config, + auth, error_logging, firebase, name, diff --git a/src/app/deployment-features.module.ts b/src/app/deployment-features.module.ts index e5c44bfdb8..1353224e7b 100644 --- a/src/app/deployment-features.module.ts +++ b/src/app/deployment-features.module.ts @@ -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 @@ -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: [AnalyticsModule, NavStackModule, AuthModule] }) export class DeploymentFeaturesModule {} diff --git a/src/app/shared/components/template/services/instance/template-action.service.ts b/src/app/shared/components/template/services/instance/template-action.service.ts index 7f8b8231d5..7e7b40f338 100644 --- a/src/app/shared/components/template/services/instance/template-action.service.ts +++ b/src/app/shared/components/template/services/instance/template-action.service.ts @@ -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"; @@ -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); } @@ -93,7 +89,6 @@ export class TemplateActionService extends SyncServiceBase { this.analyticsService, this.templateService, this.eventService, - this.authService, this.skinService, ]); } diff --git a/src/app/shared/services/auth/auth.module.ts b/src/app/shared/services/auth/auth.module.ts new file mode 100644 index 0000000000..011a4b1f8e --- /dev/null +++ b/src/app/shared/services/auth/auth.module.ts @@ -0,0 +1,14 @@ +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 + } +} diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts index affb1e16f9..7b63603342 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -1,53 +1,48 @@ -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 { toObservable } from "@angular/core/rxjs-interop"; +import { filter, firstValueFrom } from "rxjs"; @Injectable({ providedIn: "root", }) -export class AuthService extends SyncServiceBase { - private authUser$ = new BehaviorSubject(null); +export class AuthService extends AsyncServiceBase { + public authUser = signal(null); + + /** 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 ) { 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))); - } - - public async signInWithGoogle() { - return FirebaseAuthentication.signInWithGoogle(); + this.provider = getAuthProvider(this.deploymentService.config.auth?.provider); + this.registerInitFunction(this.initialise); + effect(async () => { + const authUser = this.provider.authUser(); + console.log("[Auth User]", authUser); + this.addStorageEntry(authUser); + }); } - public async signOut() { - return FirebaseAuthentication.signOut(); + /** Return a promise that resolves only after a signed in user defined */ + public async waitForUserSignedIn() { + const authUser$ = toObservable(this.authUser); + return firstValueFrom(authUser$.pipe(filter((value: IAuthUser | null) => !!value))); } - public async getCurrentUser() { - const { user } = await FirebaseAuthentication.getCurrentUser(); - return user; + private async initialise() { + await this.provider.initialise(this.injector); + this.registerTemplateActionHandlers(); } private registerTemplateActionHandlers() { @@ -55,8 +50,8 @@ export class AuthService extends SyncServiceBase { 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}"`); @@ -69,22 +64,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 })); diff --git a/src/app/shared/services/auth/providers/base.auth.ts b/src/app/shared/services/auth/providers/base.auth.ts new file mode 100644 index 0000000000..1751c0333f --- /dev/null +++ b/src/app/shared/services/auth/providers/base.auth.ts @@ -0,0 +1,16 @@ +import { Injector, signal } from "@angular/core"; +import { IAuthUser } from "../types"; + +export class AuthProviderBase { + public authUser = signal(null); + + public async initialise(injector: Injector) {} + + public async signInWithGoogle() { + return this.authUser(); + } + + public async signOut() { + return this.authUser(); + } +} diff --git a/src/app/shared/services/auth/providers/firebase.auth.ts b/src/app/shared/services/auth/providers/firebase.auth.ts new file mode 100644 index 0000000000..b7d67c913f --- /dev/null +++ b/src/app/shared/services/auth/providers/firebase.auth.ts @@ -0,0 +1,45 @@ +import { Injectable, Injector } from "@angular/core"; +import { FirebaseAuthentication, User } from "@capacitor-firebase/authentication"; +import { FirebaseService } from "../../firebase/firebase.service"; +import { AuthProviderBase } from "./base.auth"; + +@Injectable({ + providedIn: "root", +}) +export class FirebaseAuthProvider extends AuthProviderBase { + public override async initialise(injector: Injector) { + const firebaseService = injector.get(FirebaseService); + // TODO - is service required here? + if (!firebaseService.app) { + throw new Error("[Firebase Auth] app not configured"); + } + this.addAuthListeners(); + // ensure any previous signed in user is loaded + await FirebaseAuthentication.getCurrentUser(); + } + + public async signInWithGoogle() { + await FirebaseAuthentication.signInWithGoogle(); + return this.authUser(); + } + + public async signOut() { + await FirebaseAuthentication.signOut(); + return this.authUser(); + } + + public async getCurrentUser() { + const { user } = await FirebaseAuthentication.getCurrentUser(); + return user; + } + + /** + * Listen to auth state changes and update authUser signal + * This helps to ensure the signal is kept in sync with automated user sign-in/out + * */ + private addAuthListeners() { + FirebaseAuthentication.addListener("authStateChange", ({ user }) => { + this.authUser.set(user); + }); + } +} diff --git a/src/app/shared/services/auth/providers/index.ts b/src/app/shared/services/auth/providers/index.ts new file mode 100644 index 0000000000..9fe03efa66 --- /dev/null +++ b/src/app/shared/services/auth/providers/index.ts @@ -0,0 +1,17 @@ +import { IAuthProvider } from "../types"; +import { AuthProviderBase } from "./base.auth"; +import { FirebaseAuthProvider } from "./firebase.auth"; +import { SupabaseAuthProvider } from "./supabase.auth"; + +// TODO - optimise for production (only include provider used) +export const getAuthProvider = (name: IAuthProvider): AuthProviderBase => { + switch (name) { + case "firebase": + return new FirebaseAuthProvider(); + case "supabase": + return new SupabaseAuthProvider(); + default: + console.warn("[Auth Provider] not configured for: ", name); + return new AuthProviderBase(); + } +}; diff --git a/src/app/shared/services/auth/types.ts b/src/app/shared/services/auth/types.ts new file mode 100644 index 0000000000..e65c35f5da --- /dev/null +++ b/src/app/shared/services/auth/types.ts @@ -0,0 +1,7 @@ +import type { IDeploymentConfig } from "packages/data-models"; + +export type IAuthProvider = IDeploymentConfig["auth"]["provider"]; + +export interface IAuthUser { + uid: string; +} From b264af86e4f2eaff49646dc5e4eb35e5f2301bc6 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Dec 2024 17:27:59 -0800 Subject: [PATCH 2/7] refactor: enforce login --- packages/data-models/appConfig.ts | 1 - packages/data-models/deployment.model.ts | 2 ++ src/app/app.component.ts | 26 --------------------- src/app/app.module.ts | 4 ++-- src/app/shared/services/auth/auth.module.ts | 1 + 5 files changed, 5 insertions(+), 29 deletions(-) diff --git a/packages/data-models/appConfig.ts b/packages/data-models/appConfig.ts index 6e3d690c50..92741de10a 100644 --- a/packages/data-models/appConfig.ts +++ b/packages/data-models/appConfig.ts @@ -129,7 +129,6 @@ const APP_SIDEMENU_DEFAULTS = { }; const APP_AUTHENTICATION_DEFAULTS = { - enforceLogin: false, signInTemplate: "sign_in", }; diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 2e518e0de8..c37eebb9fb 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -40,6 +40,8 @@ export interface IDeploymentRuntimeConfig { 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. diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 323455791f..42335bc77b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -100,7 +100,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, @@ -125,7 +124,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() }); @@ -164,29 +162,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. @@ -231,7 +206,6 @@ export class AppComponent { this.templateService, this.templateProcessService, this.appDataService, - this.authService, this.serverService, this.seoService, this.feedbackService, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 591b129364..aedaf10727 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 }, diff --git a/src/app/shared/services/auth/auth.module.ts b/src/app/shared/services/auth/auth.module.ts index 011a4b1f8e..d81ad098ee 100644 --- a/src/app/shared/services/auth/auth.module.ts +++ b/src/app/shared/services/auth/auth.module.ts @@ -10,5 +10,6 @@ import { AuthService } from "./auth.service"; export class AuthModule { constructor(private service: AuthService) { // include service to initialise and register handlers + service.ready(); } } From 49769a5e935bc4ac9539973b39524cd57fbc3a1b Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Dec 2024 17:28:27 -0800 Subject: [PATCH 3/7] chore: update deps --- package.json | 6 +++--- yarn.lock | 34 +++++++++++++++++----------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 1d96b4ad74..9aecded662 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 8a043ef189..0e1e475f22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2048,41 +2048,41 @@ __metadata: languageName: node linkType: hard -"@capacitor-firebase/authentication@npm:^6.1.0": - version: 6.1.0 - resolution: "@capacitor-firebase/authentication@npm:6.1.0" +"@capacitor-firebase/authentication@npm:^6.3.1": + version: 6.3.1 + resolution: "@capacitor-firebase/authentication@npm:6.3.1" peerDependencies: "@capacitor/core": ^6.0.0 - firebase: ^10.9.0 + firebase: ^10.9.0 || ^11.0.0 peerDependenciesMeta: firebase: optional: true - checksum: c70c82576c46333d8d56c03dbe51ceab798cddd7974dbc2aa0f6e287059deea245d17be6343302096a09bd91d8e13b0bf8d6e8417b65ecb86e62573674492df0 + checksum: 148200081ff5100e992205a19d9940e155c32e92a428405eb490e04b009f56ac364fc1c606e0ce1c6aec35003701fbff0022dd6a3057ecb3a8fbe16cacaf4bb6 languageName: node linkType: hard -"@capacitor-firebase/crashlytics@npm:^6.1.0": - version: 6.1.0 - resolution: "@capacitor-firebase/crashlytics@npm:6.1.0" +"@capacitor-firebase/crashlytics@npm:^6.3.1": + version: 6.3.1 + resolution: "@capacitor-firebase/crashlytics@npm:6.3.1" peerDependencies: "@capacitor/core": ^6.0.0 peerDependenciesMeta: firebase: optional: true - checksum: 9d20b204545e7bb6fed9b858270342634bd8772c84b516ba467d1a6b13a870edcdc1048def10c50f2edebd7fd0c896a2f7728176594cda670d807ed0b5e0f045 + checksum: b52dc7a42d2f961aa47b30b8918038fa9ae370308240e52f3b78f2d125b3318a7eed34aa845aa98470daf826878b62c8233d8dac47e33d6fcb7c89ea59201914 languageName: node linkType: hard -"@capacitor-firebase/performance@npm:^6.1.0": - version: 6.1.0 - resolution: "@capacitor-firebase/performance@npm:6.1.0" +"@capacitor-firebase/performance@npm:^6.3.1": + version: 6.3.1 + resolution: "@capacitor-firebase/performance@npm:6.3.1" peerDependencies: "@capacitor/core": ^6.0.0 - firebase: ^10.9.0 + firebase: ^10.9.0 || ^11.0.0 peerDependenciesMeta: firebase: optional: true - checksum: 8aa094e6cece67ed8101cc0d78b2c9a0f3aa9f027de765095963ad95604be4bda70155573a8b8ef190ae571c878ae3fe3f63c688a56268f79840faddd16acaac + checksum: 25b4da6e44db02520bfa9f0d89b1428c0efa275e1a270f9b567513e469e9f2ce8157c1c9ba70d8c83b21b462aaecadea1dec807f84ec036cb59be66963284a63 languageName: node linkType: hard @@ -15100,9 +15100,9 @@ __metadata: "@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/cli": ^6.0.0 From 81c9d9a5264e45cdc69a00c0619ef5c974605469 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Dec 2024 17:30:42 -0800 Subject: [PATCH 4/7] chore: code tidying --- src/app/deployment-features.module.ts | 2 +- src/app/shared/services/auth/auth.service.ts | 43 ++++++++++++++----- .../services/auth/providers/base.auth.ts | 2 + .../services/auth/providers/firebase.auth.ts | 7 +-- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/app/deployment-features.module.ts b/src/app/deployment-features.module.ts index 1353224e7b..8c12edc419 100644 --- a/src/app/deployment-features.module.ts +++ b/src/app/deployment-features.module.ts @@ -15,5 +15,5 @@ import { AuthModule } from "./shared/services/auth/auth.module"; * * This is a feature marked for future implementation */ -@NgModule({ imports: [AnalyticsModule, NavStackModule, AuthModule] }) +@NgModule({ imports: [AuthModule, AnalyticsModule, NavStackModule] }) export class DeploymentFeaturesModule {} diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts index 7b63603342..1a803cc934 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -6,15 +6,14 @@ 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"; -import { filter, firstValueFrom } from "rxjs"; @Injectable({ providedIn: "root", }) export class AuthService extends AsyncServiceBase { - public authUser = signal(null); - /** Auth provider used */ private provider: AuthProviderBase; @@ -22,27 +21,51 @@ export class AuthService extends AsyncServiceBase { private templateActionRegistry: TemplateActionRegistry, private localStorageService: LocalStorageService, private deploymentService: DeploymentService, - private injector: Injector + private injector: Injector, + private templateService: TemplateService ) { super("Auth"); - this.provider = getAuthProvider(this.deploymentService.config.auth?.provider); + this.provider = getAuthProvider(this.config.provider); this.registerInitFunction(this.initialise); effect(async () => { const authUser = this.provider.authUser(); - console.log("[Auth User]", authUser); this.addStorageEntry(authUser); }); } - /** Return a promise that resolves only after a signed in user defined */ - public async waitForUserSignedIn() { - const authUser$ = toObservable(this.authUser); - return firstValueFrom(authUser$.pipe(filter((value: IAuthUser | null) => !!value))); + private get config() { + return this.deploymentService.config.auth || {}; } 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(); + } + } + + 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() { diff --git a/src/app/shared/services/auth/providers/base.auth.ts b/src/app/shared/services/auth/providers/base.auth.ts index 1751c0333f..2eea05c2d7 100644 --- a/src/app/shared/services/auth/providers/base.auth.ts +++ b/src/app/shared/services/auth/providers/base.auth.ts @@ -7,10 +7,12 @@ export class AuthProviderBase { 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(); } } diff --git a/src/app/shared/services/auth/providers/firebase.auth.ts b/src/app/shared/services/auth/providers/firebase.auth.ts index b7d67c913f..6001373357 100644 --- a/src/app/shared/services/auth/providers/firebase.auth.ts +++ b/src/app/shared/services/auth/providers/firebase.auth.ts @@ -1,5 +1,6 @@ import { Injectable, Injector } from "@angular/core"; -import { FirebaseAuthentication, User } from "@capacitor-firebase/authentication"; +import { FirebaseAuthentication } from "@capacitor-firebase/authentication"; +import { getAuth } from "firebase/auth"; import { FirebaseService } from "../../firebase/firebase.service"; import { AuthProviderBase } from "./base.auth"; @@ -14,8 +15,8 @@ export class FirebaseAuthProvider extends AuthProviderBase { throw new Error("[Firebase Auth] app not configured"); } this.addAuthListeners(); - // ensure any previous signed in user is loaded - await FirebaseAuthentication.getCurrentUser(); + // use firebase authStateReady to ensure any previously logged in user is available + await getAuth().authStateReady(); } public async signInWithGoogle() { From c87078731251a66f00347074744dbc6489796b1e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Dec 2024 18:50:35 -0800 Subject: [PATCH 5/7] chore: add placeholder supabase auth provider --- src/app/shared/services/auth/providers/supabase.auth.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/app/shared/services/auth/providers/supabase.auth.ts diff --git a/src/app/shared/services/auth/providers/supabase.auth.ts b/src/app/shared/services/auth/providers/supabase.auth.ts new file mode 100644 index 0000000000..cfedac6236 --- /dev/null +++ b/src/app/shared/services/auth/providers/supabase.auth.ts @@ -0,0 +1,3 @@ +import { AuthProviderBase } from "./base.auth"; + +export class SupabaseAuthProvider extends AuthProviderBase {} From 2853297be0009f0c2d52726286de303bf6b62728 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 19 Dec 2024 16:31:09 -0800 Subject: [PATCH 6/7] refactor: signInTemplate --- packages/data-models/appConfig.ts | 5 ----- packages/data-models/deployment.model.ts | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/data-models/appConfig.ts b/packages/data-models/appConfig.ts index 3f3541c4ac..c49107f00a 100644 --- a/packages/data-models/appConfig.ts +++ b/packages/data-models/appConfig.ts @@ -132,10 +132,6 @@ const APP_SIDEMENU_DEFAULTS = { should_show_deployment_name: false, }; -const APP_AUTHENTICATION_DEFAULTS = { - signInTemplate: "sign_in", -}; - type IAppLaunchAction = { type: "template_popup" | "tour_start"; value: string; @@ -196,7 +192,6 @@ const TASKS = { const APP_CONFIG = { APP_HEADER_DEFAULTS, APP_INITIALISATION_DEFAULTS, - APP_AUTHENTICATION_DEFAULTS, APP_LANGUAGES, APP_LANGUAGES_META, APP_ROUTE_DEFAULTS, diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index c37eebb9fb..566b81f3ed 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -42,6 +42,8 @@ export interface IDeploymentRuntimeConfig { provider?: "firebase" | "supabase"; /** prevent user accessing app pages without being logged in */ enforceLogin?: boolean; + /** template to display when enforceLogin enabled */ + signInTemplate?: string; }; /** * Specify if using firebase for auth and crashlytics. From 71d66909bdc2f92d0b376f8a3a68063a3f9337b2 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 19 Dec 2024 16:41:52 -0800 Subject: [PATCH 7/7] refactor: enforce login config --- packages/data-models/appConfig.ts | 7 +++++++ packages/data-models/deployment.model.ts | 6 ++---- src/app/shared/services/auth/auth.service.ts | 9 ++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/data-models/appConfig.ts b/packages/data-models/appConfig.ts index c49107f00a..8601def177 100644 --- a/packages/data-models/appConfig.ts +++ b/packages/data-models/appConfig.ts @@ -132,6 +132,12 @@ const APP_SIDEMENU_DEFAULTS = { should_show_deployment_name: false, }; +/** + * @deprecated 0.18.0 + * Use `deployment.auth` to configure auth + */ +const APP_AUTHENTICATION_DEFAULTS = {}; + type IAppLaunchAction = { type: "template_popup" | "tour_start"; value: string; @@ -192,6 +198,7 @@ const TASKS = { const APP_CONFIG = { APP_HEADER_DEFAULTS, APP_INITIALISATION_DEFAULTS, + APP_AUTHENTICATION_DEFAULTS, APP_LANGUAGES, APP_LANGUAGES_META, APP_ROUTE_DEFAULTS, diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 566b81f3ed..776b3771ee 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -40,10 +40,8 @@ export interface IDeploymentRuntimeConfig { 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; - /** template to display when enforceLogin enabled */ - signInTemplate?: string; + /** prevent user accessing app pages without being logged in. Specified template will be shown until logged in */ + enforceLoginTemplate?: string; }; /** * Specify if using firebase for auth and crashlytics. diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts index 1a803cc934..e76d98c529 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -40,20 +40,19 @@ export class AuthService extends AsyncServiceBase { private async initialise() { await this.provider.initialise(this.injector); this.registerTemplateActionHandlers(); - if (this.config.enforceLogin) { + if (this.config.enforceLoginTemplate) { // NOTE - Do not await the enforce login to allow other services to initialise in background - this.enforceLogin(); + this.enforceLogin(this.config.enforceLoginTemplate); } } - private async enforceLogin() { + private async enforceLogin(templateName: string) { // 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, { + const { modal } = await this.templateService.runStandaloneTemplate(templateName, { showCloseButton: false, waitForDismiss: false, });