Skip to content

Commit

Permalink
Merge pull request #2634 from IDEMSInternational/refactor/auth-providers
Browse files Browse the repository at this point in the history
Refactor!: auth providers
  • Loading branch information
chrismclarke authored Dec 20, 2024
2 parents 3881530 + 860be4c commit a18985e
Show file tree
Hide file tree
Showing 16 changed files with 198 additions and 109 deletions.
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
9 changes: 5 additions & 4 deletions packages/data-models/appConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,11 @@ const APP_SIDEMENU_DEFAULTS = {
should_show_deployment_name: false,
};

const APP_AUTHENTICATION_DEFAULTS = {
enforceLogin: false,
signInTemplate: "sign_in",
};
/**
* @deprecated 0.18.0
* Use `deployment.auth` to configure auth
*/
const APP_AUTHENTICATION_DEFAULTS = {};

type IAppLaunchAction = {
type: "template_popup" | "tour_start";
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. Specified template will be shown until logged in */
enforceLoginTemplate?: string;
};
/**
* 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();
}
}
96 changes: 52 additions & 44 deletions src/app/shared/services/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,79 @@
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.enforceLoginTemplate) {
// NOTE - Do not await the enforce login to allow other services to initialise in background
this.enforceLogin(this.config.enforceLoginTemplate);
}
}

public async getCurrentUser() {
const { user } = await FirebaseAuthentication.getCurrentUser();
return user;
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 { modal } = await this.templateService.runStandaloneTemplate(templateName, {
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 +86,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

0 comments on commit a18985e

Please sign in to comment.