diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dea8a32 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_API_URL=http://localhost:8000/api/v1 +VITE_AMAP_KEY= +VITE_SITE_URL=http://localhost:5173 diff --git a/AGENTS.md b/AGENTS.md index 3fb37a3..ffe1bad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,33 +9,33 @@ ## Architecture -- **Vue 3 + Vite + Tailwind CSS + Vue Router + Pinia + Supabase + AMap (高德地图) + Naive UI** +- **Vue 3 + Vite + Tailwind CSS + Vue Router + Pinia + FastAPI + AMap (高德地图) + Naive UI** - Path alias `@/` → `src/` (configured in both `vite.config.ts` and `tsconfig.app.json`) -- Supabase client singleton at `src/lib/supabase.ts` — throws at import if env vars missing +- FastAPI client at `src/lib/api.ts`; API base URL comes from `VITE_API_URL` - AMap loaded lazily via `src/lib/amap.ts` with type declarations in `src/types/amap.d.ts` - UI language is Chinese (zh-CN) -- All DB operations are direct `supabase.from(...)` calls from the browser — security depends on Supabase RLS policies +- All persistence, storage, authorization, and admin operations go through the FastAPI backend ## Directory Map | Path | Purpose | | --- | --- | -| `src/lib/` | Singletons and utilities: supabase, amap, canvas patch, cloudTypes constants, cloudBadges canvas renderer, SEO meta builder | +| `src/lib/` | Singletons and utilities: FastAPI client, amap, canvas patch, cloudTypes constants, cloudBadges canvas renderer, SEO meta builder | | `src/stores/` | Pinia stores: auth, clouds (cloud_types cache), encyclopedia (collection + unlock tracking), profile (user pages + cloud CRUD) | -| `src/composables/` | Vue composables: `useUpload` (batch upload with thumbnail generation, EXIF extraction, badge unlocking) | +| `src/composables/` | Vue composables: `useUpload` (batch multipart upload, EXIF extraction, badge result handling) | | `src/components/cloud/` | Cloud-related modals and widgets: ImageDetailModal, CloudEditModal, MapPickerModal, QuickUploadModal, MiniLocationMap | | `src/components/layout/` | AppHeader (top nav bar with auth state) | | `src/components/profile/` | ContributionHeatmap | | `src/style.css` | Global visual system hooks: shared button/card utility classes, base typography, body background | | `src/views/` | Route-level page components (see Routes below) | -| `src/types/` | TypeScript types: database models (`database.ts`), AMap declarations (`amap.d.ts`), router meta (`router.d.ts`) | +| `src/types/` | TypeScript types: domain models (`database.ts`), API DTOs (`api.ts`), AMap declarations (`amap.d.ts`), router meta (`router.d.ts`) | ## Routes | Path | View | Auth Required | | --- | --- | :---: | | `/` | MapView | No | -| `/login`, `/register`, `/auth/confirm`, `/auth/reset-password` | Auth views | No | +| `/login`, `/register`, `/forgot-password`, `/auth/confirm`, `/auth/reset-password` | Auth views | No | | `/upload` | UploadView | Yes | | `/encyclopedia` | EncyclopediaView | No | | `/encyclopedia/:id` | CloudTypeView | No | @@ -46,39 +46,52 @@ | `/profile/:id` | ProfileView (public) | No | | `/admin` | AdminView | Yes (admin) | | `/403` | ForbiddenView | No | +| `/401` | AuthRequiredView | No | | `/:pathMatch(.*)*` | NotFoundView | No | -- Route guards in `router/index.ts`: `requiresAuth` redirects to `/login`, `requiresAdmin` redirects to `/403` +- Route guards in `router/index.ts`: `requiresAuth` redirects to `/401` with the intended URL in the `redirect` query; `requiresAdmin` redirects to `/403` - SEO meta tags applied per-route via `lib/seo.ts` in `router.afterEach` ## Auth Flow -- **`main.ts` initializes auth before mounting**: `authStore.initialize()` is called, which calls `getSession()` and subscribes to `onAuthStateChange`. The app only mounts after the initial session check completes. -- **Login sets `user.value` explicitly**: `login()` extracts `data.user` from `signInWithPassword` and assigns it to the store directly, rather than relying on `onAuthStateChange`. -- **Profile is auto-created by DB trigger** (`handle_new_user` on `auth.users`), not by the frontend. -- Auth error messages are translated to Chinese in the store. -- `register()` checks username uniqueness before calling `signUp()` — username is passed via `options.data.username`. +- **`main.ts` initializes auth before mounting**: `authStore.initialize()` exchanges the HttpOnly refresh cookie for an access token. The app only mounts after this initial check. +- Access tokens live in memory only. Refresh cookies are managed by the backend and sent with `credentials: 'include'`. +- `src/lib/api.ts` performs one automatic refresh-and-retry after an authenticated request receives `401`. +- API auth state changes are synchronized back to the Pinia store through `opencloud:auth-updated` and `opencloud:auth-expired` window events. +- Email confirmation and password reset pages consume the opaque `token` query parameter generated by the backend. +- Passwords must contain at least 8 characters in registration, settings, and reset flows. +- Registration, email confirmation, login, password reset, profile creation, and uniqueness checks are backend responsibilities. ## Stores - **auth** — User session, profile, login/register/logout, username/password update, password reset. `initialize()` must be called before app mounts. -- **clouds** — Simple cache of `cloud_types` table. Fetched once, shared across views. +- **clouds** — Simple cache of cloud types returned by `GET /cloud-types`. Fetched once and shared across views. - **encyclopedia** — Cloud types + user's collection (unlock state). Tracks `unlockPercent` for progress display. Depends on `authStore`. -- **profile** — User profile pages. Fetches profile + cloud list per-user. Supports update/delete/visibility-toggle with optimistic cache patching. `deleteClouds` also removes from Supabase Storage. +- **profile** — User profile pages. Fetches profile + cloud list per-user. Supports update/delete/visibility-toggle with optimistic cache patching; backend deletion also removes stored files. -## Supabase +## FastAPI Backend -- Env var is `VITE_SUPABASE_PUBLISHABLE_KEY` (not `VITE_SUPABASE_ANON_KEY`), using Supabase's `sb_publishable_` key format. -- All tables have RLS enabled. Check `plan.md` section 10 for the full schema and RLS policies. -- Storage bucket `clouds` is public read, authenticated upload. -- Profile `role` field (`user`/`admin`) controls admin access — checked in route guard, not in JWT metadata. -- `user_collections` tracks encyclopedia unlocks with `first_cloud_id` FK to `clouds`. +- Backend project lives in the sibling directory `../opencloud-backend`. +- Default development API URL is `http://localhost:8000/api/v1`; set `VITE_API_URL` for deployed environments. +- Frontend route guards are for navigation UX only. The backend enforces ownership, authentication, admin roles, and disabled-account rules. +- Images are uploaded as multipart form data. The backend stores originals, creates thumbnails, blurs coordinates, writes database records, and atomically unlocks badges. + +## API Conventions + +- All requests must go through `src/lib/api.ts`; do not call `fetch` directly from views, stores, or composables. +- API paths passed to `apiRequest()` are relative to `VITE_API_URL`, for example `/clouds` rather than `/api/v1/clouds`. +- Public requests explicitly use `{ auth: false }`. Authenticated requests use the default and receive a Bearer access token. +- JSON request bodies are plain objects. Image uploads use `FormData`; do not set the multipart `Content-Type` header manually. +- FastAPI errors use `detail`; `ApiError` converts string and validation-array details into a user-facing message. +- Backend DTO fields use `snake_case`. Page/store adapters may expose display-only camelCase fields such as `cloudTypeName`. +- Paginated endpoints return `items`, `page`, `page_size`, `total`, and `total_pages`. +- Cloud batch mutation endpoints accept at most 100 IDs. Profile and admin code split larger selections into chunks of 100. ## Gallery Pagination - Page-based navigation (50 items/page), not infinite scroll. -- `resolveSearchFilters()` pre-fetches user IDs or cloud type IDs before the main query. -- `buildFilteredQuery()` returns a chainable query builder; `loadPage()` forks it into a `count` query and a `data` query that run in parallel via `Promise.all`. +- The backend resolves cloud-type/custom-type searches and `@username` searches. +- Each response includes `items`, `total`, `page`, `page_size`, and `total_pages`. - Search debounced at 250ms. ## Map Timeline @@ -89,7 +102,8 @@ ## Upload Flow -- `useUpload` composable handles: file selection (drag/drop/click), client-side thumbnail generation (JPEG, max 640px edge, 0.72 quality), EXIF date extraction, coordinate blurring (2 decimal places), Supabase Storage upload (original + thumbnail), DB insert with `status: 'pending'`, badge unlock detection via `user_collections` upsert. +- `useUpload` handles file selection, previews, EXIF date extraction, validation, sequential multipart uploads, progress, and badge results. Thumbnail generation, coordinate blurring, storage, database insertion, and badge unlocking run on the backend. +- Batch items upload sequentially. If a later item fails, earlier successful uploads remain saved; there is no transactional rollback or resumable upload in the frontend. - `UploadView` is the full-page batch uploader. `QuickUploadModal` is a single-image shortcut from the map page. ## Build & Deploy @@ -100,22 +114,16 @@ ## Environment Variables -Required (app won't start without): - -- `VITE_SUPABASE_URL` -- `VITE_SUPABASE_PUBLISHABLE_KEY` - Required for map features: - `VITE_AMAP_KEY` Optional: -- `VITE_SITE_URL` — canonical URL for SEO meta and sitemap (falls back to Vercel env or hardcoded default) +- `VITE_API_URL` — FastAPI base URL including `/api/v1` (defaults to `http://localhost:8000/api/v1`) +- `VITE_SITE_URL` — canonical frontend URL used by route SEO and generated sitemap; set it explicitly in deployed environments -Future (not in `.env`): - -- `OPENAI_API_KEY`, `OPENWEATHERMAP_API_KEY` +Backend-only variables such as database credentials, JWT secrets, SMTP settings, cookie policy, CORS origins, and upload paths belong in `../opencloud-backend/.env`, never in the Vite environment. ## Naive UI @@ -144,9 +152,9 @@ Future (not in `.env`): - Header action buttons use slight hover lift (`translate(-1px, -1px)`) with hard-offset shadow growth; new header-like actions should follow that interaction pattern. - Heatmap view toggles in `ContributionHeatmap` intentionally use separate buttons with gap spacing instead of `NButtonGroup`, because grouped buttons visually collide once hard shadows and hover transforms are applied. -## MVP Constraints (from plan.md) +## MVP Constraints -- No Supabase Realtime — refresh-based loading +- No realtime updates — refresh-based loading - No OAuth — email/password only, email confirmation required - No AI cloud identification — manual type selection - AMap only (China-focused), no Mapbox fallback yet diff --git a/README.md b/README.md index 33895ab..216bd10 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ -# Vue 3 + TypeScript + Vite +# OpenCloud 前端 -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `