From 895ad7785087ed5eb730fb66794903810bf12ffb Mon Sep 17 00:00:00 2001 From: Stockboy Steve Date: Tue, 31 Mar 2026 17:15:31 +0000 Subject: [PATCH] fix(auth): change users.id and user_id FKs 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 on registration. This changes User.id, UserStoreAccount.user_id, and Purchase.user_id from uuid to text, with a corresponding Alembic migration. Co-Authored-By: Paperclip --- api/alembic/versions/003_fix_user_id_text.py | 53 +++++++++++++++++++ .../src/cartsnitch_common/models/purchase.py | 2 +- common/src/cartsnitch_common/models/user.py | 5 +- .../src/cartsnitch_common/schemas/purchase.py | 4 +- common/src/cartsnitch_common/schemas/user.py | 10 ++-- 5 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 api/alembic/versions/003_fix_user_id_text.py diff --git a/api/alembic/versions/003_fix_user_id_text.py b/api/alembic/versions/003_fix_user_id_text.py new file mode 100644 index 0000000..d41cb16 --- /dev/null +++ b/api/alembic/versions/003_fix_user_id_text.py @@ -0,0 +1,53 @@ +"""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)")) diff --git a/common/src/cartsnitch_common/models/purchase.py b/common/src/cartsnitch_common/models/purchase.py index 3797ef2..5af9470 100644 --- a/common/src/cartsnitch_common/models/purchase.py +++ b/common/src/cartsnitch_common/models/purchase.py @@ -32,7 +32,7 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base): __tablename__ = "purchases" - user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False) + user_id: Mapped[str] = mapped_column(ForeignKey("users.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")) receipt_id: Mapped[str] = mapped_column(String(200), nullable=False) diff --git a/common/src/cartsnitch_common/models/user.py b/common/src/cartsnitch_common/models/user.py index 5e35e5a..91686cf 100644 --- a/common/src/cartsnitch_common/models/user.py +++ b/common/src/cartsnitch_common/models/user.py @@ -15,11 +15,12 @@ if TYPE_CHECKING: from cartsnitch_common.models.store import Store -class User(UUIDPrimaryKeyMixin, TimestampMixin, Base): +class User(TimestampMixin, Base): """Application user.""" __tablename__ = "users" + id: Mapped[str] = mapped_column(Text, primary_key=True) email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) display_name: Mapped[str | None] = mapped_column(String(100)) @@ -37,7 +38,7 @@ class UserStoreAccount(UUIDPrimaryKeyMixin, TimestampMixin, Base): __tablename__ = "user_store_accounts" __table_args__ = (UniqueConstraint("user_id", "store_id", name="uq_user_store_account"),) - user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False) + user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False) store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False) # WARNING: Contains retailer session cookies/tokens. Encryption-at-rest # required before production deployment (e.g., pgcrypto or app-level encryption). diff --git a/common/src/cartsnitch_common/schemas/purchase.py b/common/src/cartsnitch_common/schemas/purchase.py index 05959be..7091441 100644 --- a/common/src/cartsnitch_common/schemas/purchase.py +++ b/common/src/cartsnitch_common/schemas/purchase.py @@ -40,7 +40,7 @@ class PurchaseItemRead(BaseModel): class PurchaseCreate(BaseModel): - user_id: uuid.UUID + user_id: str store_id: uuid.UUID store_location_id: uuid.UUID | None = None receipt_id: str @@ -58,7 +58,7 @@ class PurchaseRead(BaseModel): model_config = {"from_attributes": True} id: uuid.UUID - user_id: uuid.UUID + user_id: str store_id: uuid.UUID store_location_id: uuid.UUID | None receipt_id: str diff --git a/common/src/cartsnitch_common/schemas/user.py b/common/src/cartsnitch_common/schemas/user.py index 2c174ba..d0256af 100644 --- a/common/src/cartsnitch_common/schemas/user.py +++ b/common/src/cartsnitch_common/schemas/user.py @@ -17,7 +17,7 @@ class UserCreate(BaseModel): class UserRead(BaseModel): model_config = {"from_attributes": True} - id: uuid.UUID + id: str email: str display_name: str | None created_at: datetime @@ -25,8 +25,8 @@ class UserRead(BaseModel): class UserStoreAccountCreate(BaseModel): - user_id: uuid.UUID - store_id: uuid.UUID + user_id: str + store_id: str session_data: dict | None = None status: AccountStatus = AccountStatus.ACTIVE @@ -35,8 +35,8 @@ class UserStoreAccountRead(BaseModel): model_config = {"from_attributes": True} id: uuid.UUID - user_id: uuid.UUID - store_id: uuid.UUID + user_id: str + store_id: str status: AccountStatus session_expires_at: datetime | None last_sync_at: datetime | None