整合資料庫、完成登入註冊忘記密碼功能

This commit is contained in:
2025-09-09 12:00:22 +08:00
parent af88c0f037
commit 32b19e9a0f
85 changed files with 11672 additions and 2350 deletions

View File

@@ -0,0 +1,621 @@
// =====================================================
// 資料庫服務層
// =====================================================
import { db } from '../database';
import type {
User,
Judge,
Team,
TeamMember,
Competition,
CompetitionRule,
CompetitionAwardType,
App,
Proposal,
AppJudgeScore,
ProposalJudgeScore,
Award,
UserFavorite,
UserLike,
UserView,
UserRating,
ChatSession,
ChatMessage,
AIAssistantConfig,
SystemSetting,
ActivityLog,
UserStatistics,
AppStatistics,
CompetitionStatistics
} from '../models';
// =====================================================
// 用戶服務
// =====================================================
export class UserService {
// 創建用戶
async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
const sql = `
INSERT INTO users (id, name, email, password_hash, avatar, department, role, join_date, total_likes, total_views, is_active, last_login)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
userData.id,
userData.name,
userData.email,
userData.password_hash,
userData.avatar || null,
userData.department,
userData.role,
userData.join_date,
userData.total_likes,
userData.total_views,
userData.is_active,
userData.last_login || null
];
await db.insert(sql, params);
return await this.findByEmail(userData.email) as User;
}
// 根據郵箱獲取用戶
async findByEmail(email: string): Promise<User | null> {
const sql = 'SELECT * FROM users WHERE email = ? AND is_active = TRUE';
return await db.queryOne<User>(sql, [email]);
}
// 根據ID獲取用戶
async findById(id: string): Promise<User | null> {
const sql = 'SELECT * FROM users WHERE id = ? AND is_active = TRUE';
return await db.queryOne<User>(sql, [id]);
}
// 更新用戶
async update(id: string, updates: Partial<User>): Promise<User | null> {
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => (updates as any)[field]);
const sql = `UPDATE users SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
const result = await db.update(sql, [...values, id]);
if (result.affectedRows > 0) {
return await this.findById(id);
}
return null;
}
// 更新最後登入時間
async updateLastLogin(id: string): Promise<boolean> {
const sql = 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?';
const result = await db.update(sql, [id]);
return result.affectedRows > 0;
}
// 獲取用戶統計
async getUserStatistics(id: string): Promise<UserStatistics | null> {
const sql = 'SELECT * FROM user_statistics WHERE id = ?';
return await db.queryOne<UserStatistics>(sql, [id]);
}
// 獲取所有用戶(管理員用)
async findAll(filters: {
search?: string;
department?: string;
role?: string;
status?: string;
page?: number;
limit?: number;
} = {}): Promise<{ users: User[]; total: number }> {
const { search, department, role, status, page = 1, limit = 10 } = filters;
// 構建查詢條件
let whereConditions = ['is_active = TRUE'];
let params: any[] = [];
if (search) {
whereConditions.push('(name LIKE ? OR email LIKE ?)');
params.push(`%${search}%`, `%${search}%`);
}
if (department && department !== 'all') {
whereConditions.push('department = ?');
params.push(department);
}
if (role && role !== 'all') {
whereConditions.push('role = ?');
params.push(role);
}
if (status && status !== 'all') {
if (status === 'active') {
whereConditions.push('last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)');
} else if (status === 'inactive') {
whereConditions.push('last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY)');
}
}
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
// 獲取總數
const countSql = `SELECT COUNT(*) as total FROM users ${whereClause}`;
const countResult = await this.query(countSql, params);
const total = countResult[0]?.total || 0;
// 獲取用戶列表
const offset = (page - 1) * limit;
const usersSql = `
SELECT
id, name, email, avatar, department, role, join_date,
total_likes, total_views, is_active, last_login, created_at, updated_at
FROM users
${whereClause}
ORDER BY created_at DESC
LIMIT ${offset}, ${limit}
`;
const users = await this.query<User>(usersSql, params);
return { users, total };
}
// 獲取用戶統計數據
async getUserStats(): Promise<{
totalUsers: number;
activeUsers: number;
adminCount: number;
developerCount: number;
inactiveUsers: number;
newThisMonth: number;
}> {
const sql = `
SELECT
COUNT(*) as total_users,
COUNT(CASE WHEN last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as active_users,
COUNT(CASE WHEN role = 'admin' THEN 1 END) as admin_count,
COUNT(CASE WHEN role = 'developer' THEN 1 END) as developer_count,
COUNT(CASE WHEN last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as inactive_users,
COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as new_this_month
FROM users
WHERE is_active = TRUE
`;
const result = await this.query(sql);
const stats = result[0] || {};
return {
totalUsers: stats.total_users || 0,
activeUsers: stats.active_users || 0,
adminCount: stats.admin_count || 0,
developerCount: stats.developer_count || 0,
inactiveUsers: stats.inactive_users || 0,
newThisMonth: stats.new_this_month || 0
};
}
// 通用查詢方法
async query<T = any>(sql: string, params: any[] = []): Promise<T[]> {
return await db.query<T>(sql, params);
}
// 通用單一查詢方法
async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
return await db.queryOne<T>(sql, params);
}
// 獲取所有用戶
async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
const sql = 'SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?';
return await db.query<User>(sql, [limit, offset]);
}
// 靜態方法保持向後兼容
static async createUser(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
const service = new UserService();
return await service.create(userData);
}
static async getUserByEmail(email: string): Promise<User | null> {
const service = new UserService();
return await service.findByEmail(email);
}
static async getUserById(id: string): Promise<User | null> {
const service = new UserService();
return await service.findById(id);
}
static async updateUser(id: string, updates: Partial<User>): Promise<boolean> {
const service = new UserService();
const result = await service.update(id, updates);
return result !== null;
}
static async getUserStatistics(id: string): Promise<UserStatistics | null> {
const service = new UserService();
return await service.getUserStatistics(id);
}
static async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
const service = new UserService();
return await service.getAllUsers(limit, offset);
}
}
// =====================================================
// 評審服務
// =====================================================
export class JudgeService {
// 創建評審
static async createJudge(judgeData: Omit<Judge, 'id' | 'created_at' | 'updated_at'>): Promise<Judge> {
const sql = `
INSERT INTO judges (id, name, title, department, expertise, avatar, is_active)
VALUES (UUID(), ?, ?, ?, ?, ?, ?)
`;
const params = [
judgeData.name,
judgeData.title,
judgeData.department,
JSON.stringify(judgeData.expertise),
judgeData.avatar || null,
judgeData.is_active
];
await db.insert(sql, params);
return await this.getJudgeByName(judgeData.name) as Judge;
}
// 根據姓名獲取評審
static async getJudgeByName(name: string): Promise<Judge | null> {
const sql = 'SELECT * FROM judges WHERE name = ? AND is_active = TRUE';
const result = await db.queryOne<Judge>(sql, [name]);
if (result) {
result.expertise = JSON.parse(result.expertise as any);
}
return result;
}
// 根據ID獲取評審
static async getJudgeById(id: string): Promise<Judge | null> {
const sql = 'SELECT * FROM judges WHERE id = ? AND is_active = TRUE';
const result = await db.queryOne<Judge>(sql, [id]);
if (result) {
result.expertise = JSON.parse(result.expertise as any);
}
return result;
}
// 獲取所有評審
static async getAllJudges(): Promise<Judge[]> {
const sql = 'SELECT * FROM judges WHERE is_active = TRUE ORDER BY created_at DESC';
const results = await db.query<Judge>(sql);
return results.map(judge => ({
...judge,
expertise: JSON.parse(judge.expertise as any)
}));
}
// 更新評審
static async updateJudge(id: string, updates: Partial<Judge>): Promise<boolean> {
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => {
if (field === 'expertise') {
return JSON.stringify((updates as any)[field]);
}
return (updates as any)[field];
});
const sql = `UPDATE judges SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
const result = await db.update(sql, [...values, id]);
return result.affectedRows > 0;
}
}
// =====================================================
// 競賽服務
// =====================================================
export class CompetitionService {
// 創建競賽
static async createCompetition(competitionData: Omit<Competition, 'id' | 'created_at' | 'updated_at'>): Promise<Competition> {
const sql = `
INSERT INTO competitions (id, name, year, month, start_date, end_date, status, description, type, evaluation_focus, max_team_size, is_active)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
competitionData.name,
competitionData.year,
competitionData.month,
competitionData.start_date,
competitionData.end_date,
competitionData.status,
competitionData.description || null,
competitionData.type,
competitionData.evaluation_focus || null,
competitionData.max_team_size || null,
competitionData.is_active
];
await db.insert(sql, params);
return await this.getCompetitionByName(competitionData.name) as Competition;
}
// 根據名稱獲取競賽
static async getCompetitionByName(name: string): Promise<Competition | null> {
const sql = 'SELECT * FROM competitions WHERE name = ? AND is_active = TRUE';
return await db.queryOne<Competition>(sql, [name]);
}
// 根據ID獲取競賽
static async getCompetitionById(id: string): Promise<Competition | null> {
const sql = 'SELECT * FROM competitions WHERE id = ? AND is_active = TRUE';
return await db.queryOne<Competition>(sql, [id]);
}
// 獲取所有競賽
static async getAllCompetitions(): Promise<Competition[]> {
const sql = 'SELECT * FROM competitions WHERE is_active = TRUE ORDER BY year DESC, month DESC';
return await db.query<Competition>(sql);
}
// 獲取競賽統計
static async getCompetitionStatistics(id: string): Promise<CompetitionStatistics | null> {
const sql = 'SELECT * FROM competition_statistics WHERE id = ?';
return await db.queryOne<CompetitionStatistics>(sql, [id]);
}
// 更新競賽
static async updateCompetition(id: string, updates: Partial<Competition>): Promise<boolean> {
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => (updates as any)[field]);
const sql = `UPDATE competitions SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
const result = await db.update(sql, [...values, id]);
return result.affectedRows > 0;
}
}
// =====================================================
// 應用服務
// =====================================================
export class AppService {
// 創建應用
static async createApp(appData: Omit<App, 'id' | 'created_at' | 'updated_at'>): Promise<App> {
const sql = `
INSERT INTO apps (id, name, description, creator_id, team_id, category, type, likes_count, views_count, rating, is_active)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
appData.name,
appData.description || null,
appData.creator_id,
appData.team_id || null,
appData.category,
appData.type,
appData.likes_count,
appData.views_count,
appData.rating,
appData.is_active
];
await db.insert(sql, params);
return await this.getAppByName(appData.name) as App;
}
// 根據名稱獲取應用
static async getAppByName(name: string): Promise<App | null> {
const sql = 'SELECT * FROM apps WHERE name = ? AND is_active = TRUE';
return await db.queryOne<App>(sql, [name]);
}
// 根據ID獲取應用
static async getAppById(id: string): Promise<App | null> {
const sql = 'SELECT * FROM apps WHERE id = ? AND is_active = TRUE';
return await db.queryOne<App>(sql, [id]);
}
// 獲取所有應用
static async getAllApps(limit = 50, offset = 0): Promise<App[]> {
const sql = 'SELECT * FROM apps WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?';
return await db.query<App>(sql, [limit, offset]);
}
// 獲取應用統計
static async getAppStatistics(id: string): Promise<AppStatistics | null> {
const sql = 'SELECT * FROM app_statistics WHERE id = ?';
return await db.queryOne<AppStatistics>(sql, [id]);
}
// 更新應用
static async updateApp(id: string, updates: Partial<App>): Promise<boolean> {
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => (updates as any)[field]);
const sql = `UPDATE apps SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
const result = await db.update(sql, [...values, id]);
return result.affectedRows > 0;
}
}
// =====================================================
// 評分服務
// =====================================================
export class ScoringService {
// 提交應用評分
static async submitAppScore(scoreData: Omit<AppJudgeScore, 'id' | 'submitted_at'>): Promise<AppJudgeScore> {
const sql = `
INSERT INTO app_judge_scores (id, judge_id, app_id, innovation_score, technical_score, usability_score, presentation_score, impact_score, total_score, comments)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
innovation_score = VALUES(innovation_score),
technical_score = VALUES(technical_score),
usability_score = VALUES(usability_score),
presentation_score = VALUES(presentation_score),
impact_score = VALUES(impact_score),
total_score = VALUES(total_score),
comments = VALUES(comments),
submitted_at = CURRENT_TIMESTAMP
`;
const params = [
scoreData.judge_id,
scoreData.app_id,
scoreData.innovation_score,
scoreData.technical_score,
scoreData.usability_score,
scoreData.presentation_score,
scoreData.impact_score,
scoreData.total_score,
scoreData.comments || null
];
await db.insert(sql, params);
return await this.getAppScore(scoreData.judge_id, scoreData.app_id) as AppJudgeScore;
}
// 獲取應用評分
static async getAppScore(judgeId: string, appId: string): Promise<AppJudgeScore | null> {
const sql = 'SELECT * FROM app_judge_scores WHERE judge_id = ? AND app_id = ?';
return await db.queryOne<AppJudgeScore>(sql, [judgeId, appId]);
}
// 獲取應用的所有評分
static async getAppScores(appId: string): Promise<AppJudgeScore[]> {
const sql = 'SELECT * FROM app_judge_scores WHERE app_id = ? ORDER BY submitted_at DESC';
return await db.query<AppJudgeScore>(sql, [appId]);
}
// 提交提案評分
static async submitProposalScore(scoreData: Omit<ProposalJudgeScore, 'id' | 'submitted_at'>): Promise<ProposalJudgeScore> {
const sql = `
INSERT INTO proposal_judge_scores (id, judge_id, proposal_id, problem_identification_score, solution_feasibility_score, innovation_score, impact_score, presentation_score, total_score, comments)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
problem_identification_score = VALUES(problem_identification_score),
solution_feasibility_score = VALUES(solution_feasibility_score),
innovation_score = VALUES(innovation_score),
impact_score = VALUES(impact_score),
presentation_score = VALUES(presentation_score),
total_score = VALUES(total_score),
comments = VALUES(comments),
submitted_at = CURRENT_TIMESTAMP
`;
const params = [
scoreData.judge_id,
scoreData.proposal_id,
scoreData.problem_identification_score,
scoreData.solution_feasibility_score,
scoreData.innovation_score,
scoreData.impact_score,
scoreData.presentation_score,
scoreData.total_score,
scoreData.comments || null
];
await db.insert(sql, params);
return await this.getProposalScore(scoreData.judge_id, scoreData.proposal_id) as ProposalJudgeScore;
}
// 獲取提案評分
static async getProposalScore(judgeId: string, proposalId: string): Promise<ProposalJudgeScore | null> {
const sql = 'SELECT * FROM proposal_judge_scores WHERE judge_id = ? AND proposal_id = ?';
return await db.queryOne<ProposalJudgeScore>(sql, [judgeId, proposalId]);
}
// 獲取提案的所有評分
static async getProposalScores(proposalId: string): Promise<ProposalJudgeScore[]> {
const sql = 'SELECT * FROM proposal_judge_scores WHERE proposal_id = ? ORDER BY submitted_at DESC';
return await db.query<ProposalJudgeScore>(sql, [proposalId]);
}
}
// =====================================================
// 獎項服務
// =====================================================
export class AwardService {
// 創建獎項
static async createAward(awardData: Omit<Award, 'id' | 'created_at'>): Promise<Award> {
const sql = `
INSERT INTO awards (id, competition_id, app_id, team_id, proposal_id, app_name, team_name, proposal_title, creator, award_type, award_name, score, year, month, icon, custom_award_type_id, competition_type, rank, category)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
awardData.competition_id,
awardData.app_id || null,
awardData.team_id || null,
awardData.proposal_id || null,
awardData.app_name || null,
awardData.team_name || null,
awardData.proposal_title || null,
awardData.creator,
awardData.award_type,
awardData.award_name,
awardData.score,
awardData.year,
awardData.month,
awardData.icon,
awardData.custom_award_type_id || null,
awardData.competition_type,
awardData.rank,
awardData.category
];
await db.insert(sql, params);
return await this.getAwardByCompetitionAndCreator(awardData.competition_id, awardData.creator) as Award;
}
// 根據競賽和創作者獲取獎項
static async getAwardByCompetitionAndCreator(competitionId: string, creator: string): Promise<Award | null> {
const sql = 'SELECT * FROM awards WHERE competition_id = ? AND creator = ? ORDER BY created_at DESC LIMIT 1';
return await db.queryOne<Award>(sql, [competitionId, creator]);
}
// 根據年份獲取獎項
static async getAwardsByYear(year: number): Promise<Award[]> {
const sql = 'SELECT * FROM awards WHERE year = ? ORDER BY month DESC, rank ASC';
return await db.query<Award>(sql, [year]);
}
// 根據競賽獲取獎項
static async getAwardsByCompetition(competitionId: string): Promise<Award[]> {
const sql = 'SELECT * FROM awards WHERE competition_id = ? ORDER BY rank ASC';
return await db.query<Award>(sql, [competitionId]);
}
}
// =====================================================
// 系統設定服務
// =====================================================
export class SystemSettingService {
// 獲取設定值
static async getSetting(key: string): Promise<string | null> {
const sql = 'SELECT value FROM system_settings WHERE `key` = ?';
const result = await db.queryOne<{ value: string }>(sql, [key]);
return result?.value || null;
}
// 設定值
static async setSetting(key: string, value: string, description?: string, category = 'general', isPublic = false): Promise<boolean> {
const sql = `
INSERT INTO system_settings (id, \`key\`, value, description, category, is_public)
VALUES (UUID(), ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
value = VALUES(value),
description = VALUES(description),
category = VALUES(category),
is_public = VALUES(is_public),
updated_at = CURRENT_TIMESTAMP
`;
const params = [key, value, description || null, category, isPublic];
const result = await db.insert(sql, params);
return result.affectedRows > 0;
}
// 獲取所有公開設定
static async getPublicSettings(): Promise<SystemSetting[]> {
const sql = 'SELECT * FROM system_settings WHERE is_public = TRUE ORDER BY category, `key`';
return await db.query<SystemSetting>(sql);
}
}

View File

@@ -0,0 +1,101 @@
// =====================================================
// 郵件服務
// =====================================================
import nodemailer from 'nodemailer';
// 郵件配置
const emailConfig = {
host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: parseInt(process.env.SMTP_PORT || '587'),
secure: false, // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
};
// 創建郵件傳輸器
const transporter = nodemailer.createTransport(emailConfig);
export class EmailService {
// 發送密碼重設郵件
static async sendPasswordResetEmail(email: string, resetToken: string, userName: string) {
try {
const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`;
const mailOptions = {
from: `"${process.env.NEXT_PUBLIC_APP_NAME || 'AI 展示平台'}" <${emailConfig.auth.user}>`,
to: email,
subject: '密碼重設請求',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center;">
<h1 style="color: white; margin: 0; font-size: 24px;">密碼重設請求</h1>
</div>
<div style="padding: 30px; background: #f9f9f9;">
<h2 style="color: #333; margin-top: 0;">親愛的 ${userName}</h2>
<p style="color: #666; line-height: 1.6;">
我們收到了您的密碼重設請求。請點擊下方按鈕來重設您的密碼:
</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${resetUrl}"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 5px;
display: inline-block;
font-weight: bold;">
重設密碼
</a>
</div>
<p style="color: #666; line-height: 1.6; font-size: 14px;">
如果按鈕無法點擊,請複製以下連結到瀏覽器中:<br>
<a href="${resetUrl}" style="color: #667eea; word-break: break-all;">${resetUrl}</a>
</p>
<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p style="color: #856404; margin: 0; font-size: 14px;">
<strong>注意:</strong>此連結將在 1 小時後過期,請盡快完成密碼重設。
</p>
</div>
<p style="color: #666; line-height: 1.6; font-size: 14px;">
如果您沒有請求密碼重設,請忽略此郵件。您的密碼將保持不變。
</p>
</div>
<div style="background: #333; color: white; padding: 20px; text-align: center; font-size: 12px;">
<p style="margin: 0;">此郵件由 ${process.env.NEXT_PUBLIC_APP_NAME || 'AI 展示平台'} 系統自動發送</p>
</div>
</div>
`,
};
const result = await transporter.sendMail(mailOptions);
console.log('密碼重設郵件發送成功:', result.messageId);
return { success: true, messageId: result.messageId };
} catch (error) {
console.error('發送密碼重設郵件失敗:', error);
return { success: false, error: error.message };
}
}
// 測試郵件配置
static async testEmailConfig() {
try {
await transporter.verify();
console.log('郵件配置驗證成功');
return { success: true };
} catch (error) {
console.error('郵件配置驗證失敗:', error);
return { success: false, error: error.message };
}
}
}

View File

@@ -0,0 +1,131 @@
// =====================================================
// 密碼重設服務
// =====================================================
import { db } from '../database';
import { v4 as uuidv4 } from 'uuid';
import bcrypt from 'bcryptjs';
export interface PasswordResetToken {
id: string;
user_id: string;
token: string;
expires_at: string;
used_at?: string;
created_at: string;
updated_at: string;
}
export class PasswordResetService {
// 創建密碼重設 token
static async createResetToken(userId: string): Promise<PasswordResetToken> {
const token = uuidv4() + '-' + Date.now();
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 小時後過期
const sql = `
INSERT INTO password_reset_tokens (id, user_id, token, expires_at)
VALUES (?, ?, ?, ?)
`;
const params = [uuidv4(), userId, token, expiresAt];
await db.insert(sql, params);
return {
id: params[0],
user_id: userId,
token,
expires_at: expiresAt.toISOString(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
}
// 驗證密碼重設 token
static async validateResetToken(token: string): Promise<PasswordResetToken | null> {
const sql = `
SELECT * FROM password_reset_tokens
WHERE token = ? AND expires_at > NOW() AND used_at IS NULL
`;
const result = await db.queryOne<PasswordResetToken>(sql, [token]);
return result;
}
// 使用密碼重設 token
static async useResetToken(token: string, newPassword: string): Promise<boolean> {
const connection = await db.getConnection();
try {
await connection.beginTransaction();
// 獲取 token 信息
const tokenInfo = await this.validateResetToken(token);
if (!tokenInfo) {
throw new Error('無效或已過期的重設 token');
}
// 加密新密碼
const saltRounds = 12;
const passwordHash = await bcrypt.hash(newPassword, saltRounds);
// 更新用戶密碼
const updateUserSql = `
UPDATE users
SET password_hash = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`;
await connection.execute(updateUserSql, [passwordHash, tokenInfo.user_id]);
// 標記 token 為已使用
const updateTokenSql = `
UPDATE password_reset_tokens
SET used_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE token = ?
`;
await connection.execute(updateTokenSql, [token]);
await connection.commit();
return true;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
// 清理過期的 tokens
static async cleanupExpiredTokens(): Promise<number> {
const sql = `
DELETE FROM password_reset_tokens
WHERE expires_at < NOW() OR used_at IS NOT NULL
`;
const result = await db.update(sql, []);
return result.affectedRows;
}
// 獲取用戶的活躍重設 tokens
static async getUserActiveTokens(userId: string): Promise<PasswordResetToken[]> {
const sql = `
SELECT * FROM password_reset_tokens
WHERE user_id = ? AND expires_at > NOW() AND used_at IS NULL
ORDER BY created_at DESC
`;
return await db.query<PasswordResetToken>(sql, [userId]);
}
// 撤銷用戶的所有重設 tokens
static async revokeUserTokens(userId: string): Promise<number> {
const sql = `
UPDATE password_reset_tokens
SET used_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE user_id = ? AND used_at IS NULL
`;
const result = await db.update(sql, [userId]);
return result.affectedRows;
}
}