Files
ai-showcase-platform/lib/services/password-reset-service.ts

132 lines
3.7 KiB
TypeScript

// =====================================================
// 密碼重設服務
// =====================================================
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;
}
}