- Add rate_limit_auth_requests (5/min) and rate_limit_auth_window_seconds (60)
settings to config.py
- Refactor rate_limit.py to use protocol/ABC pattern with InMemorySlidingWindow
and RedisSlidingWindow implementations
- Add RedisSlidingWindow using sorted sets for distributed rate limiting
- Add auth_strict_limiter for /auth/* POST endpoints (5 req/min per IP)
- Fall back to in-memory when Redis is unavailable
- Update tests to cover new functionality
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add connection pool config to SQLAlchemy async engine (pool_size=10, max_overflow=20, pool_pre_ping, pool_recycle)
- Implement Redis connection pool in CacheClient with initialize/close lifecycle
- Wire lifespan startup/shutdown to initialize and dispose pools
- Add dispose_engine() for graceful DB pool cleanup on shutdown
Closes CAR-550
Co-Authored-By: Paperclip <noreply@paperclip.ing>
CTO review: LGTM. CORS methods restricted to explicit list (no TRACE/CONNECT), headers whitelisted, nginx security headers added (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, CSP). Clean diff, CI green.
Remove dangerous default values for jwt_secret_key, service_key, and
fernet_key. Add startup validation that raises RuntimeError if these
secrets are not set via environment variables or contain placeholder
values.
Add test fixture to provide explicit test values for these secrets,
ensuring existing tests continue to pass.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
CTO review: APPROVED. Migration creates all 9 domain tables in correct FK order with idempotent guards. env.py commit fix resolves SQLAlchemy 2.0 DDL persistence issue.
SQLAlchemy 2.0 removed implicit autocommit; without an explicit
connection.commit() DDL changes from create_all() are rolled back
when the connection closes, leaving fresh databases without tables.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
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>
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>
API config.py now reads CARTSNITCH_DATABASE_URL first, falls back to
DATABASE_URL (which the infra K8s overlay sets for all pods), and finally
falls back to the hardcoded default. Also normalizes plain postgresql://
to postgresql+asyncpg:// for the asyncpg driver.
Fixes CAR-510.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Better-auth v1.5.6 stores raw 32-char tokens in sessions.token, not SHA-256
hashes. The SHA-256 fix from PR #136 causes all authenticated API calls to
return 401 because the UAT sessions table contains raw tokens.
- Remove hashlib from dependencies.py; compare tokens directly
- Remove hashlib from conftest.py; store raw tokens in test DB
- Remove hashlib from test_expired_session_rejected; use raw tokens
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Better-Auth v1.2+ stores SHA-256(raw_token) in the sessions.token
column. The cookie/Bearer header carries the raw token, so the API was
doing a plain-text lookup that would never match a hashed value —
causing all authenticated endpoints to return 401.
- Add hashlib import and hash token in _validate_session_token()
- Update conftest._create_test_user_and_session() to store hashed tokens
- Update test_expired_session_rejected() to store hashed tokens
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Better-auth sets the session cookie with the __Secure- prefix on HTTPS
deployments. The API was only reading the plain cookie name, causing all
authenticated calls to return 401 in dev/UAT/prod environments.
Check __Secure-better-auth.session_token first, fall back to
better-auth.session_token for HTTP local dev compatibility.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Default varchar(32) alembic_version column truncates long revision IDs
like 003_make_users_hashed_password_nullable (39 chars) on fresh databases.
Set version_table_column_width=128 in both context.configure() calls.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Create migration 007 to raw-SQL CREATE TABLE IF NOT EXISTS the users table
as a safety net for fresh databases where Base.metadata.create_all() may
fail due to import errors before the table is created.
Wrap the create_all call in env.py with try/except so alembic never crashes
due to create_all failures — migrations already handle table creation.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
psycopg2 compiled against libpq-dev in the build stage now has
its runtime dependency (libpq5) available in the prod stage.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Better-Auth creates users via raw SQL INSERT (not through SQLAlchemy),
so it bypasses ORM defaults and causes HTTP 500 on sign-up/sign-in.
Adds PostgreSQL server_default so INSERT without email_inbound_token
auto-generates a URL-safe token matching Python secrets.token_urlsafe(16).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Fix email format in AuthService.get_email_in_address to use
receipts+{token}@receipts.cartsnitch.com (was broken: @email.cartsnitch.com)
- Remove dead EmailInAddressResponse class and GET /auth/me/email-in-address
endpoint from auth/routes.py (endpoint moved to routes/user.py)
- Add instructions field to EmailInAddressResponse schema
- Update routes/user.py to include instructions in the response
- Update test URLs from /auth/me/email-in-address to /api/v1/me/email-in-address
Co-authored-by: CartSnitch Engineer Bot <cartnoreply@cartsnitch.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
The GET /me/email-in-address endpoint was unreachable because the Gateway
HTTPRoute routes all /auth/* traffic to Better-Auth (port 3001), not the
API service. This change:
- Moves the endpoint from the /auth router to a new /api/v1/me/ router
- Adds EmailInAddressResponse schema and get_email_in_address service method
- Updates Settings.tsx to call /api/v1/me/email-in-address
Fixes CAR-445.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Revert auth/dependencies.py to cookie+Bearer dual auth with str user IDs
- Add GET /auth/me/email-in-address endpoint for receipt email routing
- Update User model: add email_inbound_token, change id/store_id/user_id to str
- Update AuthService and UserResponse to use str user IDs
- Update route count test: 33 -> 34 routes
- Restore e2e test for email-in-address endpoint
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Revert auth/dependencies.py, auth/routes.py, services/auth.py, schemas.py
to Better-Auth session-cookie auth (removed JWT register/login/refresh)
- Preserve GET /auth/me/email-in-address endpoint
- Fix UUIDString TypeDecorator: process_result_value returns uuid.UUID
(not str) so SQLAlchemy 2.0 sentinel tracking matches UUID-to-UUID
- Fix seed_data fixture: look up real user_id from session token via
sessions table; purchases now reference actual user FK
- Update purchase_data fixture to use session-cookie auth
- Update test_auth_endpoints, test_auth_validation to cookie-based tests
- Remove TestRegistrationErrors and TestLoginErrors (no longer applicable)
- Update test_openapi.py expected routes and count
- Update test_error_handler.py to use PATCH /auth/me validation
Co-Authored-By: Paperclip <noreply@paperclip.ing>