Skip to content

Commit

Permalink
refactor(env): better handling of firebase config (#545)
Browse files Browse the repository at this point in the history
  • Loading branch information
balzdur committed Sep 20, 2024
1 parent 602f92c commit 22fadab
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 77 deletions.
3 changes: 1 addition & 2 deletions packages/app-builder/src/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ Sentry.init({
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [
getClientEnv('MARBLE_APP_DOMAIN'),
getClientEnv('MARBLE_API_DOMAIN_CLIENT'),
getClientEnv('MARBLE_API_DOMAIN_SERVER'),
getClientEnv('MARBLE_API_DOMAIN'),
],

// Capture Replay for 10% of all sessions,
Expand Down
30 changes: 18 additions & 12 deletions packages/app-builder/src/infra/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,26 @@ export type FirebaseClientWrapper = {
logout: typeof signOut;
};

export function initializeFirebaseClient({
firebaseOptions,
authEmulatorHost,
}: {
firebaseOptions: FirebaseOptions;
authEmulatorHost?: string;
}): FirebaseClientWrapper {
const app = initializeApp(firebaseOptions);
export type FirebaseConfig =
| {
withEmulator: false;
options: FirebaseOptions;
}
| {
withEmulator: true;
authEmulatorUrl: string;
options: FirebaseOptions;
};

export function initializeFirebaseClient(
config: FirebaseConfig,
): FirebaseClientWrapper {
const app = initializeApp(config.options);

const clientAuth = getAuth(app);

if (authEmulatorHost && !('emulator' in clientAuth.config)) {
const url = new URL('http://' + authEmulatorHost);
connectAuthEmulator(clientAuth, url.toString());
if (config.withEmulator) {
connectAuthEmulator(clientAuth, config.authEmulatorUrl);
}

const googleAuthProvider = new GoogleAuthProvider();
Expand All @@ -55,7 +61,7 @@ export function initializeFirebaseClient({
return {
app,
clientAuth,
isFirebaseEmulator: 'emulator' in clientAuth.config,
isFirebaseEmulator: config.withEmulator,
googleAuthProvider,
microsoftAuthProvider,
signInWithOAuth: signInWithPopup,
Expand Down
2 changes: 1 addition & 1 deletion packages/app-builder/src/services/auth/auth.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export function useSendPasswordResetEmail({
export function useBackendInfo({
authenticationClientRepository,
}: AuthenticationClientService) {
const backendUrl = getClientEnv('MARBLE_API_DOMAIN_CLIENT');
const backendUrl = getClientEnv('MARBLE_API_DOMAIN');

const getAccessToken = async () => {
try {
Expand Down
7 changes: 3 additions & 4 deletions packages/app-builder/src/services/init.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ function makeClientServices(repositories: ClientRepositories) {
}

function initClientServices() {
const firebaseClient = initializeFirebaseClient({
firebaseOptions: getClientEnv('FIREBASE_OPTIONS'),
authEmulatorHost: getClientEnv('FIREBASE_AUTH_EMULATOR_HOST'),
});
const firebaseClient = initializeFirebaseClient(
getClientEnv('FIREBASE_CONFIG'),
);
const clientRepositories = makeClientRepositories({ firebaseClient });
return makeClientServices(clientRepositories);
}
Expand Down
7 changes: 3 additions & 4 deletions packages/app-builder/src/services/init.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
makeServerRepositories,
type ServerRepositories,
} from '@app-builder/repositories/init.server';
import { checkServerEnv, getServerEnv } from '@app-builder/utils/environment';
import { checkEnv, getServerEnv } from '@app-builder/utils/environment';
import { CSRF } from 'remix-utils/csrf/server';

import { makeAuthenticationServerService } from './auth/auth.server';
Expand Down Expand Up @@ -50,10 +50,9 @@ function makeServerServices(repositories: ServerRepositories) {
}

function initServerServices() {
checkServerEnv();
checkEnv();

const devEnvironment =
getServerEnv('FIREBASE_AUTH_EMULATOR_HOST') !== undefined;
const devEnvironment = getServerEnv('FIREBASE_CONFIG').withEmulator;

const { getMarbleCoreAPIClientWithAuth } = initializeMarbleCoreAPIClient({
baseUrl: getServerEnv('MARBLE_API_DOMAIN_SERVER'),
Expand Down
149 changes: 95 additions & 54 deletions packages/app-builder/src/utils/environment.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,31 @@
import { type FirebaseConfig } from '@app-builder/infra/firebase';
import * as z from 'zod';

/**
* The separation in three types are here
* to help you to know where to define your env vars.
* To:
*
* 1.Create a new environment :
* - add ServerPublicEnvVarName to the list of public env vars
* - add ServerSecretEnvVarName to the list of secret env vars
* 1. Identify the required environment variables for deploying a new environment:
* - Include `PublicEnvVars` in the list of public environment variables.
* - Incorporate `SecretEnvVars` in the list of secret environment variables.
*
* 2. Add a new env var :
* - edit the appropriate list of env vars (dev, public, secret...)
* - if necessary, update build_and_deploy.yaml (to inject the new env var)
* - update existing environment
* 2. Create a new environment variable for use in the code:
* - Update the relevant lists of environment variables (public, secret, etc.).
* - If necessary, modify `build_and_deploy.yaml` to inject the new environment variable.
* - Ensure the existing environment is updated accordingly.
*/

/**
* These variables are defined only for development
* They are useless for other envs
*/
const DevServerEnvVarNameSchema = z.object({
FIREBASE_AUTH_EMULATOR_HOST: z.string().optional(),
});
type DevServerEnvVarName = z.infer<typeof DevServerEnvVarNameSchema>;

/**
* List of all public env vars to defined on each deployed environments
*/
const ServerPublicEnvVarNameSchema = z.object({
const PublicEnvVarsSchema = z.object({
ENV: z.string(),
NODE_ENV: z.string(),
SESSION_MAX_AGE: z.string(),
MARBLE_API_DOMAIN_CLIENT: z.string(),
MARBLE_API_DOMAIN_SERVER: z.string(),
MARBLE_APP_DOMAIN: z.string(),

FIREBASE_AUTH_EMULATOR_HOST: z.string().optional(),
FIREBASE_API_KEY: z.string(),
FIREBASE_APP_ID: z.string(),
FIREBASE_AUTH_DOMAIN: z.string(),
Expand All @@ -48,37 +40,32 @@ const ServerPublicEnvVarNameSchema = z.object({

CHATLIO_WIDGET_ID: z.string().optional(),
});
type ServerPublicEnvVarName = z.infer<typeof ServerPublicEnvVarNameSchema>;
type PublicEnvVars = z.infer<typeof PublicEnvVarsSchema>;

/**
* List of all secret env vars to defined on each deployed environments
*/
const ServerSecretEnvVarNameSchema = z.object({
const SecretEnvVarsSchema = z.object({
SESSION_SECRET: z.string(),
LICENSE_KEY: z.string(),
});
type ServerSecretEnvVarName = z.infer<typeof ServerSecretEnvVarNameSchema>;
type SecretEnvVars = z.infer<typeof SecretEnvVarsSchema>;

const EnvVarsSchema = PublicEnvVarsSchema.merge(SecretEnvVarsSchema);
type EnvVars = PublicEnvVars & SecretEnvVars;

type ServerEnvVarName = DevServerEnvVarName &
ServerPublicEnvVarName &
ServerSecretEnvVarName;
function getEnv<K extends keyof EnvVars>(envVarName: K) {
// eslint-disable-next-line no-restricted-properties
return process.env[envVarName] as EnvVars[K];
}

/**
* Used to check that all env vars are defined according to the schema
* This is called at the beginning of the server and is only used for improved DX
*/
export function checkServerEnv() {
let ServerEnvVarNameSchema = ServerPublicEnvVarNameSchema.merge(
ServerSecretEnvVarNameSchema,
);
export function checkEnv() {
// eslint-disable-next-line no-restricted-properties
if (process.env.NODE_ENV === 'development') {
ServerEnvVarNameSchema = ServerEnvVarNameSchema.merge(
DevServerEnvVarNameSchema,
);
}
// eslint-disable-next-line no-restricted-properties
const result = ServerEnvVarNameSchema.safeParse(process.env);
const result = EnvVarsSchema.safeParse(process.env);
if (!result.success) {
const { _errors, ...rest } = result.error.format();
const formatted = Object.entries(rest)
Expand All @@ -89,43 +76,63 @@ export function checkServerEnv() {
}
}

/**
* Server env vars, access it using getServerEnv('MY_ENV_VAR')
*/
interface ServerEnvVars {
ENV: string;
NODE_ENV: string;
SESSION_MAX_AGE: string;
MARBLE_API_DOMAIN_CLIENT: string;
MARBLE_API_DOMAIN_SERVER: string;
MARBLE_APP_DOMAIN: string;
FIREBASE_CONFIG: FirebaseConfig;
SENTRY_DSN?: string;
SENTRY_ENVIRONMENT?: string;
SEGMENT_WRITE_KEY?: string;
CHATLIO_WIDGET_ID?: string;
SESSION_SECRET: string;
LICENSE_KEY: string;
}

/**
* Used to access env vars inside loaders/actions code
*/
export function getServerEnv<K extends keyof ServerEnvVarName>(
export function getServerEnv<K extends keyof ServerEnvVars>(
serverEnvVarName: K,
) {
if (serverEnvVarName === 'FIREBASE_CONFIG') {
return parseFirebaseConfigFromEnv() as ServerEnvVars[K];
}
// eslint-disable-next-line no-restricted-properties
return process.env[serverEnvVarName] as ServerEnvVarName[K];
return getEnv(serverEnvVarName) as ServerEnvVars[K];
}

/**
* Browser env vars :
* - define browser env vars here
* - access it using getClientEnv('MY_ENV_VAR')
* Browser env vars, access it using getClientEnv('MY_ENV_VAR')
*
* https://remix.run/docs/en/main/guides/envvars
*/
export function getClientEnvVars() {
interface ClientEnvVars {
ENV: string;
FIREBASE_CONFIG: FirebaseConfig;
MARBLE_API_DOMAIN: string;
MARBLE_APP_DOMAIN: string;
SENTRY_DSN?: string;
SENTRY_ENVIRONMENT?: string;
CHATLIO_WIDGET_ID?: string;
}
export function getClientEnvVars(): ClientEnvVars {
return {
ENV: getServerEnv('ENV'),
FIREBASE_AUTH_EMULATOR_HOST: getServerEnv('FIREBASE_AUTH_EMULATOR_HOST'),
FIREBASE_OPTIONS: {
apiKey: getServerEnv('FIREBASE_API_KEY'),
authDomain: getServerEnv('FIREBASE_AUTH_DOMAIN'),
projectId: getServerEnv('FIREBASE_PROJECT_ID'),
storageBucket: getServerEnv('FIREBASE_STORAGE_BUCKET'),
messagingSenderId: getServerEnv('FIREBASE_MESSAGING_SENDER_ID'),
appId: getServerEnv('FIREBASE_APP_ID'),
},
MARBLE_API_DOMAIN_SERVER: getServerEnv('MARBLE_API_DOMAIN_SERVER'),
MARBLE_API_DOMAIN_CLIENT: getServerEnv('MARBLE_API_DOMAIN_CLIENT'),
FIREBASE_CONFIG: getServerEnv('FIREBASE_CONFIG'),
MARBLE_API_DOMAIN: getServerEnv('MARBLE_API_DOMAIN_CLIENT'),
MARBLE_APP_DOMAIN: getServerEnv('MARBLE_APP_DOMAIN'),
SENTRY_DSN: getServerEnv('SENTRY_DSN'),
SENTRY_ENVIRONMENT: getServerEnv('SENTRY_ENVIRONMENT'),
CHATLIO_WIDGET_ID: getServerEnv('CHATLIO_WIDGET_ID'),
};
}
type ClientEnvVars = ReturnType<typeof getClientEnvVars>;

/**
* Used to access env vars inside components code (SSR and CSR)
Expand All @@ -148,3 +155,37 @@ export function getClientEnv<K extends keyof ClientEnvVars>(

return clientEnv[clientEnvVarName];
}

function parseFirebaseConfigFromEnv(): FirebaseConfig {
const options: FirebaseConfig['options'] = {
apiKey: getEnv('FIREBASE_API_KEY'),
authDomain: getEnv('FIREBASE_AUTH_DOMAIN'),
projectId: getEnv('FIREBASE_PROJECT_ID'),
storageBucket: getEnv('FIREBASE_STORAGE_BUCKET'),
messagingSenderId: getEnv('FIREBASE_MESSAGING_SENDER_ID'),
appId: getEnv('FIREBASE_APP_ID'),
};

const firebaseAuthEmulatorHost = getEnv('FIREBASE_AUTH_EMULATOR_HOST');
if (!firebaseAuthEmulatorHost) {
return {
withEmulator: false as const,
options,
};
}

try {
const authEmulatorUrl = new URL(
'http://' + firebaseAuthEmulatorHost,
).toString();
return {
withEmulator: true as const,
authEmulatorUrl,
options,
};
} catch (e) {
throw new Error(
`Invalid FIREBASE_AUTH_EMULATOR_HOST: ${firebaseAuthEmulatorHost}`,
);
}
}

0 comments on commit 22fadab

Please sign in to comment.