Skip to content

Commit 9cde6be

Browse files
🐛 Fix: make email capture optional (#205)
* fix: Optional email service proper handling * chore(web): enable typescript strict mode * build(deps): install zod * refactor(backend): use zod for env parsing * feat: tighten validation and add logging * chore: removed unused env var: EMAILER_API_KEY * chore: update typing in email.service we can safely cast as a string, because we're explicitly checking if those env variables are present at the beginning of this addToEmailList function * chore: update emailer error message * feat(backend): add .env.example * refactor(backend): rename DEMO_SOCKET_USER to SOCKET_USER --------- Co-authored-by: Muhammed Aldulaimi <muhammed.aldulaimi98@gmail.com>
1 parent 982e272 commit 9cde6be

File tree

10 files changed

+176
-39
lines changed

10 files changed

+176
-39
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ packages/backend/.env
4242
packages/backend/.prod.env
4343

4444

45-
!.env.project
45+
!.env.example

packages/backend/.env.example

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# This is an example environment file
2+
# Rename it to .env, replace the values, and
3+
# restart the backend.
4+
5+
# Don't ever commit this file or share its contents.
6+
7+
####################################################
8+
# 1. Backend #
9+
####################################################
10+
# Location of Node server
11+
# Feel free to use http in development. However,
12+
# GCal API requires https in order to sync calendars.
13+
# So if you use http, you won't receive notifications
14+
# upon Gcal event changes
15+
BASEURL=http://localhost:3000/api
16+
CORS=http://localhost:3000,http://localhost:9080,https://app.yourdomain.com
17+
LOG_LEVEL=debug # options: error, warn, info, http, verbose, debug, silly
18+
NODE_ENV=development # options: development, production
19+
PORT=3000 # Node.js server
20+
# Unique tokens for auth
21+
# These defaults are fine for development, but
22+
# you should change them before making your app externally available
23+
TOKEN_COMPASS_SYNC=YOUR_UNIQUE_STRING
24+
TOKEN_GCAL_NOTIFICATION=ANOTHER_UNIQUE_STRING
25+
26+
27+
####################################################
28+
# 2. Database #
29+
####################################################
30+
MONGO_URI=mongodb+srv://admin:YOUR_ADMIN_PW@cluster0.m99yy.mongodb.net/dev_calendar?authSource=admin&retryWrites=true&w=majority&tls=true
31+
32+
33+
####################################################
34+
# 3. Google OAuth and API #
35+
####################################################
36+
# Get these from your Google Cloud Platform Project
37+
38+
# CLIENT_ID will look something like:
39+
# 93031928383029-imm173832181hk392938191020saasdfasd9d.apps.googleusercontent.com
40+
CLIENT_ID=UNIQUE_ID_FROM_YOUR_GOOGLE_CLOUD_PROJECT
41+
CLIENT_SECRET=UNIQUE_SECRET_FROM_YOUR_GOOGLE_CLOUD_PROJECT
42+
# The watch length in minutes for a Google Calendar channel
43+
# Set to a low value for development and higher value for production.
44+
# Make sure to refresh the production channel before it expires
45+
CHANNEL_EXPIRATION_MIN=10
46+
47+
####################################################
48+
# 4. User Sessions #
49+
####################################################
50+
51+
# SUPERTOKENS_URI will look something like:
52+
# https://9d9asdhfah2892gsjs9881hvnzmmzh-us-west-1.aws.supertokens.io:3572
53+
SUPERTOKENS_URI=UNIQUE_URI_FROM_YOUR_SUPERTOKENS_ACCOUNT
54+
# SUPERTOKENS_KEY will look something like:
55+
# h03h3mGMB9asC1jUPje9chajsdEd
56+
SUPERTOKENS_KEY=UNIQUE_KEY_FROM_YOUR_SUPERTOKENS_ACCOUNT
57+
58+
####################################################
59+
# 5. CLI (optional) #
60+
####################################################
61+
# Set these values to save time while using the CLI
62+
63+
STAGING_DOMAIN=staging.yourdomain.com
64+
PROD_DOMAIN=app.yourdomain.com
65+
66+
####################################################
67+
# 6. Email (optional) #
68+
####################################################
69+
# Get these from your ConvertKit account
70+
# Does not capture email during signup if any empty EMAILER_ value
71+
72+
EMAILER_API_SECRET=UNIQUE_SECRET_FROM_YOUR_CONVERTKIT_ACCOUNT
73+
EMAILER_LIST_ID=YOUR_LIST_ID # get this from the URL
74+
75+
####################################################
76+
# 7. Debug (optional) #
77+
####################################################
78+
SOCKET_USER=USER_ID_FROM_YOUR_MONGO_DB

packages/backend/src/__tests__/backend.test.init.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ process.env.CLIENT_SECRET = "googleSecret";
1111
process.env.CHANNEL_EXPIRATION_MIN = 5;
1212
process.env.SUPERTOKENS_URI = "sTUri";
1313
process.env.SUPERTOKENS_KEY = "sTKey";
14-
process.env.EMAILER_API_KEY = "emailerApiKey";
1514
process.env.EMAILER_API_SECRET = "emailerApiSecret";
1615
process.env.EMAILER_LIST_ID = 1234567;
1716
process.env.TOKEN_GCAL_NOTIFICATION = "secretToken1";
Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,61 @@
1+
import { z } from "zod";
12
import { NodeEnv, PORT_DEFAULT_BACKEND } from "@core/constants/core.constants";
23
import { isDev } from "@core/util/env.util";
4+
import { Logger } from "@core/logger/winston.logger";
5+
6+
const logger = Logger("app:constants");
37

48
const _nodeEnv = process.env["NODE_ENV"] as NodeEnv;
59
if (!Object.values(NodeEnv).includes(_nodeEnv)) {
610
throw new Error(`Invalid NODE_ENV value: '${_nodeEnv}'`);
711
}
812

9-
export const IS_DEV = isDev(_nodeEnv);
10-
const db = IS_DEV ? "dev_calendar" : "prod_calendar";
13+
const IS_DEV = isDev(_nodeEnv);
14+
15+
const EnvSchema = z
16+
.object({
17+
BASEURL: z.string().nonempty(),
18+
CHANNEL_EXPIRATION_MIN: z.string().nonempty().default("10"),
19+
CLIENT_ID: z.string().nonempty(),
20+
CLIENT_SECRET: z.string().nonempty(),
21+
DB: z.string().nonempty(),
22+
EMAILER_SECRET: z.string().nonempty().optional(),
23+
EMAILER_LIST_ID: z.string().nonempty().optional(),
24+
MONGO_URI: z.string().nonempty(),
25+
NODE_ENV: z.nativeEnum(NodeEnv),
26+
ORIGINS_ALLOWED: z.array(z.string().nonempty()).default([]),
27+
PORT: z.string().nonempty().default(PORT_DEFAULT_BACKEND.toString()),
28+
SUPERTOKENS_URI: z.string().nonempty(),
29+
SUPERTOKENS_KEY: z.string().nonempty(),
30+
TOKEN_GCAL_NOTIFICATION: z.string().nonempty(),
31+
TOKEN_COMPASS_SYNC: z.string().nonempty(),
32+
})
33+
.strict();
1134

12-
const _error = ">> TODO: set this value in .env <<";
35+
type Env = z.infer<typeof EnvSchema>;
1336

1437
export const ENV = {
15-
BASEURL: process.env["BASEURL"] as string,
16-
CHANNEL_EXPIRATION_MIN: process.env["CHANNEL_EXPIRATION_MIN"] || "10",
17-
CLIENT_ID: process.env["CLIENT_ID"] || _error,
18-
CLIENT_SECRET: process.env["CLIENT_SECRET"] || _error,
19-
DB: db,
20-
EMAILER_KEY: process.env["EMAILER_API_KEY"] || _error,
21-
EMAILER_SECRET: process.env["EMAILER_API_SECRET"] || _error,
22-
EMAILER_LIST_ID: process.env["EMAILER_LIST_ID"] || _error,
23-
MONGO_URI: process.env["MONGO_URI"] || _error,
38+
BASEURL: process.env["BASEURL"],
39+
CHANNEL_EXPIRATION_MIN: process.env["CHANNEL_EXPIRATION_MIN"],
40+
CLIENT_ID: process.env["CLIENT_ID"],
41+
CLIENT_SECRET: process.env["CLIENT_SECRET"],
42+
DB: IS_DEV ? "dev_calendar" : "prod_calendar",
43+
EMAILER_SECRET: process.env["EMAILER_API_SECRET"],
44+
EMAILER_LIST_ID: process.env["EMAILER_LIST_ID"],
45+
MONGO_URI: process.env["MONGO_URI"],
2446
NODE_ENV: _nodeEnv,
2547
ORIGINS_ALLOWED: process.env["CORS"] ? process.env["CORS"].split(",") : [],
26-
PORT: process.env["PORT"] || PORT_DEFAULT_BACKEND,
27-
SUPERTOKENS_URI: process.env["SUPERTOKENS_URI"] || _error,
28-
SUPERTOKENS_KEY: process.env["SUPERTOKENS_KEY"] || _error,
29-
TOKEN_GCAL_NOTIFICATION: process.env["TOKEN_GCAL_NOTIFICATION"] || _error,
30-
TOKEN_COMPASS_SYNC: process.env["TOKEN_COMPASS_SYNC"] || _error,
31-
};
32-
33-
if (Object.values(ENV).includes(_error)) {
34-
console.log(
35-
`Exiting because a critical env value is missing: ${JSON.stringify(
36-
ENV,
37-
null,
38-
2
39-
)}`
40-
);
48+
PORT: process.env["PORT"],
49+
SUPERTOKENS_URI: process.env["SUPERTOKENS_URI"],
50+
SUPERTOKENS_KEY: process.env["SUPERTOKENS_KEY"],
51+
TOKEN_GCAL_NOTIFICATION: process.env["TOKEN_GCAL_NOTIFICATION"],
52+
TOKEN_COMPASS_SYNC: process.env["TOKEN_COMPASS_SYNC"],
53+
} as Env;
54+
55+
const parsedEnv = EnvSchema.safeParse(ENV);
56+
57+
if (!parsedEnv.success) {
58+
logger.error(`Exiting because a critical env value is missing or invalid:`);
59+
console.error(parsedEnv.error.issues);
4160
process.exit(1);
4261
}

packages/backend/src/common/constants/error.constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export const DbError = {
3232
};
3333

3434
export const EmailerError = {
35+
IncorrectApiKey: {
36+
description:
37+
"Incorrect API key. Please make sure environment variables beginning with EMAILER_ are correct",
38+
status: Status.BAD_REQUEST,
39+
isOperational: true,
40+
},
3541
AddToListFailed: {
3642
description: "Failed to add email to list",
3743
status: Status.UNSURE,

packages/backend/src/sync/controllers/sync.debug.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getSync } from "../util/sync.queries";
1010
class SyncDebugController {
1111
dispatchEventToClient = (_req: Request, res: Response) => {
1212
try {
13-
const userId = process.env["DEMO_SOCKET_USER"];
13+
const userId = process.env["SOCKET_USER"];
1414
if (!userId) {
1515
console.log("No demo user");
1616
throw new Error("No demo user");

packages/backend/src/user/services/email.service.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,45 @@ const logger = Logger("app:emailer.service");
88

99
class EmailService {
1010
addToEmailList = async (email: string, firstName: string) => {
11-
const url = `https://api.convertkit.com/v3/tags/${ENV.EMAILER_LIST_ID}/subscribe?api_secret=${ENV.EMAILER_SECRET}&email=${email}&first_name=${firstName}`;
11+
if (!ENV.EMAILER_LIST_ID && !ENV.EMAILER_SECRET) {
12+
logger.warn(
13+
"Skipped adding email to list, because EMAILER_ environment variables are missing."
14+
);
15+
return;
16+
}
1217

13-
const response = await axios.post(url);
18+
const url = `https://api.convertkit.com/v3/tags/${
19+
ENV.EMAILER_LIST_ID as string
20+
}/subscribe?api_secret=${
21+
ENV.EMAILER_SECRET as string
22+
}&email=${email}&first_name=${firstName}`;
1423

15-
if (response.status !== 200) {
16-
throw error(EmailerError.AddToListFailed, "Failed to add email to list");
17-
logger.error(response.data);
18-
}
24+
try {
25+
const response = await axios.post(url);
26+
27+
if (response.status !== 200) {
28+
throw error(
29+
EmailerError.AddToListFailed,
30+
"Failed to add email to list"
31+
);
32+
logger.error(response.data);
33+
}
1934

20-
return response;
35+
return response;
36+
} catch (e) {
37+
if (
38+
axios.isAxiosError(e) &&
39+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
40+
e?.response?.data?.message === "API Key not valid"
41+
) {
42+
throw error(
43+
EmailerError.IncorrectApiKey,
44+
"Failed to add email to list. Please make sure environment variables beginning with EMAILER_ are correct"
45+
);
46+
}
47+
48+
throw e;
49+
}
2150
};
2251
}
2352

packages/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"supertokens-auth-react": "^0.46.0",
5555
"supertokens-web-js": "^0.13.0",
5656
"ts-keycode-enum": "^1.0.6",
57-
"uuid": "^9.0.0"
57+
"uuid": "^9.0.0",
58+
"zod": "^3.24.1"
5859
},
5960
"devDependencies": {
6061
"@babel/core": "^7.15.5",

packages/web/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"module": "Node16",
1313
"moduleResolution": "node16",
1414
// "declaration": true, // originally set to true, commented during refactor
15-
// "strict": true,
15+
"strict": true,
1616
// "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
1717
// "strictNullChecks": true /* Enable strict null checks. */,
1818
// "strictFunctionTypes": true /* Enable strict checking of function types. */,

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11264,3 +11264,8 @@ yoctocolors-cjs@^2.1.2:
1126411264
version "2.1.2"
1126511265
resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242"
1126611266
integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==
11267+
11268+
zod@^3.24.1:
11269+
version "3.24.1"
11270+
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
11271+
integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==

0 commit comments

Comments
 (0)