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 configurationlib/auth-client.ts- Client-side hooks:useSession,signIn,signOutmiddleware.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 inPOST /api/auth/sign-out- Sign outPOST /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 - List WeChat users (paginated + search)
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-stslibrary - 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:
basePathmust be/api/auth(NOT/studio/api/auth)BETTER_AUTH_URLmust be origin-only (NOT include path)- 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 sessionsignIn(email, password)- Email/password loginsignOut()- 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_IDCOS_SECRET_KEYCOS_BUCKETCOS_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.productionexists alongside.envBETTER_AUTH_SECRETshould 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)
- Build locally:
next build(creates .next/standalone) - rsync files to server (excluding node_modules)
- SSH to server:
npm install --production - Generate Prisma:
npx prisma generate - 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.jssetsbasePath: '/studio' - ✅ All pages/API routes served under
/studio/... - ❌ NEVER include
/studioinBETTER_AUTH_URLor hardcoded paths - ⚠️
request.nextUrl.pathnamein middleware EXCLUDES basePath (shows/levelsnot/studio/levels) - ⚠️ Next.js strips basePath from
request.urlin 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
hashPasswordfrombetter-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 componentcard.tsx- Card containerdialog.tsx- Modal dialoginput.tsx- Text inputlabel.tsx- Form labeltextarea.tsx- Textarea inputspinner.tsx- Loading spinner
Layout Components:
header.tsx- Top navigation headersidebar.tsx- Left sidebar with navigation
Feature Components:
levels/level-dialog.tsx- Create/edit level modallevels/level-list.tsx- Drag-and-drop sortable listlevels/level-card.tsx- Individual level cardlevels/image-uploader.tsx- COS image uploadusers/user-dialog.tsx- Create/edit admin user modalwx-users/wx-user-detail-dialog.tsx- View WeChat user progress
14. DATA FLOW EXAMPLES
Example 1: Create Level
- Admin clicks "Add Level" button
- UI opens
level-dialog.tsxmodal - Admin fills form (imageUrl, answer, hints)
image-uploader.tsxgets temp COS credentials from/api/cos/temp-key- Browser uploads image directly to COS
- Admin submits form
- API calls
POST /api/levelswith imageUrl, answer, hints - Backend validates session, creates Level record
- sortOrder auto-calculated (max+1)
- Level appears in list (sorted by sortOrder)
Example 2: Reorder Levels
- Admin drags level up/down in
level-list.tsx - @dnd-kit/sortable updates UI state
- Admin confirms or auto-save triggers
- API calls
PUT /api/levels/reorderwith reordered IDs - Backend updates sortOrder for all levels in transaction
- List re-renders in new order
Example 3: View WeChat Player
- Admin opens "WeChat Users" dashboard page
wx-users/page.tsxcallsGET /api/wx-users?page=1&limit=20- Backend queries paginated WxUser list
- Admin clicks on player name
- Opens
wx-user-detail-dialog.tsx - Component calls
GET /api/wx-users/<id> - Backend returns WxUser + nested levelProgress array
- 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
SharedLevelorLevelAccessmodel (permissions) - Add
InviteTokenmodel (temporary join links) - Add
/api/invitesendpoints (generate, accept, list) - Add role/permission fields to User model
- Add UI for managing shares/invites