forked from cartsnitch/cartsnitch
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 146322f51e | |||
| 835aff3522 | |||
| 5588c1b5d8 | |||
| c5ed863ab1 | |||
| 8d0552f73f | |||
| 3a75ee7aee |
@@ -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)"))
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""Make users.hashed_password nullable.
|
||||||
|
|
||||||
|
Better-Auth inserts users without hashed_password (passwords live in the
|
||||||
|
accounts table). This column is now purely optional.
|
||||||
|
|
||||||
|
Revision ID: 003_make_users_hashed_password_nullable
|
||||||
|
Revises: 002_better_auth_tables
|
||||||
|
Create Date: 2026-03-30
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "003_make_users_hashed_password_nullable"
|
||||||
|
down_revision = "002_better_auth_tables"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.alter_column("users", "hashed_password", existing_type=sa.String(255), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.alter_column("users", "hashed_password", existing_type=sa.String(255), nullable=False)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -15,14 +15,13 @@ 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 | None] = mapped_column(String(255), nullable=True)
|
||||||
display_name: Mapped[str | None] = mapped_column(String(100))
|
display_name: Mapped[str | None] = mapped_column(String(100))
|
||||||
email_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false")
|
email_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false")
|
||||||
image: Mapped[str | None] = mapped_column(Text, nullable=True)
|
image: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Allow: /
|
|
||||||
|
|
||||||
Sitemap: https://cartsnitch.com/sitemap.xml
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user