From 31dc493210dc73f60c09ce113e529b2f73f711a7 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Mon, 23 Mar 2026 19:46:33 -0700 Subject: [PATCH 1/5] feat(delete): enhance deleteCompassDataForMatchingUsers to include Google Calendar access check - Updated the deleteCompassDataForMatchingUsers function to accept a second parameter, gcalAccess, indicating whether the user has Google Calendar access. - This change allows for more granular control over the deletion process based on the user's Google Calendar connection status. --- packages/scripts/src/commands/delete.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/scripts/src/commands/delete.ts b/packages/scripts/src/commands/delete.ts index e10065bee..e1f3a06e3 100644 --- a/packages/scripts/src/commands/delete.ts +++ b/packages/scripts/src/commands/delete.ts @@ -141,7 +141,11 @@ export const deleteCompassDataForMatchingUsers = async (user: string) => { const totalSummary: Summary_Delete[] = []; for (const user of users) { const userId = user?._id.toString(); - const summary = await userService.deleteCompassDataForUser(userId); + const gcalAccess = !!user.google?.gRefreshToken; + const summary = await userService.deleteCompassDataForUser( + userId, + gcalAccess, + ); totalSummary.push(summary); } From 4f4892000769fb0e8bcd6e4ad9e5fb2844c0be54 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Mon, 23 Mar 2026 19:59:32 -0700 Subject: [PATCH 2/5] feat(auth): enhance AuthModal tests and URL handling for password reset flow - Introduced a new `RouteLocationMirror` component to synchronize URL state with the AuthModal. - Added `renderWithDayRedirectRoute` function to facilitate testing of the AuthModal with day-based routing. - Updated tests to verify that the reset password flow correctly preserves authentication parameters in the URL during redirects. - Implemented `updateCurrentUrlSearchParams` function to streamline URL parameter updates in the authentication flow. - Enhanced existing tests to ensure robust coverage of the new functionality and maintainability of the AuthModal component. --- .../components/AuthModal/AuthModal.test.tsx | 92 ++++++++++++++++++- .../AuthModal/hooks/useAuthFormHandlers.ts | 23 ++++- packages/web/src/routers/loaders.test.ts | 39 +++++++- packages/web/src/routers/loaders.ts | 15 ++- 4 files changed, 157 insertions(+), 12 deletions(-) diff --git a/packages/web/src/components/AuthModal/AuthModal.test.tsx b/packages/web/src/components/AuthModal/AuthModal.test.tsx index 8a342bb10..51f948895 100644 --- a/packages/web/src/components/AuthModal/AuthModal.test.tsx +++ b/packages/web/src/components/AuthModal/AuthModal.test.tsx @@ -1,9 +1,16 @@ -import { type ReactElement, type ReactNode } from "react"; -import { MemoryRouter } from "react-router-dom"; +import { type ReactElement, type ReactNode, useLayoutEffect } from "react"; +import { + MemoryRouter, + Outlet, + RouterProvider, + createMemoryRouter, + useLocation, +} from "react-router-dom"; import EmailPassword from "supertokens-web-js/recipe/emailpassword"; import "@testing-library/jest-dom"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { loadDayData, loadTodayData } from "@web/routers/loaders"; import { AccountIcon } from "./AccountIcon"; import { AuthModal } from "./AuthModal"; import { AuthModalProvider } from "./AuthModalProvider"; @@ -126,6 +133,63 @@ async function flushEffects() { await Promise.resolve(); } +const RouteLocationMirror = ({ children }: { children: ReactNode }) => { + const location = useLocation(); + + useLayoutEffect(() => { + mockWindowLocation( + `${location.pathname}${location.search}${location.hash}`, + ); + }, [location]); + + return <>{children}; +}; + +const DayRedirectShell = () => ( + + + + + + +); + +const renderWithDayRedirectRoute = (initialRoute: string) => { + mockWindowLocation(initialRoute); + + const router = createMemoryRouter( + [ + { + path: "/day", + Component: DayRedirectShell, + children: [ + { + index: true, + loader: loadDayData, + }, + { + path: ":dateString", + element:
Day route loaded
, + }, + ], + }, + ], + { + initialEntries: [initialRoute], + future: { + v7_relativeSplatPath: true, + }, + }, + ); + + return { + router, + ...render( + , + ), + }; +}; + describe("AuthModal", () => { beforeEach(() => { jest.clearAllMocks(); @@ -860,6 +924,25 @@ describe("URL Parameter Support", () => { }); }); + it("opens reset password after the /day redirect preserves auth params", async () => { + const { dateString } = loadTodayData(); + + renderWithDayRedirectRoute("/day?auth=reset&token=reset-token"); + + await waitFor(() => { + expect(screen.getByText("Day route loaded")).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: /set new password/i }), + ).toBeInTheDocument(); + }); + + expect(mockReplaceState).toHaveBeenCalledWith( + null, + "", + `/day/${dateString}?token=reset-token`, + ); + }); + it("submits reset password with the initial token after the URL changes", async () => { const user = userEvent.setup(); mockWindowLocation("/day?auth=reset&token=reset-token"); @@ -882,6 +965,11 @@ describe("URL Parameter Support", () => { }); }); + expect(mockReplaceState).toHaveBeenLastCalledWith(undefined, "", "/day"); + expect( + screen.getByRole("heading", { name: /hey, welcome back/i }), + ).toBeInTheDocument(); + expect(mockCompleteAuthentication).not.toHaveBeenCalled(); expect( mockEmailPassword.getResetPasswordTokenFromURL, ).not.toHaveBeenCalled(); diff --git a/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts b/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts index f1f360cc9..1ad61604a 100644 --- a/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts +++ b/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts @@ -25,6 +25,18 @@ function getResetPasswordQueryParams(): z.infer< return parsed.success ? parsed.data : {}; } +function updateCurrentUrlSearchParams( + updateSearchParams: (searchParams: URLSearchParams) => void, +): void { + if (typeof window === "undefined") return; + + const url = new URL(window.location.href); + updateSearchParams(url.searchParams); + const nextUrl = `${url.pathname}${url.search}${url.hash}`; + + window.history.replaceState(window.history.state, "", nextUrl); +} + interface UseAuthFormHandlersOptions { currentView: AuthView; closeModal: () => void; @@ -174,10 +186,10 @@ export function useAuthFormHandlers({ // We keep the first token we saw (from props or URL) so the flow still works even if the URL changes. const token = initialResetPasswordToken; - if (token && typeof window !== "undefined") { - const url = new URL(window.location.href); - url.searchParams.set("token", token); - window.history.replaceState(window.history.state, "", url.toString()); + if (token) { + updateCurrentUrlSearchParams((searchParams) => { + searchParams.set("token", token); + }); } const response = await EmailPassword.submitNewPassword({ formFields: [{ id: "password", value: data.password }], @@ -185,6 +197,9 @@ export function useAuthFormHandlers({ switch (response.status) { case "OK": + updateCurrentUrlSearchParams((searchParams) => { + searchParams.delete("token"); + }); setView("login"); return; case "FIELD_ERROR": diff --git a/packages/web/src/routers/loaders.test.ts b/packages/web/src/routers/loaders.test.ts index 0e132075f..548728b33 100644 --- a/packages/web/src/routers/loaders.test.ts +++ b/packages/web/src/routers/loaders.test.ts @@ -1,14 +1,49 @@ +import { type LoaderFunctionArgs } from "react-router-dom"; import { ROOT_ROUTES } from "@web/common/constants/routes"; -import { loadRootData, loadTodayData } from "@web/routers/loaders"; +import { loadDayData, loadRootData, loadTodayData } from "@web/routers/loaders"; + +function createLoaderArgs(url: string): LoaderFunctionArgs { + return { + request: new Request(url), + params: {}, + context: undefined, + }; +} describe("loadRootData", () => { it("redirects root route to day route with today's date", async () => { const { dateString } = loadTodayData(); - const response = await loadRootData(); + const response = await loadRootData(createLoaderArgs("http://localhost/")); expect(response.status).toBe(302); expect(response.headers.get("Location")).toBe( `${ROOT_ROUTES.DAY}/${dateString}`, ); }); + + it("preserves auth query params when redirecting to today's date", async () => { + const { dateString } = loadTodayData(); + const response = await loadRootData( + createLoaderArgs("http://localhost/?auth=login"), + ); + + expect(response.status).toBe(302); + expect(response.headers.get("Location")).toBe( + `${ROOT_ROUTES.DAY}/${dateString}?auth=login`, + ); + }); +}); + +describe("loadDayData", () => { + it("preserves auth query params when redirecting to the dated route", async () => { + const { dateString } = loadTodayData(); + const response = await loadDayData( + createLoaderArgs("http://localhost/day?auth=reset&token=abc"), + ); + + expect(response.status).toBe(302); + expect(response.headers.get("Location")).toBe( + `${ROOT_ROUTES.DAY}/${dateString}?auth=reset&token=abc`, + ); + }); }); diff --git a/packages/web/src/routers/loaders.ts b/packages/web/src/routers/loaders.ts index 5d3442914..966c14ca5 100644 --- a/packages/web/src/routers/loaders.ts +++ b/packages/web/src/routers/loaders.ts @@ -29,14 +29,21 @@ export function loadTodayData(): DayLoaderData { return { dateInView, dateString: dateInView.format(dateFormat) }; } -export async function loadDayData() { +function buildTodayRedirectUrl(request: Request): string { const { dateString } = loadTodayData(); + const url = new URL(request.url); - return redirect(`${ROOT_ROUTES.DAY}/${dateString}`); + return `${ROOT_ROUTES.DAY}/${dateString}${url.search}`; } -export async function loadRootData() { - return loadDayData(); +export function loadDayData({ + request, +}: LoaderFunctionArgs): Response { + return redirect(buildTodayRedirectUrl(request)); +} + +export function loadRootData(args: LoaderFunctionArgs): Response { + return loadDayData(args); } export async function loadSpecificDayData({ From 562d67376c74b12acdee5209b976c988c13b35f8 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Mon, 23 Mar 2026 20:12:24 -0700 Subject: [PATCH 3/5] feat(auth): enhance AuthModal for password reset flow and status messaging - Introduced a new status message in the LogInForm to inform users of successful password resets. - Updated AuthModal logic to handle the new "loginAfterReset" view, improving user experience during the authentication process. - Enhanced tests for LogInForm to verify the display of status messages, ensuring accurate feedback for users. - Refactored related hooks and components to support the updated authentication flow and maintain clarity in the codebase. --- .../components/AuthModal/AuthModal.test.tsx | 3 +++ .../src/components/AuthModal/AuthModal.tsx | 20 +++++++++++------ .../components/AuthModal/forms/LogInForm.tsx | 9 ++++++++ .../AuthModal/forms/LoginForm.test.tsx | 22 +++++++++++++++++++ .../AuthModal/hooks/useAuthFormHandlers.ts | 13 +++++++++-- .../AuthModal/hooks/useAuthModal.ts | 7 +++++- .../components/AuthModal/hooks/useZodForm.ts | 20 +++++++++-------- 7 files changed, 75 insertions(+), 19 deletions(-) diff --git a/packages/web/src/components/AuthModal/AuthModal.test.tsx b/packages/web/src/components/AuthModal/AuthModal.test.tsx index 51f948895..f2526f054 100644 --- a/packages/web/src/components/AuthModal/AuthModal.test.tsx +++ b/packages/web/src/components/AuthModal/AuthModal.test.tsx @@ -966,6 +966,9 @@ describe("URL Parameter Support", () => { }); expect(mockReplaceState).toHaveBeenLastCalledWith(undefined, "", "/day"); + expect(screen.getByRole("status")).toHaveTextContent( + "Password reset successful. Log in with your new password.", + ); expect( screen.getByRole("heading", { name: /hey, welcome back/i }), ).toBeInTheDocument(); diff --git a/packages/web/src/components/AuthModal/AuthModal.tsx b/packages/web/src/components/AuthModal/AuthModal.tsx index c23f01aed..9b3b8b40b 100644 --- a/packages/web/src/components/AuthModal/AuthModal.tsx +++ b/packages/web/src/components/AuthModal/AuthModal.tsx @@ -40,6 +40,8 @@ export const AuthModal: FC = () => { const { isOpen, currentView, openModal, closeModal, setView } = useAuthModal(); const googleAuth = useGoogleAuth(); + const isLoginView = + currentView === "login" || currentView === "loginAfterReset"; const resetPasswordToken = useRef(getInitialResetPasswordToken()).current; const { isSubmitting, @@ -68,12 +70,12 @@ export const AuthModal: FC = () => { }, [currentView]); const handleSwitchAuth = useCallback( - () => setView(currentView === "login" ? "signUp" : "login"), + () => setView(currentView === "signUp" ? "login" : "signUp"), [currentView, setView], ); const handleGoogleSignIn = useCallback(() => { - googleAuth.login(); + void googleAuth.login(); closeModal(); }, [googleAuth, closeModal]); @@ -93,11 +95,10 @@ export const AuthModal: FC = () => { return null; } - const showAuthSwitch = currentView === "login" || currentView === "signUp"; + const showAuthSwitch = isLoginView || currentView === "signUp"; const showGoogleAuth = currentView !== "resetPassword"; const showSubmitError = - submitError !== null && - (currentView === "login" || currentView === "signUp"); + submitError !== null && (isLoginView || currentView === "signUp"); const trimmedName = signUpName.trim(); const title = currentView === "forgotPassword" @@ -121,11 +122,16 @@ export const AuthModal: FC = () => { isSubmitting={isSubmitting} /> )} - {currentView === "login" && ( + {isLoginView && ( )} {currentView === "forgotPassword" && ( @@ -161,7 +167,7 @@ export const AuthModal: FC = () => { variant="outline" onClick={handleSwitchAuth} > - {currentView === "login" ? "Sign up" : "Log in"} + {isLoginView ? "Sign up" : "Log in"} )} diff --git a/packages/web/src/components/AuthModal/forms/LogInForm.tsx b/packages/web/src/components/AuthModal/forms/LogInForm.tsx index 126d4d448..9da4323bf 100644 --- a/packages/web/src/components/AuthModal/forms/LogInForm.tsx +++ b/packages/web/src/components/AuthModal/forms/LogInForm.tsx @@ -14,6 +14,8 @@ interface SignInFormProps { onForgotPassword: () => void; /** Whether form submission is in progress */ isSubmitting?: boolean; + /** Optional status message shown above the form */ + statusMessage?: string | null; } /** @@ -25,6 +27,7 @@ export const LogInForm: FC = ({ onSubmit, onForgotPassword, isSubmitting, + statusMessage, }) => { const form = useZodForm({ schema: LogInSchema, @@ -34,6 +37,12 @@ export const LogInForm: FC = ({ return (
+ {statusMessage ? ( +

+ {statusMessage} +

+ ) : null} + { ); }; +const renderSignInFormWithStatus = (statusMessage: string) => { + render( + , + ); +}; + describe("LogInForm", () => { beforeEach(() => { jest.clearAllMocks(); @@ -174,4 +184,16 @@ describe("LogInForm", () => { expect(mockOnForgotPassword).toHaveBeenCalled(); }); }); + + describe("status message", () => { + it("shows a status message above the form when provided", () => { + renderSignInFormWithStatus( + "Password reset successful. Log in with your new password.", + ); + + expect(screen.getByRole("status")).toHaveTextContent( + "Password reset successful. Log in with your new password.", + ); + }); + }); }); diff --git a/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts b/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts index 1ad61604a..a0cd2e63e 100644 --- a/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts +++ b/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts @@ -44,12 +44,21 @@ interface UseAuthFormHandlersOptions { setView: (view: AuthView) => void; } +export interface UseAuthFormHandlersResult { + isSubmitting: boolean; + submitError: string | null; + handleSignUp: (data: SignUpFormData) => Promise; + handleLogin: (data: LogInFormData) => Promise; + handleForgotPassword: (data: ForgotPasswordFormData) => Promise; + handleResetPassword: (data: ResetPasswordFormData) => Promise; +} + export function useAuthFormHandlers({ currentView, closeModal, resetPasswordToken, setView, -}: UseAuthFormHandlersOptions) { +}: UseAuthFormHandlersOptions): UseAuthFormHandlersResult { const completeAuthentication = useCompleteAuthentication(); const [isSubmitting, setIsSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); @@ -200,7 +209,7 @@ export function useAuthFormHandlers({ updateCurrentUrlSearchParams((searchParams) => { searchParams.delete("token"); }); - setView("login"); + setView("loginAfterReset"); return; case "FIELD_ERROR": setSubmitError( diff --git a/packages/web/src/components/AuthModal/hooks/useAuthModal.ts b/packages/web/src/components/AuthModal/hooks/useAuthModal.ts index 8a51ccf62..1fca97cd2 100644 --- a/packages/web/src/components/AuthModal/hooks/useAuthModal.ts +++ b/packages/web/src/components/AuthModal/hooks/useAuthModal.ts @@ -6,7 +6,12 @@ import { useState, } from "react"; -export type AuthView = "login" | "signUp" | "forgotPassword" | "resetPassword"; +export type AuthView = + | "login" + | "loginAfterReset" + | "signUp" + | "forgotPassword" + | "resetPassword"; interface AuthModalContextValue { isOpen: boolean; diff --git a/packages/web/src/components/AuthModal/hooks/useZodForm.ts b/packages/web/src/components/AuthModal/hooks/useZodForm.ts index fddf419e8..4e798e0cb 100644 --- a/packages/web/src/components/AuthModal/hooks/useZodForm.ts +++ b/packages/web/src/components/AuthModal/hooks/useZodForm.ts @@ -35,7 +35,7 @@ export interface UseZodFormReturn> { field: keyof TValues & string, ) => (e: ChangeEvent) => void; handleBlur: (field: keyof TValues & string) => () => void; - handleSubmit: (e: FormEvent) => Promise; + handleSubmit: (e: FormEvent) => void; isValid: boolean; } @@ -104,7 +104,7 @@ export function useZodForm>({ ); const handleSubmit = useCallback( - async (e: FormEvent) => { + (e: FormEvent): void => { e.preventDefault(); const allTouched = Object.keys(initialValues).reduce( @@ -115,13 +115,15 @@ export function useZodForm>({ const result = schema.safeParse(values); if (result.success) { - try { - await onSubmit(result.data); - } catch (error) { - // Error is handled by the onSubmit callback - // Swallow the error to prevent unhandled promise rejection - // since React form handlers don't await the returned promise - } + void (async () => { + try { + await onSubmit(result.data); + } catch { + // Error is handled by the onSubmit callback + // Swallow the error to prevent unhandled promise rejection + // since React form handlers don't await the returned promise + } + })(); } else { setErrors( getFieldErrors(result.error) as Partial< From 4b98d545edeb789fa2a2913e320b222c46233cf8 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Mon, 23 Mar 2026 20:29:21 -0700 Subject: [PATCH 4/5] feat(auth): enhance AuthModal tests for sign-up flow after password reset - Added a test to verify that clicking "Sign up" after a successful password reset correctly transitions the user to the sign-up view. - Updated error handling in the useZodForm hook to log errors in development mode, improving debugging capabilities during form submissions. --- .../components/AuthModal/AuthModal.test.tsx | 32 +++++++++++++++++++ .../components/AuthModal/hooks/useZodForm.ts | 5 ++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/web/src/components/AuthModal/AuthModal.test.tsx b/packages/web/src/components/AuthModal/AuthModal.test.tsx index f2526f054..960d8a49d 100644 --- a/packages/web/src/components/AuthModal/AuthModal.test.tsx +++ b/packages/web/src/components/AuthModal/AuthModal.test.tsx @@ -977,6 +977,38 @@ describe("URL Parameter Support", () => { mockEmailPassword.getResetPasswordTokenFromURL, ).not.toHaveBeenCalled(); }); + + it("switches to signUp (not back to loginAfterReset) when Sign up is clicked after reset", async () => { + const user = userEvent.setup(); + mockWindowLocation("/day?auth=reset&token=reset-token"); + mockEmailPassword.submitNewPassword.mockResolvedValue({ + status: "OK", + } as never); + renderWithProviders(
, "/day?auth=reset&token=reset-token"); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: /set new password/i }), + ).toBeInTheDocument(); + }); + + await user.type(screen.getByLabelText(/new password/i), "newpassword123"); + await user.click(screen.getByRole("button", { name: /set new password/i })); + + await waitFor(() => { + expect(screen.getByRole("status")).toHaveTextContent( + "Password reset successful. Log in with your new password.", + ); + }); + + await user.click(screen.getByRole("button", { name: /^sign up$/i })); + + await waitFor(() => { + expect( + screen.getByRole("heading", { name: /nice to meet you/i }), + ).toBeInTheDocument(); + }); + }); }); describe("AccountIcon", () => { diff --git a/packages/web/src/components/AuthModal/hooks/useZodForm.ts b/packages/web/src/components/AuthModal/hooks/useZodForm.ts index 4e798e0cb..f9c53bbbc 100644 --- a/packages/web/src/components/AuthModal/hooks/useZodForm.ts +++ b/packages/web/src/components/AuthModal/hooks/useZodForm.ts @@ -118,10 +118,13 @@ export function useZodForm>({ void (async () => { try { await onSubmit(result.data); - } catch { + } catch (error) { // Error is handled by the onSubmit callback // Swallow the error to prevent unhandled promise rejection // since React form handlers don't await the returned promise + if (process.env.NODE_ENV === "development") { + console.error(error); + } } })(); } else { From 3ecdcb7e6fbd7a989125d32c54889895db4fdcbb Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Tue, 24 Mar 2026 20:13:41 -0700 Subject: [PATCH 5/5] feat(env): rename LOCAL_WEB_URL to FRONTEND_URL across configuration and documentation --- .gitignore | 3 +-- docs/env-and-dev-modes.md | 4 ++-- docs/password-auth-flow.md | 4 ++-- packages/backend/.env.local.example | 2 +- packages/backend/src/__tests__/backend.test.init.ts | 2 +- packages/backend/src/common/constants/env.constants.ts | 4 ++-- .../src/common/middleware/supertokens.middleware.test.ts | 2 +- .../backend/src/common/middleware/supertokens.middleware.ts | 2 +- packages/scripts/src/commands/delete.ts | 2 +- packages/scripts/src/common/cli.constants.ts | 4 ++-- 10 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 497e1e0a0..05e10533a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,7 @@ packages/**/yarn.lock # DIRS # ######## # root -.claude/settings.local.json -.claude/worktrees/ +.claude/ .idea/ .mcp.json .vscode/ diff --git a/docs/env-and-dev-modes.md b/docs/env-and-dev-modes.md index eaf2c4bca..9036ef320 100644 --- a/docs/env-and-dev-modes.md +++ b/docs/env-and-dev-modes.md @@ -67,7 +67,7 @@ Important variables: - `SUPERTOKENS_KEY` - `TOKEN_GCAL_NOTIFICATION` - `TOKEN_COMPASS_SYNC` -- `LOCAL_WEB_URL` +- `FRONTEND_URL` Optional but behavior-changing: @@ -86,7 +86,7 @@ Primary files: Variables used by CLI/build flows: - `BASEURL` (required for local CLI operations; returned as-is for local API base URL) -- `LOCAL_WEB_URL` (required; used by backend auth email flows) +- `FRONTEND_URL` (required; used by backend auth email flows — set to the public-facing frontend URL for this deployment) - `STAGING_WEB_URL` (optional; used to derive `https:///api` for staging CLI runs) - `PROD_WEB_URL` (optional; used to derive `https:///api` for production CLI runs) diff --git a/docs/password-auth-flow.md b/docs/password-auth-flow.md index 2f15f255c..e5b905d31 100644 --- a/docs/password-auth-flow.md +++ b/docs/password-auth-flow.md @@ -241,7 +241,7 @@ Current behavior in `supertokens.middleware.ts`: The rewritten link shape comes from `buildResetPasswordLink()` and looks like: -The host/origin portion is taken from backend env (`LOCAL_WEB_URL`), and the route is always `/day` with `auth=reset` plus the token. +The host/origin portion is taken from backend env (`FRONTEND_URL`), and the route is always `/day` with `auth=reset` plus the token. - `http://localhost:9080/day?auth=reset&token=...` @@ -260,4 +260,4 @@ That lets password-auth users use Compass without blocking on Google connectivit ## Known Caveats - The rollout gate is not limited to `lastKnownEmail`; any `?auth=` URL currently enables the auth UI. -- Reset password links always target the `/day` route and require a valid `LOCAL_WEB_URL` in backend env. +- Reset password links always target the `/day` route and require a valid `FRONTEND_URL` in backend env. diff --git a/packages/backend/.env.local.example b/packages/backend/.env.local.example index f67a5c6df..3ae234165 100644 --- a/packages/backend/.env.local.example +++ b/packages/backend/.env.local.example @@ -58,7 +58,7 @@ SUPERTOKENS_KEY=UNIQUE_KEY_FROM_YOUR_SUPERTOKENS_ACCOUNT #################################################### # 5. Web # #################################################### -LOCAL_WEB_URL=http://localhost:9080 +FRONTEND_URL=http://localhost:9080 # STAGING_WEB_URL=https://staging.yourdomain.com # PROD_WEB_URL=https://app.yourdomain.com diff --git a/packages/backend/src/__tests__/backend.test.init.ts b/packages/backend/src/__tests__/backend.test.init.ts index efc25e527..95db1816f 100644 --- a/packages/backend/src/__tests__/backend.test.init.ts +++ b/packages/backend/src/__tests__/backend.test.init.ts @@ -15,4 +15,4 @@ process.env["EMAILER_API_SECRET"] = "emailerApiSecret"; process.env["EMAILER_USER_TAG_ID"] = "910111213"; process.env["TOKEN_GCAL_NOTIFICATION"] = "secretToken1"; process.env["TOKEN_COMPASS_SYNC"] = "secretToken2"; -process.env["LOCAL_WEB_URL"] = "http://localhost:9080"; +process.env["FRONTEND_URL"] = "http://localhost:9080"; diff --git a/packages/backend/src/common/constants/env.constants.ts b/packages/backend/src/common/constants/env.constants.ts index 3e4fd45bb..abf4febb5 100644 --- a/packages/backend/src/common/constants/env.constants.ts +++ b/packages/backend/src/common/constants/env.constants.ts @@ -22,7 +22,7 @@ const EnvSchema = z DB: z.string().nonempty(), EMAILER_SECRET: z.string().nonempty().optional(), EMAILER_USER_TAG_ID: z.string().nonempty().optional(), - LOCAL_WEB_URL: z.string().url(), + FRONTEND_URL: z.string().url(), MONGO_URI: z.string().nonempty(), NODE_ENV: z.nativeEnum(NodeEnv), TZ: z.enum(["Etc/UTC", "UTC"]), @@ -57,7 +57,7 @@ const processEnv = { DB: IS_DEV ? "dev_calendar" : "prod_calendar", EMAILER_SECRET: process.env["EMAILER_API_SECRET"], EMAILER_USER_TAG_ID: process.env["EMAILER_USER_TAG_ID"], - LOCAL_WEB_URL: process.env["LOCAL_WEB_URL"], + FRONTEND_URL: process.env["FRONTEND_URL"], MONGO_URI: process.env["MONGO_URI"], NODE_ENV: _nodeEnv, TZ: process.env["TZ"], diff --git a/packages/backend/src/common/middleware/supertokens.middleware.test.ts b/packages/backend/src/common/middleware/supertokens.middleware.test.ts index e433fd1af..2ada3fb58 100644 --- a/packages/backend/src/common/middleware/supertokens.middleware.test.ts +++ b/packages/backend/src/common/middleware/supertokens.middleware.test.ts @@ -267,7 +267,7 @@ describe("supertokens.middleware", () => { expect(buildResetPasswordLink).toHaveBeenCalledWith( "http://localhost:1234/auth/reset-password?token=abc", - ENV.LOCAL_WEB_URL, + ENV.FRONTEND_URL, ); // In test env, sending is suppressed — originalSendEmail must not be called expect(originalSendEmail).not.toHaveBeenCalled(); diff --git a/packages/backend/src/common/middleware/supertokens.middleware.ts b/packages/backend/src/common/middleware/supertokens.middleware.ts index b4e9cbd04..bfd7df8c5 100644 --- a/packages/backend/src/common/middleware/supertokens.middleware.ts +++ b/packages/backend/src/common/middleware/supertokens.middleware.ts @@ -179,7 +179,7 @@ export const initSupertokens = () => { sendEmail: async (input) => { const resetLink = buildResetPasswordLink( input.passwordResetLink, - ENV.LOCAL_WEB_URL, + ENV.FRONTEND_URL, ); if (ENV.NODE_ENV === "test") { diff --git a/packages/scripts/src/commands/delete.ts b/packages/scripts/src/commands/delete.ts index e1f3a06e3..9d84d7c1a 100644 --- a/packages/scripts/src/commands/delete.ts +++ b/packages/scripts/src/commands/delete.ts @@ -58,7 +58,7 @@ const getCleanupUrl = (): string => { return `${CLI_ENV.STAGING_WEB_URL}/cleanup`; } - return `${CLI_ENV.LOCAL_WEB_URL}/cleanup`; + return `${CLI_ENV.FRONTEND_URL}/cleanup`; }; /** diff --git a/packages/scripts/src/common/cli.constants.ts b/packages/scripts/src/common/cli.constants.ts index 39f5aa4b4..f0a3c0cd2 100644 --- a/packages/scripts/src/common/cli.constants.ts +++ b/packages/scripts/src/common/cli.constants.ts @@ -1,5 +1,5 @@ type CliEnv = { - LOCAL_WEB_URL: string; + FRONTEND_URL: string; STAGING_WEB_URL: string | undefined; PROD_WEB_URL: string | undefined; DEV_BROWSER: string | undefined; @@ -23,7 +23,7 @@ export const ENVIRONMENT = { }; export const CLI_ENV: CliEnv = { - LOCAL_WEB_URL: process.env["LOCAL_WEB_URL"] || `http://localhost:9080`, + FRONTEND_URL: process.env["FRONTEND_URL"] || `http://localhost:9080`, STAGING_WEB_URL: process.env["STAGING_WEB_URL"], PROD_WEB_URL: process.env["PROD_WEB_URL"], DEV_BROWSER: process.env["DEV_BROWSER"],