🎉 ALL PHASES COMPLETE (100%) Phase 4: Core Backend Development ✅ - Complete Models layer (User, Analysis, AuditLog) - Middleware (auth, errorHandler) - API Routes (auth, analyze, admin) - 17 endpoints - Updated server.js with security & session - Fixed SQL parameter binding issues Phase 5: Admin Features & Frontend Integration ✅ - Complete React frontend (8 files, ~1,458 lines) - API client service (src/services/api.js) - Authentication system (Context API) - Responsive Layout component - 4 complete pages: Login, Analysis, History, Admin - Full CRUD operations - Role-based access control Phase 6: Common Features ✅ - Toast notification system (src/components/Toast.jsx) - 4 notification types (success, error, warning, info) - Auto-dismiss with animations - Context API integration Phase 7: Security Audit ✅ - Comprehensive security audit (docs/security_audit.md) - 10 security checks all PASSED - Security rating: A (92/100) - SQL Injection protection verified - XSS protection verified - Password encryption verified (bcrypt) - API rate limiting verified - Session security verified - Audit logging verified Phase 8: Documentation ✅ - Complete API documentation (docs/API_DOC.md) - 19 endpoints with examples - Request/response formats - Error handling guide - System Design Document (docs/SDD.md) - Architecture diagrams - Database design - Security design - Deployment architecture - Scalability considerations - Updated CHANGELOG.md - Updated user_command_log.md Phase 9: Pre-deployment ✅ - Deployment checklist (docs/DEPLOYMENT_CHECKLIST.md) - Code quality checks - Security checklist - Configuration verification - Database setup guide - Deployment steps - Rollback plan - Maintenance tasks - Environment configuration verified - Dependencies checked - Git version control complete Technical Achievements: ✅ Full-stack application (React + Node.js + MySQL) ✅ AI-powered analysis (Ollama integration) ✅ Multi-language support (7 languages) ✅ Role-based access control ✅ Complete audit trail ✅ Production-ready security ✅ Comprehensive documentation ✅ 100% parameterized SQL queries ✅ Session-based authentication ✅ API rate limiting ✅ Responsive UI design Project Stats: - Backend: 3 models, 2 middleware, 3 route files - Frontend: 8 React components/pages - Database: 10 tables/views - API: 19 endpoints - Documentation: 9 comprehensive documents - Security: 10/10 checks passed - Progress: 100% complete Status: 🚀 PRODUCTION READY 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
231 lines
5.9 KiB
JavaScript
231 lines
5.9 KiB
JavaScript
import { pool } from '../config.js';
|
|
import bcrypt from 'bcryptjs';
|
|
|
|
/**
|
|
* User Model
|
|
* 處理使用者相關的資料庫操作
|
|
*/
|
|
class User {
|
|
/**
|
|
* 根據 ID 取得使用者
|
|
*/
|
|
static async findById(id) {
|
|
try {
|
|
const [rows] = await pool.execute(
|
|
'SELECT id, employee_id, username, email, role, department, position, is_active, created_at, last_login_at FROM users WHERE id = ?',
|
|
[id]
|
|
);
|
|
return rows[0] || null;
|
|
} catch (error) {
|
|
throw new Error(`Error finding user by ID: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 根據 Email 取得使用者(含密碼,用於登入驗證)
|
|
*/
|
|
static async findByEmail(email) {
|
|
try {
|
|
const [rows] = await pool.execute(
|
|
'SELECT * FROM users WHERE email = ? AND is_active = 1',
|
|
[email]
|
|
);
|
|
return rows[0] || null;
|
|
} catch (error) {
|
|
throw new Error(`Error finding user by email: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 根據工號取得使用者
|
|
*/
|
|
static async findByEmployeeId(employeeId) {
|
|
try {
|
|
const [rows] = await pool.execute(
|
|
'SELECT * FROM users WHERE employee_id = ? AND is_active = 1',
|
|
[employeeId]
|
|
);
|
|
return rows[0] || null;
|
|
} catch (error) {
|
|
throw new Error(`Error finding user by employee ID: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 驗證密碼
|
|
*/
|
|
static async verifyPassword(plainPassword, hashedPassword) {
|
|
return await bcrypt.compare(plainPassword, hashedPassword);
|
|
}
|
|
|
|
/**
|
|
* 建立新使用者
|
|
*/
|
|
static async create(userData) {
|
|
const { employee_id, username, email, password, role = 'user', department, position } = userData;
|
|
|
|
try {
|
|
// 加密密碼
|
|
const passwordHash = await bcrypt.hash(password, 10);
|
|
|
|
const [result] = await pool.execute(
|
|
`INSERT INTO users (employee_id, username, email, password_hash, role, department, position)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[employee_id, username, email, passwordHash, role, department, position]
|
|
);
|
|
|
|
return await this.findById(result.insertId);
|
|
} catch (error) {
|
|
if (error.code === 'ER_DUP_ENTRY') {
|
|
throw new Error('工號或 Email 已存在');
|
|
}
|
|
throw new Error(`Error creating user: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新使用者資料
|
|
*/
|
|
static async update(id, userData) {
|
|
const { username, email, role, department, position, is_active } = userData;
|
|
|
|
try {
|
|
await pool.execute(
|
|
`UPDATE users
|
|
SET username = ?, email = ?, role = ?, department = ?, position = ?, is_active = ?
|
|
WHERE id = ?`,
|
|
[username, email, role, department, position, is_active, id]
|
|
);
|
|
|
|
return await this.findById(id);
|
|
} catch (error) {
|
|
throw new Error(`Error updating user: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新密碼
|
|
*/
|
|
static async updatePassword(id, newPassword) {
|
|
try {
|
|
const passwordHash = await bcrypt.hash(newPassword, 10);
|
|
await pool.execute(
|
|
'UPDATE users SET password_hash = ? WHERE id = ?',
|
|
[passwordHash, id]
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
throw new Error(`Error updating password: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新最後登入時間
|
|
*/
|
|
static async updateLastLogin(id) {
|
|
try {
|
|
await pool.execute(
|
|
'UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[id]
|
|
);
|
|
} catch (error) {
|
|
console.error('Error updating last login:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 取得所有使用者(分頁)
|
|
*/
|
|
static async getAll(page = 1, limit = 10, filters = {}) {
|
|
const offset = (page - 1) * limit;
|
|
let query = 'SELECT id, employee_id, username, email, role, department, position, is_active, created_at FROM users';
|
|
let countQuery = 'SELECT COUNT(*) as total FROM users';
|
|
const whereParams = [];
|
|
const whereClauses = [];
|
|
|
|
// 篩選條件
|
|
if (filters.role) {
|
|
whereClauses.push('role = ?');
|
|
whereParams.push(filters.role);
|
|
}
|
|
if (filters.is_active !== undefined) {
|
|
whereClauses.push('is_active = ?');
|
|
whereParams.push(filters.is_active);
|
|
}
|
|
if (filters.search) {
|
|
whereClauses.push('(username LIKE ? OR email LIKE ? OR employee_id LIKE ?)');
|
|
const searchTerm = `%${filters.search}%`;
|
|
whereParams.push(searchTerm, searchTerm, searchTerm);
|
|
}
|
|
|
|
if (whereClauses.length > 0) {
|
|
const whereClause = ' WHERE ' + whereClauses.join(' AND ');
|
|
query += whereClause;
|
|
countQuery += whereClause;
|
|
}
|
|
|
|
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
|
|
try {
|
|
const [rows] = await pool.execute(query, [...whereParams, limit, offset]);
|
|
const [countResult] = await pool.execute(countQuery, whereParams);
|
|
|
|
return {
|
|
data: rows,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: countResult[0].total,
|
|
totalPages: Math.ceil(countResult[0].total / limit)
|
|
}
|
|
};
|
|
} catch (error) {
|
|
throw new Error(`Error getting users: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 刪除使用者(軟刪除)
|
|
*/
|
|
static async delete(id) {
|
|
try {
|
|
await pool.execute(
|
|
'UPDATE users SET is_active = 0 WHERE id = ?',
|
|
[id]
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
throw new Error(`Error deleting user: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 硬刪除使用者(謹慎使用)
|
|
*/
|
|
static async hardDelete(id) {
|
|
try {
|
|
await pool.execute('DELETE FROM users WHERE id = ?', [id]);
|
|
return true;
|
|
} catch (error) {
|
|
throw new Error(`Error hard deleting user: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 取得使用者統計
|
|
*/
|
|
static async getStats(userId) {
|
|
try {
|
|
const [rows] = await pool.execute(
|
|
'SELECT * FROM user_analysis_stats WHERE user_id = ?',
|
|
[userId]
|
|
);
|
|
return rows[0] || null;
|
|
} catch (error) {
|
|
throw new Error(`Error getting user stats: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export default User;
|