Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ai/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ wishpicks/
| Migrations | Alembic | Version-controlled schema changes |
| Validation | Pydantic v2 | Request/response schemas, settings management |
| Auth | Custom JWT (`python-jose`) | Zero cost, full control, no vendor lock-in |
| Password hashing | bcrypt via `passlib` | Industry standard, cost factor 12 |
| Password hashing | `bcrypt` (direct) | passlib incompatible with bcrypt >= 4.0; use bcrypt library directly |
| URL scraping | `httpx` + `BeautifulSoup4` | Async HTTP + HTML parsing for product import |
| Rate limiting | slowapi + Redis (Upstash) | Persistent rate limit counters across restarts and multiple workers |
| Cache | Redis (Upstash) | URL parser cache, session blocklist; free serverless tier via HTTP |
Expand Down
209 changes: 209 additions & 0 deletions .ai/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# Design System (Minimalist Black & White)

## 1. Visual Theme & Atmosphere

This design system is built on confident minimalism — a black-and-white interface where every element has a clear purpose and nothing exists purely for decoration.

The entire experience is based on a strict duality:
- deep black (`#121212`)
- pure white (`#ffffff`)

There are no unnecessary mid-tones or visual noise.

Typography relies on geometric sans-serif fonts with a clean and engineered feel:
- headings are bold and authoritative
- body text is highly readable and functional

The interface makes heavy use of:
- **pill-shaped elements (999px border-radius)**
- **card-based layouts**
- **subtle shadows**
- **compact, information-dense spacing**

---

## 2. Color Palette & Roles

See `apps\web\assets\css\main.css` for color palette of the project. Use this for building interfaces, don't make up the new colors
without any need.

### Principle
- No gradients
- Only solid colors

---

## 3. Typography Rules

### Font Family
- Primary: `system-ui`, `Inter`, `DM Sans`

### Hierarchy

| Role | Size | Weight | Line Height |
|------|------|--------|------------|
| Hero | 52px | 700 | 1.23 |
| Section Heading | 36px | 700 | 1.22 |
| Card Title | 32px | 700 | 1.25 |
| Subheading | 24px | 700 | 1.33 |
| Small Heading | 20px | 700 | 1.40 |
| UI Text | 18px | 500 | 1.33 |
| Body | 16px | 400-500 | 1.25-1.50 |
| Caption | 14px | 400 | 1.4 |
| Micro | 12px | 400 | 1.6 |

### Principles
- Headings are always bold
- Body text is medium or regular weight
- No decorative typography

---

## 4. Component Stylings

### Buttons

**Primary**
- Background: Black
- Text: White
- Padding: 10px 12px
- Radius: 999px

**Secondary**
- Background: White
- Text: Black
- Hover: `#e2e2e2`
- Radius: 999px

**Chip**
- Background: `#efefef`
- Radius: 999px

**Floating Action**
- Background: White
- Shadow: `rgba(0,0,0,0.16)`
- Radius: 999px

---

### Cards
- Radius: 8px (standard), 12px (featured)
- Shadow: `rgba(0,0,0,0.12)`
- No borders

---

### Inputs
- Border: 1px solid black
- Radius: 8px
- Background: white

---

### Navigation
- Sticky top bar
- Minimal design
- Pill-style navigation elements

---

## 5. Layout Principles

### Spacing
- Base unit: 8px
- Scale: 4px → 32px

### Container
- Max width: ~1136px

### Philosophy
- Efficiency over airiness
- High information density

---

## 6. Depth & Elevation

| Level | Treatment |
|------|----------|
| 0 | No shadow |
| 1 | `rgba(0,0,0,0.12)` |
| 2 | `rgba(0,0,0,0.16)` |
| 3 | Floating elements |
| 4 | Inset (pressed state) |

---

## 7. Nuxt UI Usage Guidelines

### Core Principle

When building the interface, **prefer using Nuxt UI components whenever possible**.

### Rules

- Do NOT create custom components if:
- an equivalent exists in Nuxt UI
- it can be adapted via props, slots, or styling

### When Custom Markup is Allowed

Use plain HTML + CSS (or Tailwind) only if:
- the required component does not exist in Nuxt UI
- the behavior is too specific or complex
- a unique layout cannot be achieved with existing components

### Anti-patterns

- ❌ Rebuilding buttons from scratch
- ❌ Duplicating Nuxt UI logic
- ❌ Mixing too many custom and library components inconsistently

### Goal

- UI consistency
- faster development
- easier maintenance

---

## 8. Do's and Don'ts

### Do
- Use black and white as the primary palette
- Use pill-shaped buttons and controls
- Keep layouts compact
- Use subtle shadows

### Don't
- Do not use gradients
- Do not introduce unnecessary colors
- Do not create overly spacious layouts
- Do not use heavy shadows

---

## 9. Responsive Behavior

### Breakpoints

| Name | Width |
|------|------|
| Mobile | 320–600px |
| Tablet | 768–1119px |
| Desktop | 1120px+ |

### Rules
- Layouts stack vertically on smaller screens
- Buttons must be at least 44px height
- Grids collapse into single column

---

## 10. Agent Prompt Guide

### Principles
- Be explicit about colors (`#121212`, `#ffffff`)
- Always specify border-radius for buttons (999px)
- Keep layouts compact and structured
- Prefer consistency over creativity
45 changes: 45 additions & 0 deletions .ai/sessions/2026-04-30-phase1-foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Phase 1 Foundation

## Description

Implemented the full Phase 1 foundation: initial database schema migration (all 7 tables) and complete auth system with JWT HTTP-only cookies, refresh token rotation, and theft detection.

## Session Log

1. Created `migrations/versions/6fc915cac3e1_initial_schema.py` via `alembic revision --autogenerate`, then manually patched `ix_users_google_id` to be a partial index (`WHERE google_id IS NOT NULL`) — autogenerate cannot detect partial index conditions.

2. Built `app/core/security.py` — JWT create/decode utilities using python-jose.

3. Extracted `app/core/limiter.py` — slowapi Limiter instance lives here (not in main.py) to avoid circular imports, since routers import limiter and main.py imports routers.

4. Built `app/schemas/auth.py` — RegisterRequest, LoginRequest, UserResponse, AuthResponse. Required switching `pydantic` to `pydantic[email]` for EmailStr.

5. Replaced `passlib` with direct `bcrypt` usage in `app/services/auth.py`. passlib raises `ValueError: password cannot be longer than 72 bytes` during `CryptContext` initialization (`detect_wrap_bug()`) when bcrypt >= 4.0 is installed. Removed passlib from `pyproject.toml`.

6. Built full `app/services/auth.py`: register, login, logout, refresh_tokens (rotation + theft detection), `_revoke_all_user_tokens`, `_issue_tokens` helper.

7. Built `app/dependencies/get_current_user.py` — reads access_token from Cookie, validates JWT type=="access", loads User from DB.

8. Built `app/routers/auth.py` — 5 endpoints: POST /register (201, 3/min), POST /login (200, 5/min), POST /logout (204, 10/min), POST /refresh (200, 10/min), GET /me (200, 10/min). Each needs `Request` as first param for slowapi.

9. Updated `app/main.py` — wired slowapi, disabled OpenAPI in production, added custom HTTPException handler to enforce `{"data":{}}` / `{"error":{}}` envelope (FastAPI's default wraps detail in `{"detail": ...}`).

10. Removed all tests from the project. `tests/` directory deleted, pytest/pytest-asyncio removed from `pyproject.toml`.

11. Created `apps/api/README.md` and `apps/web/README.md`.

## Session Outcomes

- All 7 tables created and migrated: users, wishlists, wish_items, reservations, refresh_tokens, saved_wishlists, saved_items
- Auth endpoints fully functional and verified via curl
- `ruff check app/` passes cleanly
- Both READMEs written

## Lessons Learned

- **passlib + bcrypt >= 4.0 incompatible**: use `bcrypt` directly — `bcrypt.hashpw()` / `bcrypt.checkpw()`
- **Alembic partial indexes**: autogenerate misses `WHERE` clause — always review and patch manually after generation
- **slowapi circular import**: keep Limiter in its own `core/limiter.py`, not in `main.py`
- **FastAPI HTTPException envelope**: default behavior wraps `detail` in `{"detail": ...}` — need a custom exception handler to enforce project envelope format
- **Swagger Set-Cookie**: browsers block reading Set-Cookie as a forbidden header — this is expected behavior, not a bug; use curl `-v` or Network tab to verify cookies are set
- **asyncpg + Windows**: incompatible with ProactorEventLoop — all backend CLI commands must run inside Docker (`docker exec wishpicks-api-1 ...`)
38 changes: 38 additions & 0 deletions .ai/sessions/2026-05-05-google-oauth-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Google OAuth — Full Implementation (API + Web)

## Description

Implemented Google OAuth login flow end-to-end: FastAPI backend handles the full code exchange and cookie issuance; the Nuxt frontend had most of the UI pre-built, the only missing piece was surfacing the `?error=oauth_failed` redirect on the login page.

## Session Log

### API

1. Created `app/services/google_oauth.py` — `get_google_auth_url()` builds the consent screen URL; `exchange_code_for_profile(code)` exchanges the authorization code for a Google userinfo profile via httpx; `upsert_google_user(db, profile)` applies 3-step upsert: match by `google_id` → match by email (links `google_id` to existing account) → create new Google-only account (no `password_hash`).

2. Added `google_login(db, code)` to `app/services/auth.py` — orchestrates the google_oauth service + existing `_issue_tokens`, follows the same return shape `(user, access_token, refresh_token)` as `register` and `login`.

3. Added `GET /api/auth/google` and `GET /api/auth/google/callback` to `app/routers/auth.py`. The callback redirects to `{FRONTEND_URL}/dashboard` on success with JWT HTTP-only cookies set, or to `{FRONTEND_URL}/login?error=oauth_failed` on any failure (missing code, Google error param, exchange failure). Rate limit: 10/minute/IP.

4. Discovered that `docker restart` does not re-read `env_file` — must use `docker compose up -d --force-recreate api` to pick up `.env` changes.

### Web

5. The frontend was already ~95% built: Google buttons on login/register pages, `loginWithGoogle()` composable, `googleAuthUrl()` API method, `/auth/google/callback` loading page, and all translations existed. `GOOGLE_REDIRECT_URI` points to the backend (`localhost:8000/api/auth/google/callback`), so the backend owns the full OAuth exchange and redirects the browser to `/dashboard` directly — the frontend callback page is not in the active redirect path.

6. The only missing piece: `pages/login.vue` did not read the `?error` query param, so users who denied access on Google's consent screen were silently redirected to `/login` with no feedback. Fixed by initialising `errorCode` from `route.query.error` via `useRoute()`.

7. Added `auth.errors.oauth_failed` to `locales/en.json` ("Google sign-in failed. Please try again.") and `locales/uk.json` ("Вхід через Google не вдався. Спробуй ще раз.").

## Session Outcomes

- `GET /api/auth/google` and `GET /api/auth/google/callback` fully functional
- Smoke tested with real Google credentials — redirect URL contains correct `client_id`
- Error fallbacks verified: missing code and `error=access_denied` both redirect to `/login?error=oauth_failed`
- `ruff check` passes on all modified API files
- Login page now surfaces `oauth_failed` error message in both locales when Google auth is denied or fails

## Lessons Learned

- **`docker restart` vs `--force-recreate`**: `restart` keeps the existing environment snapshot; `env_file` changes only take effect after `docker compose up --force-recreate`.
- **Frontend OAuth callback page vs direct dashboard redirect**: `GOOGLE_REDIRECT_URI` points to the backend, so the backend sets cookies and redirects the browser to `/dashboard` directly. The `/auth/google/callback` Nuxt page exists but is not in the active flow — `auth.client.ts` plugin calls `initialize()` on every page load, so landing on `/dashboard` with cookies already set works correctly.
29 changes: 29 additions & 0 deletions .github/cliff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[changelog]
header = ""
body = """
{% for group, commits in commits | group_by(attribute="group") %}\
### {{ group | upper_first }}

{% for commit in commits %}\
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }} ([`{{ commit.id | truncate(length=7, end="") }}`](https://github.com/denvudd/wishpicks/commit/{{ commit.id }}))
{% endfor %}
{% endfor %}
"""
trim = true

[git]
conventional_commits = true
filter_unconventional = true
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^docs", group = "Documentation" },
{ message = "^chore|^ci|^build|^style|^test", group = "Miscellaneous" },
{ message = "^revert", group = "Reverted" },
]
filter_commits = false
tag_pattern = "v[0-9]*"
topo_order = false
sort_commits = "oldest"
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI

on:
pull_request:
branches: [main]

jobs:
lint-api:
name: Lint API
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/api

steps:
- uses: actions/checkout@v4

- name: Setup uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Install dependencies
run: uv sync --dev

- name: Ruff lint
run: uv run ruff check .

- name: Ruff format check
run: uv run ruff format --check .

lint-web:
name: Lint Web
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci
env:
HUSKY: 0

- name: ESLint
working-directory: apps/web
run: npm run lint

- name: Typecheck
working-directory: apps/web
run: npx nuxt typecheck
Loading
Loading