first commit

This commit is contained in:
luoyangwei
2026-06-06 00:49:07 +08:00
commit 2b860d201e
134 changed files with 22773 additions and 0 deletions
@@ -0,0 +1,96 @@
# Wallora Searcher Architecture Reference
## Surfaces And Roles
The project has three user-facing surfaces:
- Public API: available to external users. Use JWT login/authorization for protected API access. Keep contracts documented in `swagger.yaml`.
- Admin: available only after account/password login. Use NextAuth-backed sessions to persist admin login state and gate admin features.
- Public website: available to anyone on the public internet. Optimize pages for SEO, metadata, crawlability, and public performance.
Keep these surfaces separate in routing, middleware, authorization helpers, and mental model.
## Route Boundaries
Prefer clear route groups as the app grows:
- Public website: `app/(site)/...`
- Admin UI: `app/(admin)/admin/...`
- Protected admin UI: `app/(admin)/admin/(protected)/...`
- Public API: `app/api/...`
- Auth endpoints: `app/api/auth/...` for NextAuth
If the existing tree does not yet use route groups, introduce them only when it makes the current change clearer.
## Authentication And Authorization
- Public API authorization: validate JWTs explicitly for protected API routes. Define who issues the token before implementing token verification.
- Current app-user JWTs are issued by `/api/v1/auth/login` and `/api/v1/auth/register`, signed with `APP_JWT_SECRET` or `AUTH_SECRET`, and validated by server-only helpers before accessing `/api/v1/users/me/...`.
- Disabled app users should not be able to log in or use existing Bearer tokens because token validation reloads current `wg_app_users.status`.
- Admin authorization: use NextAuth account/password login and session checks. Do not rely on public API JWT logic as the admin session source.
- Only `/admin/login` is public under the admin surface. All other `/admin` pages need middleware/proxy protection and server-side `auth()` checks, preferably via a protected route-group layout.
- Server code must keep secrets server-only. Never expose service-role Supabase keys, Aliyun OSS secrets, NextAuth secrets, or JWT signing secrets in `NEXT_PUBLIC_*`.
- Keep authorization decisions close to the server boundary: route handlers, server actions, middleware, or server-only helpers.
## Data Storage
Use Supabase for application data.
- Prefix project-owned tables, indexes, triggers, and helper functions with `wg_` when practical.
- Enable and design RLS for tables exposed through Supabase APIs.
- Do not use user-editable metadata claims for authorization decisions.
- Model admin roles and API consumer permissions in trusted tables or trusted auth metadata.
- Verify Supabase behavior against current documentation before implementing schema, RLS, or auth-sensitive behavior.
- Use service role credentials only in server-only code, one-shot scripts, cron workers, or protected internal route handlers.
- Treat raw gallery rows as admin data. Public photo APIs should return picked rows only.
- App-facing gallery list/detail endpoints may accept an optional Bearer app-user JWT to add user-specific fields such as `isFavorited`; anonymous responses must still work.
- Keep picked state and OSS mirror metadata on `wg_gallery_photos`; use collections tables to group picked photos into public sets.
- Admin collection writes should validate that every collection item references a picked photo.
- Public collection APIs should filter on `is_published = true` and keep collection photo pagination compatible with infinite scroll.
- App user favorites should accept picked photos only, and preferences/download preferences should be stored as JSON in `wg_user_settings`.
- Keep phone and WeChat identity support as additive fields on `wg_app_users`; do not split them into separate user tables unless provider-specific complexity requires it later.
## Image Storage
Use Aliyun OSS for image files.
- Store files in OSS buckets; store metadata such as object key, URL, owner, MIME type, size, visibility, and related entity in Supabase.
- Prefer server-side signed upload or server-mediated upload for private/admin flows.
- Avoid placing OSS access keys in browser-exposed code.
- Existing OSS env support accepts `OSS_REGION`, `OSS_ACCESS_KEY_ID`, `OSS_ACCESS_KEY_SECRET`, `OSS_BUCKET`, and either `OSS_PUBLIC_URL` or `OSS_PUBLIC_BASE_URL`.
- Decide whether each image class is public, signed, or private before generating URLs.
## UI System
Use shadcn/ui as the base UI layer.
- Initialize and inspect shadcn project config before adding components.
- Use the project package runner: `pnpm dlx shadcn@latest ...`.
- Prefer existing shadcn components before writing custom controls.
- Use semantic tokens and component variants instead of raw color overrides.
- Compose forms, tables, dialogs, sheets, empty states, loading states, and feedback with shadcn primitives where available.
## SEO Website Pages
For public website pages:
- Use server-rendered pages by default.
- Keep page-level metadata accurate with Next.js metadata APIs.
- Use semantic HTML, crawlable content, canonical URLs where needed, and fast-loading assets.
- Avoid hiding important public content behind client-only rendering.
## API Documentation
Maintain `swagger.yaml` for public API contracts.
- Update it whenever API endpoints, auth requirements, request schemas, response schemas, status codes, or error formats change.
- Keep operation IDs stable and descriptive.
- Document JWT requirements per route instead of assuming all routes share the same auth behavior.
- Treat generated examples as contract examples, not placeholders.
## Scheduled Jobs
- Do not assume Vercel Cron. This project is not planned for Vercel.
- Prefer a one-shot script that can be called by crontab, PM2, Docker, or another scheduler.
- Keep scheduled routes protected with a secret when an HTTP trigger is useful.
- Never rely on `setInterval` inside a Next.js request/serverless process for durable jobs.
@@ -0,0 +1,109 @@
# Wallora Searcher Project Reference
## Snapshot
- Root: `/Users/luoyangwei/Documents/workspace/wallora.top/searcher`
- Package: `searcher`
- Framework: Next.js `16.2.6` with App Router
- React: `19.2.4`
- Styling: Tailwind CSS v4 via `@import "tailwindcss";`
- TypeScript: enabled
- Package manager: `pnpm`
- Git: this directory is not currently a Git repository
- Intended UI system: shadcn/ui
- Intended data store: Supabase with `wg_` table prefixes for project-owned tables
- Intended image store: Aliyun OSS
- Intended auth foundation: NextAuth plus JWT authorization for public APIs
- Intended API documentation: `swagger.yaml`
## Important Files
- `app/layout.tsx`: root HTML shell, metadata, body layout classes, CSS-variable font tokens
- `app/(site)/page.tsx`: public website home page
- `app/(admin)/admin/login/page.tsx`: public admin login page
- `app/(admin)/admin/(protected)/...`: admin pages that require a NextAuth session
- `app/(admin)/admin/(protected)/gallery/page.tsx`: admin gallery picker with picked/color filters
- `app/(admin)/admin/(protected)/collections/page.tsx`: admin collections CRUD surface
- `components/admin/collections/CollectionsManager.tsx`: create/edit/delete published collections from picked photos
- `app/api/admin/gallery-photos/route.ts`: protected admin gallery list and pick/unpick endpoint
- `app/api/admin/photo-collections/route.ts`: protected admin collection list/create API
- `app/api/admin/photo-collections/[id]/route.ts`: protected admin collection read/update/delete API
- `app/api/v1/gallery/photos/route.ts`: public picked-photo API for waterfall pagination
- `app/api/v1/gallery/photos/[id]/route.ts`: public picked-photo detail API plus favorite/unfavorite shortcut
- `app/api/v1/gallery/collections/route.ts`: public published collection list API
- `app/api/v1/gallery/collections/[slug]/route.ts`: public published collection detail API with paginated photos
- `app/api/v1/auth/register/route.ts`: app user email/password registration
- `app/api/v1/auth/login/route.ts`: app user email/password login and Bearer JWT issuance
- `app/api/v1/users/me/route.ts`: current app user profile from Bearer JWT
- `app/api/v1/users/me/favorites/...`: app user favorites list/add/remove APIs
- `app/api/v1/users/me/settings/route.ts`: app user preferences and download preferences APIs
- `app/(admin)/admin/(protected)/users/page.tsx`: protected admin app user list
- `components/admin/users/UsersManager.tsx`: admin app user list with enable/disable actions
- `app/api/admin/app-users/[id]/route.ts`: protected admin app user status update API
- `app/api/cron/gallery-photos/route.ts`: protected gallery sync trigger endpoint
- `app/globals.css`: Tailwind import, CSS variables for background/foreground, font theme tokens
- `package.json`: scripts and dependencies
- `next.config.ts`: Next.js config
- `public/`: starter SVG assets from create-next-app
- `swagger.yaml`: create or update when public API contracts are introduced or changed
- `supabase/migrations/`: SQL migration scripts, including `wg_` gallery tables
- `scripts/sync-gallery-photos.mjs`: one-shot gallery sync script for server cron/process managers
## Commands
```bash
pnpm dev
pnpm lint
pnpm build
pnpm start
pnpm sync:gallery
```
Use `pnpm format:check`, `pnpm lint`, and `pnpm build` for verification. Use `pnpm sync:gallery` only when intentionally touching the real Supabase database.
## Current Baseline Notes
- The public home page still contains create-next-app starter content unless updated later.
- The admin dashboard uses shadcn `dashboard-01` under `/admin/dashboard`.
- The admin login page uses shadcn `login-03` and NextAuth credentials under `/admin/login`.
- The admin gallery supports picked/unpicked filters, color-family filters, pick/unpick actions, and OSS mirror status.
- The admin collections page supports create, edit, delete, publish/draft status, cover selection, and selecting photos from picked photos.
- Public photo consumption should use `/api/v1/gallery/photos`, which returns picked photos only.
- App photo detail should use `/api/v1/gallery/photos/{id}`. Public callers can omit auth; authenticated callers receive `isFavorited` and can `POST`/`DELETE` the same route to favorite/unfavorite.
- Public collection consumption should use `/api/v1/gallery/collections` and `/api/v1/gallery/collections/{slug}`; collection photos are picked photos only.
- App users register and login with email/password via `/api/v1/auth/register` and `/api/v1/auth/login`; the returned Bearer JWT is for public app APIs only.
- App user favorites and settings live under `/api/v1/users/me/...`.
- Admins can review app users and enable/disable accounts under `/admin/users`.
- Bare `/dashboard` and `/login` routes should stay absent; admin templates belong under `/admin`.
## Coding Conventions
- Keep components typed with TypeScript and follow existing double-quote import style.
- Prefer server components unless client behavior is necessary.
- Put global tokens and truly global styles in `app/globals.css`; keep page-specific styling near components with Tailwind classes.
- Keep font tokens in CSS variables; avoid `next/font/google` unless the deployment environment can reliably fetch Google Fonts during build.
- Avoid broad dependency additions for small UI tasks; use the platform and existing stack first.
- Use `pnpm dlx shadcn@latest ...` for shadcn CLI tasks.
- Verify current Supabase, NextAuth, and Aliyun OSS APIs before implementing auth-sensitive or storage-sensitive behavior.
- For database objects created by this app, use the `wg_` prefix, e.g. `wg_gallery_photos`.
- Do not read or print secrets from `.env.local`; report only whether required variables are present.
- This project is not targeting Vercel. Prefer server cron, process manager jobs, or external scheduler calls over `vercel.json` cron config.
## Design Conventions
- Replace starter content with domain-specific product UI when asked to build the app experience.
- Keep cards at `8px` radius or less unless a future design system says otherwise.
- Do not nest cards inside cards.
- Avoid one-note palettes and generic purple/blue gradient-heavy layouts.
- Make text fit inside controls and panels at mobile and desktop widths.
## Architecture Summary
- Public API: available to external users; use JWT login/authorization and document routes in `swagger.yaml`.
- Admin: account/password login; use NextAuth sessions to persist login state and gate admin features.
- Public website: open internet access; optimize for SEO, metadata, crawlability, and performance.
- Supabase stores application data with `wg_` table prefixes; Aliyun OSS stores image files while Supabase stores image metadata.
- Gallery photo ingestion writes to `wg_gallery_photos` and tracks progress in `wg_gallery_sync_state`.
- Picked photo metadata lives on `wg_gallery_photos` via `is_picked`, `picked_at`, `picked_by`, and `oss_*` fields.
- Collections live in `wg_photo_collections` and `wg_photo_collection_items`.
- App users/favorites/settings live in `wg_app_users`, `wg_user_favorites`, and `wg_user_settings`; identity fields include email now and schema placeholders for phone/WeChat later.
@@ -0,0 +1,132 @@
# Supabase Gallery Sync Reference
## Naming
Use `wg_` for project-owned Supabase objects.
- Gallery table: `wg_gallery_photos`
- Sync state table: `wg_gallery_sync_state`
- Collections tables: `wg_photo_collections`, `wg_photo_collection_items`
- User tables: `wg_app_users`, `wg_user_favorites`, `wg_user_settings`
- Example trigger/function names: `set_wg_gallery_photos_updated_at`, `set_wg_updated_at`
Do not introduce unprefixed replacements such as `gallery_photos`.
## Files
- `supabase/migrations/20260530000000_create_wg_gallery_tables.sql`: table/RLS/index/trigger migration.
- `lib/supabase/service-role.ts`: server-only Supabase service-role client.
- `supabase/migrations/20260601000000_extend_gallery_pick_collections_users.sql`: picked-photo, OSS metadata, collections, and user/favorite/settings schema extension.
- `server/api/unsplash.ts`: official Unsplash API fetch and response validation.
- `server/admin/gallery-sync.ts`: reusable sync implementation for Next route handlers.
- `server/admin/gallery-pick.ts`: admin pick/unpick implementation and OSS mirror call.
- `server/storage/aliyun-oss.ts`: server-only Aliyun OSS PUT Object helper.
- `app/api/admin/gallery-photos/route.ts`: protected admin list + pick/unpick API.
- `app/api/admin/photo-collections/route.ts`: protected admin collection list/create API.
- `app/api/admin/photo-collections/[id]/route.ts`: protected admin collection read/update/delete API.
- `app/api/v1/gallery/photos/route.ts`: public picked photos API.
- `app/api/v1/gallery/collections/route.ts`: public published collections API.
- `app/api/v1/gallery/collections/[slug]/route.ts`: public published collection detail API.
- `app/api/cron/gallery-photos/route.ts`: protected HTTP trigger, guarded by `CRON_SECRET`.
- `scripts/sync-gallery-photos.mjs`: one-shot Node script for server cron/process managers.
## Environment
Required for sync:
- `NEXT_PUBLIC_SUPABASE_URL`
- `SUPABASE_SERVICE_ROLE_KEY`
- `UNSPLASH_ACCESS_KEY`
Required for OSS mirroring when an admin marks a photo as picked:
- `OSS_REGION`
- `OSS_ACCESS_KEY_ID`
- `OSS_ACCESS_KEY_SECRET`
- `OSS_BUCKET`
- `OSS_PUBLIC_URL` or `OSS_PUBLIC_BASE_URL`
Optional for OSS:
- `OSS_ENDPOINT` to override the region-derived endpoint
Required for HTTP trigger:
- `CRON_SECRET`
Never print secret values. It is fine to report whether each variable is set.
## Scheduler
This project is not intended for Vercel deployment. Do not add `vercel.json` cron config unless the deployment target changes.
Preferred server cron:
```bash
*/10 * * * * cd /Users/luoyangwei/Documents/workspace/wallora.top/searcher && pnpm sync:gallery
```
The script runs once and exits. That makes it suitable for crontab, PM2 cron, Docker scheduled jobs, or an external scheduler.
## Sync Behavior
Source endpoint:
```txt
https://api.unsplash.com/topics/wallpapers/photos?page=<page>&per_page=20
```
Use the official Unsplash API with `Authorization: Client-ID <UNSPLASH_ACCESS_KEY>`. Do not rely on `unsplash.com/napi/...`; it is a website-internal endpoint and can return `401`, anti-bot HTML, or other non-contract responses.
Workflow:
1. Read `wg_gallery_sync_state` for source `unsplash-wallpapers`.
2. Fetch the current page from the official Unsplash API with `per_page = 20`.
3. Require JSON and validate that the response is an array of photo-like objects.
4. Map every item into `wg_gallery_photos`.
5. Populate `color_family` from the Unsplash hex color so admin color filters work.
6. Upsert with `onConflict: "unsplash_id"` and `ignoreDuplicates: true`.
7. Update `wg_gallery_sync_state` with next page, seen count, inserted count, timestamp, and last error.
If Unsplash returns an error, anti-bot HTML, or any non-JSON response, record the error in `wg_gallery_sync_state.last_error` and do not write photo rows.
## Picked Photos
- Raw gallery rows are for admin curation.
- Public consumers should query picked rows only through `/api/v1/gallery/photos`.
- RLS should expose `wg_gallery_photos` to `anon`/`authenticated` only when `is_picked = true`.
- Admin pick/unpick operations run through server-only code using the Supabase service-role key.
- Picking a photo attempts to mirror the selected Unsplash image into Aliyun OSS and stores `oss_bucket`, `oss_object_key`, `oss_url`, `oss_synced_at`, and `oss_sync_error`.
- If OSS is missing or upload fails, the photo can still be marked picked, but `oss_sync_error` should be visible to admin users.
## Collections
- Collections are admin-managed under `/admin/collections`.
- Admin APIs are session-protected and run with the Supabase service-role client.
- Collection items must reference picked photos only.
- Published collections are exposed publicly; draft collections are not returned by public APIs.
- Public collection detail uses `page` and `pageSize` for waterfall-friendly pagination of collection photos.
- Public gallery photo detail lives at `/api/v1/gallery/photos/[id]`; it is picked-only and supports authenticated favorite/unfavorite shortcuts.
## App Users
- App users are separate from admin users and live in `wg_app_users`.
- Email/password auth uses server-side password hashing and Bearer JWT issuance under `/api/v1/auth/*`.
- Protected app-user routes live under `/api/v1/users/me/*` and must validate Bearer JWTs server-side.
- Admin app-user review and status control live under `/admin/users` and `/api/admin/app-users/[id]`.
- Favorites live in `wg_user_favorites` and should only reference picked photos.
- Preferences and download preferences live in `wg_user_settings` as JSON objects.
- `wg_app_users` has additive identity columns for future phone and WeChat login support.
## Verification
Use these checks after touching sync code:
```bash
node --check scripts/sync-gallery-photos.mjs
pnpm format:check
pnpm lint
pnpm build
```
Only run `pnpm sync:gallery` when the user explicitly wants to write to the configured Supabase database.