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(env): better handling of firebase config #545

Merged
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
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}`,
);
}
}