132 lines
3.7 KiB
TypeScript
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;
|
|
}
|
|
}
|