新增資料庫、用戶註冊、登入的功能

This commit is contained in:
2025-08-05 10:56:22 +08:00
parent 94e3763402
commit a288a966ba
41 changed files with 4362 additions and 289 deletions

View File

@@ -1,195 +0,0 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { CheckCircle, Edit, Loader2 } from "lucide-react"
export default function ScoringFormTestPage() {
const [showScoringForm, setShowScoringForm] = useState(false)
const [manualScoring, setManualScoring] = useState({
judgeId: "judge1",
participantId: "app1",
scores: {
"創新性": 0,
"技術性": 0,
"實用性": 0,
"展示效果": 0,
"影響力": 0
},
comments: ""
})
const [isLoading, setIsLoading] = useState(false)
const scoringRules = [
{ name: "創新性", description: "技術創新程度和獨特性", weight: 25 },
{ name: "技術性", description: "技術實現的複雜度和穩定性", weight: 20 },
{ name: "實用性", description: "實際應用價值和用戶體驗", weight: 25 },
{ name: "展示效果", description: "演示效果和表達能力", weight: 15 },
{ name: "影響力", description: "對行業和社會的潛在影響", weight: 15 }
]
const calculateTotalScore = (scores: Record<string, number>): number => {
let totalScore = 0
let totalWeight = 0
scoringRules.forEach(rule => {
const score = scores[rule.name] || 0
const weight = rule.weight || 1
totalScore += score * weight
totalWeight += weight
})
return totalWeight > 0 ? Math.round(totalScore / totalWeight) : 0
}
const handleSubmitScore = async () => {
setIsLoading(true)
// 模擬提交
setTimeout(() => {
setIsLoading(false)
setShowScoringForm(false)
}, 2000)
}
return (
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-3xl font-bold"></h1>
<p className="text-gray-600"></p>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Button onClick={() => setShowScoringForm(true)} size="lg">
<Edit className="w-5 h-5 mr-2" />
</Button>
</CardContent>
</Card>
<Dialog open={showScoringForm} onOpenChange={setShowScoringForm}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Edit className="w-5 h-5" />
<span></span>
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 評分項目 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold"></h3>
{scoringRules.map((rule, index) => (
<div key={index} className="space-y-4 p-6 border rounded-lg bg-white shadow-sm">
<div className="flex justify-between items-start">
<div className="flex-1">
<Label className="text-lg font-semibold text-gray-900">{rule.name}</Label>
<p className="text-sm text-gray-600 mt-2 leading-relaxed">{rule.description}</p>
<p className="text-xs text-purple-600 mt-2 font-medium">{rule.weight}%</p>
</div>
<div className="text-right ml-4">
<span className="text-2xl font-bold text-blue-600">
{manualScoring.scores[rule.name] || 0} / 10
</span>
</div>
</div>
{/* 評分按鈕 */}
<div className="flex flex-wrap gap-3">
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => (
<button
key={score}
type="button"
onClick={() => setManualScoring({
...manualScoring,
scores: { ...manualScoring.scores, [rule.name]: score }
})}
className={`w-12 h-12 rounded-lg border-2 font-semibold text-lg transition-all duration-200 ${
(manualScoring.scores[rule.name] || 0) === score
? 'bg-blue-600 text-white border-blue-600 shadow-lg scale-105'
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400 hover:bg-blue-50 hover:scale-105'
}`}
>
{score}
</button>
))}
</div>
</div>
))}
</div>
{/* 總分顯示 */}
<div className="p-6 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border-2 border-blue-200">
<div className="flex justify-between items-center">
<div>
<span className="text-xl font-bold text-gray-900"></span>
<p className="text-sm text-gray-600 mt-1"></p>
</div>
<div className="flex items-center space-x-3">
<span className="text-4xl font-bold text-blue-600">
{calculateTotalScore(manualScoring.scores)}
</span>
<span className="text-xl text-gray-500 font-medium">/ 10</span>
</div>
</div>
</div>
{/* 評審意見 */}
<div className="space-y-3">
<Label className="text-lg font-semibold"> *</Label>
<Textarea
placeholder="請詳細填寫評審意見、優點分析、改進建議等..."
value={manualScoring.comments}
onChange={(e) => setManualScoring({ ...manualScoring, comments: e.target.value })}
rows={6}
className="min-h-[120px] resize-none"
/>
<p className="text-xs text-gray-500"></p>
</div>
</div>
<div className="flex justify-end space-x-4 pt-6 border-t border-gray-200">
<Button
variant="outline"
size="lg"
onClick={() => setShowScoringForm(false)}
className="px-8"
>
</Button>
<Button
onClick={handleSubmitScore}
disabled={isLoading}
size="lg"
className="px-8 bg-blue-600 hover:bg-blue-700"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
...
</>
) : (
<>
<CheckCircle className="w-5 h-5 mr-2" />
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,13 +0,0 @@
import { ScoringManagement } from "@/components/admin/scoring-management"
export default function ScoringTestPage() {
return (
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-3xl font-bold"></h1>
<p className="text-gray-600"></p>
</div>
<ScoringManagement />
</div>
)
}

115
app/api/auth/login/route.ts Normal file
View File

@@ -0,0 +1,115 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
import { generateToken, validatePassword, comparePassword } from '@/lib/auth';
import { logger } from '@/lib/logger';
export async function POST(request: NextRequest) {
const startTime = Date.now();
try {
const body = await request.json();
const { email, password } = body;
// 驗證輸入
if (!email || !password) {
return NextResponse.json(
{ error: '請提供電子郵件和密碼' },
{ status: 400 }
);
}
// 驗證密碼格式
const passwordValidation = await validatePassword(password);
if (!passwordValidation.isValid) {
return NextResponse.json(
{ error: '密碼格式不正確', details: passwordValidation.errors },
{ status: 400 }
);
}
// 查詢用戶
const user = await db.queryOne<{
id: string;
name: string;
email: string;
password_hash: string;
avatar?: string;
department: string;
role: 'user' | 'developer' | 'admin';
join_date: string;
total_likes: number;
total_views: number;
created_at: string;
updated_at: string;
}>(
'SELECT * FROM users WHERE email = ?',
[email]
);
if (!user) {
logger.logAuth('login', email, false, request.ip || 'unknown');
return NextResponse.json(
{ error: '電子郵件或密碼不正確' },
{ status: 401 }
);
}
// 驗證密碼
const isPasswordValid = await comparePassword(password, user.password_hash);
if (!isPasswordValid) {
logger.logAuth('login', email, false, request.ip || 'unknown');
return NextResponse.json(
{ error: '電子郵件或密碼不正確' },
{ status: 401 }
);
}
// 生成 JWT Token
const token = generateToken({
id: user.id,
email: user.email,
role: user.role
});
// 更新最後登入時間
await db.update(
'users',
{ updated_at: new Date().toISOString().slice(0, 19).replace('T', ' ') },
{ id: user.id }
);
// 記錄成功登入
logger.logAuth('login', email, true, request.ip || 'unknown');
const duration = Date.now() - startTime;
logger.logRequest('POST', '/api/auth/login', 200, duration, user.id);
return NextResponse.json({
message: '登入成功',
user: {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
department: user.department,
role: user.role,
joinDate: user.join_date,
totalLikes: user.total_likes,
totalViews: user.total_views
},
token,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
});
} catch (error) {
logger.logError(error as Error, 'Login API');
const duration = Date.now() - startTime;
logger.logRequest('POST', '/api/auth/login', 500, duration);
return NextResponse.json(
{ error: '內部伺服器錯誤' },
{ status: 500 }
);
}
}

47
app/api/auth/me/route.ts Normal file
View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';
import { authenticateUser } from '@/lib/auth';
import { logger } from '@/lib/logger';
export async function GET(request: NextRequest) {
const startTime = Date.now();
try {
// 驗證用戶
const user = await authenticateUser(request);
if (!user) {
return NextResponse.json(
{ error: '未授權訪問' },
{ status: 401 }
);
}
const duration = Date.now() - startTime;
logger.logRequest('GET', '/api/auth/me', 200, duration, user.id);
return NextResponse.json({
user: {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
department: user.department,
role: user.role,
joinDate: user.joinDate,
totalLikes: user.totalLikes,
totalViews: user.totalViews
}
});
} catch (error) {
logger.logError(error as Error, 'Get Current User API');
const duration = Date.now() - startTime;
logger.logRequest('GET', '/api/auth/me', 500, duration);
return NextResponse.json(
{ error: '內部伺服器錯誤' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, generateId } from '@/lib/database';
import { validateUserData, validatePassword, hashPassword } from '@/lib/auth';
import { logger } from '@/lib/logger';
export async function POST(request: NextRequest) {
const startTime = Date.now();
try {
console.log('開始處理註冊請求...');
const body = await request.json();
console.log('請求體:', body);
const { name, email, password, department, role = 'user' } = body;
// 驗證用戶資料
console.log('驗證用戶資料...');
const userValidation = validateUserData({ name, email, department, role });
if (!userValidation.isValid) {
console.log('用戶資料驗證失敗:', userValidation.errors);
return NextResponse.json(
{ error: '用戶資料驗證失敗', details: userValidation.errors },
{ status: 400 }
);
}
// 驗證密碼
console.log('驗證密碼...');
const passwordValidation = await validatePassword(password);
if (!passwordValidation.isValid) {
console.log('密碼驗證失敗:', passwordValidation.errors);
return NextResponse.json(
{ error: '密碼格式不正確', details: passwordValidation.errors },
{ status: 400 }
);
}
// 檢查電子郵件是否已存在
console.log('檢查電子郵件是否已存在...');
const existingUser = await db.queryOne(
'SELECT id FROM users WHERE email = ?',
[email]
);
if (existingUser) {
console.log('電子郵件已存在');
return NextResponse.json(
{ error: '此電子郵件地址已被註冊' },
{ status: 409 }
);
}
// 加密密碼
console.log('加密密碼...');
const passwordHash = await hashPassword(password);
console.log('密碼加密完成');
// 準備用戶資料
console.log('準備用戶資料...');
const userId = generateId();
const userData = {
id: userId,
name: name.trim(),
email: email.toLowerCase().trim(),
password_hash: passwordHash,
department: department.trim(),
role,
join_date: new Date().toISOString().split('T')[0],
total_likes: 0,
total_views: 0,
created_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
updated_at: new Date().toISOString().slice(0, 19).replace('T', ' ')
};
console.log('插入用戶資料...');
// 插入用戶資料
await db.insert('users', userData);
console.log('用戶資料插入成功');
// 記錄註冊成功
logger.logAuth('register', email, true, 'unknown');
const duration = Date.now() - startTime;
logger.logRequest('POST', '/api/auth/register', 201, duration, userId);
return NextResponse.json({
message: '註冊成功',
user: {
id: userData.id,
name: userData.name,
email: userData.email,
department: userData.department,
role: userData.role,
joinDate: userData.join_date,
totalLikes: userData.total_likes,
totalViews: userData.total_views
}
}, { status: 201 });
} catch (error) {
console.error('註冊 API 錯誤:', error);
logger.logError(error as Error, 'Register API');
const duration = Date.now() - startTime;
logger.logRequest('POST', '/api/auth/register', 500, duration);
return NextResponse.json(
{ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
import { hashPassword } from '@/lib/auth';
import { codeMap } from '../request/route';
export async function POST(request: NextRequest) {
try {
const { email, code, newPassword } = await request.json();
if (!email || !code || !newPassword) return NextResponse.json({ error: '缺少參數' }, { status: 400 });
const validCode = codeMap.get(email);
if (!validCode || validCode !== code) return NextResponse.json({ error: '驗證碼錯誤' }, { status: 400 });
const passwordHash = await hashPassword(newPassword);
await db.update('users', { password_hash: passwordHash }, { email });
codeMap.delete(email);
return NextResponse.json({ message: '密碼重設成功' });
} catch (error) {
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
const codeMap = new Map();
export async function POST(request: NextRequest) {
try {
const { email } = await request.json();
if (!email) return NextResponse.json({ error: '請提供 email' }, { status: 400 });
const user = await db.queryOne('SELECT id FROM users WHERE email = ?', [email]);
if (!user) return NextResponse.json({ error: '用戶不存在' }, { status: 404 });
const code = Math.floor(100000 + Math.random() * 900000).toString();
codeMap.set(email, code);
// 實際應發送 email這裡直接回傳
return NextResponse.json({ message: '驗證碼已產生', code });
} catch (error) {
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}
export { codeMap };

35
app/api/route.ts Normal file
View File

@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
export async function GET(request: NextRequest) {
try {
// 健康檢查
const isHealthy = await db.healthCheck();
if (!isHealthy) {
return NextResponse.json(
{ error: 'Database connection failed' },
{ status: 503 }
);
}
// 獲取基本統計
const stats = await db.getDatabaseStats();
return NextResponse.json({
message: 'AI Platform API is running',
version: '1.0.0',
timestamp: new Date().toISOString(),
database: {
status: 'connected',
stats
}
});
} catch (error) {
console.error('API Health Check Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

47
app/api/users/route.ts Normal file
View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin } from '@/lib/auth';
import { db } from '@/lib/database';
import { logger } from '@/lib/logger';
export async function GET(request: NextRequest) {
const startTime = Date.now();
try {
// 驗證管理員權限
const admin = await requireAdmin(request);
// 查詢參數
const { searchParams } = new URL(request.url);
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
const limit = Math.max(1, Math.min(100, parseInt(searchParams.get('limit') || '20', 10)));
const offset = (page - 1) * limit;
// 查詢用戶總數
const countResult = await db.queryOne<{ total: number }>('SELECT COUNT(*) as total FROM users');
const total = countResult?.total || 0;
// 查詢用戶列表
const users = await db.query(
`SELECT id, name, email, avatar, department, role, join_date, total_likes, total_views, created_at, updated_at FROM users ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`
);
// 分頁資訊
const totalPages = Math.ceil(total / limit);
const hasNext = page < totalPages;
const hasPrev = page > 1;
return NextResponse.json({
users: users.map(user => ({
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
department: user.department,
role: user.role,
joinDate: user.join_date,
totalLikes: user.total_likes,
totalViews: user.total_views,
createdAt: user.created_at,
updatedAt: user.updated_at
})),
pagination: { page, limit, total, totalPages, hasNext, hasPrev }
});
} catch (error) {
logger.logError(error as Error, 'Users List API');
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin } from '@/lib/auth';
import { db } from '@/lib/database';
export async function GET(request: NextRequest) {
try {
await requireAdmin(request);
const total = await db.queryOne<{ count: number }>('SELECT COUNT(*) as count FROM users');
const admin = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'admin'");
const developer = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'developer'");
const user = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'user'");
const today = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE join_date = CURDATE()");
return NextResponse.json({
total: total?.count || 0,
admin: admin?.count || 0,
developer: developer?.count || 0,
user: user?.count || 0,
today: today?.count || 0
});
} catch (error) {
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}

View File

@@ -135,7 +135,7 @@ export default function CompetitionPage() {
const filteredAwards = getFilteredAwards()
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex flex-col">
{/* Header */}
<header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -165,9 +165,9 @@ export default function CompetitionPage() {
</div>
</div>
</div>
</header>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Hero Section */}
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">AI </h2>

View File

@@ -344,7 +344,7 @@ export default function AIShowcasePlatform() {
const filteredAwards = getFilteredAwards()
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex flex-col">
{/* Header */}
<header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -401,9 +401,9 @@ export default function AIShowcasePlatform() {
</div>
</div>
</div>
</header>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{showCompetition ? (
// Competition Content
<>
@@ -999,7 +999,7 @@ export default function AIShowcasePlatform() {
</div>
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-16">
<footer className="bg-white border-t border-gray-200 mt-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="text-sm text-gray-500 mb-4 md:mb-0">