實作註冊、登入功能

This commit is contained in:
2025-09-28 23:34:32 +08:00
parent 36eb088983
commit 9b224fa7e1
17 changed files with 4468 additions and 57 deletions

85
DATABASE_SETUP.md Normal file
View 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 密碼已設置在環境變數中

View 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 }
)
}
}

View 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 }
)
}
}

View 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
View 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
View 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
}

View 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
View 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)
}

View File

@@ -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
View 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
View 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
View File

@@ -38,6 +38,7 @@
"@radix-ui/react-tooltip": "1.1.6",
"@vercel/analytics": "1.3.1",
"autoprefixer": "^10.4.20",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
@@ -45,9 +46,12 @@
"embla-carousel-react": "8.5.1",
"geist": "^1.3.1",
"input-otp": "1.4.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.454.0",
"mysql2": "^3.15.1",
"next": "14.2.16",
"next-themes": "^0.4.6",
"node-fetch": "^3.3.2",
"react": "^18",
"react-day-picker": "9.8.0",
"react-dom": "^18",
@@ -63,6 +67,8 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
@@ -2241,6 +2247,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": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@@ -2304,6 +2317,24 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"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": {
"version": "22.18.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz",
@@ -2421,6 +2452,15 @@
"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": {
"version": "2.8.6",
"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"
}
},
"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": {
"version": "4.26.2",
"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_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": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -2708,6 +2763,15 @@
"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": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
@@ -2730,6 +2794,15 @@
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
@@ -2756,6 +2829,15 @@
"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": {
"version": "1.5.223",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz",
@@ -2828,6 +2910,41 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
@@ -2859,6 +2976,15 @@
"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": {
"version": "1.0.1",
"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==",
"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": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz",
@@ -2893,6 +3035,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": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz",
@@ -2909,6 +3057,49 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"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": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
@@ -3154,6 +3345,54 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"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": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -3166,6 +3405,30 @@
"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": {
"version": "0.454.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz",
@@ -3208,6 +3471,44 @@
"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": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -3314,6 +3615,44 @@
"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": {
"version": "2.0.21",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
@@ -3605,6 +3944,32 @@
"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": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -3614,6 +3979,23 @@
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
@@ -3639,6 +4021,15 @@
"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": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
@@ -3898,6 +4289,15 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",

View File

@@ -6,7 +6,12 @@
"build": "next build",
"dev": "next dev",
"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": {
"@hookform/resolvers": "^3.10.0",
@@ -39,6 +44,7 @@
"@radix-ui/react-tooltip": "1.1.6",
"@vercel/analytics": "1.3.1",
"autoprefixer": "^10.4.20",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
@@ -46,9 +52,12 @@
"embla-carousel-react": "8.5.1",
"geist": "^1.3.1",
"input-otp": "1.4.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.454.0",
"mysql2": "^3.15.1",
"next": "14.2.16",
"next-themes": "^0.4.6",
"node-fetch": "^3.3.2",
"react": "^18",
"react-day-picker": "9.8.0",
"react-dom": "^18",
@@ -64,6 +73,8 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
@@ -72,4 +83,4 @@
"tw-animate-css": "1.3.3",
"typescript": "^5"
}
}
}

3221
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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)
}