Skip to content

Commit 8a397ee

Browse files
✨ Feat: Add user authentication for seed command (#541)
* ✨ Feat: Add user authentication for seed command * ✨ Feat: Add test event for seed command * ✨ Feat: Move auth logic to auth service * ✨ Feat: Simplify event creation in seed command * 🐛Fix: Use UserId instead of email for creating user session * ✨ Feat: Call auth service for session creation * 🐛Fix: localhost env variable * 🧹 Chore: Correct capitalization in seed command warning * 🔧 Fix: Validate user input as ObjectId in seed command * 🔧 Fix: Update user option syntax in seed command for clarity --------- Co-authored-by: Tyler Dane <tyler@switchback.tech>
1 parent 3f1a910 commit 8a397ee

File tree

8 files changed

+98
-32
lines changed

8 files changed

+98
-32
lines changed

packages/backend/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ SUPERTOKENS_KEY=UNIQUE_KEY_FROM_YOUR_SUPERTOKENS_ACCOUNT
6060
####################################################
6161
# Set these values to save time while using the CLI
6262

63+
LOCAL_DOMAIN=localhost:3000
6364
STAGING_DOMAIN=staging.yourdomain.com
6465
PROD_DOMAIN=app.yourdomain.com
6566

packages/backend/src/auth/controllers/auth.controller.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,12 @@ class AuthController {
4949
}
5050

5151
if (cUserId) {
52-
const sUserId = supertokens.convertToRecipeUserId(cUserId);
53-
await Session.createNewSession(req, res, "public", sUserId);
54-
}
55-
56-
const user = await findCompassUserBy("_id", cUserId);
57-
58-
if (!user) {
52+
await compassAuthService.createSessionForUser(cUserId);
53+
} else {
5954
res.promise({ error: "User doesn't exist" });
6055
return;
6156
}
6257

63-
const sUserId = supertokens.convertToRecipeUserId(user._id.toString());
64-
await Session.createNewSession(req, res, "public", sUserId);
65-
6658
res.promise({
6759
message: `User session created for ${cUserId}`,
6860
});

packages/backend/src/auth/services/compass.auth.service.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import supertokens from "supertokens-node";
12
import Session from "supertokens-node/recipe/session";
3+
import { Logger } from "@core/logger/winston.logger";
24
import { error } from "@backend/common/errors/handlers/error.handler";
35
import { SyncError } from "@backend/common/errors/sync/sync.errors";
46
import { getSync } from "@backend/sync/util/sync.queries";
57
import { canDoIncrementalSync } from "@backend/sync/util/sync.util";
68
import { findCompassUserBy } from "@backend/user/queries/user.queries";
79

10+
const logger = Logger("app:auth.service");
11+
812
class CompassAuthService {
913
determineAuthMethod = async (gUserId: string) => {
1014
const user = await findCompassUserBy("google.googleId", gUserId);
@@ -28,6 +32,29 @@ class CompassAuthService {
2832
return { authMethod, user };
2933
};
3034

35+
createSessionForUser = async (cUserId: string) => {
36+
const userId = cUserId;
37+
const sUserId = supertokens.convertToRecipeUserId(cUserId);
38+
39+
try {
40+
const session = await Session.createNewSessionWithoutRequestResponse(
41+
"public",
42+
sUserId,
43+
);
44+
const accessToken = session.getAccessToken();
45+
logger.info(`user session created for ${userId}`);
46+
return {
47+
sessionHandle: session.getHandle(),
48+
userId: sUserId,
49+
compassUserId: userId,
50+
accessToken,
51+
};
52+
} catch (err) {
53+
logger.error("Error creating session:", err);
54+
throw new Error("Failed to create session");
55+
}
56+
};
57+
3158
revokeSessionsByUser = async (userId: string) => {
3259
const sessionsRevoked = await Session.revokeAllSessionsForUser(userId);
3360
return { sessionsRevoked: sessionsRevoked.length };

packages/scripts/src/cli.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// sort-imports-ignore
22
import { Command } from "commander";
33
import "./init";
4-
// eslint-disable-next-line prettier/prettier
4+
55
import { CliValidator } from "./cli.validator";
66
import { runBuild } from "./commands/build";
77
import { startDeleteFlow } from "./commands/delete";
@@ -86,10 +86,7 @@ class CompassCli {
8686
program
8787
.command("seed")
8888
.description("seed the database with events")
89-
.option(
90-
"-u, --user [id | email]",
91-
"specify which user to seed events for",
92-
);
89+
.option("-u, --user <id>", "specify which user to seed events for");
9390
return program;
9491
}
9592
}

packages/scripts/src/commands/seed.ts

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,73 @@
1+
import axios from "axios";
2+
import dayjs from "dayjs";
13
import pkg from "inquirer";
2-
import { log } from "@scripts/common/cli.utils";
4+
import { ObjectId } from "mongodb";
5+
import { getApiBaseUrl, log } from "@scripts/common/cli.utils";
6+
import { Schema_Event } from "@core/types/event.types";
7+
import { createMockStandaloneEvent } from "@core/util/test/ccal.event.factory";
8+
import compassAuthService from "@backend/auth/services/compass.auth.service";
39
import mongoService from "@backend/common/services/mongo.service";
410
import { findCompassUserBy } from "@backend/user/queries/user.queries";
511

612
const { prompt } = pkg;
713

14+
async function createEvent(
15+
events: Schema_Event[],
16+
baseUrl: string,
17+
accessToken: string,
18+
) {
19+
await axios.post(`${baseUrl}/event`, events, {
20+
headers: { Cookie: `sAccessToken=${accessToken}` },
21+
});
22+
}
23+
824
async function seedEvents(userInput: string) {
925
try {
26+
// Validate userInput as ObjectId
27+
if (!ObjectId.isValid(userInput)) {
28+
log.error(
29+
`Provided user id is not a valid ObjectId: ${userInput}.
30+
Please provide your Compass user's _id value.`,
31+
);
32+
process.exit(1);
33+
}
34+
1035
// Connect to MongoDB
1136
await mongoService.waitUntilConnected();
1237

13-
// Determine if input is email or ID and get the user ID
14-
const isEmail = userInput.includes("@");
15-
const user = await findCompassUserBy(isEmail ? "email" : "_id", userInput);
38+
const user = await findCompassUserBy("_id", userInput);
1639

1740
if (!user) {
18-
log.error(
19-
`User not found with ${isEmail ? "email" : "ID"}: ${userInput}`,
20-
);
41+
log.error(`User not found with Compass ID: ${userInput}`);
2142
process.exit(1);
2243
}
2344

2445
const userId = user._id.toString();
25-
log.info(`Running seed command for user: ${userId}...`);
46+
log.info(`Running seed command for user: ${userId} (${user.email})...`);
47+
48+
// Creates user session
49+
const { accessToken } =
50+
await compassAuthService.createSessionForUser(userId);
51+
const baseUrl = await getApiBaseUrl("local");
52+
53+
// Test Event
54+
const eventOverrides = {
55+
user: userId,
56+
isAllDay: false,
57+
isSomeday: false,
58+
startDate: dayjs().hour(10).minute(0).second(0).toISOString(),
59+
endDate: dayjs().hour(11).minute(0).second(0).toISOString(),
60+
};
61+
const event = createMockStandaloneEvent(eventOverrides);
62+
const events: Schema_Event[] = [event];
63+
64+
await createEvent(events, baseUrl, accessToken);
2665

27-
// TODO: Add logic to create events
66+
// TODO: Create a variety of events for seeding
2867

29-
log.success(`Successfully created events for user: ${userInput}`);
68+
log.success(
69+
`Successfully created events for user: ${user.email} with id: ${userId}`,
70+
);
3071
} catch (error) {
3172
log.error("Failed to seed events:");
3273
console.error(error);
@@ -48,8 +89,8 @@ export async function runSeed(userInput: string, force = false) {
4889
"⚠️ WARNING ⚠️",
4990
"",
5091
"This command will:",
51-
"• Create multiple events in your Compass calendar database",
52-
"• Create multiple events in your primary Google calendar",
92+
"• Create multiple events in your Compass Calendar database",
93+
"• Create multiple events in your primary Google Calendar",
5394
"",
5495
"🔔 RECOMMENDATION:",
5596
"It's strongly recommended to use this command with a test account",

packages/scripts/src/common/cli.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const CATEGORY_VM = {
1515
};
1616

1717
export const CLI_ENV = {
18+
LOCAL_DOMAIN: process.env["LOCAL_DOMAIN"] || `localhost:3000`,
1819
STAGING_DOMAIN: process.env["STAGING_DOMAIN"],
1920
PROD_DOMAIN: process.env["PROD_DOMAIN"],
2021
};

packages/scripts/src/common/cli.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ export type Options_Cli = Options_Cli_Root &
2121
Options_Cli_Build &
2222
Options_Cli_Delete;
2323

24-
export type Environment_Cli = "staging" | "production";
24+
export type Environment_Cli = "local" | "staging" | "production";

packages/scripts/src/common/cli.utils.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ export const fileExists = (file: string) => {
1212

1313
export const getApiBaseUrl = async (environment: Environment_Cli) => {
1414
const category = environment ? environment : await getEnvironmentAnswer();
15-
const isStaging = category === "staging";
16-
const domain = await getDomainAnswer(isStaging);
17-
const baseUrl = `https://${domain}/api`;
15+
const domain = await getDomainAnswer(category);
16+
const baseUrl =
17+
environment === "local" ? `http://${domain}/api` : `https://${domain}/api`;
1818

1919
return baseUrl;
2020
};
@@ -41,7 +41,14 @@ export const getClientId = async (environment: Environment_Cli) => {
4141
throw Error("Invalid destination");
4242
};
4343

44-
const getDomainAnswer = async (isStaging: boolean) => {
44+
const getDomainAnswer = async (env: string) => {
45+
const isLocal = env === "local";
46+
const isStaging = env === "staging";
47+
48+
if (isLocal && CLI_ENV.LOCAL_DOMAIN !== undefined) {
49+
return CLI_ENV.LOCAL_DOMAIN;
50+
}
51+
4552
if (isStaging && CLI_ENV.STAGING_DOMAIN !== undefined) {
4653
return CLI_ENV.STAGING_DOMAIN;
4754
}

0 commit comments

Comments
 (0)