diff --git a/src/cartsnitch_api/auth/dependencies.py b/src/cartsnitch_api/auth/dependencies.py index 451ae70..a3735eb 100644 --- a/src/cartsnitch_api/auth/dependencies.py +++ b/src/cartsnitch_api/auth/dependencies.py @@ -5,7 +5,6 @@ Sessions are verified by querying the shared sessions table directly. """ from datetime import UTC, datetime -from uuid import UUID from fastapi import Cookie, Depends, Header, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer @@ -27,10 +26,10 @@ SESSION_COOKIE_NAMES = [ ] -async def _validate_session_token(token: str, db: AsyncSession) -> UUID: +async def _validate_session_token(token: str, db: AsyncSession) -> str: """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 str) 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. @@ -60,14 +59,14 @@ async def _validate_session_token(token: str, db: AsyncSession) -> UUID: detail="Session expired", ) - return UUID(str(user_id)) + return str(user_id) async def get_current_user( request: Request, credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), db: AsyncSession = Depends(get_db), -) -> UUID: +) -> str: """Extract and validate the session token from cookie or Authorization header. Checks in order: diff --git a/src/cartsnitch_api/auth/jwt.py b/src/cartsnitch_api/auth/jwt.py index 100c77b..4e127bc 100644 --- a/src/cartsnitch_api/auth/jwt.py +++ b/src/cartsnitch_api/auth/jwt.py @@ -2,22 +2,21 @@ from datetime import UTC, datetime, timedelta from typing import Any, cast -from uuid import UUID from jose import JWTError, jwt from cartsnitch_api.config import settings -def create_access_token(user_id: UUID) -> str: +def create_access_token(user_id: str) -> str: expire = datetime.now(UTC) + timedelta(minutes=settings.jwt_access_token_expire_minutes) - payload = {"sub": str(user_id), "exp": expire, "type": "access"} + payload = {"sub": user_id, "exp": expire, "type": "access"} return cast(str, jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)) -def create_refresh_token(user_id: UUID) -> str: +def create_refresh_token(user_id: str) -> str: expire = datetime.now(UTC) + timedelta(days=settings.jwt_refresh_token_expire_days) - payload = {"sub": str(user_id), "exp": expire, "type": "refresh"} + payload = {"sub": user_id, "exp": expire, "type": "refresh"} return cast(str, jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)) diff --git a/src/cartsnitch_api/auth/routes.py b/src/cartsnitch_api/auth/routes.py index 81cae2f..2c547a4 100644 --- a/src/cartsnitch_api/auth/routes.py +++ b/src/cartsnitch_api/auth/routes.py @@ -5,8 +5,6 @@ the Better-Auth service (auth/). This router provides user profile endpoints that query our own user data from the shared database. """ -from uuid import UUID - from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession @@ -23,7 +21,7 @@ router = APIRouter(prefix="/auth", tags=["auth"]) @router.get("/me", response_model=UserResponse) async def get_me( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = AuthService(db) @@ -38,7 +36,7 @@ async def get_me( @router.patch("/me", response_model=UserResponse) async def update_me( body: UpdateUserRequest, - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = AuthService(db) @@ -54,7 +52,7 @@ async def update_me( @router.delete("/me", status_code=status.HTTP_204_NO_CONTENT) async def delete_me( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = AuthService(db) diff --git a/src/cartsnitch_api/routes/alerts.py b/src/cartsnitch_api/routes/alerts.py index 45ab33f..9b3fe8f 100644 --- a/src/cartsnitch_api/routes/alerts.py +++ b/src/cartsnitch_api/routes/alerts.py @@ -1,7 +1,5 @@ """Alert routes: list alerts, manage settings.""" -from uuid import UUID - from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession @@ -15,7 +13,7 @@ router = APIRouter(prefix="/alerts", tags=["alerts"]) @router.get("", response_model=list[AlertResponse]) async def list_alerts( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = AlertService(db) @@ -24,7 +22,7 @@ async def list_alerts( @router.get("/settings", response_model=AlertSettingsResponse) async def get_alert_settings( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = AlertService(db) @@ -34,7 +32,7 @@ async def get_alert_settings( @router.put("/settings") async def update_alert_settings( body: AlertSettingsRequest, - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): raise HTTPException( diff --git a/src/cartsnitch_api/routes/coupons.py b/src/cartsnitch_api/routes/coupons.py index d33d98a..9e43fbc 100644 --- a/src/cartsnitch_api/routes/coupons.py +++ b/src/cartsnitch_api/routes/coupons.py @@ -16,7 +16,7 @@ router = APIRouter(prefix="/coupons", tags=["coupons"]) @router.get("", response_model=list[CouponResponse]) async def list_coupons( store_id: UUID | None = Query(None), - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = CouponService(db) @@ -25,7 +25,7 @@ async def list_coupons( @router.get("/relevant", response_model=list[CouponResponse]) async def relevant_coupons( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = CouponService(db) diff --git a/src/cartsnitch_api/routes/prices.py b/src/cartsnitch_api/routes/prices.py index 487dd92..c39a1ce 100644 --- a/src/cartsnitch_api/routes/prices.py +++ b/src/cartsnitch_api/routes/prices.py @@ -20,7 +20,7 @@ router = APIRouter(prefix="/prices", tags=["prices"]) @router.get("/trends", response_model=list[PriceTrendResponse]) async def price_trends( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), category: str | None = Query(None), db: AsyncSession = Depends(get_db), ): @@ -30,7 +30,7 @@ async def price_trends( @router.get("/increases", response_model=list[PriceIncreaseResponse]) async def price_increases( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = PriceService(db) @@ -40,7 +40,7 @@ async def price_increases( @router.get("/comparison", response_model=list[PriceComparisonResponse]) async def price_comparison( product_ids: Annotated[list[UUID], Query()], - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = PriceService(db) diff --git a/src/cartsnitch_api/routes/products.py b/src/cartsnitch_api/routes/products.py index 473cefe..84205e8 100644 --- a/src/cartsnitch_api/routes/products.py +++ b/src/cartsnitch_api/routes/products.py @@ -15,7 +15,7 @@ router = APIRouter(prefix="/products", tags=["products"]) @router.get("", response_model=list[ProductResponse]) async def list_products( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), q: str | None = Query(None), category: str | None = Query(None), page: int = Query(1, ge=1), @@ -29,7 +29,7 @@ async def list_products( @router.get("/{product_id}", response_model=ProductDetailResponse) async def get_product( product_id: UUID, - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = ProductService(db) @@ -44,7 +44,7 @@ async def get_product( @router.get("/{product_id}/prices", response_model=PriceTrendResponse) async def get_product_prices( product_id: UUID, - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = ProductService(db) diff --git a/src/cartsnitch_api/routes/purchases.py b/src/cartsnitch_api/routes/purchases.py index eba86ac..a337c8e 100644 --- a/src/cartsnitch_api/routes/purchases.py +++ b/src/cartsnitch_api/routes/purchases.py @@ -15,7 +15,7 @@ router = APIRouter(prefix="/purchases", tags=["purchases"]) @router.get("", response_model=list[PurchaseResponse]) async def list_purchases( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), store_id: UUID | None = Query(None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), @@ -27,7 +27,7 @@ async def list_purchases( @router.get("/stats", response_model=PurchaseStatsResponse) async def purchase_stats( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = PurchaseService(db) @@ -37,7 +37,7 @@ async def purchase_stats( @router.get("/{purchase_id}", response_model=PurchaseDetailResponse) async def get_purchase( purchase_id: UUID, - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = PurchaseService(db) diff --git a/src/cartsnitch_api/routes/scraping.py b/src/cartsnitch_api/routes/scraping.py index d8bbd5f..2804212 100644 --- a/src/cartsnitch_api/routes/scraping.py +++ b/src/cartsnitch_api/routes/scraping.py @@ -1,7 +1,5 @@ """Scraping routes: trigger sync, check status (proxy to ReceiptWitness).""" -from uuid import UUID - from fastapi import APIRouter, Depends, HTTPException, status from httpx import HTTPStatusError, RequestError @@ -13,7 +11,7 @@ router = APIRouter(prefix="/scraping", tags=["scraping"]) @router.post("/{store_slug}/sync", response_model=SyncTriggerResponse) -async def trigger_sync(store_slug: str, user_id: UUID = Depends(get_current_user)): +async def trigger_sync(store_slug: str, user_id: str = Depends(get_current_user)): client = ReceiptWitnessClient() try: result = await client.trigger_sync(str(user_id), store_slug) @@ -31,7 +29,7 @@ async def trigger_sync(store_slug: str, user_id: UUID = Depends(get_current_user @router.get("/status", response_model=list[SyncStatusResponse]) -async def sync_status(user_id: UUID = Depends(get_current_user)): +async def sync_status(user_id: str = Depends(get_current_user)): client = ReceiptWitnessClient() try: return await client.get_sync_status(str(user_id)) diff --git a/src/cartsnitch_api/routes/shopping.py b/src/cartsnitch_api/routes/shopping.py index c64d5fd..f7c3d0e 100644 --- a/src/cartsnitch_api/routes/shopping.py +++ b/src/cartsnitch_api/routes/shopping.py @@ -1,7 +1,5 @@ """Shopping routes: optimize list, saved lists.""" -from uuid import UUID - from fastapi import APIRouter, Depends, HTTPException, status from httpx import HTTPStatusError, RequestError @@ -13,7 +11,7 @@ router = APIRouter(prefix="/shopping", tags=["shopping"]) @router.post("/optimize", response_model=OptimizeResponse) -async def optimize_shopping(body: OptimizeRequest, user_id: UUID = Depends(get_current_user)): +async def optimize_shopping(body: OptimizeRequest, user_id: str = Depends(get_current_user)): client = ClipArtistClient() try: result = await client.optimize( @@ -37,7 +35,7 @@ async def optimize_shopping(body: OptimizeRequest, user_id: UUID = Depends(get_c @router.get("/lists", response_model=list[ShoppingListResponse]) -async def list_shopping_lists(user_id: UUID = Depends(get_current_user)): +async def list_shopping_lists(user_id: str = Depends(get_current_user)): client = ClipArtistClient() try: return await client.get_shopping_lists(str(user_id)) diff --git a/src/cartsnitch_api/routes/stores.py b/src/cartsnitch_api/routes/stores.py index 1ab7947..1525933 100644 --- a/src/cartsnitch_api/routes/stores.py +++ b/src/cartsnitch_api/routes/stores.py @@ -1,7 +1,5 @@ """Store routes: list stores, manage user store connections.""" -from uuid import UUID - from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession @@ -21,7 +19,7 @@ async def list_stores(db: AsyncSession = Depends(get_db)): @router.get("/me/stores", response_model=list[StoreAccountResponse]) async def list_user_stores( - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = StoreService(db) @@ -36,7 +34,7 @@ async def list_user_stores( async def connect_store( store_slug: str, body: ConnectStoreRequest, - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = StoreService(db) @@ -51,7 +49,7 @@ async def connect_store( @router.delete("/me/stores/{store_slug}", status_code=status.HTTP_204_NO_CONTENT) async def disconnect_store( store_slug: str, - user_id: UUID = Depends(get_current_user), + user_id: str = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = StoreService(db) diff --git a/src/cartsnitch_api/services/alerts.py b/src/cartsnitch_api/services/alerts.py index fc3ddd4..cc03d60 100644 --- a/src/cartsnitch_api/services/alerts.py +++ b/src/cartsnitch_api/services/alerts.py @@ -4,8 +4,6 @@ Alerts are generated by StickerShock and ShrinkRay services and written to the D This service reads them for the API gateway. """ -from uuid import UUID - from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -15,7 +13,7 @@ class AlertService: def __init__(self, db: AsyncSession) -> None: self.db = db - async def list_alerts(self, user_id: UUID) -> list[dict]: + async def list_alerts(self, user_id: str) -> list[dict]: """List shrinkflation events for products the user has purchased.""" from cartsnitch_api.models import Purchase, PurchaseItem, ShrinkflationEvent @@ -57,7 +55,7 @@ class AlertService: for e in events ] - async def get_settings(self, user_id: UUID) -> dict: + async def get_settings(self, user_id: str) -> dict: # Alert settings would be stored in a user_settings table. # For now, return defaults since the table doesn't exist yet in common lib. return { @@ -66,7 +64,7 @@ class AlertService: "email_notifications": False, } - async def update_settings(self, user_id: UUID, **fields) -> dict: + async def update_settings(self, user_id: str, **fields) -> dict: # Would update user_settings table. Return merged defaults for now. current = await self.get_settings(user_id) for k, v in fields.items(): diff --git a/src/cartsnitch_api/services/auth.py b/src/cartsnitch_api/services/auth.py index 91724af..4894150 100644 --- a/src/cartsnitch_api/services/auth.py +++ b/src/cartsnitch_api/services/auth.py @@ -5,8 +5,6 @@ handled by the Better-Auth service (auth/). This service provides user lookup and profile update operations for the API gateway. """ -from uuid import UUID - from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -15,7 +13,7 @@ class AuthService: def __init__(self, db: AsyncSession) -> None: self.db = db - async def get_user(self, user_id: UUID) -> dict: + async def get_user(self, user_id: str) -> dict: from cartsnitch_api.models import User result = await self.db.execute(select(User).where(User.id == user_id)) @@ -30,7 +28,7 @@ class AuthService: "created_at": user.created_at, } - async def update_user(self, user_id: UUID, **fields) -> dict: + async def update_user(self, user_id: str, **fields) -> dict: from cartsnitch_api.models import User result = await self.db.execute(select(User).where(User.id == user_id)) @@ -58,7 +56,7 @@ class AuthService: "created_at": user.created_at, } - async def delete_user(self, user_id: UUID) -> None: + async def delete_user(self, user_id: str) -> None: from cartsnitch_api.models import User result = await self.db.execute(select(User).where(User.id == user_id)) diff --git a/src/cartsnitch_api/services/coupons.py b/src/cartsnitch_api/services/coupons.py index 9b1543e..a5b8a2c 100644 --- a/src/cartsnitch_api/services/coupons.py +++ b/src/cartsnitch_api/services/coupons.py @@ -29,7 +29,7 @@ class CouponService: coupons = result.scalars().all() return [self._to_dict(c) for c in coupons] - async def relevant_coupons(self, user_id: UUID) -> list[dict]: + async def relevant_coupons(self, user_id: str) -> list[dict]: """Coupons for products the user has purchased.""" from cartsnitch_api.models import Coupon, PurchaseItem diff --git a/src/cartsnitch_api/services/purchases.py b/src/cartsnitch_api/services/purchases.py index 41776f4..10ca0a4 100644 --- a/src/cartsnitch_api/services/purchases.py +++ b/src/cartsnitch_api/services/purchases.py @@ -13,7 +13,7 @@ class PurchaseService: async def list_purchases( self, - user_id: UUID, + user_id: str, store_id: UUID | None = None, page: int = 1, page_size: int = 20, @@ -56,7 +56,7 @@ class PurchaseService: for p, item_count, store_name in result.all() ] - async def get_purchase(self, purchase_id: UUID, user_id: UUID) -> dict: + async def get_purchase(self, purchase_id: UUID, user_id: str) -> dict: from cartsnitch_api.models import Purchase result = await self.db.execute( @@ -88,7 +88,7 @@ class PurchaseService: ], } - async def get_stats(self, user_id: UUID) -> dict: + async def get_stats(self, user_id: str) -> dict: from cartsnitch_api.models import Purchase result = await self.db.execute( diff --git a/src/cartsnitch_api/services/stores.py b/src/cartsnitch_api/services/stores.py index 610f47e..c7d43ec 100644 --- a/src/cartsnitch_api/services/stores.py +++ b/src/cartsnitch_api/services/stores.py @@ -1,7 +1,6 @@ """Store service — list stores, manage user store account connections.""" import json -from uuid import UUID from cryptography.fernet import Fernet from sqlalchemy import select @@ -35,7 +34,7 @@ class StoreService: for s in stores ] - async def list_user_stores(self, user_id: UUID) -> list[dict]: + async def list_user_stores(self, user_id: str) -> list[dict]: from cartsnitch_api.models import UserStoreAccount result = await self.db.execute( @@ -60,7 +59,7 @@ class StoreService: for a in accounts ] - async def connect_store(self, user_id: UUID, store_slug: str, credentials: dict | None) -> dict: + async def connect_store(self, user_id: str, store_slug: str, credentials: dict | None) -> dict: from cartsnitch_api.models import Store, UserStoreAccount result = await self.db.execute(select(Store).where(Store.slug == store_slug)) @@ -107,7 +106,7 @@ class StoreService: "sync_status": "active", } - async def disconnect_store(self, user_id: UUID, store_slug: str) -> None: + async def disconnect_store(self, user_id: str, store_slug: str) -> None: from cartsnitch_api.models import Store, UserStoreAccount result = await self.db.execute(select(Store).where(Store.slug == store_slug))