1
This commit is contained in:
82
api/utils/account_utils.js
Normal file
82
api/utils/account_utils.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 账号工具函数
|
||||
* 提供账号相关的工具方法
|
||||
*/
|
||||
|
||||
const dayjs = require('dayjs');
|
||||
const utc = require('dayjs/plugin/utc');
|
||||
const timezone = require('dayjs/plugin/timezone');
|
||||
|
||||
// 启用 UTC 和时区插件
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
/**
|
||||
* 计算剩余天数(使用 UTC 时间避免时区问题)
|
||||
* @param {Date|string|null} authorizationDate - 授权日期
|
||||
* @param {number} authorizationDays - 授权天数
|
||||
* @returns {number} 剩余天数
|
||||
*/
|
||||
function calculateRemainingDays(authorizationDate, authorizationDays) {
|
||||
if (!authorizationDate || !authorizationDays || authorizationDays <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 使用 UTC 时间计算,避免时区问题
|
||||
const startDate = dayjs(authorizationDate).utc().startOf('day');
|
||||
const endDate = startDate.add(authorizationDays, 'day');
|
||||
const now = dayjs().utc().startOf('day');
|
||||
|
||||
// 计算剩余天数
|
||||
const remaining = endDate.diff(now, 'day', false); // 使用整数天数
|
||||
return Math.max(0, remaining);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查授权是否有效
|
||||
* @param {Date|string|null} authorizationDate - 授权日期
|
||||
* @param {number} authorizationDays - 授权天数
|
||||
* @returns {boolean} 授权是否有效
|
||||
*/
|
||||
function isAuthorizationValid(authorizationDate, authorizationDays) {
|
||||
const remainingDays = calculateRemainingDays(authorizationDate, authorizationDays);
|
||||
return remainingDays > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为账号对象添加 remaining_days 字段
|
||||
* @param {Object} account - 账号对象(可以是 Sequelize 实例或普通对象)
|
||||
* @returns {Object} 添加了 remaining_days 的账号对象
|
||||
*/
|
||||
function addRemainingDays(account) {
|
||||
// 如果是 Sequelize 实例,转换为普通对象
|
||||
const accountData = account.toJSON ? account.toJSON() : account;
|
||||
|
||||
const authDate = accountData.authorization_date;
|
||||
const authDays = accountData.authorization_days || 0;
|
||||
|
||||
accountData.remaining_days = calculateRemainingDays(authDate, authDays);
|
||||
|
||||
return accountData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为账号数组添加 remaining_days 字段
|
||||
* @param {Array} accounts - 账号数组
|
||||
* @returns {Array} 添加了 remaining_days 的账号数组
|
||||
*/
|
||||
function addRemainingDaysToAccounts(accounts) {
|
||||
if (!Array.isArray(accounts)) {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
return accounts.map(account => addRemainingDays(account));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
calculateRemainingDays,
|
||||
isAuthorizationValid,
|
||||
addRemainingDays,
|
||||
addRemainingDaysToAccounts
|
||||
};
|
||||
|
||||
181
api/utils/crypto_utils.js
Normal file
181
api/utils/crypto_utils.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 加密工具函数
|
||||
* 提供密码加密、验证等安全相关功能
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
// 配置参数
|
||||
const SALT_LENGTH = 16; // salt 长度
|
||||
const KEY_LENGTH = 64; // 密钥长度
|
||||
const ITERATIONS = 100000; // 迭代次数
|
||||
const DIGEST = 'sha256'; // 摘要算法
|
||||
|
||||
/**
|
||||
* 生成密码哈希
|
||||
* @param {string} password - 明文密码
|
||||
* @returns {Promise<string>} 加密后的密码字符串 (格式: salt$hash)
|
||||
*/
|
||||
async function hashPassword(password) {
|
||||
if (!password || typeof password !== 'string') {
|
||||
throw new Error('密码不能为空');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 生成随机 salt
|
||||
const salt = crypto.randomBytes(SALT_LENGTH).toString('hex');
|
||||
|
||||
// 使用 pbkdf2 生成密钥
|
||||
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const hash = derivedKey.toString('hex');
|
||||
// 返回格式: salt$hash
|
||||
resolve(`${salt}$${hash}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
* @param {string} password - 明文密码
|
||||
* @param {string} hashedPassword - 加密后的密码 (格式: salt$hash)
|
||||
* @returns {Promise<boolean>} 是否匹配
|
||||
*/
|
||||
async function verifyPassword(password, hashedPassword) {
|
||||
if (!password || !hashedPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 解析 salt 和 hash
|
||||
const parts = hashedPassword.split('$');
|
||||
if (parts.length !== 2) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const [salt, originalHash] = parts;
|
||||
|
||||
// 使用相同的 salt 生成密钥
|
||||
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const hash = derivedKey.toString('hex');
|
||||
// 使用恒定时间比较,防止时序攻击
|
||||
resolve(crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(originalHash)));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成安全的随机 token
|
||||
* @param {number} length - token 长度(字节数),默认 32
|
||||
* @returns {string} 十六进制字符串
|
||||
*/
|
||||
function generateToken(length = 32) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成设备 ID
|
||||
* @param {string} prefix - 前缀,默认 'device'
|
||||
* @returns {string} 设备 ID
|
||||
*/
|
||||
function generateDeviceId(prefix = 'device') {
|
||||
const timestamp = Date.now();
|
||||
const random = crypto.randomBytes(8).toString('hex');
|
||||
return `${prefix}_${timestamp}_${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证设备 ID 格式
|
||||
* @param {string} deviceId - 设备 ID
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
function validateDeviceId(deviceId) {
|
||||
if (!deviceId || typeof deviceId !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查格式: prefix_timestamp_randomhex
|
||||
const pattern = /^[a-z]+_\d{13}_[a-f0-9]{16}$/i;
|
||||
return pattern.test(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏处理 - 手机号
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {string} 脱敏后的手机号
|
||||
*/
|
||||
function maskPhone(phone) {
|
||||
if (!phone || typeof phone !== 'string') {
|
||||
return '';
|
||||
}
|
||||
if (phone.length < 11) {
|
||||
return phone;
|
||||
}
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏处理 - 邮箱
|
||||
* @param {string} email - 邮箱
|
||||
* @returns {string} 脱敏后的邮箱
|
||||
*/
|
||||
function maskEmail(email) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return '';
|
||||
}
|
||||
const parts = email.split('@');
|
||||
if (parts.length !== 2) {
|
||||
return email;
|
||||
}
|
||||
const [username, domain] = parts;
|
||||
if (username.length <= 2) {
|
||||
return `*@${domain}`;
|
||||
}
|
||||
const masked = username[0] + '***' + username[username.length - 1];
|
||||
return `${masked}@${domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏处理 - 通用对象(用于日志)
|
||||
* @param {Object} obj - 要脱敏的对象
|
||||
* @param {Array<string>} sensitiveFields - 敏感字段列表
|
||||
* @returns {Object} 脱敏后的对象
|
||||
*/
|
||||
function maskSensitiveData(obj, sensitiveFields = ['password', 'pwd', 'token', 'secret', 'key']) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const masked = { ...obj };
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (masked[field]) {
|
||||
masked[field] = '***MASKED***';
|
||||
}
|
||||
}
|
||||
|
||||
return masked;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
generateToken,
|
||||
generateDeviceId,
|
||||
validateDeviceId,
|
||||
maskPhone,
|
||||
maskEmail,
|
||||
maskSensitiveData
|
||||
};
|
||||
Reference in New Issue
Block a user