新增 App 建立、資料呈現

This commit is contained in:
2025-08-05 16:13:09 +08:00
parent d0c4adf243
commit 5b407ff29c
51 changed files with 6039 additions and 78 deletions

View File

@@ -0,0 +1,151 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
import { requireDeveloperOrAdmin } from '@/lib/auth';
import { logger } from '@/lib/logger';
import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
// POST /api/apps/[id]/upload - 上傳應用程式檔案
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const startTime = Date.now();
try {
// 驗證用戶權限
const user = await requireDeveloperOrAdmin(request);
const { id } = params;
// 檢查應用程式是否存在
const existingApp = await db.queryOne('SELECT * FROM apps WHERE id = ?', [id]);
if (!existingApp) {
return NextResponse.json(
{ error: '應用程式不存在' },
{ status: 404 }
);
}
// 檢查權限:只有創建者或管理員可以上傳檔案
if (existingApp.creator_id !== user.id && user.role !== 'admin') {
return NextResponse.json(
{ error: '您沒有權限為此應用程式上傳檔案' },
{ status: 403 }
);
}
// 解析 FormData
const formData = await request.formData();
const file = formData.get('file') as File;
const type = formData.get('type') as string;
if (!file) {
return NextResponse.json(
{ error: '請選擇要上傳的檔案' },
{ status: 400 }
);
}
// 驗證檔案類型
const validTypes = ['screenshot', 'document', 'source_code'];
if (!validTypes.includes(type)) {
return NextResponse.json(
{ error: '無效的檔案類型' },
{ status: 400 }
);
}
// 驗證檔案大小 (最大 10MB)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
return NextResponse.json(
{ error: '檔案大小不能超過 10MB' },
{ status: 400 }
);
}
// 驗證檔案格式
const allowedExtensions = {
screenshot: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
document: ['.pdf', '.doc', '.docx', '.txt', '.md'],
source_code: ['.zip', '.rar', '.7z', '.tar.gz']
};
const fileName = file.name.toLowerCase();
const fileExtension = fileName.substring(fileName.lastIndexOf('.'));
const allowedExts = allowedExtensions[type as keyof typeof allowedExtensions];
if (!allowedExts.includes(fileExtension)) {
return NextResponse.json(
{ error: `此檔案類型不支援 ${type} 上傳` },
{ status: 400 }
);
}
// 創建上傳目錄
const uploadDir = join(process.cwd(), 'public', 'uploads', 'apps', id);
if (!existsSync(uploadDir)) {
await mkdir(uploadDir, { recursive: true });
}
// 生成唯一檔案名
const timestamp = Date.now();
const uniqueFileName = `${type}_${timestamp}_${file.name}`;
const filePath = join(uploadDir, uniqueFileName);
const relativePath = `/uploads/apps/${id}/${uniqueFileName}`;
// 將檔案寫入磁碟
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
await writeFile(filePath, buffer);
// 更新應用程式資料
let updateData: any = {};
if (type === 'screenshot') {
// 獲取現有的截圖列表
const currentScreenshots = existingApp.screenshots ? JSON.parse(existingApp.screenshots) : [];
currentScreenshots.push(relativePath);
updateData.screenshots = JSON.stringify(currentScreenshots);
} else if (type === 'source_code') {
// 更新檔案路徑
updateData.file_path = relativePath;
}
if (Object.keys(updateData).length > 0) {
await db.update('apps', updateData, { id });
}
// 記錄活動
logger.logActivity(user.id, 'app', id, 'upload_file', {
fileName: file.name,
fileType: type,
fileSize: file.size,
filePath: relativePath
});
const duration = Date.now() - startTime;
logger.logRequest('POST', `/api/apps/${id}/upload`, 200, duration, user.id);
return NextResponse.json({
message: '檔案上傳成功',
fileName: file.name,
fileType: type,
filePath: relativePath,
fileSize: file.size
});
} catch (error) {
logger.logError(error as Error, 'Apps Upload API');
const duration = Date.now() - startTime;
logger.logRequest('POST', `/api/apps/${params.id}/upload`, 500, duration);
return NextResponse.json(
{ error: '檔案上傳失敗' },
{ status: 500 }
);
}
}