Compare commits

..

1 Commits

Author SHA1 Message Date
Stockboy Steve 53e802746c fix(api): run Alembic migrations on startup to fix auth 500s
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-31 19:52:00 +00:00
9 changed files with 26 additions and 56 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ jobs:
run: | run: |
CHROME_PATH=$(find /home/runner/.cache/ms-playwright -name chrome -type f 2>/dev/null | head -1) CHROME_PATH=$(find /home/runner/.cache/ms-playwright -name chrome -type f 2>/dev/null | head -1)
npm install -g @lhci/cli npm install -g @lhci/cli
CHROME_PATH="$CHROME_PATH" lhci autorun --chrome-flags="--headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage" LHCI_CHROME_PATH="$CHROME_PATH" lhci autorun
build-and-push: build-and-push:
runs-on: runners-cartsnitch runs-on: runners-cartsnitch
+4 -12
View File
@@ -19,12 +19,8 @@ from cartsnitch_api.database import get_db
# but we support Bearer tokens for service-to-service or mobile clients. # but we support Bearer tokens for service-to-service or mobile clients.
bearer_scheme = HTTPBearer(auto_error=False) bearer_scheme = HTTPBearer(auto_error=False)
# Better-Auth session cookie names. # Better-Auth session cookie name
# Over HTTPS Better-Auth adds the __Secure- prefix automatically. SESSION_COOKIE_NAME = "better-auth.session_token"
SESSION_COOKIE_NAMES = [
"__Secure-better-auth.session_token", # HTTPS (deployed)
"better-auth.session_token", # HTTP (local dev)
]
async def _validate_session_token(token: str, db: AsyncSession) -> UUID: async def _validate_session_token(token: str, db: AsyncSession) -> UUID:
@@ -71,12 +67,8 @@ async def get_current_user(
""" """
token: str | None = None token: str | None = None
# 1. Check session cookie (try both names for HTTP/HTTPS compatibility) # 1. Check session cookie
cookie_token = None cookie_token = request.cookies.get(SESSION_COOKIE_NAME)
for name in SESSION_COOKIE_NAMES:
cookie_token = request.cookies.get(name)
if cookie_token:
break
if cookie_token: if cookie_token:
token = cookie_token token = cookie_token
+10 -14
View File
@@ -2,7 +2,7 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import APIRouter, FastAPI from fastapi import FastAPI
from cartsnitch_api.auth.routes import router as auth_router from cartsnitch_api.auth.routes import router as auth_router
from cartsnitch_api.middleware.cors import add_cors_middleware from cartsnitch_api.middleware.cors import add_cors_middleware
@@ -46,19 +46,15 @@ def create_app() -> FastAPI:
# Routers # Routers
app.include_router(health_router) app.include_router(health_router)
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(stores_router)
# Data endpoints mounted under /api/v1 app.include_router(purchases_router)
v1_router = APIRouter(prefix="/api/v1") app.include_router(products_router)
v1_router.include_router(stores_router) app.include_router(prices_router)
v1_router.include_router(purchases_router) app.include_router(coupons_router)
v1_router.include_router(products_router) app.include_router(shopping_router)
v1_router.include_router(prices_router) app.include_router(alerts_router)
v1_router.include_router(coupons_router) app.include_router(scraping_router)
v1_router.include_router(shopping_router) app.include_router(public_router)
v1_router.include_router(alerts_router)
v1_router.include_router(scraping_router)
v1_router.include_router(public_router)
app.include_router(v1_router)
return app return app
+1 -6
View File
@@ -3,12 +3,7 @@
"collect": { "collect": {
"staticDistDir": "./dist", "staticDistDir": "./dist",
"url": ["http://localhost:4173/"], "url": ["http://localhost:4173/"],
"numberOfRuns": 1, "numberOfRuns": 1
"settings": {
"chromeFlags": ["--headless=new", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"],
"skipAudits": ["bf-cache"],
"disableFullPageScreenshot": true
}
}, },
"assert": { "assert": {
"assertions": { "assertions": {
+2 -2
View File
@@ -35,7 +35,7 @@ export function useProduct(id: string) {
export function usePriceHistory(productId: string) { export function usePriceHistory(productId: string) {
return useQuery({ return useQuery({
queryKey: ['priceHistory', productId], queryKey: ['priceHistory', productId],
queryFn: () => api.get<PriceHistory[]>(`/products/${productId}/prices`), queryFn: () => api.get<PriceHistory[]>(`/products/${productId}/price-history`),
enabled: !!productId, enabled: !!productId,
}) })
} }
@@ -50,6 +50,6 @@ export function useCoupons() {
export function usePriceAlerts() { export function usePriceAlerts() {
return useQuery({ return useQuery({
queryKey: ['priceAlerts'], queryKey: ['priceAlerts'],
queryFn: () => api.get<PriceAlert[]>('/alerts'), queryFn: () => api.get<PriceAlert[]>('/price-alerts'),
}) })
} }
+2 -2
View File
@@ -15,7 +15,7 @@ const mockRoutes: Record<string, (path: string) => unknown> = {
'/purchases': () => mockPurchases, '/purchases': () => mockPurchases,
'/products': () => mockProducts, '/products': () => mockProducts,
'/coupons': () => mockCoupons, '/coupons': () => mockCoupons,
'/alerts': () => mockAlerts, '/price-alerts': () => mockAlerts,
} }
function matchMockRoute<T>(path: string): T | null { function matchMockRoute<T>(path: string): T | null {
@@ -30,7 +30,7 @@ function matchMockRoute<T>(path: string): T | null {
} }
// /products/:id/price-history // /products/:id/price-history
const priceHistoryMatch = path.match(/^\/products\/(.+)\/prices$/) const priceHistoryMatch = path.match(/^\/products\/(.+)\/price-history$/)
if (priceHistoryMatch) { if (priceHistoryMatch) {
return getMockPriceHistory(priceHistoryMatch[1]) as T return getMockPriceHistory(priceHistoryMatch[1]) as T
} }
+1 -7
View File
@@ -31,14 +31,8 @@ export function Login() {
throw new Error(authError.message ?? 'Sign in failed') throw new Error(authError.message ?? 'Sign in failed')
} }
// After successful signIn, force a session fetch to confirm the cookie is set setAuthenticated(true)
// before navigating to the protected route
const sessionResult = await authClient.getSession()
if (sessionResult.data) {
navigate('/') navigate('/')
} else {
setError('Sign in failed. Please try again.')
}
} catch { } catch {
if (import.meta.env.VITE_MOCK_AUTH === 'true') { if (import.meta.env.VITE_MOCK_AUTH === 'true') {
setAuthenticated(true) setAuthenticated(true)
+1 -8
View File
@@ -38,15 +38,8 @@ export function Register() {
throw new Error(authError.message ?? 'Registration failed') throw new Error(authError.message ?? 'Registration failed')
} }
// After successful signUp, force a session fetch to confirm the cookie is set setAuthenticated(true)
// before navigating to the protected route
const sessionResult = await authClient.getSession()
if (sessionResult.data) {
navigate('/') navigate('/')
} else {
// Session not established — show success message and link to login
setError('Account created! Please sign in.')
}
} catch { } catch {
if (import.meta.env.VITE_MOCK_AUTH === 'true') { if (import.meta.env.VITE_MOCK_AUTH === 'true') {
setAuthenticated(true) setAuthenticated(true)
+1 -1
View File
@@ -61,5 +61,5 @@ export const handlers = [
http.get('/api/v1/products', () => HttpResponse.json(mockProducts)), http.get('/api/v1/products', () => HttpResponse.json(mockProducts)),
http.get('/api/v1/products/prod_1', () => HttpResponse.json(mockProducts[0])), http.get('/api/v1/products/prod_1', () => HttpResponse.json(mockProducts[0])),
http.get('/api/v1/coupons', () => HttpResponse.json(mockCoupons)), http.get('/api/v1/coupons', () => HttpResponse.json(mockCoupons)),
http.get('/api/v1/alerts', () => HttpResponse.json(mockAlerts)), http.get('/api/v1/price-alerts', () => HttpResponse.json(mockAlerts)),
] ]