實作註冊、登入功能
This commit is contained in:
85
DATABASE_SETUP.md
Normal file
85
DATABASE_SETUP.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 資料庫設置指南
|
||||||
|
|
||||||
|
## 環境變數設置
|
||||||
|
|
||||||
|
請在專案根目錄建立 `.env.local` 檔案,並加入以下內容:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 資料庫配置
|
||||||
|
DB_HOST=mysql.theaken.com
|
||||||
|
DB_PORT=33306
|
||||||
|
DB_NAME=db_hr_assessment
|
||||||
|
DB_USER=hr_assessment
|
||||||
|
DB_PASSWORD=QFOts8FlibiI
|
||||||
|
|
||||||
|
# Next.js 環境變數
|
||||||
|
NEXTAUTH_SECRET=your-secret-key-here
|
||||||
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# 其他配置
|
||||||
|
NODE_ENV=development
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安裝依賴
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# 或
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 資料庫初始化
|
||||||
|
|
||||||
|
資料庫會在應用程式啟動時自動初始化,包括:
|
||||||
|
|
||||||
|
1. 建立用戶表
|
||||||
|
2. 建立預設管理員和測試用戶
|
||||||
|
|
||||||
|
### 預設用戶
|
||||||
|
|
||||||
|
- **管理員帳號**: admin@company.com / admin123
|
||||||
|
- **測試用戶**: user@company.com / user123
|
||||||
|
|
||||||
|
## 資料庫結構
|
||||||
|
|
||||||
|
### users 表
|
||||||
|
|
||||||
|
| 欄位 | 類型 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | VARCHAR(36) | 用戶唯一識別碼 (UUID) |
|
||||||
|
| name | VARCHAR(255) | 用戶姓名 |
|
||||||
|
| email | VARCHAR(255) | 電子郵件 (唯一) |
|
||||||
|
| password | VARCHAR(255) | 密碼 |
|
||||||
|
| department | VARCHAR(100) | 部門 |
|
||||||
|
| role | ENUM('admin', 'user') | 角色 |
|
||||||
|
| created_at | TIMESTAMP | 建立時間 |
|
||||||
|
| updated_at | TIMESTAMP | 更新時間 |
|
||||||
|
|
||||||
|
## 手動種子資料庫
|
||||||
|
|
||||||
|
如果需要重新種子資料庫,可以執行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx lib/database/seed.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 密碼安全
|
||||||
|
|
||||||
|
- 所有密碼都使用 bcrypt 進行雜湊處理
|
||||||
|
- 雜湊強度:12 rounds
|
||||||
|
- 密碼在資料庫中以雜湊形式儲存,不會以明文顯示
|
||||||
|
|
||||||
|
## 可用的腳本
|
||||||
|
|
||||||
|
- `npm run test-db` - 測試資料庫連接
|
||||||
|
- `npm run check-passwords` - 檢查密碼雜湊狀態
|
||||||
|
- `npm run reset-users` - 重新建立用戶數據(使用雜湊密碼)
|
||||||
|
- `npm run test-login` - 測試登入功能(需要先啟動開發伺服器)
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
1. 確保資料庫伺服器可訪問
|
||||||
|
2. 密碼已使用 bcrypt 進行雜湊處理,安全性更高
|
||||||
|
3. 環境變數檔案 `.env.local` 不會被提交到版本控制
|
||||||
|
4. 資料庫連接使用連接池以提高效能
|
||||||
|
5. JWT 密碼已設置在環境變數中
|
60
app/api/auth/login/route.ts
Normal file
60
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { verifyUserPassword } from '@/lib/database/models/user'
|
||||||
|
import { initializeDatabase } from '@/lib/database/init'
|
||||||
|
import { generateToken, generateRefreshToken } from '@/lib/utils/jwt'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// 確保資料庫已初始化
|
||||||
|
await initializeDatabase()
|
||||||
|
|
||||||
|
const { email, password } = await request.json()
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '請提供電子郵件和密碼' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證用戶密碼
|
||||||
|
const user = await verifyUserPassword(email, password)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '帳號或密碼錯誤' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 JWT tokens
|
||||||
|
const tokenPayload = {
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
name: user.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = generateToken(tokenPayload)
|
||||||
|
const refreshToken = generateRefreshToken(tokenPayload)
|
||||||
|
|
||||||
|
// 移除密碼並返回用戶資料
|
||||||
|
const { password: _, ...userWithoutPassword } = user
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
...userWithoutPassword,
|
||||||
|
createdAt: user.created_at,
|
||||||
|
},
|
||||||
|
accessToken,
|
||||||
|
refreshToken
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登入錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '登入失敗,請稍後再試' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
68
app/api/auth/register/route.ts
Normal file
68
app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { createUser, isEmailExists } from '@/lib/database/models/user'
|
||||||
|
import { initializeDatabase } from '@/lib/database/init'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// 確保資料庫已初始化
|
||||||
|
await initializeDatabase()
|
||||||
|
|
||||||
|
const { name, email, password, department, role } = await request.json()
|
||||||
|
|
||||||
|
if (!name || !email || !password || !department) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '請填寫所有必填欄位' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '密碼長度至少需要6個字元' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查電子郵件是否已存在
|
||||||
|
const emailExists = await isEmailExists(email)
|
||||||
|
if (emailExists) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '該電子郵件已被使用' },
|
||||||
|
{ status: 409 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立新用戶
|
||||||
|
const newUser = await createUser({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password, // 在實際應用中應該加密
|
||||||
|
department,
|
||||||
|
role: role || 'user'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!newUser) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '註冊失敗,請稍後再試' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除密碼並返回用戶資料
|
||||||
|
const { password: _, ...userWithoutPassword } = newUser
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
...userWithoutPassword,
|
||||||
|
createdAt: newUser.created_at,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('註冊錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '註冊失敗,請稍後再試' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
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 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() {
|
export function useAuth() {
|
||||||
const [user, setUser] = useState<User | null>(null)
|
const [user, setUser] = useState<User | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 初始化預設用戶數據
|
|
||||||
const existingUsers = localStorage.getItem("hr_users")
|
|
||||||
if (!existingUsers) {
|
|
||||||
localStorage.setItem("hr_users", JSON.stringify(defaultUsers))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 檢查是否有已登入的用戶
|
// 檢查是否有已登入的用戶
|
||||||
const currentUser = localStorage.getItem("hr_current_user")
|
const currentUser = localStorage.getItem("hr_current_user")
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
@@ -63,40 +35,55 @@ export function useAuth() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const login = async (email: string, password: string): Promise<boolean> => {
|
const login = async (email: string, password: string): Promise<boolean> => {
|
||||||
const users = JSON.parse(localStorage.getItem("hr_users") || "[]")
|
try {
|
||||||
const user = users.find((u: any) => u.email === email && u.password === password)
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
})
|
||||||
|
|
||||||
if (user) {
|
const data = await response.json()
|
||||||
const { password: _, ...userWithoutPassword } = user
|
|
||||||
setUser(userWithoutPassword)
|
if (data.success && data.user) {
|
||||||
localStorage.setItem("hr_current_user", JSON.stringify(userWithoutPassword))
|
setUser(data.user)
|
||||||
return true
|
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 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),
|
||||||
|
})
|
||||||
|
|
||||||
// 檢查電子郵件是否已存在
|
const data = await response.json()
|
||||||
if (users.some((u: any) => u.email === userData.email)) {
|
|
||||||
|
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 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 = () => {
|
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)
|
||||||
|
}
|
400
package-lock.json
generated
400
package-lock.json
generated
@@ -38,6 +38,7 @@
|
|||||||
"@radix-ui/react-tooltip": "1.1.6",
|
"@radix-ui/react-tooltip": "1.1.6",
|
||||||
"@vercel/analytics": "1.3.1",
|
"@vercel/analytics": "1.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
@@ -45,9 +46,12 @@
|
|||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
|
"mysql2": "^3.15.1",
|
||||||
"next": "14.2.16",
|
"next": "14.2.16",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "9.8.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
@@ -63,6 +67,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
@@ -2241,6 +2247,13 @@
|
|||||||
"tailwindcss": "4.1.13"
|
"tailwindcss": "4.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcryptjs": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-array": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
@@ -2304,6 +2317,24 @@
|
|||||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||||
|
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.18.6",
|
"version": "22.18.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz",
|
||||||
@@ -2421,6 +2452,15 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aws-ssl-profiles": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.6",
|
"version": "2.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
|
||||||
@@ -2430,6 +2470,15 @@
|
|||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.26.2",
|
"version": "4.26.2",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
|
||||||
@@ -2463,6 +2512,12 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@@ -2708,6 +2763,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/data-uri-to-buffer": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
@@ -2730,6 +2794,15 @@
|
|||||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/denque": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
|
||||||
@@ -2756,6 +2829,15 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.223",
|
"version": "1.5.223",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz",
|
||||||
@@ -2828,6 +2910,41 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fetch-blob": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-domexception": "^1.0.0",
|
||||||
|
"web-streams-polyfill": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20 || >= 14.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/formdata-polyfill": {
|
||||||
|
"version": "4.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
|
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fetch-blob": "^3.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/frac": {
|
"node_modules/frac": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
@@ -2859,6 +2976,15 @@
|
|||||||
"next": ">=13.2.0"
|
"next": ">=13.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/generate-function": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-property": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-nonce": {
|
"node_modules/get-nonce": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
@@ -2874,6 +3000,22 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/input-otp": {
|
"node_modules/input-otp": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz",
|
||||||
@@ -2893,6 +3035,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-property": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz",
|
||||||
@@ -2909,6 +3057,49 @@
|
|||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^3.2.2",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^1.4.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||||
@@ -3154,6 +3345,54 @@
|
|||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -3166,6 +3405,30 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "7.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||||
|
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru.min": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=1.0.0",
|
||||||
|
"deno": ">=1.30.0",
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wellwelwel"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.454.0",
|
"version": "0.454.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz",
|
||||||
@@ -3208,6 +3471,44 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/mysql2": {
|
||||||
|
"version": "3.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.1.tgz",
|
||||||
|
"integrity": "sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"aws-ssl-profiles": "^1.1.1",
|
||||||
|
"denque": "^2.1.0",
|
||||||
|
"generate-function": "^2.3.1",
|
||||||
|
"iconv-lite": "^0.7.0",
|
||||||
|
"long": "^5.2.1",
|
||||||
|
"lru.min": "^1.0.0",
|
||||||
|
"named-placeholders": "^1.1.3",
|
||||||
|
"seq-queue": "^0.0.5",
|
||||||
|
"sqlstring": "^2.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/named-placeholders": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^7.14.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -3314,6 +3615,44 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-domexception": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
|
"deprecated": "Use your platform's native DOMException instead",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"data-uri-to-buffer": "^4.0.0",
|
||||||
|
"fetch-blob": "^3.1.4",
|
||||||
|
"formdata-polyfill": "^4.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/node-fetch"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.21",
|
"version": "2.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
|
||||||
@@ -3605,6 +3944,32 @@
|
|||||||
"decimal.js-light": "^2.4.1"
|
"decimal.js-light": "^2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
@@ -3614,6 +3979,23 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/seq-queue": {
|
||||||
|
"version": "0.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||||
|
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||||
|
},
|
||||||
"node_modules/server-only": {
|
"node_modules/server-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
|
||||||
@@ -3639,6 +4021,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sqlstring": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ssf": {
|
"node_modules/ssf": {
|
||||||
"version": "0.11.2",
|
"version": "0.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
@@ -3898,6 +4289,15 @@
|
|||||||
"d3-timer": "^3.0.1"
|
"d3-timer": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wmf": {
|
"node_modules/wmf": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
15
package.json
15
package.json
@@ -6,7 +6,12 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"test-db": "node scripts/test-db.js",
|
||||||
|
"test-login": "node scripts/test-login.js",
|
||||||
|
"check-passwords": "node scripts/check-passwords.js",
|
||||||
|
"seed-db": "npx tsx lib/database/seed.ts",
|
||||||
|
"reset-users": "npx tsx lib/database/reset-users.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
@@ -39,6 +44,7 @@
|
|||||||
"@radix-ui/react-tooltip": "1.1.6",
|
"@radix-ui/react-tooltip": "1.1.6",
|
||||||
"@vercel/analytics": "1.3.1",
|
"@vercel/analytics": "1.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
@@ -46,9 +52,12 @@
|
|||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
|
"mysql2": "^3.15.1",
|
||||||
"next": "14.2.16",
|
"next": "14.2.16",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "9.8.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
@@ -64,6 +73,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
@@ -72,4 +83,4 @@
|
|||||||
"tw-animate-css": "1.3.3",
|
"tw-animate-css": "1.3.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3221
pnpm-lock.yaml
generated
3221
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
40
scripts/check-passwords.js
Normal file
40
scripts/check-passwords.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const mysql = require('mysql2/promise')
|
||||||
|
|
||||||
|
async function checkPasswords() {
|
||||||
|
const config = {
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 正在檢查用戶密碼狀態...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const connection = await mysql.createConnection(config)
|
||||||
|
|
||||||
|
const [rows] = await connection.execute('SELECT id, name, email, password FROM users ORDER BY created_at')
|
||||||
|
|
||||||
|
console.log('\n📋 用戶密碼狀態:')
|
||||||
|
console.log('=' .repeat(80))
|
||||||
|
|
||||||
|
rows.forEach((user, index) => {
|
||||||
|
const isHashed = user.password.startsWith('$2b$') || user.password.startsWith('$2a$')
|
||||||
|
const passwordStatus = isHashed ? '✅ 已雜湊' : '❌ 明文'
|
||||||
|
const passwordPreview = isHashed ? user.password.substring(0, 20) + '...' : user.password
|
||||||
|
|
||||||
|
console.log(`${index + 1}. ${user.name} (${user.email})`)
|
||||||
|
console.log(` 密碼狀態: ${passwordStatus}`)
|
||||||
|
console.log(` 密碼內容: ${passwordPreview}`)
|
||||||
|
console.log('')
|
||||||
|
})
|
||||||
|
|
||||||
|
await connection.end()
|
||||||
|
console.log('✅ 檢查完成')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 檢查失敗:', error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPasswords()
|
36
scripts/test-db.js
Normal file
36
scripts/test-db.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const mysql = require('mysql2/promise')
|
||||||
|
|
||||||
|
async function testDatabaseConnection() {
|
||||||
|
const config = {
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 正在測試資料庫連接...')
|
||||||
|
console.log('連接資訊:', {
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
user: config.user,
|
||||||
|
database: config.database
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const connection = await mysql.createConnection(config)
|
||||||
|
await connection.ping()
|
||||||
|
console.log('✅ 資料庫連接成功!')
|
||||||
|
|
||||||
|
// 測試查詢
|
||||||
|
const [rows] = await connection.execute('SELECT 1 as test')
|
||||||
|
console.log('✅ 查詢測試成功:', rows)
|
||||||
|
|
||||||
|
await connection.end()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 資料庫連接失敗:', error.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testDatabaseConnection()
|
43
scripts/test-login.js
Normal file
43
scripts/test-login.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const fetch = require('node-fetch')
|
||||||
|
|
||||||
|
async function testLogin() {
|
||||||
|
console.log('🔄 正在測試登入功能...')
|
||||||
|
|
||||||
|
const testUsers = [
|
||||||
|
{ email: 'admin@company.com', password: 'admin123' },
|
||||||
|
{ email: 'user@company.com', password: 'user123' },
|
||||||
|
{ email: 'manager@company.com', password: 'manager123' },
|
||||||
|
{ email: 'test@company.com', password: 'test123' }
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const user of testUsers) {
|
||||||
|
try {
|
||||||
|
console.log(`\n測試用戶: ${user.email}`)
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:3000/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(user),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
console.log(`✅ 登入成功: ${data.user.name}`)
|
||||||
|
console.log(` Role: ${data.user.role}`)
|
||||||
|
console.log(` Token: ${data.accessToken ? '已生成' : '未生成'}`)
|
||||||
|
} else {
|
||||||
|
console.log(`❌ 登入失敗: ${data.error}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`❌ 請求失敗: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果直接執行此檔案,則執行測試
|
||||||
|
if (require.main === module) {
|
||||||
|
testLogin().catch(console.error)
|
||||||
|
}
|
Reference in New Issue
Block a user