Files
MemeStudio/EXPLORATION_REPORT.md
2026-05-01 08:44:56 +08:00

24 KiB

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)

// 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)

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)

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)

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)

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)

model Verification {
  id         String   @id @default(uuid())
  identifier String
  value      String
  expiresAt  DateTime
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
}

5. Level (Game levels)

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)

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)

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

GET /api/levels
Response: Level[]
Sorting: By sortOrder (ASC)

POST - Create level

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

PUT /api/levels
Body: {
  id: string (required),
  imageUrl?: string,
  answer?: string,
  hint1?: string,
  hint2?: string,
  hint3?: string
}
Response: Updated Level

DELETE - Delete level

DELETE /api/levels?id=<levelId>
Response: { success: true }

5.3 Levels Reorder API

Route: /api/levels/reorder
Auth Required: Yes

PUT - Batch update sort order

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

GET /api/users
Response: Array<{
  id, email, emailVerified, name, image, createdAt, updatedAt
}>
Sorting: By createdAt DESC

POST - Create new admin user

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

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

DELETE /api/users?id=<userId>
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 /api/wx-users?search=<text>&page=<1>&limit=<20>
Response: {
  users: Array<WxUser>,
  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

GET /api/wx-users/<userId>
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

DELETE /api/wx-users/level-progress
Body: {
  ids: Array<string>  # 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

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

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

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

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

const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''

export function apiFetch(input: string, init?: RequestInit): Promise<Response> {
  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

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<TempKeyResult>
  // 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

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=<tencent cloud secret>
COS_SECRET_KEY=<tencent cloud secret>
COS_BUCKET=bucket-name
COS_REGION=ap-guangzhou
COS_APPID=<tencent app id>

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

// 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

// 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:

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

await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({ data: {...} })
  await tx.account.create({ data: {...} })
  return user
})

11.5 Pagination Pattern

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/<id>
  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