forked from cartsnitch/cartsnitch
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f37f0501f | |||
| 4c36fd4156 | |||
| c9172f088f | |||
| ac4cba2b0d | |||
| 0c47be8ef3 | |||
| 440f92e96e | |||
| 97bbdf68a5 | |||
| 02e5bee390 | |||
| d475b3876a | |||
| 76bcc53992 | |||
| 470b615528 | |||
| f26f8f7e56 | |||
| 78b7831d43 | |||
| e45b510519 | |||
| f25044ea7e | |||
| b637fd9c11 | |||
| 983ee2c398 | |||
| b21a30b2e7 | |||
| 361ad3acc2 | |||
| 5e165d277e |
@@ -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
|
||||||
LHCI_CHROME_PATH="$CHROME_PATH" lhci autorun
|
CHROME_PATH="$CHROME_PATH" lhci autorun --chrome-flags="--headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage"
|
||||||
|
|
||||||
build-and-push:
|
build-and-push:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: runners-cartsnitch
|
||||||
|
|||||||
@@ -19,18 +19,27 @@ 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 name
|
# Better-Auth session cookie names.
|
||||||
SESSION_COOKIE_NAME = "better-auth.session_token"
|
# Over HTTPS Better-Auth adds the __Secure- prefix automatically.
|
||||||
|
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:
|
||||||
"""Validate a Better-Auth session token against the sessions table.
|
"""Validate a Better-Auth session token against the sessions table.
|
||||||
|
|
||||||
Returns the user_id (as UUID) if the session is valid and not expired.
|
Returns the user_id (as UUID) if the session is valid and not expired.
|
||||||
|
Better-Auth v1.5.6 stores raw tokens in the DB. The session cookie
|
||||||
|
is signed: ``rawToken.base64HMACSignature``. Strip the signature
|
||||||
|
before querying.
|
||||||
"""
|
"""
|
||||||
|
# Signed cookie format: rawToken.hmacSignature — split and use only the token part
|
||||||
|
raw_token = token.split(".")[0] if "." in token else token
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("SELECT user_id, expires_at FROM sessions WHERE token = :token"),
|
text("SELECT user_id, expires_at FROM sessions WHERE token = :token"),
|
||||||
{"token": token},
|
{"token": raw_token},
|
||||||
)
|
)
|
||||||
row = result.first()
|
row = result.first()
|
||||||
|
|
||||||
@@ -67,8 +76,12 @@ async def get_current_user(
|
|||||||
"""
|
"""
|
||||||
token: str | None = None
|
token: str | None = None
|
||||||
|
|
||||||
# 1. Check session cookie
|
# 1. Check session cookie (try both names for HTTP/HTTPS compatibility)
|
||||||
cookie_token = request.cookies.get(SESSION_COOKIE_NAME)
|
cookie_token = None
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import APIRouter, 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,15 +46,19 @@ 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)
|
|
||||||
app.include_router(purchases_router)
|
# Data endpoints mounted under /api/v1
|
||||||
app.include_router(products_router)
|
v1_router = APIRouter(prefix="/api/v1")
|
||||||
app.include_router(prices_router)
|
v1_router.include_router(stores_router)
|
||||||
app.include_router(coupons_router)
|
v1_router.include_router(purchases_router)
|
||||||
app.include_router(shopping_router)
|
v1_router.include_router(products_router)
|
||||||
app.include_router(alerts_router)
|
v1_router.include_router(prices_router)
|
||||||
app.include_router(scraping_router)
|
v1_router.include_router(coupons_router)
|
||||||
app.include_router(public_router)
|
v1_router.include_router(shopping_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
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -3,7 +3,12 @@
|
|||||||
"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": {
|
||||||
@@ -16,4 +21,4 @@
|
|||||||
"target": "temporary-public-storage"
|
"target": "temporary-public-storage"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -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}/price-history`),
|
queryFn: () => api.get<PriceHistory[]>(`/products/${productId}/prices`),
|
||||||
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[]>('/price-alerts'),
|
queryFn: () => api.get<PriceAlert[]>('/alerts'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -15,7 +15,7 @@ const mockRoutes: Record<string, (path: string) => unknown> = {
|
|||||||
'/purchases': () => mockPurchases,
|
'/purchases': () => mockPurchases,
|
||||||
'/products': () => mockProducts,
|
'/products': () => mockProducts,
|
||||||
'/coupons': () => mockCoupons,
|
'/coupons': () => mockCoupons,
|
||||||
'/price-alerts': () => mockAlerts,
|
'/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\/(.+)\/price-history$/)
|
const priceHistoryMatch = path.match(/^\/products\/(.+)\/prices$/)
|
||||||
if (priceHistoryMatch) {
|
if (priceHistoryMatch) {
|
||||||
return getMockPriceHistory(priceHistoryMatch[1]) as T
|
return getMockPriceHistory(priceHistoryMatch[1]) as T
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -31,8 +31,14 @@ export function Login() {
|
|||||||
throw new Error(authError.message ?? 'Sign in failed')
|
throw new Error(authError.message ?? 'Sign in failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuthenticated(true)
|
// After successful signIn, force a session fetch to confirm the cookie is set
|
||||||
navigate('/')
|
// before navigating to the protected route
|
||||||
|
const sessionResult = await authClient.getSession()
|
||||||
|
if (sessionResult.data) {
|
||||||
|
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)
|
||||||
|
|||||||
@@ -38,8 +38,15 @@ export function Register() {
|
|||||||
throw new Error(authError.message ?? 'Registration failed')
|
throw new Error(authError.message ?? 'Registration failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuthenticated(true)
|
// After successful signUp, force a session fetch to confirm the cookie is set
|
||||||
navigate('/')
|
// 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.')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (import.meta.env.VITE_MOCK_AUTH === 'true') {
|
if (import.meta.env.VITE_MOCK_AUTH === 'true') {
|
||||||
setAuthenticated(true)
|
setAuthenticated(true)
|
||||||
|
|||||||
@@ -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/price-alerts', () => HttpResponse.json(mockAlerts)),
|
http.get('/api/v1/alerts', () => HttpResponse.json(mockAlerts)),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user