From 46b004093242c5e1ee5cfc07984fbf2b33424ac3 Mon Sep 17 00:00:00 2001 From: Max Patiiuk Date: Sun, 14 Apr 2024 11:45:41 -0700 Subject: [PATCH] refactor: Make env variables config more robust I really like having config variables in a JavaScript or a TypeScript file, but Vercel is setup to expect .env files. --- backend/.env | 4 ++ backend/.env.development | 5 +++ backend/.env.example | 4 ++ backend/.env.production | 5 +++ backend/.gitignore | 2 +- backend/README.md | 26 +++++------ backend/app/api/auth/route.ts | 14 +++--- backend/example.config.js | 16 ------- src/src/components/Background/index.ts | 17 ++++++- src/webpack.config.js | 61 +++++++++++++++++--------- 10 files changed, 96 insertions(+), 58 deletions(-) create mode 100644 backend/.env create mode 100644 backend/.env.development create mode 100644 backend/.env.example create mode 100644 backend/.env.production delete mode 100644 backend/example.config.js diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..dfa50bc --- /dev/null +++ b/backend/.env @@ -0,0 +1,4 @@ +# Restrict access to the API to certain origins only. Replace kgbd.. +# with the ID of your extension. Or, you can allow access to the API from all +# origins by setting ACCESS_CONTROL_ALLOW_ORIGIN=* +ACCESS_CONTROL_ALLOW_ORIGIN=chrome-extension://kgbbebdcmdgkbopcffmpgkgcmcoomhmh \ No newline at end of file diff --git a/backend/.env.development b/backend/.env.development new file mode 100644 index 0000000..7453f15 --- /dev/null +++ b/backend/.env.development @@ -0,0 +1,5 @@ +# The URL at which the /api/auth endpoint from the backend is hosted +AUTH_URL=https://calendar-plus.patii.uk/api/auth + +# Replace kgbb... with the ID of the extension +GOOGLE_CLIENT_REDIRECT_URL=https://kgbbebdcmdgkbopcffmpgkgcmcoomhmh.chromiumapp.org/ \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..d8905bd --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,4 @@ +# Fill this in with the values received after creating a Google OAuth2 +# credentials +GOOGLE_CLIENT_ID=aaaaaaaaaaaa-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=AAAAAA-BBBBBBBBBBBBBBBBBBBBBBBBBBBB diff --git a/backend/.env.production b/backend/.env.production new file mode 100644 index 0000000..64b0f19 --- /dev/null +++ b/backend/.env.production @@ -0,0 +1,5 @@ +# The URL at which the /api/auth endpoint from the backend is hosted +AUTH_URL=http://localhost:3000/api/auth + +# Replace kgbb... with the ID of the extension +GOOGLE_CLIENT_REDIRECT_URL=https://kgbbebdcmdgkbopcffmpgkgcmcoomhmh.chromiumapp.org/ \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index a4bbb98..858d431 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,4 +1,4 @@ -/config.js +/.env*.local .next .swc /node_modules \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index bd409c9..d6ba0e8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -19,22 +19,21 @@ This backend is used to generate a more-persistent token. 2. Create [Google OAuth2 client](https://github.com/googleapis/google-api-nodejs-client?tab=readme-ov-file#oauth2-client) - - Create 2 clients - one for development, another for production + - Create 2 Client ides - one for development, another for production - Set type to "Web application" - - Set authorized redirect URI to `https://EXTENSION_ID.chromiumapp.org/` - (replace `EXTENSION_ID` with the extension ID) - - For development set: - - Authorized redirect URIs: `https://calendar-plus.patii.uk/api/route` - - For production set: - - Authorized JavaScript origins: set to domain on which `backend` will be - hosted (`https://calendar-plus.patii.uk`) + - Set authorized redirect URI to + `https://kgbbebdcmdgkbopcffmpgkgcmcoomhmh.chromiumapp.org/` (replace + `kgbb...` with the extension ID) -3. Copy [./example.config.js](./example.config.js) into `./config.js` and fill - it in according to instructions in that file and the credentials you received - in the previous file. Fill it out with development credentials in - development, and production credentials when the app is deployed to Vercel. +3. (optional) Edit the configuration in [./.env](./.env), + [./.env.development](./.env.development) and + [./.env.production](./.env.production) -4. Install dependencies: +4. Copy [./.env.example](./.env.example) into `./.env.development.local` and + `./.env.production.local` and fill it in according to the instructions in + that file and the credentials you received in the previous step. + +5. Install dependencies: ```sh npm install @@ -48,3 +47,4 @@ This backend is used to generate a more-persistent token. 1. Create new vercel.com project from this repository 2. Change the "Root Directory" setting to current directory (/packages/backend) +3. Set environment variables as per `./.env.production.local` diff --git a/backend/app/api/auth/route.ts b/backend/app/api/auth/route.ts index b02c5a9..fb3cad8 100644 --- a/backend/app/api/auth/route.ts +++ b/backend/app/api/auth/route.ts @@ -4,18 +4,18 @@ */ import { NextRequest } from 'next/server'; -import { - googleClientId, - googleClientSecret, - accessControlAllowOrigin, -} from '../../../config.js'; export const runtime = 'edge'; +const googleClientId = process.env.GOOGLE_CLIENT_ID; if (googleClientId === undefined) throw new Error('GOOGLE_CLIENT_ID is not defined'); +const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET; if (googleClientSecret === undefined) throw new Error('GOOGLE_CLIENT_SECRET is not defined'); +const accessControlAllowOrigin = process.env.ACCESS_CONTROL_ALLOW_ORIGIN; +if (accessControlAllowOrigin === undefined) + throw new Error('ACCESS_CONTROL_ALLOW_ORIGIN is not defined'); export async function POST(request: NextRequest): Promise { const origin = request.headers.get('origin'); @@ -46,8 +46,8 @@ export async function POST(request: NextRequest): Promise { const response = await fetch( formatUrl('https://oauth2.googleapis.com/token', { ...payload, - client_id: googleClientId, - client_secret: googleClientSecret, + client_id: googleClientId!, + client_secret: googleClientSecret!, }), { method: 'POST', diff --git a/backend/example.config.js b/backend/example.config.js deleted file mode 100644 index ab2424f..0000000 --- a/backend/example.config.js +++ /dev/null @@ -1,16 +0,0 @@ -// Fill this in with the values received after creating a Google OAuth2 -// credentials -export const googleClientId='aaaaaaaaaaaa-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.apps.googleusercontent.com'; -export const googleClientSecret='AAAAAA-BBBBBBBBBBBBBBBBBBBBBBBBBBBB'; - -// Replace kgbd... with the ID of the extension -export const googleClientRedirectUrl='https://kgbbebdcmdgkbopcffmpgkgcmcoomhmh.chromiumapp.org/'; - -// Restrict access to the API to certain origins only. Replace kgbd.. -// with the ID of your extension. Or, you can allow access to the API from all -// origins by setting ACCESS_CONTROL_ALLOW_ORIGIN=* -export const accessControlAllowOrigin='chrome-extension://kgbbebdcmdgkbopcffmpgkgcmcoomhmh'; - -// The URL at which the /api/auth endpoint from the backend is hosted -export const productionAuthUrl = 'https://calendar-plus.patii.uk/api/auth'; -export const developmentAuthUrl = 'http://localhost:3000/api/auth'; \ No newline at end of file diff --git a/src/src/components/Background/index.ts b/src/src/components/Background/index.ts index 5c18f1b..41eabcb 100644 --- a/src/src/components/Background/index.ts +++ b/src/src/components/Background/index.ts @@ -106,6 +106,14 @@ const requestHandlers: { const response = await fetch(resolveUrl, { method: 'POST' }); const responseText = await response.text(); + // Example response: + // { + // "access_token": "REDACTED", + // "expires_in": 3599, + // "scope": "https://www.googleapis.com/auth/calendar.readonly", + // "token_type": "Bearer" + // } + if (response.status === 200) { try { const data = JSON.parse(responseText); @@ -132,8 +140,15 @@ const requestHandlers: { }); const response = await fetch(authUrl, { method: 'POST' }); - const responseText = await response.text(); + // Example response: + // { + // "access_token": "REDACTED", + // "expires_in": 3599, + // "scope": "https://www.googleapis.com/auth/calendar.readonly", + // "token_type": "Bearer" + // } + if (response.status === 200) { try { const data = JSON.parse(responseText); diff --git a/src/webpack.config.js b/src/webpack.config.js index 7b0a21b..d225e45 100644 --- a/src/webpack.config.js +++ b/src/webpack.config.js @@ -2,25 +2,49 @@ * WebPack config for development and production */ -import path from 'path'; +import path from 'node:path'; import webpack from 'webpack'; -import { - developmentAuthUrl, - productionAuthUrl, - googleClientId, -} from '../backend/config.js'; -import { fileURLToPath } from 'url'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +function parseEnv(name) { + /* + * Next.js expects env files to be in it's root. Webpack needs to reuse some + * of those variables, so we must piggyback on Next.js's env files. + */ + const envFileLocation = `../auth-backend/${name}`; + const envFile = fs.readFileSync(envFileLocation, 'utf8'); + return Object.fromEntries( + envFile + .split('\n') + .filter((line) => line.includes('=') && !line.startsWith('#')) + .map((line) => line.split('=')) + .map(([name, value]) => [name.trim(), value.trim()]), + ); +} + +const productionGoogleClientId = parseEnv( + '.env.production.local', +).GOOGLE_CLIENT_ID; +if (productionGoogleClientId === undefined) + throw new Error('Production GOOGLE_CLIENT_ID is not defined'); +const developmentGoogleClientId = parseEnv( + '.env.development.local', +).GOOGLE_CLIENT_ID; +if (developmentGoogleClientId === undefined) + throw new Error('Development GOOGLE_CLIENT_ID is not defined'); +const productionAuthUrl = parseEnv('.env.production').AUTH_URL; +if (productionAuthUrl === undefined) + throw new Error('Production AUTH_URL is not defined'); +const developmentAuthUrl = parseEnv('.env.development').AUTH_URL; +if (developmentAuthUrl === undefined) + throw new Error('Development AUTH_URL is not defined'); const outputPath = path.resolve( path.dirname(fileURLToPath(import.meta.url)), 'dist', ); -function ensureDefined(value, error) { - if (value === undefined) throw new Error(error); - return value; -} - export default (_env, argv) => /** @type { import('webpack').Configuration } */ ({ module: { @@ -70,7 +94,7 @@ export default (_env, argv) => // Set appropriate process.env.NODE_ENV mode: argv.mode, /* - * User recommended source map type in production + * Use recommended source map type in production * Can't use the recommended "eval-source-map" in development due to * https://stackoverflow.com/questions/48047150/chrome-extension-compiled-by-webpack-throws-unsafe-eval-error */ @@ -86,15 +110,12 @@ export default (_env, argv) => plugins: [ new webpack.DefinePlugin({ 'process.env.AUTH_URL': JSON.stringify( - ensureDefined( - argv.mode === 'development' - ? developmentAuthUrl - : productionAuthUrl, - 'AUTH_URL is not defined', - ), + argv.mode === 'development' ? developmentAuthUrl : productionAuthUrl, ), 'process.env.GOOGLE_CLIENT_ID': JSON.stringify( - ensureDefined(googleClientId, 'GOOGLE_CLIENT_ID is not defined'), + argv.mode === 'development' + ? developmentGoogleClientId + : productionGoogleClientId, ), }), argv.mode === 'development'