新增資料庫、用戶註冊、登入的功能
This commit is contained in:
209
lib/auth.ts
Normal file
209
lib/auth.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { db } from './database';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
// JWT 配置
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'ai_platform_jwt_secret_key_2024';
|
||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
||||
|
||||
// 用戶角色類型
|
||||
export type UserRole = 'user' | 'developer' | 'admin';
|
||||
|
||||
// 用戶介面
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
department: string;
|
||||
role: UserRole;
|
||||
joinDate: string;
|
||||
totalLikes: number;
|
||||
totalViews: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// JWT Payload 介面
|
||||
export interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: UserRole;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
// 生成 JWT Token
|
||||
export function generateToken(user: { id: string; email: string; role: UserRole }): string {
|
||||
const payload: Omit<JWTPayload, 'iat' | 'exp'> = {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
};
|
||||
|
||||
return jwt.sign(payload, JWT_SECRET, {
|
||||
expiresIn: JWT_EXPIRES_IN
|
||||
});
|
||||
}
|
||||
|
||||
// 驗證 JWT Token
|
||||
export function verifyToken(token: string): JWTPayload | null {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET) as JWTPayload;
|
||||
} catch (error) {
|
||||
console.error('Token verification failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 從請求中提取 Token
|
||||
export function extractToken(request: NextRequest): string | null {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return null;
|
||||
}
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
|
||||
// 驗證用戶權限
|
||||
export function hasPermission(userRole: UserRole, requiredRole: UserRole): boolean {
|
||||
const roleHierarchy = {
|
||||
user: 1,
|
||||
developer: 2,
|
||||
admin: 3
|
||||
};
|
||||
|
||||
return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
|
||||
}
|
||||
|
||||
// 用戶認證中間件
|
||||
export async function authenticateUser(request: NextRequest): Promise<User | null> {
|
||||
try {
|
||||
const token = extractToken(request);
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = verifyToken(token);
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 從資料庫獲取最新用戶資料
|
||||
const user = await db.queryOne<User>(
|
||||
'SELECT * FROM users WHERE id = ? AND email = ?',
|
||||
[payload.userId, payload.email]
|
||||
);
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 檢查管理員權限
|
||||
export async function requireAdmin(request: NextRequest): Promise<User> {
|
||||
const user = await authenticateUser(request);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
if (user.role !== 'admin') {
|
||||
throw new Error('Admin permission required');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// 檢查開發者或管理員權限
|
||||
export async function requireDeveloperOrAdmin(request: NextRequest): Promise<User> {
|
||||
const user = await authenticateUser(request);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
if (user.role !== 'developer' && user.role !== 'admin') {
|
||||
throw new Error('Developer or admin permission required');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// 密碼驗證
|
||||
export async function validatePassword(password: string): Promise<{ isValid: boolean; errors: string[] }> {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (password.length < 8) {
|
||||
errors.push('密碼長度至少需要8個字符');
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.push('密碼需要包含至少一個大寫字母');
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push('密碼需要包含至少一個小寫字母');
|
||||
}
|
||||
|
||||
if (!/\d/.test(password)) {
|
||||
errors.push('密碼需要包含至少一個數字');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
// 加密密碼
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 12;
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
// 驗證密碼
|
||||
export async function comparePassword(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
// 生成隨機密碼
|
||||
export function generateRandomPassword(length: number = 12): string {
|
||||
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
// 用戶資料驗證
|
||||
export function validateUserData(data: any): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!data.name || data.name.trim().length < 2) {
|
||||
errors.push('姓名至少需要2個字符');
|
||||
}
|
||||
|
||||
if (!data.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
||||
errors.push('請提供有效的電子郵件地址');
|
||||
}
|
||||
|
||||
if (!data.department || data.department.trim().length < 2) {
|
||||
errors.push('部門名稱至少需要2個字符');
|
||||
}
|
||||
|
||||
if (data.role && !['user', 'developer', 'admin'].includes(data.role)) {
|
||||
errors.push('無效的用戶角色');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
213
lib/database.ts
Normal file
213
lib/database.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
// 資料庫配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'AI_Platform',
|
||||
password: process.env.DB_PASSWORD || 'Aa123456',
|
||||
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
// 連接池配置
|
||||
connectionLimit: 10,
|
||||
acquireTimeout: 60000,
|
||||
timeout: 60000,
|
||||
reconnect: true
|
||||
};
|
||||
|
||||
// 創建連接池
|
||||
let pool: mysql.Pool | null = null;
|
||||
|
||||
export class Database {
|
||||
private static instance: Database;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): Database {
|
||||
if (!Database.instance) {
|
||||
Database.instance = new Database();
|
||||
}
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
// 獲取連接池
|
||||
public async getPool(): Promise<mysql.Pool> {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(dbConfig);
|
||||
|
||||
// 測試連接
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
console.log('✅ 資料庫連接池建立成功');
|
||||
connection.release();
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫連接池建立失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
// 執行查詢
|
||||
public async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
||||
const pool = await this.getPool();
|
||||
try {
|
||||
const [rows] = await pool.execute(sql, params);
|
||||
return rows as T[];
|
||||
} catch (error) {
|
||||
console.error('查詢執行失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 執行單一查詢 (返回第一筆結果)
|
||||
public async queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
|
||||
const results = await this.query<T>(sql, params);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
}
|
||||
|
||||
// 執行插入
|
||||
public async insert(table: string, data: Record<string, any>): Promise<number> {
|
||||
const columns = Object.keys(data);
|
||||
const values = Object.values(data);
|
||||
const placeholders = columns.map(() => '?').join(', ');
|
||||
|
||||
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
|
||||
const pool = await this.getPool();
|
||||
try {
|
||||
const [result] = await pool.execute(sql, values);
|
||||
return (result as any).insertId;
|
||||
} catch (error) {
|
||||
console.error('插入執行失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 執行更新
|
||||
public async update(table: string, data: Record<string, any>, where: Record<string, any>): Promise<number> {
|
||||
const setColumns = Object.keys(data).map(col => `${col} = ?`).join(', ');
|
||||
const whereColumns = Object.keys(where).map(col => `${col} = ?`).join(' AND ');
|
||||
|
||||
const sql = `UPDATE ${table} SET ${setColumns} WHERE ${whereColumns}`;
|
||||
const values = [...Object.values(data), ...Object.values(where)];
|
||||
|
||||
const pool = await this.getPool();
|
||||
try {
|
||||
const [result] = await pool.execute(sql, values);
|
||||
return (result as any).affectedRows;
|
||||
} catch (error) {
|
||||
console.error('更新執行失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 執行刪除
|
||||
public async delete(table: string, where: Record<string, any>): Promise<number> {
|
||||
const whereColumns = Object.keys(where).map(col => `${col} = ?`).join(' AND ');
|
||||
const sql = `DELETE FROM ${table} WHERE ${whereColumns}`;
|
||||
const values = Object.values(where);
|
||||
|
||||
const pool = await this.getPool();
|
||||
try {
|
||||
const [result] = await pool.execute(sql, values);
|
||||
return (result as any).affectedRows;
|
||||
} catch (error) {
|
||||
console.error('刪除執行失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 開始事務
|
||||
public async beginTransaction(): Promise<mysql.PoolConnection> {
|
||||
const pool = await this.getPool();
|
||||
const connection = await pool.getConnection();
|
||||
await connection.beginTransaction();
|
||||
return connection;
|
||||
}
|
||||
|
||||
// 提交事務
|
||||
public async commitTransaction(connection: mysql.PoolConnection): Promise<void> {
|
||||
await connection.commit();
|
||||
connection.release();
|
||||
}
|
||||
|
||||
// 回滾事務
|
||||
public async rollbackTransaction(connection: mysql.PoolConnection): Promise<void> {
|
||||
await connection.rollback();
|
||||
connection.release();
|
||||
}
|
||||
|
||||
// 關閉連接池
|
||||
public async close(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
console.log('🔌 資料庫連接池已關閉');
|
||||
}
|
||||
}
|
||||
|
||||
// 健康檢查
|
||||
public async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.queryOne('SELECT 1 as health');
|
||||
return result?.health === 1;
|
||||
} catch (error) {
|
||||
console.error('資料庫健康檢查失敗:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取資料庫統計
|
||||
public async getDatabaseStats(): Promise<{
|
||||
tables: number;
|
||||
users: number;
|
||||
competitions: number;
|
||||
apps: number;
|
||||
judges: number;
|
||||
}> {
|
||||
try {
|
||||
const tablesResult = await this.queryOne(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
`, [dbConfig.database]);
|
||||
|
||||
const usersResult = await this.queryOne('SELECT COUNT(*) as count FROM users');
|
||||
const competitionsResult = await this.queryOne('SELECT COUNT(*) as count FROM competitions');
|
||||
const appsResult = await this.queryOne('SELECT COUNT(*) as count FROM apps');
|
||||
const judgesResult = await this.queryOne('SELECT COUNT(*) as count FROM judges');
|
||||
|
||||
return {
|
||||
tables: tablesResult?.count || 0,
|
||||
users: usersResult?.count || 0,
|
||||
competitions: competitionsResult?.count || 0,
|
||||
apps: appsResult?.count || 0,
|
||||
judges: judgesResult?.count || 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('獲取資料庫統計失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 導出單例實例
|
||||
export const db = Database.getInstance();
|
||||
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
// 工具函數
|
||||
export const generateId = (): string => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
};
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
const saltRounds = 12;
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
};
|
||||
|
||||
export const comparePassword = async (password: string, hash: string): Promise<boolean> => {
|
||||
return bcrypt.compare(password, hash);
|
||||
};
|
163
lib/logger.ts
Normal file
163
lib/logger.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// 日誌級別
|
||||
export enum LogLevel {
|
||||
ERROR = 0,
|
||||
WARN = 1,
|
||||
INFO = 2,
|
||||
DEBUG = 3
|
||||
}
|
||||
|
||||
// 日誌配置
|
||||
const LOG_LEVEL = (process.env.LOG_LEVEL as LogLevel) || LogLevel.INFO;
|
||||
const LOG_FILE = process.env.LOG_FILE || './logs/app.log';
|
||||
|
||||
// 確保日誌目錄存在
|
||||
const logDir = path.dirname(LOG_FILE);
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 日誌顏色
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
green: '\x1b[32m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
// 日誌級別名稱
|
||||
const levelNames = {
|
||||
[LogLevel.ERROR]: 'ERROR',
|
||||
[LogLevel.WARN]: 'WARN',
|
||||
[LogLevel.INFO]: 'INFO',
|
||||
[LogLevel.DEBUG]: 'DEBUG'
|
||||
};
|
||||
|
||||
// 日誌級別顏色
|
||||
const levelColors = {
|
||||
[LogLevel.ERROR]: colors.red,
|
||||
[LogLevel.WARN]: colors.yellow,
|
||||
[LogLevel.INFO]: colors.green,
|
||||
[LogLevel.DEBUG]: colors.blue
|
||||
};
|
||||
|
||||
// 寫入檔案日誌
|
||||
function writeToFile(level: LogLevel, message: string, data?: any) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const levelName = levelNames[level];
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
level: levelName,
|
||||
message,
|
||||
data: data || null
|
||||
};
|
||||
|
||||
const logLine = JSON.stringify(logEntry) + '\n';
|
||||
|
||||
try {
|
||||
fs.appendFileSync(LOG_FILE, logLine);
|
||||
} catch (error) {
|
||||
console.error('Failed to write to log file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 控制台輸出
|
||||
function consoleOutput(level: LogLevel, message: string, data?: any) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const levelName = levelNames[level];
|
||||
const color = levelColors[level];
|
||||
|
||||
let output = `${color}[${levelName}]${colors.reset} ${timestamp} - ${message}`;
|
||||
|
||||
if (data) {
|
||||
output += `\n${color}Data:${colors.reset} ${JSON.stringify(data, null, 2)}`;
|
||||
}
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
// 主日誌函數
|
||||
function log(level: LogLevel, message: string, data?: any) {
|
||||
if (level <= LOG_LEVEL) {
|
||||
consoleOutput(level, message, data);
|
||||
writeToFile(level, message, data);
|
||||
}
|
||||
}
|
||||
|
||||
// 日誌類別
|
||||
export class Logger {
|
||||
private context: string;
|
||||
|
||||
constructor(context: string = 'App') {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
error(message: string, data?: any) {
|
||||
log(LogLevel.ERROR, `[${this.context}] ${message}`, data);
|
||||
}
|
||||
|
||||
warn(message: string, data?: any) {
|
||||
log(LogLevel.WARN, `[${this.context}] ${message}`, data);
|
||||
}
|
||||
|
||||
info(message: string, data?: any) {
|
||||
log(LogLevel.INFO, `[${this.context}] ${message}`, data);
|
||||
}
|
||||
|
||||
debug(message: string, data?: any) {
|
||||
log(LogLevel.DEBUG, `[${this.context}] ${message}`, data);
|
||||
}
|
||||
|
||||
// API 請求日誌
|
||||
logRequest(method: string, url: string, statusCode: number, duration: number, userId?: string) {
|
||||
this.info('API Request', {
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
duration: `${duration}ms`,
|
||||
userId
|
||||
});
|
||||
}
|
||||
|
||||
// 認證日誌
|
||||
logAuth(action: string, email: string, success: boolean, ip?: string) {
|
||||
this.info('Authentication', {
|
||||
action,
|
||||
email,
|
||||
success,
|
||||
ip
|
||||
});
|
||||
}
|
||||
|
||||
// 資料庫操作日誌
|
||||
logDatabase(operation: string, table: string, duration: number, success: boolean) {
|
||||
this.debug('Database Operation', {
|
||||
operation,
|
||||
table,
|
||||
duration: `${duration}ms`,
|
||||
success
|
||||
});
|
||||
}
|
||||
|
||||
// 錯誤日誌
|
||||
logError(error: Error, context?: string) {
|
||||
this.error('Application Error', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
context: context || this.context
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 預設日誌實例
|
||||
export const logger = new Logger();
|
||||
|
||||
// 建立特定上下文的日誌實例
|
||||
export function createLogger(context: string): Logger {
|
||||
return new Logger(context);
|
||||
}
|
Reference in New Issue
Block a user