# MemeStudio Server Project - Thorough Exploration Report ## 1. PROJECT OVERVIEW **Project Name:** Meme Studio (Homophone Pun Game Operation Platform) **Framework:** Next.js 14 (App Router) **Type:** Backend/Admin Dashboard for a wordplay game **Location:** `/Users/richard/Documents/code/xieyingeng/MemeStudio` ### Tech Stack - **Framework**: Next.js 14 App Router - **Runtime**: Node.js (Standalone output) - **Language**: TypeScript - **ORM**: Prisma v6.5.0 - **Database**: MySQL - **Authentication**: Better Auth v1.2.7 (email/password with Prisma adapter) - **Frontend UI**: shadcn/ui + Tailwind CSS - **State Management**: TanStack Query v5.69.0 - **Drag & Drop**: @dnd-kit/sortable - **File Upload**: Tencent COS (Cloud Object Storage) - **Password Hashing**: bcryptjs v3.0.2 - **Process Manager**: PM2 (ecosystem.config.js) --- ## 2. PROJECT STRUCTURE ``` MemeStudio/ ├── app/ # Next.js App Router │ ├── (auth)/ # Auth routes (no sidebar) │ │ ├── layout.tsx │ │ └── login/page.tsx │ ├── (dashboard)/ # Protected routes (with sidebar) │ │ ├── layout.tsx │ │ ├── levels/page.tsx # Game levels management │ │ ├── users/page.tsx # Admin user management │ │ └── wx-users/page.tsx # WeChat mini program users │ ├── api/ # API routes │ │ ├── auth/[...all]/ # Better Auth endpoints │ │ ├── levels/ # Level CRUD │ │ ├── users/ # User CRUD │ │ ├── wx-users/ # WeChat user endpoints │ │ ├── cos/temp-key/ # Tencent COS credentials │ │ └── v1/wechat-game/ # (empty, future API) │ ├── layout.tsx # Root layout │ ├── page.tsx # Root page (redirects to /dashboard) │ ├── providers.tsx # Client providers (Query, etc) │ └── globals.css ├── lib/ # Core utilities │ ├── auth.ts # Better Auth config │ ├── auth-client.ts # Client-side auth hooks │ ├── prisma.ts # Prisma client singleton │ ├── api.ts # API fetch wrapper with basePath │ ├── cos.ts # Tencent COS utilities │ └── utils.ts # UI utilities (cn()) ├── components/ # React components │ ├── ui/ # shadcn/ui components (button, card, dialog, input, etc) │ ├── layout/ # Header, sidebar │ ├── levels/ # Level-related components │ ├── users/ # User dialog │ └── wx-users/ # WeChat user detail dialog ├── types/index.ts # TypeScript interfaces ├── middleware.ts # Next.js middleware (session check) ├── prisma/ │ ├── schema.prisma # Prisma schema │ └── seed.ts # Admin user seeding ├── public/ # Static assets ├── next.config.js # Next.js config (basePath, COS remotePatterns) ├── tsconfig.json ├── tailwind.config.ts ├── package.json ├── ecosystem.config.js # PM2 config └── deploy.sh # Deployment script ``` --- ## 3. AUTHENTICATION & MIDDLEWARE ### Middleware Pattern (`middleware.ts`) ```typescript // Non-blocking: API routes, static files, login page // Blocking: All dashboard pages require session cookie // Cookie detection: - 'better-auth.session_token' (HTTP) - '__Secure-better-auth.session_token' (HTTPS) // Redirect pattern: - Unauthenticated users → /login?callbackUrl=/original/path - Note: basePath (/studio) is NOT included in pathname within middleware ``` ### Auth Implementation (`lib/auth.ts`) ```typescript betterAuth({ database: prismaAdapter(prisma), emailAndPassword: { enabled: true }, basePath: '/api/auth', // CRITICAL: NO basePath prefix here secret: process.env.BETTER_AUTH_SECRET, session: { expiresIn: 7 days, updateAge: 1 day, }, }) ``` ### Key Auth Files - `lib/auth.ts` - Server-side Better Auth configuration - `lib/auth-client.ts` - Client-side hooks: `useSession`, `signIn`, `signOut` - `middleware.ts` - Edge Runtime middleware (cannot use Prisma, only cookie check) - `app/api/auth/[...all]/route.ts` - Better Auth endpoints --- ## 4. DATABASE SCHEMA (Prisma) ### Models #### 1. **User** (Admin/staff users) ```prisma model User { id String @id @default(uuid()) email String @unique emailVerified Boolean @default(false) name String? image String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt sessions Session[] accounts Account[] } ``` #### 2. **Session** (Better Auth) ```prisma model Session { id String @id expiresAt DateTime token String @unique # Critical for session validation ipAddress String? userAgent String? userId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) } ``` #### 3. **Account** (Provider credentials) ```prisma model Account { id String @id @default(uuid()) accountId String providerId String # 'credential' for email/password userId String password String? # Only for 'credential' provider accessToken String? refreshToken String? # ... expires, scope, tokens user User @relation(fields: [userId], references: [id], onDelete: Cascade) } ``` #### 4. **Verification** (Email verification tokens) ```prisma model Verification { id String @id @default(uuid()) identifier String value String expiresAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` #### 5. **Level** (Game levels) ```prisma model Level { id String @id @default(uuid()) imageUrl String # COS URL answer String # Correct answer (Chinese pun text) hint1 String? hint2 String? hint3 String? sortOrder Int @default(0) # Display order createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userProgress WxUserLevelProgress[] } ``` #### 6. **WxUser** (WeChat mini program players) ```prisma model WxUser { id String @id @default(uuid()) openid String @unique # WeChat OpenID sessionKey String? # WeChat session key nickname String? avatarUrl String? points Int @default(10) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt levelProgress WxUserLevelProgress[] } ``` #### 7. **WxUserLevelProgress** (Game progress tracking) ```prisma model WxUserLevelProgress { id String @id @default(uuid()) userId String levelId String completedAt DateTime @default(now()) # When level was completed user WxUser @relation(fields: [userId], references: [id], onDelete: Cascade) level Level @relation(fields: [levelId], references: [id]) } ``` ### Database Configuration - **Provider**: MySQL - **Binary Targets**: `["native", "rhel-openssl-3.0.x"]` (macOS dev → Linux server compatibility) - **Environment Variable**: `DATABASE_URL=mysql://...` --- ## 5. API ROUTES & ENDPOINTS ### 5.1 Authentication Endpoints **Route:** `/api/auth/[...all]` **Handler:** Better Auth (`toNextJsHandler(auth)`) **Methods:** GET, POST **Auth Required:** No (this is the auth endpoint itself) Common endpoints: - `POST /api/auth/sign-in/email` - Email sign in - `POST /api/auth/sign-out` - Sign out - `POST /api/auth/sign-up/email` - Create new account (might be disabled) --- ### 5.2 Levels API **Route:** `/api/levels` **Auth Required:** Yes (session check) #### GET - Fetch all levels ```typescript GET /api/levels Response: Level[] Sorting: By sortOrder (ASC) ``` #### POST - Create level ```typescript POST /api/levels Body: { imageUrl: string (required), answer: string (required), hint1?: string, hint2?: string, hint3?: string } Response: Level (with auto-generated UUID, sortOrder = max+1) ``` #### PUT - Update level ```typescript PUT /api/levels Body: { id: string (required), imageUrl?: string, answer?: string, hint1?: string, hint2?: string, hint3?: string } Response: Updated Level ``` #### DELETE - Delete level ```typescript DELETE /api/levels?id= Response: { success: true } ``` --- ### 5.3 Levels Reorder API **Route:** `/api/levels/reorder` **Auth Required:** Yes #### PUT - Batch update sort order ```typescript PUT /api/levels/reorder Body: { orders: Array<{ id: string, sortOrder: number }> } Response: { success: true } Implementation: Prisma $transaction (atomic operation) ``` **Use Case:** Drag & drop reordering in UI --- ### 5.4 Users API (Admin) **Route:** `/api/users` **Auth Required:** Yes **Purpose:** Manage admin/staff accounts #### GET - Fetch all users ```typescript GET /api/users Response: Array<{ id, email, emailVerified, name, image, createdAt, updatedAt }> Sorting: By createdAt DESC ``` #### POST - Create new admin user ```typescript POST /api/users Body: { email: string (required), password: string (required), name?: string } Response: User object - Auto-generates UUID for user ID - Hashes password with better-auth/crypto - Creates User + Account records in transaction - Error: "该邮箱已被注册" if email exists ``` #### PUT - Update user ```typescript PUT /api/users Body: { id: string (required), email?: string, password?: string, name?: string } Response: Updated User - Can update email (checks uniqueness vs other users) - Can update password (hashes with better-auth) - Transaction: Updates User + Account records ``` #### DELETE - Delete user ```typescript DELETE /api/users?id= Response: { success: true } - Prevents self-deletion: "不能删除自己的账户" - Cascading delete via Prisma relations ``` **Password Hashing:** Always uses `hashPassword` from `better-auth/crypto` for consistency --- ### 5.5 WeChat Users API **Route:** `/api/wx-users` **Auth Required:** Yes **Purpose:** Manage WeChat mini program player accounts #### GET - List WeChat users (paginated + search) ```typescript GET /api/wx-users?search=&page=<1>&limit=<20> Response: { users: Array, meta: { total, page, limit, totalPages } } Filtering: Search by nickname OR openid (contains search) Sorting: By createdAt DESC Dynamic Rendering: export const dynamic = 'force-dynamic' ``` --- ### 5.6 WeChat User Detail **Route:** `/api/wx-users/[id]` **Auth Required:** Yes **Method:** GET ```typescript GET /api/wx-users/ Response: WxUser with nested levelProgress array Include: - All WxUser fields - levelProgress: Array of { id, userId, levelId, completedAt, level: { id, answer } } Sorting: levelProgress by completedAt DESC ``` --- ### 5.7 WeChat User Level Progress **Route:** `/api/wx-users/level-progress` **Auth Required:** Yes **Method:** DELETE ```typescript DELETE /api/wx-users/level-progress Body: { ids: Array # Array of progress record IDs } Response: { deleted: number } Validation: ids must be non-empty array of strings ``` **Use Case:** Batch delete player progress (e.g., reset levels) --- ### 5.8 Tencent COS Credentials **Route:** `/api/cos/temp-key` **Auth Required:** Yes **Method:** GET ```typescript GET /api/cos/temp-key Response: { credentials: { tmpSecretId: string, tmpSecretKey: string, sessionToken: string }, startTime: number, expiredTime: number, bucket: string, region: string } ``` **Implementation Details:** - Uses `qcloud-cos-sts` library - Policy allows: `cos:PutObject`, `cos:PostObject` - Limited to: `mini_game/images/*` directory - Duration: 30 minutes (1800 seconds) - Returns bucket config for frontend upload --- ### 5.9 Empty/Stub Routes - `/api/v1/wechat-game/` - Directory structure exists but no handlers --- ## 6. LIB UTILITIES ### 6.1 `lib/auth.ts` - Better Auth Configuration ```typescript export const auth = betterAuth({ database: prismaAdapter(prisma), emailAndPassword: { enabled: true }, basePath: '/api/auth', // CRITICAL GOTCHA trustedOrigins: [process.env.BETTER_AUTH_URL], secret: process.env.BETTER_AUTH_SECRET, session: { expiresIn: 7 days, updateAge: 1 day } }) export type Auth = typeof auth ``` **Critical Gotchas:** 1. `basePath` must be `/api/auth` (NOT `/studio/api/auth`) 2. `BETTER_AUTH_URL` must be origin-only (NOT include path) 3. Better Auth's `withPath()` silently ignores basePath if URL has path component --- ### 6.2 `lib/auth-client.ts` - Client Auth Hooks ```typescript const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '/studio' const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001' export const authClient = createAuthClient({ baseURL: `${appUrl}${basePath}/api/auth` }) export const { signIn, signOut, useSession } = authClient ``` **Usage:** - `useSession()` - Hook to get current session - `signIn(email, password)` - Email/password login - `signOut()` - Logout --- ### 6.3 `lib/prisma.ts` - Prisma Singleton ```typescript const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } export const prisma = globalForPrisma.prisma ?? new PrismaClient() if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma ``` **Purpose:** Prevent multiple Prisma client instances in dev mode --- ### 6.4 `lib/api.ts` - API Fetch Helper ```typescript const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' export function apiFetch(input: string, init?: RequestInit): Promise { const url = input.startsWith('/') ? `${basePath}${input}` : input return fetch(url, init) } ``` **Purpose:** Wrapper that auto-prepends basePath to relative URLs (client-side) --- ### 6.5 `lib/cos.ts` - Tencent COS Utilities ```typescript interface TempKeyResult { credentials: { tmpSecretId: string tmpSecretKey: string sessionToken: string } startTime: number expiredTime: number } export function getBucketName(): string // Returns "bucket-appid" format export async function getTempKey(): Promise // STS.getCredential with 30-min duration // Policy: PUT/POST to mini_game/images/* export function getBucketConfig() // Returns { bucket, region } ``` **Environment Variables:** - `COS_SECRET_ID` - `COS_SECRET_KEY` - `COS_BUCKET` - `COS_REGION` (default: ap-guangzhou) - `COS_APPID` --- ### 6.6 `lib/utils.ts` - UI Utilities ```typescript export function cn(...inputs: ClassValue[]) // Combines clsx + tailwind-merge for class deduplication ``` --- ## 7. ENVIRONMENT VARIABLES **Required Variables:** ``` DATABASE_URL=mysql://user:pass@host:port/dbname BETTER_AUTH_SECRET=<32+ chars, openssl rand -base64 32> BETTER_AUTH_URL=http://localhost:3001 # Origin only, NO path NEXT_PUBLIC_APP_URL=http://localhost:3001 NEXT_PUBLIC_BASE_PATH=/studio ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=password123 COS_SECRET_ID= COS_SECRET_KEY= COS_BUCKET=bucket-name COS_REGION=ap-guangzhou COS_APPID= ``` **Production Considerations:** - `.env.production` exists alongside `.env` - `BETTER_AUTH_SECRET` should be 32+ characters - HTTPS deployment adds `__Secure-` cookie prefix - Database should use SSL connection --- ## 8. DEPLOYMENT ARCHITECTURE ### Standalone Output ```javascript // next.config.js module.exports = { output: 'standalone', # Self-contained binary basePath: '/studio', # Reverse proxy path images: { remotePatterns: [{ protocol: 'https', hostname: '*.myqcloud.com' # Tencent COS pattern }] } } ``` ### Deployment Flow (deploy.sh) 1. Build locally: `next build` (creates .next/standalone) 2. rsync files to server (excluding node_modules) 3. SSH to server: `npm install --production` 4. Generate Prisma: `npx prisma generate` 5. PM2 restart: `pm2 restart ecosystem.config.js` ### Server Details - **Host:** root@119.91.211.52 - **Path:** /root/apps/meme-studio - **Process Manager:** PM2 - **Config:** ecosystem.config.js ### PM2 Configuration ```javascript // ecosystem.config.js (typical setup) module.exports = { apps: [{ name: 'meme-studio', script: './.next/standalone/server.js', instances: 1, exec_mode: 'cluster' }] } ``` --- ## 9. CRITICAL GOTCHAS & IMPORTANT NOTES ### 9.1 basePath Handling - ✅ `next.config.js` sets `basePath: '/studio'` - ✅ All pages/API routes served under `/studio/...` - ❌ **NEVER** include `/studio` in `BETTER_AUTH_URL` or hardcoded paths - ⚠️ `request.nextUrl.pathname` in middleware EXCLUDES basePath (shows `/levels` not `/studio/levels`) - ⚠️ Next.js strips basePath from `request.url` in route handlers ### 9.2 Authentication Patterns - ✅ Session validation: `await auth.api.getSession({ headers: request.headers })` - ✅ Session check in middleware uses cookies (Edge Runtime compatible) - ⚠️ Password hashing: ALWAYS use `hashPassword` from `better-auth/crypto` - ⚠️ Session token field has unique constraint in database ### 9.3 Database & ORM - ✅ Prisma adapter with MySQL provider - ✅ Binary targets: `["native", "rhel-openssl-3.0.x"]` for cross-platform deployment - ⚠️ Middleware cannot use Prisma (Edge Runtime incompatible) - ⚠️ Transaction usage for consistency: `prisma.$transaction([...])` ### 9.4 File Upload (COS) - ✅ Frontend gets temporary credentials via `/api/cos/temp-key` - ✅ Browser uploads directly to COS (S3-compatible) - ✅ Limited to `mini_game/images/*` directory by policy - ⚠️ Policy duration: 30 minutes (1800 seconds) ### 9.5 Git & Development - ⚠️ **Commit messages must be written in Chinese** - ✅ ESLint configured: `npm run lint` - ✅ Dev server runs on port 3001: `npm run dev` - ✅ Database seeding: `npm run db:seed` (creates/updates admin user) --- ## 10. SHARE/INVITE LOGIC **Current Status:** ❌ **NO existing share or invite functionality** No database models, API endpoints, or UI components for: - Sharing levels with other users - Generating shareable links/tokens - Inviting users to teams or projects - Publishing/unpublishing levels - Level access control **All levels are:** - Managed by admin users - Accessible to all authenticated admins - Automatically available in the WeChat mini program --- ## 11. KEY PATTERNS USED ### 11.1 API Route Pattern All protected API routes follow: ```typescript import { auth } from '@/lib/auth' export async function GET(request: NextRequest) { const session = await auth.api.getSession({ headers: request.headers }) if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) // ... handler logic } ``` ### 11.2 Error Handling - 400: Bad request (validation errors) - 401: Unauthorized (no session) - 404: Not found (resource doesn't exist) - 500: Server error (logged to console) ### 11.3 Response Format - Success: `NextResponse.json(data)` - Error: `NextResponse.json({ error: string }, { status: number })` - Batch operations: `{ success: true }` or `{ deleted: count }` ### 11.4 Transaction Pattern ```typescript await prisma.$transaction(async (tx) => { const user = await tx.user.create({ data: {...} }) await tx.account.create({ data: {...} }) return user }) ``` ### 11.5 Pagination Pattern ```typescript const skip = (page - 1) * limit const [data, total] = await Promise.all([ prisma.model.findMany({ skip, take: limit }), prisma.model.count() ]) return { data, meta: { total, page, limit, totalPages: Math.ceil(total/limit) } } ``` --- ## 12. FILE STRUCTURE SUMMARY | File | Purpose | |------|---------| | `middleware.ts` | Session validation, redirect to login | | `next.config.js` | basePath, COS image patterns, standalone output | | `lib/auth.ts` | Better Auth server configuration | | `lib/auth-client.ts` | Client-side auth hooks | | `lib/prisma.ts` | Prisma singleton | | `lib/api.ts` | Fetch wrapper with basePath | | `lib/cos.ts` | Tencent COS STS credentials | | `prisma/schema.prisma` | Database schema (7 models) | | `prisma/seed.ts` | Admin user seeding | | `app/api/auth/[...all]/route.ts` | Better Auth endpoints | | `app/api/levels/route.ts` | Level CRUD (GET, POST, PUT, DELETE) | | `app/api/levels/reorder/route.ts` | Batch reorder (PUT) | | `app/api/users/route.ts` | Admin user CRUD (GET, POST, PUT, DELETE) | | `app/api/wx-users/route.ts` | List WeChat users (GET, paginated) | | `app/api/wx-users/[id]/route.ts` | Get user + progress (GET) | | `app/api/wx-users/level-progress/route.ts` | Delete progress (DELETE) | | `app/api/cos/temp-key/route.ts` | Get COS credentials (GET) | | `app/(dashboard)/levels/page.tsx` | Levels management UI | | `app/(dashboard)/users/page.tsx` | Admin users management UI | | `app/(dashboard)/wx-users/page.tsx` | WeChat users viewer UI | | `app/(auth)/login/page.tsx` | Login page | | `types/index.ts` | TypeScript interfaces (Level, User, WxUser, etc) | --- ## 13. COMPONENT STRUCTURE **UI Components (shadcn/ui):** - `button.tsx` - Button component - `card.tsx` - Card container - `dialog.tsx` - Modal dialog - `input.tsx` - Text input - `label.tsx` - Form label - `textarea.tsx` - Textarea input - `spinner.tsx` - Loading spinner **Layout Components:** - `header.tsx` - Top navigation header - `sidebar.tsx` - Left sidebar with navigation **Feature Components:** - `levels/level-dialog.tsx` - Create/edit level modal - `levels/level-list.tsx` - Drag-and-drop sortable list - `levels/level-card.tsx` - Individual level card - `levels/image-uploader.tsx` - COS image upload - `users/user-dialog.tsx` - Create/edit admin user modal - `wx-users/wx-user-detail-dialog.tsx` - View WeChat user progress --- ## 14. DATA FLOW EXAMPLES ### Example 1: Create Level 1. Admin clicks "Add Level" button 2. UI opens `level-dialog.tsx` modal 3. Admin fills form (imageUrl, answer, hints) 4. `image-uploader.tsx` gets temp COS credentials from `/api/cos/temp-key` 5. Browser uploads image directly to COS 6. Admin submits form 7. API calls `POST /api/levels` with imageUrl, answer, hints 8. Backend validates session, creates Level record 9. sortOrder auto-calculated (max+1) 10. Level appears in list (sorted by sortOrder) ### Example 2: Reorder Levels 1. Admin drags level up/down in `level-list.tsx` 2. @dnd-kit/sortable updates UI state 3. Admin confirms or auto-save triggers 4. API calls `PUT /api/levels/reorder` with reordered IDs 5. Backend updates sortOrder for all levels in transaction 6. List re-renders in new order ### Example 3: View WeChat Player 1. Admin opens "WeChat Users" dashboard page 2. `wx-users/page.tsx` calls `GET /api/wx-users?page=1&limit=20` 3. Backend queries paginated WxUser list 4. Admin clicks on player name 5. Opens `wx-user-detail-dialog.tsx` 6. Component calls `GET /api/wx-users/` 7. Backend returns WxUser + nested levelProgress array 8. Dialog displays player info and completed levels --- ## 15. SECURITY CONSIDERATIONS ### Implemented - ✅ Session-based authentication (Better Auth) - ✅ Password hashing (bcryptjs via Better Auth) - ✅ CSRF protection (Better Auth built-in) - ✅ Middleware session validation - ✅ COS temporary credentials (limited scope, 30-min expiry) - ✅ SQL injection protection (Prisma ORM) - ✅ Email uniqueness constraints ### Not Implemented - ❌ Role-based access control (all authenticated users have admin access) - ❌ Audit logging - ❌ Rate limiting - ❌ Input validation/sanitization (rely on Prisma types) - ❌ API key authentication (only cookie-based) - ❌ CORS configuration (not needed for same-origin) --- ## SUMMARY MemeStudio is a **well-structured Next.js admin dashboard** for managing a WeChat mini program game. It has: - **Clean Auth**: Better Auth with Prisma adapter, session-based - **Full CRUD**: Levels, admin users, WeChat player tracking - **Cloud Integration**: Tencent COS for image uploads - **Scalable ORM**: Prisma with MySQL - **No Sharing Logic**: All data is globally accessible to admins **For Adding Share/Invite Feature:** - Add `SharedLevel` or `LevelAccess` model (permissions) - Add `InviteToken` model (temporary join links) - Add `/api/invites` endpoints (generate, accept, list) - Add role/permission fields to User model - Add UI for managing shares/invites