feat: migrate authentication to Better-Auth (Phase 1)

Replace hand-rolled JWT auth with Better-Auth session-based authentication.

- Scaffold auth/ Node.js service with Better-Auth, bcrypt password compat,
  Postgres adapter mapped to existing users table
- Add Alembic migration (002) creating sessions, accounts, verifications
  tables and migrating password hashes to accounts table
- Update FastAPI auth dependency to validate sessions via shared DB
  (supports both cookie and Bearer token)
- Remove registration/login/refresh endpoints from API gateway (now
  handled by Better-Auth service)
- Update frontend to use better-auth/react client with httpOnly cookies
  (no tokens in localStorage or memory)
- Rewrite auth store, Login, Register, Dashboard, Settings, ProtectedRoute
  to use session-based auth
- Update all tests to create sessions directly in DB instead of JWT tokens

Resolves CAR-27
See plan: CAR-26#document-plan

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Coupon Carl
2026-03-28 04:46:10 +00:00
parent b7e6f637a7
commit 906d7023a9
11 changed files with 505 additions and 479 deletions
+11 -5
View File
@@ -10,9 +10,9 @@ from decimal import Decimal
from uuid import UUID
import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from cartsnitch_api.auth.jwt import decode_token
from cartsnitch_api.models import (
Coupon,
NormalizedProduct,
@@ -126,10 +126,16 @@ async def seed_data(db_engine, auth_headers):
session.add_all(prices)
await session.flush()
# -- Purchases (need the user_id from the registered test user) --
token = auth_headers["Authorization"].split(" ")[1]
payload = decode_token(token)
user_id = UUID(payload["sub"])
# -- Get the user_id from the session token in auth_headers --
cookie_str = auth_headers.get("Cookie", "")
session_token = cookie_str.split("=", 1)[1] if "=" in cookie_str else ""
result = await session.execute(
text("SELECT user_id FROM sessions WHERE token = :token"),
{"token": session_token},
)
row = result.first()
user_id = UUID(row[0])
purchase1 = Purchase(
user_id=user_id,