Compare commits

..

4 Commits

Author SHA1 Message Date
Barcode Betty 75be08ccf3 feat: add dedicated UAT seed user with known credentials
Add guaranteed UAT test user (uat@cartsnitch.com / CartSnitch-UAT-2026!)
seeded via Better-Auth bcrypt path. Idempotent — re-running the seed
skips the user if it already exists.

- Add 002_better_auth_tables Alembic migration (sessions, accounts,
  verifications tables + email_verified/image on users)
- Add bcrypt>=4.0,<6.0 to [seed] extra (CTO feedback: was bcrypt>=0.15,<1.0
  which matches zero installable versions)
- Fix account_id to use str(UAT_USER_ID) to match migration convention
  (CTO feedback: was using UAT_EMAIL which was inconsistent)
- Document credentials in common/README.md under Test Users

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 21:20:31 +00:00
cartsnitch-ceo[bot] b43ec1fb9b fix(ci): add owner and repositories params to GitHub App token for cross-repo infra access
fix(ci): add owner and repositories params to GitHub App token for cross-repo infra access
2026-03-29 19:33:33 +00:00
Flea Flicker 129f0adc96 fix(ci): add owner and repositories params to GitHub App token for cross-repo infra access
The deploy-dev job fails because actions/create-github-app-token@v1 defaults to
the current repository. Adding owner + repositories scopes the token to include
cartsnitch/infra so the subsequent checkout step succeeds.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-29 19:01:40 +00:00
cartsnitch-engineer[bot] ea789378dd ci: remove trigger-uat job from cartsnitch workflow
Merged by CEO (Coupon Carl) after QA + CTO approval. Removes dead trigger-uat CI job. Part of CAR-115 / CAR-117.
2026-03-29 12:22:20 +00:00
5 changed files with 193 additions and 0 deletions
+2
View File
@@ -118,6 +118,8 @@ jobs:
with: with:
app-id: ${{ secrets.CARTSNITCH_APP_ID }} app-id: ${{ secrets.CARTSNITCH_APP_ID }}
private-key: ${{ secrets.CARTSNITCH_APP_PRIVATE_KEY }} private-key: ${{ secrets.CARTSNITCH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: infra
- name: Checkout infra repo - name: Checkout infra repo
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -0,0 +1,101 @@
"""Add Better-Auth tables and extend users table.
Creates sessions, accounts, and verifications tables for Better-Auth.
Adds email_verified and image columns to existing users table.
Migrates password hashes from users.hashed_password to accounts.password.
Revision ID: 002_better_auth_tables
Revises: 001_encrypt_session_data
Create Date: 2026-03-28
"""
import sqlalchemy as sa
from sqlalchemy import text
from alembic import op
revision = "002_better_auth_tables"
down_revision = "001_encrypt_session_data"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- Extend users table for Better-Auth compatibility ---
op.add_column("users", sa.Column("email_verified", sa.Boolean(), nullable=False, server_default="false"))
op.add_column("users", sa.Column("image", sa.Text(), nullable=True))
# --- Create sessions table ---
op.create_table(
"sessions",
sa.Column("id", sa.Text(), nullable=False),
sa.Column("token", sa.Text(), nullable=False),
sa.Column("user_id", sa.Text(), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("ip_address", sa.Text(), nullable=True),
sa.Column("user_agent", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_sessions_token", "sessions", ["token"], unique=True)
op.create_index("ix_sessions_user_id", "sessions", ["user_id"])
# --- Create accounts table ---
op.create_table(
"accounts",
sa.Column("id", sa.Text(), nullable=False),
sa.Column("user_id", sa.Text(), nullable=False),
sa.Column("account_id", sa.Text(), nullable=False),
sa.Column("provider_id", sa.Text(), nullable=False),
sa.Column("access_token", sa.Text(), nullable=True),
sa.Column("refresh_token", sa.Text(), nullable=True),
sa.Column("access_token_expires_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("refresh_token_expires_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("scope", sa.Text(), nullable=True),
sa.Column("id_token", sa.Text(), nullable=True),
sa.Column("password", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_accounts_user_id", "accounts", ["user_id"])
# --- Create verifications table ---
op.create_table(
"verifications",
sa.Column("id", sa.Text(), nullable=False),
sa.Column("identifier", sa.Text(), nullable=False),
sa.Column("value", sa.Text(), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
# --- Migrate existing password hashes to accounts table ---
# For each user with a hashed_password, create a 'credential' account row
conn = op.get_bind()
users = conn.execute(
text("SELECT id, hashed_password FROM users WHERE hashed_password IS NOT NULL")
).fetchall()
for user_id, hashed_password in users:
user_id_str = str(user_id)
conn.execute(
text(
"INSERT INTO accounts (id, user_id, account_id, provider_id, password, created_at, updated_at) "
"VALUES (gen_random_uuid()::text, :user_id, :account_id, 'credential', :password, now(), now())"
),
{"user_id": user_id_str, "account_id": user_id_str, "password": hashed_password},
)
def downgrade() -> None:
op.drop_table("verifications")
op.drop_table("accounts")
op.drop_index("ix_sessions_user_id", table_name="sessions")
op.drop_index("ix_sessions_token", table_name="sessions")
op.drop_table("sessions")
op.drop_column("users", "image")
op.drop_column("users", "email_verified")
+28
View File
@@ -0,0 +1,28 @@
# CartSnitch Common
Shared models, schemas, and utilities for CartSnitch services.
## Test Users
The following users are seeded by `cartsnitch-seed` and can be used for local development and UAT.
| Email | Password | Display Name | Notes |
|---|---|---|---|
| `uat@cartsnitch.com` | `CartSnitch-UAT-2026!` | UAT Tester | Primary UAT account. Use for regression testing in the CartSnitch frontend. Created by the seed runner via Better-Auth's bcrypt path — credentials work against the live auth service. Idempotent; re-running the seed skips this user if it already exists. |
### Running the Seed
```bash
# Install with seed dependencies
pip install -e "cartsnitch-common[seed]"
# Run (requires CARTSNITCH_DATABASE_URL_SYNC)
CARTSNITCH_DATABASE_URL_SYNC=postgresql://user:pass@localhost:5432/cartsnitch \
cartsnitch-seed
```
### Architecture
- **Models** live in `src/cartsnitch_common/models/`
- **Alembic migrations** run via the `api` service (`api/alembic/`)
- **Seed runner** runs via `cartsnitch-seed` (installed as a package entry point)
+1
View File
@@ -27,6 +27,7 @@ dev = [
] ]
seed = [ seed = [
"faker>=33.0,<34.0", "faker>=33.0,<34.0",
"bcrypt>=4.0,<6.0",
] ]
[project.scripts] [project.scripts]
@@ -2,8 +2,10 @@
import random import random
import time import time
import uuid
from typing import Any from typing import Any
import bcrypt
from faker import Faker from faker import Faker
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -184,6 +186,65 @@ def run_seed(
session.commit() session.commit()
_seed_uat_user(session)
elapsed = time.monotonic() - t0 elapsed = time.monotonic() - t0
_log("") _log("")
_log(f"Seed complete in {elapsed:.1f}s") _log(f"Seed complete in {elapsed:.1f}s")
# ---------------------------------------------------------------------------
# UAT seed user
# ---------------------------------------------------------------------------
UAT_EMAIL = "uat@cartsnitch.com"
UAT_PASSWORD = "CartSnitch-UAT-2026!"
UAT_DISPLAY_NAME = "UAT Tester"
UAT_USER_ID = uuid.UUID("00000000-0000-0000-0000-000000000001")
def _seed_uat_user(session: Session) -> None:
"""Insert or verify the dedicated UAT test user.
The user is created via Better-Auth's bcrypt hashing path so credentials
work against the live auth service. Idempotent — skips if the user already
exists.
"""
existing = session.execute(
text("SELECT id FROM users WHERE email = :email"),
{"email": UAT_EMAIL},
).fetchone()
if existing is not None:
_log(f"UAT user {UAT_EMAIL} already exists — skipping")
return
password_hash = bcrypt.hashpw(UAT_PASSWORD.encode(), bcrypt.gensalt()).decode()
session.execute(
text(
"INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) "
"VALUES (:id, :email, :hashed_password, :display_name, true, now(), now())"
),
{
"id": str(UAT_USER_ID),
"email": UAT_EMAIL,
"hashed_password": password_hash,
"display_name": UAT_DISPLAY_NAME,
},
)
session.execute(
text(
"INSERT INTO accounts (id, user_id, account_id, provider_id, password, created_at, updated_at) "
"VALUES (gen_random_uuid()::text, :user_id, :account_id, 'credential', :password, now(), now())"
),
{
"user_id": str(UAT_USER_ID),
"account_id": str(UAT_USER_ID),
"password": password_hash,
},
)
session.commit()
_log(f"UAT user {UAT_EMAIL} created")