Compare commits

..

4 Commits

Author SHA1 Message Date
Barcode Betty 0be7ccd4b4 fix(api): import Base from models package to register all ORM tables
The models/__init__.py imports all ORM model classes (Store, Product,
Coupon, etc.) which registers their table definitions with Base.metadata.
Importing Base directly from models.base skips this registration, so
alembic's create_all() on fresh databases fails to create app tables.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 21:12:13 +00:00
cartsnitch-cto[bot] 6d37cecdba Merge pull request #143 from cartsnitch/betty/fix-session-cookie-parsing
fix(auth): parse compound Better-Auth cookie/bearer token
2026-04-04 20:39:09 +00:00
Barcode Betty 3745f5be69 fix(auth): parse compound Better-Auth cookie/bearer token to extract token part
Better-Auth sets the session cookie as "token.sessionId". The DB stores
only the token part, so passing the full compound value caused 401s.

Splits on "." for both cookie and Bearer paths.

Tests added for compound cookie, raw token cookie (regression), and
compound Bearer token.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 20:32:43 +00:00
cartsnitch-cto[bot] abec954320 Merge pull request #141 from cartsnitch/betty/fix-api-database-url-fallback
fix(api): accept DATABASE_URL as fallback for shared DB with auth service
2026-04-04 20:05:47 +00:00
3 changed files with 56 additions and 3 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
from alembic import context from alembic import context
from cartsnitch_api.models.base import Base # noqa: F401 — imports all models for autogenerate from cartsnitch_api.models import Base # noqa: F401 — imports all models for autogenerate
config = context.config config = context.config
if config.config_file_name is not None: if config.config_file_name is not None:
+5 -2
View File
@@ -71,11 +71,14 @@ async def get_current_user(
# 1. Check session cookie — prefer __Secure- variant (HTTPS) over plain (HTTP dev) # 1. Check session cookie — prefer __Secure- variant (HTTPS) over plain (HTTP dev)
cookie_token = request.cookies.get(SECURE_SESSION_COOKIE_NAME) or request.cookies.get(SESSION_COOKIE_NAME) cookie_token = request.cookies.get(SECURE_SESSION_COOKIE_NAME) or request.cookies.get(SESSION_COOKIE_NAME)
if cookie_token: if cookie_token:
token = cookie_token # Better-Auth cookie format is "token.sessionId" — extract just the token part
token = cookie_token.split(".")[0] if "." in cookie_token else cookie_token
# 2. Fall back to Bearer header # 2. Fall back to Bearer header
if not token and credentials: if not token and credentials:
token = credentials.credentials # Callers might pass the compound value here too
raw = credentials.credentials
token = raw.split(".")[0] if "." in raw else raw
if not token: if not token:
raise HTTPException( raise HTTPException(
@@ -71,6 +71,56 @@ async def test_delete_me(client, auth_headers):
assert resp.status_code == 404 assert resp.status_code == 404
@pytest.mark.asyncio
async def test_get_me_compound_cookie(client, db_engine):
"""Compound cookie value (token.sessionId) must be parsed to extract the token part."""
from tests.conftest import _create_test_user_and_session
_, session_token = await _create_test_user_and_session(
client, db_engine, email="compound@example.com", display_name="Compound User"
)
compound = f"{session_token}.B0atkJCFxK1rZlwWPMK97nVO2LnyDun7"
resp = await client.get(
"/auth/me",
headers={"Cookie": f"better-auth.session_token={compound}"},
)
assert resp.status_code == 200
assert resp.json()["email"] == "compound@example.com"
@pytest.mark.asyncio
async def test_get_me_raw_token_cookie(client, db_engine):
"""Raw token (no dot) in cookie must still work — regression guard."""
from tests.conftest import _create_test_user_and_session
_, session_token = await _create_test_user_and_session(
client, db_engine, email="rawcookie@example.com", display_name="Raw Cookie User"
)
resp = await client.get(
"/auth/me",
headers={"Cookie": f"better-auth.session_token={session_token}"},
)
assert resp.status_code == 200
assert resp.json()["email"] == "rawcookie@example.com"
@pytest.mark.asyncio
async def test_get_me_compound_bearer(client, db_engine):
"""Compound Bearer token (token.sessionId) must be parsed to extract the token part."""
from tests.conftest import _create_test_user_and_session
_, session_token = await _create_test_user_and_session(
client, db_engine, email="compoundbearer@example.com", display_name="Compound Bearer User"
)
compound = f"{session_token}.B0atkJCFxK1rZlwWPMK97nVO2LnyDun7"
resp = await client.get(
"/auth/me",
headers={"Authorization": f"Bearer {compound}"},
)
assert resp.status_code == 200
assert resp.json()["email"] == "compoundbearer@example.com"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_expired_session_rejected(client, db_engine): async def test_expired_session_rejected(client, db_engine):
"""Expired sessions must be rejected.""" """Expired sessions must be rejected."""