380 lines
10 KiB
TypeScript
380 lines
10 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { db } from '@/lib/database';
|
|
import { authenticateUser, requireDeveloperOrAdmin } from '@/lib/auth';
|
|
import { logger } from '@/lib/logger';
|
|
import { AppUpdateRequest } from '@/types/app';
|
|
|
|
// GET /api/apps/[id] - 獲取單個應用程式詳細資料
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 驗證用戶權限
|
|
const user = await authenticateUser(request);
|
|
if (!user) {
|
|
return NextResponse.json(
|
|
{ error: '需要登入才能查看應用程式' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const { id } = params;
|
|
|
|
// 查詢應用程式詳細資料
|
|
const sql = `
|
|
SELECT
|
|
a.*,
|
|
u.name as user_creator_name,
|
|
u.email as creator_email,
|
|
u.department as creator_department,
|
|
u.role as creator_role,
|
|
t.name as team_name,
|
|
t.department as team_department,
|
|
t.contact_email as team_contact_email,
|
|
t.leader_id as team_leader_id
|
|
FROM apps a
|
|
LEFT JOIN users u ON a.creator_id = u.id
|
|
LEFT JOIN teams t ON a.team_id = t.id
|
|
WHERE a.id = ?
|
|
`;
|
|
|
|
const app = await db.queryOne(sql, [id]);
|
|
|
|
if (!app) {
|
|
return NextResponse.json(
|
|
{ error: '應用程式不存在' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// 格式化回應資料
|
|
const formattedApp = {
|
|
id: app.id,
|
|
name: app.name,
|
|
description: app.description,
|
|
creatorId: app.creator_id,
|
|
teamId: app.team_id,
|
|
status: app.status,
|
|
type: app.type,
|
|
filePath: app.file_path,
|
|
techStack: app.tech_stack ? JSON.parse(app.tech_stack) : [],
|
|
tags: app.tags ? JSON.parse(app.tags) : [],
|
|
screenshots: app.screenshots ? JSON.parse(app.screenshots) : [],
|
|
demoUrl: app.demo_url,
|
|
githubUrl: app.github_url,
|
|
docsUrl: app.docs_url,
|
|
version: app.version,
|
|
icon: app.icon,
|
|
iconColor: app.icon_color,
|
|
likesCount: app.likes_count,
|
|
viewsCount: app.views_count,
|
|
rating: app.rating,
|
|
createdAt: app.created_at,
|
|
updatedAt: app.updated_at,
|
|
lastUpdated: app.last_updated,
|
|
department: app.department,
|
|
creator: {
|
|
id: app.creator_id,
|
|
name: app.creator_name || app.user_creator_name,
|
|
email: app.creator_email,
|
|
department: app.department || app.creator_department,
|
|
role: app.creator_role
|
|
},
|
|
team: app.team_id ? {
|
|
id: app.team_id,
|
|
name: app.team_name,
|
|
department: app.team_department,
|
|
contactEmail: app.team_contact_email,
|
|
leaderId: app.team_leader_id
|
|
} : undefined
|
|
};
|
|
|
|
// 增加瀏覽次數
|
|
await db.update(
|
|
'apps',
|
|
{ views_count: app.views_count + 1 },
|
|
{ id }
|
|
);
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('GET', `/api/apps/${id}`, 200, duration, user.id);
|
|
|
|
return NextResponse.json(formattedApp);
|
|
|
|
} catch (error) {
|
|
logger.logError(error as Error, 'Apps API - GET by ID');
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('GET', `/api/apps/${params.id}`, 500, duration);
|
|
|
|
return NextResponse.json(
|
|
{ error: '獲取應用程式詳細資料失敗' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// PUT /api/apps/[id] - 更新應用程式
|
|
export async function PUT(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 驗證用戶權限
|
|
const user = await requireDeveloperOrAdmin(request);
|
|
|
|
const { id } = params;
|
|
const body = await request.json();
|
|
const {
|
|
name,
|
|
description,
|
|
type,
|
|
teamId,
|
|
status,
|
|
techStack,
|
|
tags,
|
|
screenshots,
|
|
demoUrl,
|
|
githubUrl,
|
|
docsUrl,
|
|
version,
|
|
icon,
|
|
iconColor
|
|
}: AppUpdateRequest = body;
|
|
|
|
// 檢查應用程式是否存在
|
|
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 }
|
|
);
|
|
}
|
|
|
|
// 驗證更新資料
|
|
const updateData: any = {};
|
|
|
|
if (name !== undefined) {
|
|
if (name.length < 2 || name.length > 200) {
|
|
return NextResponse.json(
|
|
{ error: '應用程式名稱長度必須在 2-200 個字符之間' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
updateData.name = name;
|
|
}
|
|
|
|
if (description !== undefined) {
|
|
if (description.length < 10) {
|
|
return NextResponse.json(
|
|
{ error: '應用程式描述至少需要 10 個字符' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
updateData.description = description;
|
|
}
|
|
|
|
if (type !== undefined) {
|
|
const validTypes = [
|
|
'productivity', 'ai_model', 'automation', 'data_analysis', 'educational',
|
|
'healthcare', 'finance', 'iot_device', 'blockchain', 'ar_vr',
|
|
'machine_learning', 'computer_vision', 'nlp', 'robotics', 'cybersecurity',
|
|
'cloud_service', 'other'
|
|
];
|
|
if (!validTypes.includes(type)) {
|
|
return NextResponse.json(
|
|
{ error: '無效的應用程式類型' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
updateData.type = type;
|
|
}
|
|
|
|
if (status !== undefined) {
|
|
const validStatuses = ['draft', 'submitted', 'under_review', 'approved', 'rejected', 'published'];
|
|
if (!validStatuses.includes(status)) {
|
|
return NextResponse.json(
|
|
{ error: '無效的應用程式狀態' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
updateData.status = status;
|
|
}
|
|
|
|
if (teamId !== undefined) {
|
|
// 如果指定了團隊,驗證團隊存在且用戶是團隊成員
|
|
if (teamId) {
|
|
const teamMember = await db.queryOne(
|
|
'SELECT * FROM team_members WHERE team_id = ? AND user_id = ?',
|
|
[teamId, user.id]
|
|
);
|
|
|
|
if (!teamMember) {
|
|
return NextResponse.json(
|
|
{ error: '您不是該團隊的成員,無法將應用程式分配給該團隊' },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
}
|
|
updateData.team_id = teamId || null;
|
|
}
|
|
|
|
if (techStack !== undefined) {
|
|
updateData.tech_stack = techStack ? JSON.stringify(techStack) : null;
|
|
}
|
|
|
|
if (tags !== undefined) {
|
|
updateData.tags = tags ? JSON.stringify(tags) : null;
|
|
}
|
|
|
|
if (screenshots !== undefined) {
|
|
updateData.screenshots = screenshots ? JSON.stringify(screenshots) : null;
|
|
}
|
|
|
|
if (demoUrl !== undefined) {
|
|
updateData.demo_url = demoUrl || null;
|
|
}
|
|
|
|
if (githubUrl !== undefined) {
|
|
updateData.github_url = githubUrl || null;
|
|
}
|
|
|
|
if (docsUrl !== undefined) {
|
|
updateData.docs_url = docsUrl || null;
|
|
}
|
|
|
|
if (version !== undefined) {
|
|
updateData.version = version;
|
|
}
|
|
|
|
if (icon !== undefined) {
|
|
updateData.icon = icon;
|
|
}
|
|
|
|
if (iconColor !== undefined) {
|
|
updateData.icon_color = iconColor;
|
|
}
|
|
|
|
// 更新應用程式
|
|
if (Object.keys(updateData).length > 0) {
|
|
await db.update('apps', updateData, { id });
|
|
|
|
// 記錄活動
|
|
logger.logActivity(user.id, 'app', id, 'update', updateData);
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('PUT', `/api/apps/${id}`, 200, duration, user.id);
|
|
|
|
return NextResponse.json({
|
|
message: '應用程式更新成功',
|
|
appId: id
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.logError(error as Error, 'Apps API - PUT');
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('PUT', `/api/apps/${params.id}`, 500, duration);
|
|
|
|
return NextResponse.json(
|
|
{ error: '更新應用程式失敗' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// DELETE /api/apps/[id] - 刪除應用程式
|
|
export async function DELETE(
|
|
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 }
|
|
);
|
|
}
|
|
|
|
// 開始事務
|
|
const connection = await db.beginTransaction();
|
|
|
|
try {
|
|
// 刪除相關的按讚記錄
|
|
await connection.execute('DELETE FROM user_likes WHERE app_id = ?', [id]);
|
|
|
|
// 刪除相關的收藏記錄
|
|
await connection.execute('DELETE FROM user_favorites WHERE app_id = ?', [id]);
|
|
|
|
// 刪除相關的評分記錄
|
|
await connection.execute('DELETE FROM judge_scores WHERE app_id = ?', [id]);
|
|
|
|
// 刪除應用程式
|
|
await connection.execute('DELETE FROM apps WHERE id = ?', [id]);
|
|
|
|
// 提交事務
|
|
await db.commitTransaction(connection);
|
|
|
|
// 記錄活動
|
|
logger.logActivity(user.id, 'app', id, 'delete', {
|
|
name: existingApp.name,
|
|
type: existingApp.type
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('DELETE', `/api/apps/${id}`, 200, duration, user.id);
|
|
|
|
return NextResponse.json({
|
|
message: '應用程式刪除成功',
|
|
appId: id
|
|
});
|
|
|
|
} catch (error) {
|
|
// 回滾事務
|
|
await db.rollbackTransaction(connection);
|
|
throw error;
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.logError(error as Error, 'Apps API - DELETE');
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('DELETE', `/api/apps/${params.id}`, 500, duration);
|
|
|
|
return NextResponse.json(
|
|
{ error: '刪除應用程式失敗' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|