From 9bfbd67cb4ca5bdb6632508d80860bbe8dc7c8eb Mon Sep 17 00:00:00 2001 From: Paperclip Date: Tue, 14 Apr 2026 14:43:46 +0000 Subject: [PATCH 1/6] fix: update vite to 6.4.2 to patch high-severity vulnerabilities Vite 6.4.1 has two high-severity vulnerabilities: - GHSA-4w7w-66w2-5vf9: Path Traversal in Optimized Deps .map Handling - GHSA-p9ff-h696-f583: Arbitrary File Read via Vite Dev Server WebSocket Updated to vite 6.4.2. Fixes CAR-599. Co-Authored-By: Paperclip --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a56c4d4..709106e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9805,9 +9805,9 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "devOptional": true, "license": "MIT", "dependencies": { From a11726b8e60242a3ee069f1e03b57859e9db2a5e Mon Sep 17 00:00:00 2001 From: Paperclip Date: Tue, 14 Apr 2026 15:37:24 +0000 Subject: [PATCH 2/6] fix: remove VITE_MOCK_AUTH bypass from production code Co-Authored-By: Paperclip --- e2e/journeys/j1-registration-login.spec.ts | 1 - playwright.config.ts | 2 +- src/components/ProtectedRoute.tsx | 17 ----------------- src/pages/Login.tsx | 9 +-------- src/pages/Register.tsx | 9 +-------- 5 files changed, 3 insertions(+), 35 deletions(-) diff --git a/e2e/journeys/j1-registration-login.spec.ts b/e2e/journeys/j1-registration-login.spec.ts index ec116ab..b1b28a4 100644 --- a/e2e/journeys/j1-registration-login.spec.ts +++ b/e2e/journeys/j1-registration-login.spec.ts @@ -10,7 +10,6 @@ test.describe('J1: Registration and Login', () => { await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!'); await page.click('button[type="submit"]'); - // With VITE_MOCK_AUTH=true the app navigates to "/" on success await expect(page).toHaveURL('http://localhost:5173/'); await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible(); }); diff --git a/playwright.config.ts b/playwright.config.ts index b22d74a..a2d7b0b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ }, ], webServer: { - command: 'VITE_MOCK_AUTH=true npm run dev', + command: 'npm run dev', url: 'http://localhost:5173', reuseExistingServer: !process.env.CI, }, diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index cf92831..294ec4f 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,25 +1,8 @@ -import { useEffect } from 'react' import { Navigate, Outlet } from 'react-router-dom' import { authClient } from '../lib/auth-client.ts' -import { useAuthStore } from '../stores/auth.ts' export function ProtectedRoute() { - const isMockAuth = import.meta.env.VITE_MOCK_AUTH === 'true' const { data: session, isPending } = authClient.useSession() - const isAuthenticated = useAuthStore((s) => s.isAuthenticated) - const setAuthenticated = useAuthStore((s) => s.setAuthenticated) - - useEffect(() => { - if (!isMockAuth) { - setAuthenticated(!!session) - } - }, [session, setAuthenticated, isMockAuth]) - - // In mock auth mode, rely on Zustand store (set by Login/Register pages) - if (isMockAuth) { - if (!isAuthenticated) return - return - } if (isPending) { return ( diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index ae7fc0c..5044613 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,7 +1,6 @@ import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { authClient } from '../lib/auth-client.ts' -import { useAuthStore } from '../stores/auth.ts' export function Login() { const [email, setEmail] = useState('') @@ -9,7 +8,6 @@ export function Login() { const [error, setError] = useState('') const [loading, setLoading] = useState(false) const navigate = useNavigate() - const setAuthenticated = useAuthStore((s) => s.setAuthenticated) async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -40,12 +38,7 @@ export function Login() { setError('Sign in failed. Please try again.') } } catch { - if (import.meta.env.VITE_MOCK_AUTH === 'true') { - setAuthenticated(true) - navigate('/') - } else { - setError('Invalid email or password. Please try again.') - } + setError('Invalid email or password. Please try again.') } finally { setLoading(false) } diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index c75e2d6..960aa0a 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,7 +1,6 @@ import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { authClient } from '../lib/auth-client.ts' -import { useAuthStore } from '../stores/auth.ts' export function Register() { const [name, setName] = useState('') @@ -10,7 +9,6 @@ export function Register() { const [error, setError] = useState('') const [loading, setLoading] = useState(false) const navigate = useNavigate() - const setAuthenticated = useAuthStore((s) => s.setAuthenticated) async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -48,12 +46,7 @@ export function Register() { setError('Account created! Please sign in.') } } catch { - if (import.meta.env.VITE_MOCK_AUTH === 'true') { - setAuthenticated(true) - navigate('/') - } else { - setError('Registration failed. Please try again.') - } + setError('Registration failed. Please try again.') } finally { setLoading(false) } From 95284f69c518623fb8dc8a34b04f74a105cf38bb Mon Sep 17 00:00:00 2001 From: Paperclip Date: Tue, 14 Apr 2026 15:56:33 +0000 Subject: [PATCH 3/6] fix: update vite to resolve high-severity npm audit vulnerabilities --- package-lock.json | 134 +++++++++++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 104 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index a56c4d4..f15c280 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "tailwindcss": "^4.0.0", "typescript": "^5.7.3", "typescript-eslint": "^8.56.1", - "vite": "^6.3.5", + "vite": "^6.4.2", "vite-plugin-pwa": "^0.21.2", "vitest": "^3.2.4" } @@ -1867,6 +1867,40 @@ "node": ">=18" } }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -2669,6 +2703,25 @@ "node": ">=18" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@noble/ciphers": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", @@ -3659,6 +3712,17 @@ } } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4166,33 +4230,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, "node_modules/@vitest/pretty-format": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", @@ -9473,6 +9510,14 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9805,9 +9850,9 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -10020,6 +10065,33 @@ } } }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index bd2fd0c..e946241 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "tailwindcss": "^4.0.0", "typescript": "^5.7.3", "typescript-eslint": "^8.56.1", - "vite": "^6.3.5", + "vite": "^6.4.2", "vite-plugin-pwa": "^0.21.2", "vitest": "^3.2.4" }, From 6fe91c748cae3fe189807d34988cb64531a1c075 Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Wed, 15 Apr 2026 03:30:44 +0000 Subject: [PATCH 4/6] feat(auth): enable email verification with Resend Co-Authored-By: Paperclip --- src/App.tsx | 2 + src/pages/Register.tsx | 56 ++++++++++++++++--- src/pages/VerifyEmail.tsx | 113 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 src/pages/VerifyEmail.tsx diff --git a/src/App.tsx b/src/App.tsx index ee4c2dc..953553a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import { AccountLinking } from './pages/AccountLinking.tsx' import { Login } from './pages/Login.tsx' import { Register } from './pages/Register.tsx' import { ForgotPassword } from './pages/ForgotPassword.tsx' +import { VerifyEmail } from './pages/VerifyEmail.tsx' const queryClient = new QueryClient({ defaultOptions: { @@ -47,6 +48,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index c75e2d6..f40ea2d 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -9,6 +9,9 @@ export function Register() { const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) + const [registrationComplete, setRegistrationComplete] = useState(false) + const [resendLoading, setResendLoading] = useState(false) + const [resendMessage, setResendMessage] = useState('') const navigate = useNavigate() const setAuthenticated = useAuthStore((s) => s.setAuthenticated) @@ -38,15 +41,7 @@ export function Register() { throw new Error(authError.message ?? 'Registration failed') } - // After successful signUp, force a session fetch to confirm the cookie is set - // before navigating to the protected route - const sessionResult = await authClient.getSession() - if (sessionResult.data) { - navigate('/') - } else { - // Session not established — show success message and link to login - setError('Account created! Please sign in.') - } + setRegistrationComplete(true) } catch { if (import.meta.env.VITE_MOCK_AUTH === 'true') { setAuthenticated(true) @@ -59,6 +54,49 @@ export function Register() { } } + async function handleResendVerification() { + setResendLoading(true) + setResendMessage('') + try { + const { error } = await authClient.sendVerificationEmail({ email }) + if (error) { + setResendMessage('Failed to resend. Please try again.') + } else { + setResendMessage('Verification email sent!') + } + } finally { + setResendLoading(false) + } + } + + if (registrationComplete) { + return ( +
+

Check your email

+

+ We sent a verification link to {email}. Click it to activate your account. +

+ + {resendMessage && ( +

{resendMessage}

+ )} +

+ Already have an account?{' '} + + Sign in + +

+
+ ) + } + return (

Create Account

diff --git a/src/pages/VerifyEmail.tsx b/src/pages/VerifyEmail.tsx new file mode 100644 index 0000000..64da657 --- /dev/null +++ b/src/pages/VerifyEmail.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { authClient } from "../lib/auth-client.ts"; + +export function VerifyEmail() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [status, setStatus] = useState<"verifying" | "success" | "error">("verifying"); + const [resendEmail, setResendEmail] = useState(""); + const [showResend, setShowResend] = useState(false); + const [resending, setResending] = useState(false); + const [resendMessage, setResendMessage] = useState(""); + + useEffect(() => { + const token = searchParams.get("token"); + const callbackURL = searchParams.get("callbackURL") || "/"; + + if (!token) { + setStatus("error"); + return; + } + + authClient.verifyEmail({ query: { token } }) + .then(() => { + setStatus("success"); + setTimeout(() => { + navigate(callbackURL); + }, 2000); + }) + .catch(() => { + setStatus("error"); + }); + }, [searchParams, navigate]); + + async function handleResend() { + if (!resendEmail) { + setResendMessage("Please enter your email address."); + return; + } + + setResending(true); + setResendMessage(""); + + try { + const { error } = await authClient.sendVerificationEmail({ email: resendEmail }); + if (error) { + setResendMessage("Failed to resend. Please try again."); + } else { + setResendMessage("Verification email sent!"); + setShowResend(false); + } + } finally { + setResending(false); + } + } + + return ( +
+ {status === "verifying" && ( + <> +
+

Verifying your email...

+

Please wait while we verify your email address.

+ + )} + + {status === "success" && ( + <> +

Email verified!

+

Redirecting you shortly...

+ + )} + + {status === "error" && ( + <> +

Verification failed

+

The verification link may have expired or is invalid.

+ + {!showResend ? ( + + ) : ( +
+ setResendEmail(e.target.value)} + className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" + /> + + {resendMessage && ( +

{resendMessage}

+ )} +
+ )} + + )} +
+ ); +} \ No newline at end of file From 7651e0e72c67aee5d122810aaaaf5e8942096637 Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Tue, 14 Apr 2026 13:18:13 +0000 Subject: [PATCH 5/6] Enable Better-Auth email verification with Resend - Add emailVerification.sendVerificationEmail config to auth/src/auth.ts using Resend to send verification emails on sign-up - Add resend npm package to auth/package.json - Update auth/.env.example with RESEND_API_KEY and FROM_EMAIL - Create VerifyEmail.tsx page with token verification flow, spinner UX, success/Error states, and resend option - Update Register.tsx to redirect to /verify-email after signup instead of auto-navigating to dashboard - Add /verify-email route to App.tsx - Frontend shows 'check your email' step after registration Co-Authored-By: Paperclip --- src/pages/VerifyEmail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/VerifyEmail.tsx b/src/pages/VerifyEmail.tsx index 64da657..d1c5fb3 100644 --- a/src/pages/VerifyEmail.tsx +++ b/src/pages/VerifyEmail.tsx @@ -110,4 +110,4 @@ export function VerifyEmail() { )}
); -} \ No newline at end of file +} From 710a9ab47a638e607980b5f51289194ae251365b Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Wed, 15 Apr 2026 03:57:01 +0000 Subject: [PATCH 6/6] fix: remove unused navigate variable from Register.tsx Co-Authored-By: Paperclip --- src/pages/Register.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index ba039ba..a36e7c5 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' +import { Link } from 'react-router-dom' import { authClient } from '../lib/auth-client.ts' export function Register() { @@ -11,7 +11,6 @@ export function Register() { const [registrationComplete, setRegistrationComplete] = useState(false) const [resendLoading, setResendLoading] = useState(false) const [resendMessage, setResendMessage] = useState('') - const navigate = useNavigate() async function handleSubmit(e: React.FormEvent) { e.preventDefault()