實作註冊、登入功能
This commit is contained in:
77
lib/database/connection.ts
Normal file
77
lib/database/connection.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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 || 'hr_assessment',
|
||||
password: process.env.DB_PASSWORD || 'QFOts8FlibiI',
|
||||
database: process.env.DB_NAME || 'db_hr_assessment',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
acquireTimeout: 60000,
|
||||
timeout: 60000,
|
||||
reconnect: true,
|
||||
}
|
||||
|
||||
// 建立連接池
|
||||
let pool: mysql.Pool | null = null
|
||||
|
||||
export function getConnectionPool(): mysql.Pool {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(dbConfig)
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// 測試資料庫連接
|
||||
export async function testConnection(): Promise<boolean> {
|
||||
try {
|
||||
const connection = await getConnectionPool().getConnection()
|
||||
await connection.ping()
|
||||
connection.release()
|
||||
console.log('✅ 資料庫連接成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫連接失敗:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 關閉連接池
|
||||
export async function closeConnectionPool(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end()
|
||||
pool = null
|
||||
}
|
||||
}
|
||||
|
||||
// 執行查詢的輔助函數
|
||||
export async function executeQuery<T = any>(
|
||||
query: string,
|
||||
params: any[] = []
|
||||
): Promise<T[]> {
|
||||
try {
|
||||
const [rows] = await getConnectionPool().execute(query, params)
|
||||
return rows as T[]
|
||||
} catch (error) {
|
||||
console.error('查詢執行失敗:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 執行單一查詢的輔助函數
|
||||
export async function executeQueryOne<T = any>(
|
||||
query: string,
|
||||
params: any[] = []
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
const [rows] = await getConnectionPool().execute(query, params)
|
||||
const results = rows as T[]
|
||||
return results.length > 0 ? results[0] : null
|
||||
} catch (error) {
|
||||
console.error('查詢執行失敗:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
31
lib/database/init.ts
Normal file
31
lib/database/init.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { testConnection } from './connection'
|
||||
import { createUsersTable } from './models/user'
|
||||
|
||||
// 初始化資料庫
|
||||
export async function initializeDatabase(): Promise<boolean> {
|
||||
try {
|
||||
console.log('🔄 正在初始化資料庫...')
|
||||
|
||||
// 測試連接
|
||||
const isConnected = await testConnection()
|
||||
if (!isConnected) {
|
||||
console.error('❌ 無法連接到資料庫')
|
||||
return false
|
||||
}
|
||||
|
||||
// 建立用戶表
|
||||
await createUsersTable()
|
||||
|
||||
console.log('✅ 資料庫初始化完成')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫初始化失敗:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 在應用程式啟動時自動初始化
|
||||
if (typeof window === 'undefined') {
|
||||
// 只在伺服器端執行
|
||||
initializeDatabase().catch(console.error)
|
||||
}
|
152
lib/database/models/user.ts
Normal file
152
lib/database/models/user.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { executeQuery, executeQueryOne } from '../connection'
|
||||
import { hashPassword, verifyPassword } from '../../utils/password'
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
department: string
|
||||
role: 'admin' | 'user'
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface CreateUserData {
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
department: string
|
||||
role: 'admin' | 'user'
|
||||
}
|
||||
|
||||
export interface UpdateUserData {
|
||||
name?: string
|
||||
email?: string
|
||||
password?: string
|
||||
department?: string
|
||||
role?: 'admin' | 'user'
|
||||
}
|
||||
|
||||
// 建立用戶表(如果不存在)
|
||||
export async function createUsersTable(): Promise<void> {
|
||||
const createTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
department VARCHAR(100) NOT NULL,
|
||||
role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`
|
||||
|
||||
await executeQuery(createTableQuery)
|
||||
console.log('✅ 用戶表建立成功')
|
||||
}
|
||||
|
||||
// 根據 email 查找用戶
|
||||
export async function findUserByEmail(email: string): Promise<User | null> {
|
||||
const query = 'SELECT * FROM users WHERE email = ?'
|
||||
return await executeQueryOne<User>(query, [email])
|
||||
}
|
||||
|
||||
// 根據 ID 查找用戶
|
||||
export async function findUserById(id: string): Promise<User | null> {
|
||||
const query = 'SELECT * FROM users WHERE id = ?'
|
||||
return await executeQueryOne<User>(query, [id])
|
||||
}
|
||||
|
||||
// 建立新用戶
|
||||
export async function createUser(userData: CreateUserData): Promise<User | null> {
|
||||
const query = `
|
||||
INSERT INTO users (id, name, email, password, department, role)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
const { name, email, password, department, role } = userData
|
||||
|
||||
try {
|
||||
// 生成簡單的 UUID
|
||||
const userId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// 雜湊密碼
|
||||
const hashedPassword = await hashPassword(password)
|
||||
await executeQuery(query, [userId, name, email, hashedPassword, department, role])
|
||||
return await findUserByEmail(email)
|
||||
} catch (error) {
|
||||
console.error('建立用戶失敗:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用戶
|
||||
export async function updateUser(id: string, userData: UpdateUserData): Promise<User | null> {
|
||||
const fields = Object.keys(userData).filter(key => userData[key as keyof UpdateUserData] !== undefined)
|
||||
|
||||
if (fields.length === 0) {
|
||||
return await findUserById(id)
|
||||
}
|
||||
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ')
|
||||
const query = `UPDATE users SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
|
||||
|
||||
const values = fields.map(field => userData[field as keyof UpdateUserData])
|
||||
values.push(id)
|
||||
|
||||
try {
|
||||
await executeQuery(query, values)
|
||||
return await findUserById(id)
|
||||
} catch (error) {
|
||||
console.error('更新用戶失敗:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 刪除用戶
|
||||
export async function deleteUser(id: string): Promise<boolean> {
|
||||
const query = 'DELETE FROM users WHERE id = ?'
|
||||
|
||||
try {
|
||||
await executeQuery(query, [id])
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('刪除用戶失敗:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取所有用戶
|
||||
export async function getAllUsers(): Promise<User[]> {
|
||||
const query = 'SELECT * FROM users ORDER BY created_at DESC'
|
||||
return await executeQuery<User>(query)
|
||||
}
|
||||
|
||||
// 根據部門獲取用戶
|
||||
export async function getUsersByDepartment(department: string): Promise<User[]> {
|
||||
const query = 'SELECT * FROM users WHERE department = ? ORDER BY created_at DESC'
|
||||
return await executeQuery<User>(query, [department])
|
||||
}
|
||||
|
||||
// 檢查 email 是否已存在
|
||||
export async function isEmailExists(email: string): Promise<boolean> {
|
||||
const user = await findUserByEmail(email)
|
||||
return user !== null
|
||||
}
|
||||
|
||||
// 驗證用戶密碼
|
||||
export async function verifyUserPassword(email: string, password: string): Promise<User | null> {
|
||||
const user = await findUserByEmail(email)
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isValidPassword = await verifyPassword(password, user.password)
|
||||
if (!isValidPassword) {
|
||||
return null
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
88
lib/database/reset-users.ts
Normal file
88
lib/database/reset-users.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { executeQuery } from './connection'
|
||||
import { initializeDatabase } from './init'
|
||||
import { hashPasswordSync } from '../utils/password'
|
||||
|
||||
// 重新建立用戶數據(使用明文密碼)
|
||||
const resetUsers = [
|
||||
{
|
||||
name: "系統管理員",
|
||||
email: "admin@company.com",
|
||||
password: "admin123",
|
||||
department: "人力資源部",
|
||||
role: "admin",
|
||||
},
|
||||
{
|
||||
name: "張小明",
|
||||
email: "user@company.com",
|
||||
password: "user123",
|
||||
department: "資訊技術部",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
name: "李經理",
|
||||
email: "manager@company.com",
|
||||
password: "manager123",
|
||||
department: "管理部",
|
||||
role: "admin",
|
||||
},
|
||||
{
|
||||
name: "王測試",
|
||||
email: "test@company.com",
|
||||
password: "test123",
|
||||
department: "測試部",
|
||||
role: "user",
|
||||
}
|
||||
]
|
||||
|
||||
export async function resetUsersData(): Promise<void> {
|
||||
try {
|
||||
console.log('🔄 正在重新建立用戶數據...')
|
||||
|
||||
// 確保資料庫已初始化
|
||||
await initializeDatabase()
|
||||
|
||||
// 清空現有用戶數據
|
||||
await executeQuery('DELETE FROM users')
|
||||
console.log('✅ 已清空現有用戶數據')
|
||||
|
||||
// 重新插入用戶數據
|
||||
for (const user of resetUsers) {
|
||||
// 生成簡單的 UUID
|
||||
const userId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// 雜湊密碼
|
||||
const hashedPassword = hashPasswordSync(user.password)
|
||||
|
||||
const query = `
|
||||
INSERT INTO users (id, name, email, password, department, role)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
await executeQuery(query, [
|
||||
userId,
|
||||
user.name,
|
||||
user.email,
|
||||
hashedPassword,
|
||||
user.department,
|
||||
user.role
|
||||
])
|
||||
|
||||
console.log(`✅ 建立用戶: ${user.name} (${user.email}) - 密碼已雜湊`)
|
||||
}
|
||||
|
||||
console.log('✅ 用戶數據重新建立完成')
|
||||
console.log('\n📋 可用帳號:')
|
||||
resetUsers.forEach(user => {
|
||||
console.log(` ${user.name}: ${user.email} / ${user.password} (${user.role})`)
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 重新建立用戶數據失敗:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接執行此檔案,則執行重置
|
||||
if (require.main === module) {
|
||||
resetUsersData().catch(console.error)
|
||||
}
|
51
lib/database/seed.ts
Normal file
51
lib/database/seed.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createUser, isEmailExists } from './models/user'
|
||||
import { initializeDatabase } from './init'
|
||||
|
||||
// 預設用戶數據
|
||||
const defaultUsers = [
|
||||
{
|
||||
name: "系統管理員",
|
||||
email: "admin@company.com",
|
||||
password: "admin123",
|
||||
department: "人力資源部",
|
||||
role: "admin" as const,
|
||||
},
|
||||
{
|
||||
name: "張小明",
|
||||
email: "user@company.com",
|
||||
password: "user123",
|
||||
department: "資訊技術部",
|
||||
role: "user" as const,
|
||||
},
|
||||
]
|
||||
|
||||
// 種子資料庫
|
||||
export async function seedDatabase(): Promise<void> {
|
||||
try {
|
||||
console.log('🔄 正在種子資料庫...')
|
||||
|
||||
// 確保資料庫已初始化
|
||||
await initializeDatabase()
|
||||
|
||||
// 檢查並建立預設用戶
|
||||
for (const userData of defaultUsers) {
|
||||
const exists = await isEmailExists(userData.email)
|
||||
if (!exists) {
|
||||
await createUser(userData)
|
||||
console.log(`✅ 建立預設用戶: ${userData.name} (${userData.email})`)
|
||||
} else {
|
||||
console.log(`⏭️ 用戶已存在: ${userData.name} (${userData.email})`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ 資料庫種子完成')
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫種子失敗:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接執行此檔案,則執行種子
|
||||
if (require.main === module) {
|
||||
seedDatabase().catch(console.error)
|
||||
}
|
@@ -21,39 +21,11 @@ interface AuthContextType {
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
// 預設用戶數據
|
||||
const defaultUsers = [
|
||||
{
|
||||
id: "admin-1",
|
||||
name: "系統管理員",
|
||||
email: "admin@company.com",
|
||||
password: "admin123",
|
||||
department: "人力資源部",
|
||||
role: "admin" as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "user-1",
|
||||
name: "張小明",
|
||||
email: "user@company.com",
|
||||
password: "user123",
|
||||
department: "資訊技術部",
|
||||
role: "user" as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
|
||||
export function useAuth() {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化預設用戶數據
|
||||
const existingUsers = localStorage.getItem("hr_users")
|
||||
if (!existingUsers) {
|
||||
localStorage.setItem("hr_users", JSON.stringify(defaultUsers))
|
||||
}
|
||||
|
||||
// 檢查是否有已登入的用戶
|
||||
const currentUser = localStorage.getItem("hr_current_user")
|
||||
if (currentUser) {
|
||||
@@ -63,40 +35,55 @@ export function useAuth() {
|
||||
}, [])
|
||||
|
||||
const login = async (email: string, password: string): Promise<boolean> => {
|
||||
const users = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
||||
const user = users.find((u: any) => u.email === email && u.password === password)
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
|
||||
if (user) {
|
||||
const { password: _, ...userWithoutPassword } = user
|
||||
setUser(userWithoutPassword)
|
||||
localStorage.setItem("hr_current_user", JSON.stringify(userWithoutPassword))
|
||||
return true
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success && data.user) {
|
||||
setUser(data.user)
|
||||
localStorage.setItem("hr_current_user", JSON.stringify(data.user))
|
||||
return true
|
||||
} else {
|
||||
console.error('登入失敗:', data.error)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登入錯誤:', error)
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const register = async (userData: Omit<User, "id" | "createdAt"> & { password: string }): Promise<boolean> => {
|
||||
const users = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
||||
try {
|
||||
const response = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(userData),
|
||||
})
|
||||
|
||||
// 檢查電子郵件是否已存在
|
||||
if (users.some((u: any) => u.email === userData.email)) {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success && data.user) {
|
||||
setUser(data.user)
|
||||
localStorage.setItem("hr_current_user", JSON.stringify(data.user))
|
||||
return true
|
||||
} else {
|
||||
console.error('註冊失敗:', data.error)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('註冊錯誤:', error)
|
||||
return false
|
||||
}
|
||||
|
||||
const newUser = {
|
||||
...userData,
|
||||
id: `user-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
users.push(newUser)
|
||||
localStorage.setItem("hr_users", JSON.stringify(users))
|
||||
|
||||
const { password: _, ...userWithoutPassword } = newUser
|
||||
setUser(userWithoutPassword)
|
||||
localStorage.setItem("hr_current_user", JSON.stringify(userWithoutPassword))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
|
40
lib/utils/jwt.ts
Normal file
40
lib/utils/jwt.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'hr_assessment_super_secret_key_2024_secure_random_string'
|
||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'
|
||||
|
||||
export interface JWTPayload {
|
||||
userId: string
|
||||
email: string
|
||||
role: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// 生成 JWT Token
|
||||
export function generateToken(payload: JWTPayload): string {
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN })
|
||||
}
|
||||
|
||||
// 驗證 JWT Token
|
||||
export function verifyToken(token: string): JWTPayload | null {
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload
|
||||
return decoded
|
||||
} catch (error) {
|
||||
console.error('JWT 驗證失敗:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 從 Authorization header 中提取 token
|
||||
export function extractTokenFromHeader(authHeader: string | null): string | null {
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return null
|
||||
}
|
||||
return authHeader.substring(7)
|
||||
}
|
||||
|
||||
// 生成刷新 token
|
||||
export function generateRefreshToken(payload: JWTPayload): string {
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: '30d' })
|
||||
}
|
23
lib/utils/password.ts
Normal file
23
lib/utils/password.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
const SALT_ROUNDS = 12
|
||||
|
||||
// 雜湊密碼
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return await bcrypt.hash(password, SALT_ROUNDS)
|
||||
}
|
||||
|
||||
// 驗證密碼
|
||||
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||
return await bcrypt.compare(password, hashedPassword)
|
||||
}
|
||||
|
||||
// 同步雜湊密碼(用於種子數據)
|
||||
export function hashPasswordSync(password: string): string {
|
||||
return bcrypt.hashSync(password, SALT_ROUNDS)
|
||||
}
|
||||
|
||||
// 同步驗證密碼
|
||||
export function verifyPasswordSync(password: string, hashedPassword: string): boolean {
|
||||
return bcrypt.compareSync(password, hashedPassword)
|
||||
}
|
Reference in New Issue
Block a user