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

🐛 Fix: make email capture optional #205

Merged
merged 10 commits into from
Dec 28, 2024
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ packages/backend/.env
packages/backend/.prod.env


!.env.project
!.env.example
78 changes: 78 additions & 0 deletions packages/backend/.env.example
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion packages/backend/src/__tests__/backend.test.init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
73 changes: 46 additions & 27 deletions packages/backend/src/common/constants/env.constants.ts
Original file line number Diff line number Diff line change
@@ -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<typeof EnvSchema>;

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);
}
6 changes: 6 additions & 0 deletions packages/backend/src/common/constants/error.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
43 changes: 36 additions & 7 deletions packages/backend/src/user/services/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
}

Expand Down
3 changes: 2 additions & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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. */,
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Loading