Compare commits

..

4 Commits

Author SHA1 Message Date
Barcode Betty 5588c1b5d8 fix: use same-origin default for auth URL instead of localhost
Avoids ERR_CONNECTION_REFUSED in deployed environments where
VITE_AUTH_URL is not set at build time. Empty-string fallback
routes auth requests to same origin, which the HTTPRoute forwards
to the auth service.

cc @cpfarhood

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 15:50:51 +00:00
cartsnitch-ceo[bot] c5ed863ab1 fix: align frontend auth with API token response contract
fix: align frontend auth with API token response contract
2026-03-30 15:20:56 +00:00
Barcode Betty 8d0552f73f Merge branch 'origin/main' into fix/auth-contract-mismatch
# Conflicts:
#	src/pages/Login.tsx
#	src/pages/Register.tsx
2026-03-30 13:12:32 +00:00
Barcode Betty 3a75ee7aee fix: align frontend auth with API token response contract
- Register sends display_name instead of name
- Register/Login handle TokenResponse (access_token, not token)
- Fetch /auth/me after register/login to populate user object

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-30 11:00:52 +00:00
11 changed files with 36 additions and 186 deletions
-25
View File
@@ -46,31 +46,6 @@ jobs:
- name: Run tests - name: Run tests
run: npx vitest run run: npx vitest run
lighthouse:
runs-on: runners-cartsnitch
needs: [test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
- run: npm ci
- run: npm run build
- name: Install Chromium for Lighthouse
run: |
npm install -g playwright
npx playwright install --with-deps chromium
- name: Start preview server
run: |
npm run preview &
npx wait-on http://localhost:4173/ --timeout 30000
- name: Run Lighthouse CI
run: |
CHROME_PATH=$(find /home/runner/.cache/ms-playwright -name chrome -type f 2>/dev/null | head -1)
npm install -g @lhci/cli
LHCI_CHROME_PATH="$CHROME_PATH" lhci autorun
build-and-push: build-and-push:
runs-on: runners-cartsnitch runs-on: runners-cartsnitch
needs: [lint, test] needs: [lint, test]
@@ -1,53 +0,0 @@
"""Change users.id and FK columns from uuid to text.
Better-Auth generates nanoid-style text IDs (e.g. pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI),
but the users table was using PostgreSQL uuid type, causing INSERT failures.
Revision ID: 003_fix_user_id_text
Revises: 002_better_auth_tables
Create Date: 2026-03-31
"""
import sqlalchemy as sa
from sqlalchemy import text
from alembic import op
revision = "003_fix_user_id_text"
down_revision = "002_better_auth_tables"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Step 1: Drop FK constraints that reference users.id
op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey"))
op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey"))
# Step 2: Alter users.id from uuid to text
op.alter_column("users", "id", existing_type=sa.UUID(), type_=sa.Text(), existing_nullable=False, postgresql_using="id::text")
# Step 3: Alter user_store_accounts.user_id from uuid to text
op.alter_column("user_store_accounts", "user_id", existing_type=sa.UUID(), type_=sa.Text(), existing_nullable=False, postgresql_using="user_id::text")
# Step 4: Alter purchases.user_id from uuid to text
op.alter_column("purchases", "user_id", existing_type=sa.UUID(), type_=sa.Text(), existing_nullable=False, postgresql_using="user_id::text")
# Step 5: Re-add FK constraints
op.execute(text("ALTER TABLE user_store_accounts ADD CONSTRAINT user_store_accounts_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id)"))
op.execute(text("ALTER TABLE purchases ADD CONSTRAINT purchases_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id)"))
def downgrade() -> None:
# Drop FK constraints
op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey"))
op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey"))
# Alter back to UUID
op.alter_column("purchases", "user_id", existing_type=sa.Text(), type_=sa.UUID(), existing_nullable=False, postgresql_using="user_id::uuid")
op.alter_column("user_store_accounts", "user_id", existing_type=sa.Text(), type_=sa.UUID(), existing_nullable=False, postgresql_using="user_id::uuid")
op.alter_column("users", "id", existing_type=sa.Text(), type_=sa.UUID(), existing_nullable=False, postgresql_using="id::uuid")
# Re-add FK constraints
op.execute(text("ALTER TABLE user_store_accounts ADD CONSTRAINT user_store_accounts_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id)"))
op.execute(text("ALTER TABLE purchases ADD CONSTRAINT purchases_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id)"))
-9
View File
@@ -36,15 +36,6 @@ export const auth = betterAuth({
}, },
session: { session: {
modelName: "sessions",
fields: {
userId: "user_id",
expiresAt: "expires_at",
ipAddress: "ip_address",
userAgent: "user_agent",
createdAt: "created_at",
updatedAt: "updated_at",
},
expiresIn: 60 * 60 * 24 * 7, // 7 days expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // refresh after 1 day updateAge: 60 * 60 * 24, // refresh after 1 day
cookieCache: { cookieCache: {
@@ -1,61 +0,0 @@
"""Change users.id and user_id FKs from uuid to text.
Revision ID: 003_fix_user_id_text
Revises:
Create Date: 2026-03-31
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "003_fix_user_id_text"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Drop FK constraints first
op.execute("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey")
op.execute("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey")
# Alter users.id from uuid to text
op.execute("ALTER TABLE users ALTER COLUMN id TYPE text USING id::text")
# Alter user_id columns from uuid to text
op.execute("ALTER TABLE user_store_accounts ALTER COLUMN user_id TYPE text USING user_id::text")
op.execute("ALTER TABLE purchases ALTER COLUMN user_id TYPE text USING user_id::text")
# Re-add FK constraints
op.execute(
"ALTER TABLE user_store_accounts ADD CONSTRAINT user_store_accounts_user_id_fkey "
"FOREIGN KEY (user_id) REFERENCES users(id)"
)
op.execute(
"ALTER TABLE purchases ADD CONSTRAINT purchases_user_id_fkey "
"FOREIGN KEY (user_id) REFERENCES users(id)"
)
def downgrade() -> None:
# Drop FK constraints
op.execute("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey")
op.execute("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey")
# Alter back to uuid
op.execute("ALTER TABLE users ALTER COLUMN id TYPE uuid USING id::uuid")
op.execute("ALTER TABLE user_store_accounts ALTER COLUMN user_id TYPE uuid USING user_id::uuid")
op.execute("ALTER TABLE purchases ALTER COLUMN user_id TYPE uuid USING user_id::uuid")
# Re-add FK constraints
op.execute(
"ALTER TABLE user_store_accounts ADD CONSTRAINT user_store_accounts_user_id_fkey "
"FOREIGN KEY (user_id) REFERENCES users(id)"
)
op.execute(
"ALTER TABLE purchases ADD CONSTRAINT purchases_user_id_fkey "
"FOREIGN KEY (user_id) REFERENCES users(id)"
)
@@ -32,7 +32,7 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base):
__tablename__ = "purchases" __tablename__ = "purchases"
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False) store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False)
store_location_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("store_locations.id")) store_location_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("store_locations.id"))
receipt_id: Mapped[str] = mapped_column(String(200), nullable=False) receipt_id: Mapped[str] = mapped_column(String(200), nullable=False)
+2 -3
View File
@@ -15,12 +15,11 @@ if TYPE_CHECKING:
from cartsnitch_common.models.store import Store from cartsnitch_common.models.store import Store
class User(TimestampMixin, Base): class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Application user.""" """Application user."""
__tablename__ = "users" __tablename__ = "users"
id: Mapped[str] = mapped_column(Text, primary_key=True)
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
display_name: Mapped[str | None] = mapped_column(String(100)) display_name: Mapped[str | None] = mapped_column(String(100))
@@ -38,7 +37,7 @@ class UserStoreAccount(UUIDPrimaryKeyMixin, TimestampMixin, Base):
__tablename__ = "user_store_accounts" __tablename__ = "user_store_accounts"
__table_args__ = (UniqueConstraint("user_id", "store_id", name="uq_user_store_account"),) __table_args__ = (UniqueConstraint("user_id", "store_id", name="uq_user_store_account"),)
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False) store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False)
# WARNING: Contains retailer session cookies/tokens. Encryption-at-rest # WARNING: Contains retailer session cookies/tokens. Encryption-at-rest
# required before production deployment (e.g., pgcrypto or app-level encryption). # required before production deployment (e.g., pgcrypto or app-level encryption).
@@ -40,7 +40,7 @@ class PurchaseItemRead(BaseModel):
class PurchaseCreate(BaseModel): class PurchaseCreate(BaseModel):
user_id: str user_id: uuid.UUID
store_id: uuid.UUID store_id: uuid.UUID
store_location_id: uuid.UUID | None = None store_location_id: uuid.UUID | None = None
receipt_id: str receipt_id: str
@@ -58,7 +58,7 @@ class PurchaseRead(BaseModel):
model_config = {"from_attributes": True} model_config = {"from_attributes": True}
id: uuid.UUID id: uuid.UUID
user_id: str user_id: uuid.UUID
store_id: uuid.UUID store_id: uuid.UUID
store_location_id: uuid.UUID | None store_location_id: uuid.UUID | None
receipt_id: str receipt_id: str
+3 -3
View File
@@ -17,7 +17,7 @@ class UserCreate(BaseModel):
class UserRead(BaseModel): class UserRead(BaseModel):
model_config = {"from_attributes": True} model_config = {"from_attributes": True}
id: str id: uuid.UUID
email: str email: str
display_name: str | None display_name: str | None
created_at: datetime created_at: datetime
@@ -25,7 +25,7 @@ class UserRead(BaseModel):
class UserStoreAccountCreate(BaseModel): class UserStoreAccountCreate(BaseModel):
user_id: str user_id: uuid.UUID
store_id: uuid.UUID store_id: uuid.UUID
session_data: dict | None = None session_data: dict | None = None
status: AccountStatus = AccountStatus.ACTIVE status: AccountStatus = AccountStatus.ACTIVE
@@ -35,7 +35,7 @@ class UserStoreAccountRead(BaseModel):
model_config = {"from_attributes": True} model_config = {"from_attributes": True}
id: uuid.UUID id: uuid.UUID
user_id: str user_id: uuid.UUID
store_id: uuid.UUID store_id: uuid.UUID
status: AccountStatus status: AccountStatus
session_expires_at: datetime | None session_expires_at: datetime | None
-25
View File
@@ -1,25 +0,0 @@
{
"ci": {
"collect": {
"staticDistDir": "./dist",
"url": ["http://localhost:4173/"],
"numberOfRuns": 1
},
"assert": {
"preset": "lighthouse:no-pwa",
"assertions": {
"categories:performance": ["warn", { "minScore": 0.5 }],
"categories:accessibility": ["error", { "minScore": 0.7 }],
"categories:best-practices": ["warn", { "minScore": 0.8 }],
"categories:seo": ["warn", { "minScore": 0.7 }],
"errors-in-console": ["warn"],
"network-dependency-tree-insight": ["warn"],
"robots-txt": ["warn"],
"unused-javascript": ["warn"]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://cartsnitch.com/sitemap.xml
+28
View File
@@ -1,8 +1,36 @@
import { createAuthClient } from "better-auth/react" import { createAuthClient } from "better-auth/react"
import type { BetterFetchPlugin } from "@better-fetch/fetch"
/**
* Maps 'name' -> 'display_name' in register requests to match the API's RegisterRequest schema.
*/
const displayNameMapper: BetterFetchPlugin = {
id: "display-name-mapper",
name: "display-name-mapper",
hooks: {
onRequest: async (context) => {
const url = typeof context.url === "string" ? context.url : context.url.pathname
if (
url.endsWith("/auth/register") &&
context.method === "POST" &&
context.body &&
"name" in context.body
) {
context.body = {
...context.body,
display_name: context.body.name as string,
name: undefined,
}
}
return context
},
},
}
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_AUTH_URL || "", baseURL: import.meta.env.VITE_AUTH_URL || "",
basePath: "/auth", basePath: "/auth",
fetchPlugins: [displayNameMapper],
}) })
export const { useSession, signIn, signUp, signOut } = authClient export const { useSession, signIn, signUp, signOut } = authClient