From 441cc8dd48817390f09dc3ca7c206fc1dfdff022 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sun, 15 Mar 2026 23:00:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=9F=E4=BA=A7?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E9=85=8D=E7=BD=AE=EF=BC=8C=E9=87=8D=E6=9E=84?= =?UTF-8?q?=20API=20=E8=AF=B7=E6=B1=82=EF=BC=8C=E6=9B=B4=E6=96=B0=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC=E5=92=8C=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.production | 24 ++++++++++++++ app/(dashboard)/levels/page.tsx | 11 ++++--- app/(dashboard)/users/page.tsx | 9 +++--- components/levels/image-uploader.tsx | 3 +- deploy.sh | 47 ++++++++++++++++++++++++++++ ecosystem.config.js | 18 +++++++++++ lib/api.ts | 6 ++++ lib/auth-client.ts | 5 ++- lib/auth.ts | 3 +- middleware.ts | 13 +++++--- next.config.js | 2 ++ package.json | 7 +++-- prisma/schema.prisma | 3 +- public/.gitkeep | 0 14 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 .env.production create mode 100755 deploy.sh create mode 100644 ecosystem.config.js create mode 100644 lib/api.ts create mode 100644 public/.gitkeep diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..b580388 --- /dev/null +++ b/.env.production @@ -0,0 +1,24 @@ +# Database (MySQL) - Production +DATABASE_URL="mysql://root:MemeMind@2026@127.0.0.1:13306/mememind" + +# Better Auth +BETTER_AUTH_SECRET="WU+pYbaBiDkBEzQBrdhsWAuZGmEX6mSFi9Lyw0C3BaI=" +BETTER_AUTH_URL="https://ilookai.cn" + +# Public URLs (for client-side) +NEXT_PUBLIC_APP_URL="https://ilookai.cn" +NEXT_PUBLIC_BASE_PATH="/studio" + +# Admin Account +ADMIN_EMAIL="richardwei1995@gmail.com" +ADMIN_PASSWORD="richard_123456" + +# Tencent COS +COS_SECRET_ID=AKIDTs7B2f5NVSFqIYaP1QbZQE0bAuc9h4EB +COS_SECRET_KEY=pRs6yiHcNyVs21pRq11nOlupY4OVPOH1 +COS_BUCKET=lookai-1308511832 +COS_REGION=ap-guangzhou +COS_APPID=1308511832 + +# Server Port +PORT=3001 diff --git a/app/(dashboard)/levels/page.tsx b/app/(dashboard)/levels/page.tsx index 86d2b28..8516267 100644 --- a/app/(dashboard)/levels/page.tsx +++ b/app/(dashboard)/levels/page.tsx @@ -9,6 +9,7 @@ import { LevelDialog } from '@/components/levels/level-dialog' import { Spinner } from '@/components/ui/spinner' import { Level, LevelFormData } from '@/types' import { Plus } from 'lucide-react' +import { apiFetch } from '@/lib/api' export default function LevelsPage() { const queryClient = useQueryClient() @@ -20,7 +21,7 @@ export default function LevelsPage() { const { data: levels, isLoading, error } = useQuery({ queryKey: ['levels'], queryFn: async () => { - const res = await fetch('/api/levels') + const res = await apiFetch('/api/levels') if (!res.ok) throw new Error('Failed to fetch levels') return res.json() }, @@ -29,7 +30,7 @@ export default function LevelsPage() { // Create level mutation const createMutation = useMutation({ mutationFn: async (data: LevelFormData) => { - const res = await fetch('/api/levels', { + const res = await apiFetch('/api/levels', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -48,7 +49,7 @@ export default function LevelsPage() { // Update level mutation const updateMutation = useMutation({ mutationFn: async ({ id, data }: { id: string; data: LevelFormData }) => { - const res = await fetch('/api/levels', { + const res = await apiFetch('/api/levels', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, ...data }), @@ -67,7 +68,7 @@ export default function LevelsPage() { // Delete level mutation const deleteMutation = useMutation({ mutationFn: async (id: string) => { - const res = await fetch(`/api/levels?id=${id}`, { + const res = await apiFetch(`/api/levels?id=${id}`, { method: 'DELETE', }) if (!res.ok) { @@ -85,7 +86,7 @@ export default function LevelsPage() { // Reorder mutation const reorderMutation = useMutation({ mutationFn: async (orders: { id: string; sortOrder: number }[]) => { - const res = await fetch('/api/levels/reorder', { + const res = await apiFetch('/api/levels/reorder', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders }), diff --git a/app/(dashboard)/users/page.tsx b/app/(dashboard)/users/page.tsx index cfaf603..b108339 100644 --- a/app/(dashboard)/users/page.tsx +++ b/app/(dashboard)/users/page.tsx @@ -8,6 +8,7 @@ import { UserDialog } from '@/components/users/user-dialog' import { Spinner } from '@/components/ui/spinner' import { User, UserFormData } from '@/types' import { Plus, Pencil, Trash2 } from 'lucide-react' +import { apiFetch } from '@/lib/api' export default function UsersPage() { const queryClient = useQueryClient() @@ -19,7 +20,7 @@ export default function UsersPage() { const { data: users, isLoading, error } = useQuery({ queryKey: ['users'], queryFn: async () => { - const res = await fetch('/api/users') + const res = await apiFetch('/api/users') if (!res.ok) throw new Error('Failed to fetch users') return res.json() }, @@ -28,7 +29,7 @@ export default function UsersPage() { // Create user mutation const createMutation = useMutation({ mutationFn: async (data: UserFormData) => { - const res = await fetch('/api/users', { + const res = await apiFetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -47,7 +48,7 @@ export default function UsersPage() { // Update user mutation const updateMutation = useMutation({ mutationFn: async ({ id, data }: { id: string; data: UserFormData }) => { - const res = await fetch('/api/users', { + const res = await apiFetch('/api/users', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, ...data }), @@ -66,7 +67,7 @@ export default function UsersPage() { // Delete user mutation const deleteMutation = useMutation({ mutationFn: async (id: string) => { - const res = await fetch(`/api/users?id=${id}`, { + const res = await apiFetch(`/api/users?id=${id}`, { method: 'DELETE', }) if (!res.ok) { diff --git a/components/levels/image-uploader.tsx b/components/levels/image-uploader.tsx index be4b38b..9505383 100644 --- a/components/levels/image-uploader.tsx +++ b/components/levels/image-uploader.tsx @@ -6,6 +6,7 @@ import { X, Image as ImageIcon } from 'lucide-react' import Image from 'next/image' import { Spinner } from '@/components/ui/spinner' import COS from 'cos-js-sdk-v5' +import { apiFetch } from '@/lib/api' interface ImageUploaderProps { value: string @@ -38,7 +39,7 @@ export function ImageUploader({ value, onChange }: ImageUploaderProps) { try { // Get temp key - const keyRes = await fetch('/api/cos/temp-key') + const keyRes = await apiFetch('/api/cos/temp-key') if (!keyRes.ok) { throw new Error('获取上传凭证失败') } diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..0bc5fef --- /dev/null +++ b/deploy.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ─── 配置 ─────────────────────────────────────────── +SERVER="root@119.91.211.52" +REMOTE_DIR="/root/apps/meme-studio" +APP_NAME="meme-studio" + +# ─── 颜色 ─────────────────────────────────────────── +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +NC='\033[0m' + +step() { echo -e "\n${GREEN}▶ $1${NC}"; } +warn() { echo -e "${YELLOW}⚠ $1${NC}"; } +fail() { echo -e "${RED}✖ $1${NC}"; exit 1; } + +START_TIME=$(date +%s) + +# ─── 1. 本地构建 ──────────────────────────────────── +step "构建项目..." +pnpm run build || fail "构建失败" + +# ─── 2. 创建远程目录 ──────────────────────────────── +step "初始化远程目录..." +ssh "$SERVER" "mkdir -p $REMOTE_DIR/.next/static $REMOTE_DIR/public" + +# ─── 3. 同步文件(排除 node_modules,由服务器安装)─── +step "同步文件到服务器..." + +rsync -az --delete --exclude='node_modules' .next/standalone/ "$SERVER:$REMOTE_DIR/" +rsync -az .next/static/ "$SERVER:$REMOTE_DIR/.next/static/" +rsync -az public/ "$SERVER:$REMOTE_DIR/public/" +rsync -az prisma/ "$SERVER:$REMOTE_DIR/prisma/" +rsync -az .env.production "$SERVER:$REMOTE_DIR/.env" +rsync -az ecosystem.config.js "$SERVER:$REMOTE_DIR/" +rsync -az package.json package-lock.json "$SERVER:$REMOTE_DIR/" + +# ─── 4. 远程安装依赖 & 重启 ───────────────────────── +step "安装依赖并重启服务..." +ssh "$SERVER" "cd $REMOTE_DIR && npm install --production && npx prisma generate && (pm2 restart $APP_NAME 2>/dev/null || pm2 start ecosystem.config.js)" + +# ─── 完成 ─────────────────────────────────────────── +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) +echo -e "\n${GREEN}✔ 部署完成!耗时 ${ELAPSED}s${NC}" diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..df93212 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,18 @@ +module.exports = { + apps: [ + { + name: 'meme-studio', + script: 'server.js', + cwd: '/root/apps/meme-studio', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: { + NODE_ENV: 'production', + PORT: 3001, + HOSTNAME: '0.0.0.0', + }, + }, + ], +} diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..20e0f03 --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,6 @@ +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) +} diff --git a/lib/auth-client.ts b/lib/auth-client.ts index 5183c83..1a72070 100644 --- a/lib/auth-client.ts +++ b/lib/auth-client.ts @@ -1,7 +1,10 @@ import { createAuthClient } from 'better-auth/react' +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: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000', + baseURL: `${appUrl}${basePath}/api/auth`, }) export const { signIn, signOut, useSession } = authClient diff --git a/lib/auth.ts b/lib/auth.ts index 51d3ee1..3a6c382 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -9,7 +9,8 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, }, - trustedOrigins: [process.env.BETTER_AUTH_URL || 'http://localhost:3000'], + basePath: '/api/auth', + trustedOrigins: [process.env.BETTER_AUTH_URL || 'http://localhost:3001'], secret: process.env.BETTER_AUTH_SECRET, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days diff --git a/middleware.ts b/middleware.ts index 66028e6..f56f4a0 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,11 +1,14 @@ import { NextRequest, NextResponse } from 'next/server' +const basePath = '/studio' + export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl - // Allow auth API routes and static files + // Allow all API routes and static files + // Note: pathname does NOT include basePath when basePath is configured if ( - pathname.startsWith('/api/auth') || + pathname.startsWith('/api/') || pathname.startsWith('/_next') || pathname.startsWith('/favicon') || pathname.includes('.') @@ -19,10 +22,12 @@ export async function middleware(request: NextRequest) { } // Check if session cookie exists (simple check, full validation happens in server) + // Better Auth adds "__Secure-" prefix when served over HTTPS const sessionToken = request.cookies.get('better-auth.session_token') + || request.cookies.get('__Secure-better-auth.session_token') if (!sessionToken?.value) { - const loginUrl = new URL('/login', request.url) + const loginUrl = new URL(`${basePath}/login`, request.url) loginUrl.searchParams.set('callbackUrl', pathname) return NextResponse.redirect(loginUrl) } @@ -31,5 +36,5 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'], + matcher: '/:path*', } diff --git a/next.config.js b/next.config.js index e336ae9..70a0607 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'standalone', + basePath: '/studio', images: { remotePatterns: [ { diff --git a/package.json b/package.json index 3f986fc..06bf57c 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,16 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 3001", "build": "next build", - "start": "next start", + "start": "next start -p 3001", "lint": "next lint", "db:generate": "prisma generate", "db:push": "prisma db push", "db:migrate": "prisma migrate dev", "db:studio": "prisma studio", - "db:seed": "tsx prisma/seed.ts" + "db:seed": "tsx prisma/seed.ts", + "deploy": "./deploy.sh" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 00103b6..292dde0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2,7 +2,8 @@ // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + binaryTargets = ["native", "rhel-openssl-3.0.x"] } datasource db { diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29