diff --git a/.gitignore b/.gitignore index 0bb0c57f..41166bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,4 @@ packages/backend/.env packages/backend/.prod.env -!.env.project \ No newline at end of file +!.env.example \ No newline at end of file diff --git a/packages/backend/.env.example b/packages/backend/.env.example new file mode 100644 index 00000000..168bb57d --- /dev/null +++ b/packages/backend/.env.example @@ -0,0 +1,78 @@ +# This is an example environment file +# Rename it to .env, replace the values, and +# restart the backend. + +# Don't ever commit this file or share its contents. + +#################################################### +# 1. Backend # +#################################################### +# Location of Node server +# Feel free to use http in development. However, +# GCal API requires https in order to sync calendars. +# So if you use http, you won't receive notifications +# upon Gcal event changes +BASEURL=http://localhost:3000/api +CORS=http://localhost:3000,http://localhost:9080,https://app.yourdomain.com +LOG_LEVEL=debug # options: error, warn, info, http, verbose, debug, silly +NODE_ENV=development # options: development, production +PORT=3000 # Node.js server +# Unique tokens for auth +# These defaults are fine for development, but +# you should change them before making your app externally available +TOKEN_COMPASS_SYNC=YOUR_UNIQUE_STRING +TOKEN_GCAL_NOTIFICATION=ANOTHER_UNIQUE_STRING + + +#################################################### +# 2. Database # +#################################################### +MONGO_URI=mongodb+srv://admin:YOUR_ADMIN_PW@cluster0.m99yy.mongodb.net/dev_calendar?authSource=admin&retryWrites=true&w=majority&tls=true + + +#################################################### +# 3. Google OAuth and API # +#################################################### +# Get these from your Google Cloud Platform Project + +# CLIENT_ID will look something like: +# 93031928383029-imm173832181hk392938191020saasdfasd9d.apps.googleusercontent.com +CLIENT_ID=UNIQUE_ID_FROM_YOUR_GOOGLE_CLOUD_PROJECT +CLIENT_SECRET=UNIQUE_SECRET_FROM_YOUR_GOOGLE_CLOUD_PROJECT +# The watch length in minutes for a Google Calendar channel +# Set to a low value for development and higher value for production. +# Make sure to refresh the production channel before it expires +CHANNEL_EXPIRATION_MIN=10 + +#################################################### +# 4. User Sessions # +#################################################### + +# SUPERTOKENS_URI will look something like: +# https://9d9asdhfah2892gsjs9881hvnzmmzh-us-west-1.aws.supertokens.io:3572 +SUPERTOKENS_URI=UNIQUE_URI_FROM_YOUR_SUPERTOKENS_ACCOUNT +# SUPERTOKENS_KEY will look something like: +# h03h3mGMB9asC1jUPje9chajsdEd +SUPERTOKENS_KEY=UNIQUE_KEY_FROM_YOUR_SUPERTOKENS_ACCOUNT + +#################################################### +# 5. CLI (optional) # +#################################################### +# Set these values to save time while using the CLI + +STAGING_DOMAIN=staging.yourdomain.com +PROD_DOMAIN=app.yourdomain.com + +#################################################### +# 6. Email (optional) # +#################################################### +# Get these from your ConvertKit account +# Does not capture email during signup if any empty EMAILER_ value + +EMAILER_API_SECRET=UNIQUE_SECRET_FROM_YOUR_CONVERTKIT_ACCOUNT +EMAILER_LIST_ID=YOUR_LIST_ID # get this from the URL + +#################################################### +# 7. Debug (optional) # +#################################################### +SOCKET_USER=USER_ID_FROM_YOUR_MONGO_DB \ No newline at end of file diff --git a/packages/backend/src/__tests__/backend.test.init.js b/packages/backend/src/__tests__/backend.test.init.js index 7f8a2265..9641b65c 100644 --- a/packages/backend/src/__tests__/backend.test.init.js +++ b/packages/backend/src/__tests__/backend.test.init.js @@ -11,7 +11,6 @@ process.env.CLIENT_SECRET = "googleSecret"; process.env.CHANNEL_EXPIRATION_MIN = 5; process.env.SUPERTOKENS_URI = "sTUri"; process.env.SUPERTOKENS_KEY = "sTKey"; -process.env.EMAILER_API_KEY = "emailerApiKey"; process.env.EMAILER_API_SECRET = "emailerApiSecret"; process.env.EMAILER_LIST_ID = 1234567; process.env.TOKEN_GCAL_NOTIFICATION = "secretToken1"; diff --git a/packages/backend/src/common/constants/env.constants.ts b/packages/backend/src/common/constants/env.constants.ts index 315c1492..2edea214 100644 --- a/packages/backend/src/common/constants/env.constants.ts +++ b/packages/backend/src/common/constants/env.constants.ts @@ -1,42 +1,61 @@ +import { z } from "zod"; import { NodeEnv, PORT_DEFAULT_BACKEND } from "@core/constants/core.constants"; import { isDev } from "@core/util/env.util"; +import { Logger } from "@core/logger/winston.logger"; + +const logger = Logger("app:constants"); const _nodeEnv = process.env["NODE_ENV"] as NodeEnv; if (!Object.values(NodeEnv).includes(_nodeEnv)) { throw new Error(`Invalid NODE_ENV value: '${_nodeEnv}'`); } -export const IS_DEV = isDev(_nodeEnv); -const db = IS_DEV ? "dev_calendar" : "prod_calendar"; +const IS_DEV = isDev(_nodeEnv); + +const EnvSchema = z + .object({ + BASEURL: z.string().nonempty(), + CHANNEL_EXPIRATION_MIN: z.string().nonempty().default("10"), + CLIENT_ID: z.string().nonempty(), + CLIENT_SECRET: z.string().nonempty(), + DB: z.string().nonempty(), + EMAILER_SECRET: z.string().nonempty().optional(), + EMAILER_LIST_ID: z.string().nonempty().optional(), + MONGO_URI: z.string().nonempty(), + NODE_ENV: z.nativeEnum(NodeEnv), + ORIGINS_ALLOWED: z.array(z.string().nonempty()).default([]), + PORT: z.string().nonempty().default(PORT_DEFAULT_BACKEND.toString()), + SUPERTOKENS_URI: z.string().nonempty(), + SUPERTOKENS_KEY: z.string().nonempty(), + TOKEN_GCAL_NOTIFICATION: z.string().nonempty(), + TOKEN_COMPASS_SYNC: z.string().nonempty(), + }) + .strict(); -const _error = ">> TODO: set this value in .env <<"; +type Env = z.infer; export const ENV = { - BASEURL: process.env["BASEURL"] as string, - CHANNEL_EXPIRATION_MIN: process.env["CHANNEL_EXPIRATION_MIN"] || "10", - CLIENT_ID: process.env["CLIENT_ID"] || _error, - CLIENT_SECRET: process.env["CLIENT_SECRET"] || _error, - DB: db, - EMAILER_KEY: process.env["EMAILER_API_KEY"] || _error, - EMAILER_SECRET: process.env["EMAILER_API_SECRET"] || _error, - EMAILER_LIST_ID: process.env["EMAILER_LIST_ID"] || _error, - MONGO_URI: process.env["MONGO_URI"] || _error, + BASEURL: process.env["BASEURL"], + CHANNEL_EXPIRATION_MIN: process.env["CHANNEL_EXPIRATION_MIN"], + CLIENT_ID: process.env["CLIENT_ID"], + CLIENT_SECRET: process.env["CLIENT_SECRET"], + DB: IS_DEV ? "dev_calendar" : "prod_calendar", + EMAILER_SECRET: process.env["EMAILER_API_SECRET"], + EMAILER_LIST_ID: process.env["EMAILER_LIST_ID"], + MONGO_URI: process.env["MONGO_URI"], NODE_ENV: _nodeEnv, ORIGINS_ALLOWED: process.env["CORS"] ? process.env["CORS"].split(",") : [], - PORT: process.env["PORT"] || PORT_DEFAULT_BACKEND, - SUPERTOKENS_URI: process.env["SUPERTOKENS_URI"] || _error, - SUPERTOKENS_KEY: process.env["SUPERTOKENS_KEY"] || _error, - TOKEN_GCAL_NOTIFICATION: process.env["TOKEN_GCAL_NOTIFICATION"] || _error, - TOKEN_COMPASS_SYNC: process.env["TOKEN_COMPASS_SYNC"] || _error, -}; - -if (Object.values(ENV).includes(_error)) { - console.log( - `Exiting because a critical env value is missing: ${JSON.stringify( - ENV, - null, - 2 - )}` - ); + PORT: process.env["PORT"], + SUPERTOKENS_URI: process.env["SUPERTOKENS_URI"], + SUPERTOKENS_KEY: process.env["SUPERTOKENS_KEY"], + TOKEN_GCAL_NOTIFICATION: process.env["TOKEN_GCAL_NOTIFICATION"], + TOKEN_COMPASS_SYNC: process.env["TOKEN_COMPASS_SYNC"], +} as Env; + +const parsedEnv = EnvSchema.safeParse(ENV); + +if (!parsedEnv.success) { + logger.error(`Exiting because a critical env value is missing or invalid:`); + console.error(parsedEnv.error.issues); process.exit(1); } diff --git a/packages/backend/src/common/constants/error.constants.ts b/packages/backend/src/common/constants/error.constants.ts index a7a1c466..e030599d 100644 --- a/packages/backend/src/common/constants/error.constants.ts +++ b/packages/backend/src/common/constants/error.constants.ts @@ -32,6 +32,12 @@ export const DbError = { }; export const EmailerError = { + IncorrectApiKey: { + description: + "Incorrect API key. Please make sure environment variables beginning with EMAILER_ are correct", + status: Status.BAD_REQUEST, + isOperational: true, + }, AddToListFailed: { description: "Failed to add email to list", status: Status.UNSURE, diff --git a/packages/backend/src/sync/controllers/sync.debug.controller.ts b/packages/backend/src/sync/controllers/sync.debug.controller.ts index c5350e2d..1b3e9b6f 100644 --- a/packages/backend/src/sync/controllers/sync.debug.controller.ts +++ b/packages/backend/src/sync/controllers/sync.debug.controller.ts @@ -10,7 +10,7 @@ import { getSync } from "../util/sync.queries"; class SyncDebugController { dispatchEventToClient = (_req: Request, res: Response) => { try { - const userId = process.env["DEMO_SOCKET_USER"]; + const userId = process.env["SOCKET_USER"]; if (!userId) { console.log("No demo user"); throw new Error("No demo user"); diff --git a/packages/backend/src/user/services/email.service.ts b/packages/backend/src/user/services/email.service.ts index fe395f04..3c01f35f 100644 --- a/packages/backend/src/user/services/email.service.ts +++ b/packages/backend/src/user/services/email.service.ts @@ -8,16 +8,45 @@ const logger = Logger("app:emailer.service"); class EmailService { addToEmailList = async (email: string, firstName: string) => { - const url = `https://api.convertkit.com/v3/tags/${ENV.EMAILER_LIST_ID}/subscribe?api_secret=${ENV.EMAILER_SECRET}&email=${email}&first_name=${firstName}`; + if (!ENV.EMAILER_LIST_ID && !ENV.EMAILER_SECRET) { + logger.warn( + "Skipped adding email to list, because EMAILER_ environment variables are missing." + ); + return; + } - const response = await axios.post(url); + const url = `https://api.convertkit.com/v3/tags/${ + ENV.EMAILER_LIST_ID as string + }/subscribe?api_secret=${ + ENV.EMAILER_SECRET as string + }&email=${email}&first_name=${firstName}`; - if (response.status !== 200) { - throw error(EmailerError.AddToListFailed, "Failed to add email to list"); - logger.error(response.data); - } + try { + const response = await axios.post(url); + + if (response.status !== 200) { + throw error( + EmailerError.AddToListFailed, + "Failed to add email to list" + ); + logger.error(response.data); + } - return response; + return response; + } catch (e) { + if ( + axios.isAxiosError(e) && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + e?.response?.data?.message === "API Key not valid" + ) { + throw error( + EmailerError.IncorrectApiKey, + "Failed to add email to list. Please make sure environment variables beginning with EMAILER_ are correct" + ); + } + + throw e; + } }; } diff --git a/packages/web/package.json b/packages/web/package.json index f8c47e96..59787cfe 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -54,7 +54,8 @@ "supertokens-auth-react": "^0.46.0", "supertokens-web-js": "^0.13.0", "ts-keycode-enum": "^1.0.6", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zod": "^3.24.1" }, "devDependencies": { "@babel/core": "^7.15.5", diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json index d4dcc549..8160000e 100644 --- a/packages/web/tsconfig.json +++ b/packages/web/tsconfig.json @@ -12,7 +12,7 @@ "module": "Node16", "moduleResolution": "node16", // "declaration": true, // originally set to true, commented during refactor - // "strict": true, + "strict": true, // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, // "strictNullChecks": true /* Enable strict null checks. */, // "strictFunctionTypes": true /* Enable strict checking of function types. */, diff --git a/yarn.lock b/yarn.lock index 7c7846dd..88764f40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11264,3 +11264,8 @@ yoctocolors-cjs@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== + +zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==