Compare commits

..

1 Commits

Author SHA1 Message Date
Paperclip 121dc5724e fix: remove VITE_MOCK_AUTH bypass from production code
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-14 15:37:24 +00:00
10 changed files with 39 additions and 153 deletions
+6 -12
View File
@@ -4,23 +4,17 @@ import pg from "pg";
const { Pool } = pg;
const pool = new Pool({
connectionString:
process.env.DATABASE_URL ??
"postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch",
});
const secret = process.env.BETTER_AUTH_SECRET;
if (!secret) {
throw new Error("BETTER_AUTH_SECRET environment variable is required");
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.warn(
"WARNING: DATABASE_URL is not set — using default localhost connection. " +
"Set DATABASE_URL for production deployments."
);
}
const pool = new Pool({
connectionString: databaseUrl ?? "postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch",
});
export const auth = betterAuth({
database: pool,
basePath: "/auth",
+1 -88
View File
@@ -1,4 +1,4 @@
import { test as base, expect, type Page } from "@playwright/test";
import { test as base, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
export const test = base.extend<{ axeCheck: void }>({
@@ -10,90 +10,3 @@ export const test = base.extend<{ axeCheck: void }>({
});
export { expect } from "@playwright/test";
const MOCK_USER_ID = "mock_user_123";
const MOCK_SESSION_ID = "mock_session_456";
function mockAuthRoutes(page: Page, authenticated = false) {
page.route(/.*\/auth\/sign-up\/email.*/, async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
token: null,
user: {
id: MOCK_USER_ID,
email: "mock@cartsnitch.test",
name: "Mock User",
emailVerified: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}),
});
});
page.route(/.*\/auth\/sign-in\/email.*/, async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
redirect: false,
token: "mock_token_123",
user: {
id: MOCK_USER_ID,
email: "mock@cartsnitch.test",
name: "Mock User",
emailVerified: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}),
});
});
page.route(/.*\/auth\/get-session.*/, async (route) => {
if (authenticated) {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
session: {
id: MOCK_SESSION_ID,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
ipAddress: null,
userAgent: null,
},
user: {
id: MOCK_USER_ID,
email: "mock@cartsnitch.test",
name: "Mock User",
emailVerified: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}),
});
} else {
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({ error: "Unauthorized" }),
});
}
});
}
export function mockSessionPending(page: Page) {
page.route(/.*\/auth\/session.*/, async (route) => {
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({ error: "Unauthorized" }),
});
});
}
export { mockAuthRoutes };
+18 -6
View File
@@ -1,18 +1,17 @@
import { test, expect } from '@playwright/test';
import { mockAuthRoutes } from '../fixtures';
const uniqueEmail = () => `betty+e2e-${Date.now()}@cartsnitch.test`;
test.describe('J1: Registration and Login', () => {
test('can register a new account and see check your email screen', async ({ page }) => {
mockAuthRoutes(page, true);
test('can register a new account and lands on dashboard', async ({ page }) => {
await page.goto('/register');
await page.fill('[placeholder="Full Name"]', 'Betty Tester');
await page.fill('[placeholder="Email"]', uniqueEmail());
await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!');
await page.click('button[type="submit"]');
await expect(page.getByRole('heading', { name: /check your email/i })).toBeVisible();
await expect(page).toHaveURL('http://localhost:5173/');
await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible();
});
test('shows validation error when registration fields are empty', async ({ page }) => {
@@ -31,9 +30,22 @@ test.describe('J1: Registration and Login', () => {
});
test('can sign in with credentials and land on dashboard', async ({ page }) => {
mockAuthRoutes(page, true);
// Register first so we have a real account
const email = uniqueEmail();
await page.goto('/register');
await page.fill('[placeholder="Full Name"]', 'Login Betty');
await page.fill('[placeholder="Email"]', email);
await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('http://localhost:5173/');
// Sign out by clearing the mock session (reload with no session)
await page.goto('/');
await page.reload();
// Now sign in
await page.goto('/login');
await page.fill('[placeholder="Email"]', 'test@cartsnitch.test');
await page.fill('[placeholder="Email"]', email);
await page.fill('[placeholder="Password"]', 'TestPass123!');
await page.click('button[type="submit"]');
+8 -6
View File
@@ -1,9 +1,8 @@
import { test, expect } from '@playwright/test';
import { mockAuthRoutes } from '../fixtures';
test.describe('J8: Unauthenticated Access', () => {
test('redirects /dashboard (/) to /login when not authenticated', async ({ page }) => {
mockAuthRoutes(page, false);
// No session cookie — start fresh
await page.context().clearCookies();
await page.goto('/');
@@ -12,7 +11,6 @@ test.describe('J8: Unauthenticated Access', () => {
});
test('redirects /purchases to /login when not authenticated', async ({ page }) => {
mockAuthRoutes(page, false);
await page.context().clearCookies();
await page.goto('/purchases');
@@ -21,7 +19,6 @@ test.describe('J8: Unauthenticated Access', () => {
});
test('redirects /products to /login when not authenticated', async ({ page }) => {
mockAuthRoutes(page, false);
await page.context().clearCookies();
await page.goto('/products');
@@ -30,7 +27,6 @@ test.describe('J8: Unauthenticated Access', () => {
});
test('redirects /coupons to /login when not authenticated', async ({ page }) => {
mockAuthRoutes(page, false);
await page.context().clearCookies();
await page.goto('/coupons');
@@ -39,9 +35,15 @@ test.describe('J8: Unauthenticated Access', () => {
});
test('shows loading spinner while auth session is pending', async ({ page }) => {
mockAuthRoutes(page, false);
// Intercept but don't respond — session stays pending
await page.context().clearCookies();
await page.request.fetch('/api/auth/session', {
method: 'GET',
});
// Just navigate to a protected route — ProtectedRoute will show spinner while session is pending
await page.goto('/purchases');
// Spinner is visible briefly; once resolved, should redirect to login
await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
});
});
+1 -2
View File
@@ -1,7 +1,6 @@
import { test, expect, mockAuthRoutes } from './fixtures';
import { test, expect } from './fixtures';
test('app loads', async ({ page }) => {
mockAuthRoutes(page, false);
await page.goto('/');
// Unauthenticated users are redirected to /login
await expect(page).toHaveURL(/\/login/);
+3 -3
View File
@@ -9805,9 +9805,9 @@
}
},
"node_modules/vite": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
-3
View File
@@ -12,9 +12,6 @@ export default defineConfig({
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
env: {
VITE_MOCK_AUTH: 'true',
},
},
use: {
baseURL: 'http://localhost:5173',
-17
View File
@@ -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 <Navigate to="/login" replace />
return <Outlet />
}
if (isPending) {
return (
+1 -8
View File
@@ -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)
}
+1 -8
View File
@@ -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)
}