Skip to content

Commit 7b4db2f

Browse files
authored
Merge pull request #7 from denvudd/dev
release: v1.0.0
2 parents d8bade2 + 2f8d141 commit 7b4db2f

63 files changed

Lines changed: 12433 additions & 7514 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ai/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ wishpicks/
6161
| Migrations | Alembic | Version-controlled schema changes |
6262
| Validation | Pydantic v2 | Request/response schemas, settings management |
6363
| Auth | Custom JWT (`python-jose`) | Zero cost, full control, no vendor lock-in |
64-
| Password hashing | bcrypt via `passlib` | Industry standard, cost factor 12 |
64+
| Password hashing | `bcrypt` (direct) | passlib incompatible with bcrypt >= 4.0; use bcrypt library directly |
6565
| URL scraping | `httpx` + `BeautifulSoup4` | Async HTTP + HTML parsing for product import |
6666
| Rate limiting | slowapi + Redis (Upstash) | Persistent rate limit counters across restarts and multiple workers |
6767
| Cache | Redis (Upstash) | URL parser cache, session blocklist; free serverless tier via HTTP |

.ai/DESIGN.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Design System (Minimalist Black & White)
2+
3+
## 1. Visual Theme & Atmosphere
4+
5+
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.
6+
7+
The entire experience is based on a strict duality:
8+
- deep black (`#121212`)
9+
- pure white (`#ffffff`)
10+
11+
There are no unnecessary mid-tones or visual noise.
12+
13+
Typography relies on geometric sans-serif fonts with a clean and engineered feel:
14+
- headings are bold and authoritative
15+
- body text is highly readable and functional
16+
17+
The interface makes heavy use of:
18+
- **pill-shaped elements (999px border-radius)**
19+
- **card-based layouts**
20+
- **subtle shadows**
21+
- **compact, information-dense spacing**
22+
23+
---
24+
25+
## 2. Color Palette & Roles
26+
27+
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
28+
without any need.
29+
30+
### Principle
31+
- No gradients
32+
- Only solid colors
33+
34+
---
35+
36+
## 3. Typography Rules
37+
38+
### Font Family
39+
- Primary: `system-ui`, `Inter`, `DM Sans`
40+
41+
### Hierarchy
42+
43+
| Role | Size | Weight | Line Height |
44+
|------|------|--------|------------|
45+
| Hero | 52px | 700 | 1.23 |
46+
| Section Heading | 36px | 700 | 1.22 |
47+
| Card Title | 32px | 700 | 1.25 |
48+
| Subheading | 24px | 700 | 1.33 |
49+
| Small Heading | 20px | 700 | 1.40 |
50+
| UI Text | 18px | 500 | 1.33 |
51+
| Body | 16px | 400-500 | 1.25-1.50 |
52+
| Caption | 14px | 400 | 1.4 |
53+
| Micro | 12px | 400 | 1.6 |
54+
55+
### Principles
56+
- Headings are always bold
57+
- Body text is medium or regular weight
58+
- No decorative typography
59+
60+
---
61+
62+
## 4. Component Stylings
63+
64+
### Buttons
65+
66+
**Primary**
67+
- Background: Black
68+
- Text: White
69+
- Padding: 10px 12px
70+
- Radius: 999px
71+
72+
**Secondary**
73+
- Background: White
74+
- Text: Black
75+
- Hover: `#e2e2e2`
76+
- Radius: 999px
77+
78+
**Chip**
79+
- Background: `#efefef`
80+
- Radius: 999px
81+
82+
**Floating Action**
83+
- Background: White
84+
- Shadow: `rgba(0,0,0,0.16)`
85+
- Radius: 999px
86+
87+
---
88+
89+
### Cards
90+
- Radius: 8px (standard), 12px (featured)
91+
- Shadow: `rgba(0,0,0,0.12)`
92+
- No borders
93+
94+
---
95+
96+
### Inputs
97+
- Border: 1px solid black
98+
- Radius: 8px
99+
- Background: white
100+
101+
---
102+
103+
### Navigation
104+
- Sticky top bar
105+
- Minimal design
106+
- Pill-style navigation elements
107+
108+
---
109+
110+
## 5. Layout Principles
111+
112+
### Spacing
113+
- Base unit: 8px
114+
- Scale: 4px → 32px
115+
116+
### Container
117+
- Max width: ~1136px
118+
119+
### Philosophy
120+
- Efficiency over airiness
121+
- High information density
122+
123+
---
124+
125+
## 6. Depth & Elevation
126+
127+
| Level | Treatment |
128+
|------|----------|
129+
| 0 | No shadow |
130+
| 1 | `rgba(0,0,0,0.12)` |
131+
| 2 | `rgba(0,0,0,0.16)` |
132+
| 3 | Floating elements |
133+
| 4 | Inset (pressed state) |
134+
135+
---
136+
137+
## 7. Nuxt UI Usage Guidelines
138+
139+
### Core Principle
140+
141+
When building the interface, **prefer using Nuxt UI components whenever possible**.
142+
143+
### Rules
144+
145+
- Do NOT create custom components if:
146+
- an equivalent exists in Nuxt UI
147+
- it can be adapted via props, slots, or styling
148+
149+
### When Custom Markup is Allowed
150+
151+
Use plain HTML + CSS (or Tailwind) only if:
152+
- the required component does not exist in Nuxt UI
153+
- the behavior is too specific or complex
154+
- a unique layout cannot be achieved with existing components
155+
156+
### Anti-patterns
157+
158+
- ❌ Rebuilding buttons from scratch
159+
- ❌ Duplicating Nuxt UI logic
160+
- ❌ Mixing too many custom and library components inconsistently
161+
162+
### Goal
163+
164+
- UI consistency
165+
- faster development
166+
- easier maintenance
167+
168+
---
169+
170+
## 8. Do's and Don'ts
171+
172+
### Do
173+
- Use black and white as the primary palette
174+
- Use pill-shaped buttons and controls
175+
- Keep layouts compact
176+
- Use subtle shadows
177+
178+
### Don't
179+
- Do not use gradients
180+
- Do not introduce unnecessary colors
181+
- Do not create overly spacious layouts
182+
- Do not use heavy shadows
183+
184+
---
185+
186+
## 9. Responsive Behavior
187+
188+
### Breakpoints
189+
190+
| Name | Width |
191+
|------|------|
192+
| Mobile | 320–600px |
193+
| Tablet | 768–1119px |
194+
| Desktop | 1120px+ |
195+
196+
### Rules
197+
- Layouts stack vertically on smaller screens
198+
- Buttons must be at least 44px height
199+
- Grids collapse into single column
200+
201+
---
202+
203+
## 10. Agent Prompt Guide
204+
205+
### Principles
206+
- Be explicit about colors (`#121212`, `#ffffff`)
207+
- Always specify border-radius for buttons (999px)
208+
- Keep layouts compact and structured
209+
- Prefer consistency over creativity
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Phase 1 Foundation
2+
3+
## Description
4+
5+
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.
6+
7+
## Session Log
8+
9+
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.
10+
11+
2. Built `app/core/security.py` — JWT create/decode utilities using python-jose.
12+
13+
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.
14+
15+
4. Built `app/schemas/auth.py` — RegisterRequest, LoginRequest, UserResponse, AuthResponse. Required switching `pydantic` to `pydantic[email]` for EmailStr.
16+
17+
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`.
18+
19+
6. Built full `app/services/auth.py`: register, login, logout, refresh_tokens (rotation + theft detection), `_revoke_all_user_tokens`, `_issue_tokens` helper.
20+
21+
7. Built `app/dependencies/get_current_user.py` — reads access_token from Cookie, validates JWT type=="access", loads User from DB.
22+
23+
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.
24+
25+
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": ...}`).
26+
27+
10. Removed all tests from the project. `tests/` directory deleted, pytest/pytest-asyncio removed from `pyproject.toml`.
28+
29+
11. Created `apps/api/README.md` and `apps/web/README.md`.
30+
31+
## Session Outcomes
32+
33+
- All 7 tables created and migrated: users, wishlists, wish_items, reservations, refresh_tokens, saved_wishlists, saved_items
34+
- Auth endpoints fully functional and verified via curl
35+
- `ruff check app/` passes cleanly
36+
- Both READMEs written
37+
38+
## Lessons Learned
39+
40+
- **passlib + bcrypt >= 4.0 incompatible**: use `bcrypt` directly — `bcrypt.hashpw()` / `bcrypt.checkpw()`
41+
- **Alembic partial indexes**: autogenerate misses `WHERE` clause — always review and patch manually after generation
42+
- **slowapi circular import**: keep Limiter in its own `core/limiter.py`, not in `main.py`
43+
- **FastAPI HTTPException envelope**: default behavior wraps `detail` in `{"detail": ...}` — need a custom exception handler to enforce project envelope format
44+
- **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
45+
- **asyncpg + Windows**: incompatible with ProactorEventLoop — all backend CLI commands must run inside Docker (`docker exec wishpicks-api-1 ...`)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Google OAuth — Full Implementation (API + Web)
2+
3+
## Description
4+
5+
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.
6+
7+
## Session Log
8+
9+
### API
10+
11+
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`).
12+
13+
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`.
14+
15+
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.
16+
17+
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.
18+
19+
### Web
20+
21+
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.
22+
23+
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()`.
24+
25+
7. Added `auth.errors.oauth_failed` to `locales/en.json` ("Google sign-in failed. Please try again.") and `locales/uk.json` ("Вхід через Google не вдався. Спробуй ще раз.").
26+
27+
## Session Outcomes
28+
29+
- `GET /api/auth/google` and `GET /api/auth/google/callback` fully functional
30+
- Smoke tested with real Google credentials — redirect URL contains correct `client_id`
31+
- Error fallbacks verified: missing code and `error=access_denied` both redirect to `/login?error=oauth_failed`
32+
- `ruff check` passes on all modified API files
33+
- Login page now surfaces `oauth_failed` error message in both locales when Google auth is denied or fails
34+
35+
## Lessons Learned
36+
37+
- **`docker restart` vs `--force-recreate`**: `restart` keeps the existing environment snapshot; `env_file` changes only take effect after `docker compose up --force-recreate`.
38+
- **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.

.github/cliff.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[changelog]
2+
header = ""
3+
body = """
4+
{% for group, commits in commits | group_by(attribute="group") %}\
5+
### {{ group | upper_first }}
6+
7+
{% for commit in commits %}\
8+
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }} ([`{{ commit.id | truncate(length=7, end="") }}`](https://github.com/denvudd/wishpicks/commit/{{ commit.id }}))
9+
{% endfor %}
10+
{% endfor %}
11+
"""
12+
trim = true
13+
14+
[git]
15+
conventional_commits = true
16+
filter_unconventional = true
17+
commit_parsers = [
18+
{ message = "^feat", group = "Features" },
19+
{ message = "^fix", group = "Bug Fixes" },
20+
{ message = "^perf", group = "Performance" },
21+
{ message = "^refactor", group = "Refactor" },
22+
{ message = "^docs", group = "Documentation" },
23+
{ message = "^chore|^ci|^build|^style|^test", group = "Miscellaneous" },
24+
{ message = "^revert", group = "Reverted" },
25+
]
26+
filter_commits = false
27+
tag_pattern = "v[0-9]*"
28+
topo_order = false
29+
sort_commits = "oldest"

.github/workflows/ci.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
jobs:
8+
lint-api:
9+
name: Lint API
10+
runs-on: ubuntu-latest
11+
defaults:
12+
run:
13+
working-directory: apps/api
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Setup uv
19+
uses: astral-sh/setup-uv@v5
20+
with:
21+
enable-cache: true
22+
23+
- name: Install dependencies
24+
run: uv sync --dev
25+
26+
- name: Ruff lint
27+
run: uv run ruff check .
28+
29+
- name: Ruff format check
30+
run: uv run ruff format --check .
31+
32+
lint-web:
33+
name: Lint Web
34+
runs-on: ubuntu-latest
35+
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- name: Setup Node
40+
uses: actions/setup-node@v4
41+
with:
42+
node-version: 22
43+
cache: npm
44+
cache-dependency-path: package-lock.json
45+
46+
- name: Install dependencies
47+
run: npm ci
48+
env:
49+
HUSKY: 0
50+
51+
- name: ESLint
52+
working-directory: apps/web
53+
run: npm run lint
54+
55+
- name: Typecheck
56+
working-directory: apps/web
57+
run: npx nuxt typecheck

0 commit comments

Comments
 (0)