otp/utils/format.js
“xHuPo” 2b8870a40e init
2025-06-09 13:35:15 +08:00

131 lines
No EOL
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 格式化相关工具函数
*/
/**
* 格式化数字为两位数字符串
* @param {number} n 数字
* @returns {string} 格式化后的字符串
*/
const formatNumber = n => {
n = n.toString();
return n[1] ? n : '0' + n;
};
/**
* 格式化时间
* @param {Date} date 日期对象
* @returns {string} 格式化后的时间字符串
*/
const formatTime = date => {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':');
};
/**
* 解析OTP URL格式
* @param {string} url - OTP URL字符串
* @returns {Object|null} 解析后的令牌数据对象如果解析失败返回null
*/
const parseURL = (url) => {
try {
// 基本格式验证
if (!url.startsWith('otpauth://')) {
return null;
}
// 解析URL
const urlObj = new URL(url);
const type = urlObj.hostname.toLowerCase();
const pathParts = urlObj.pathname.substring(1).split(':');
const issuer = decodeURIComponent(pathParts[0]);
const account = pathParts.length > 1 ? decodeURIComponent(pathParts[1]) : '';
// 解析查询参数
const params = {};
urlObj.searchParams.forEach((value, key) => {
params[key.toLowerCase()] = decodeURIComponent(value);
});
// 构建令牌数据
const tokenData = {
type: type,
issuer: issuer,
remark: account,
secret: params.secret || '',
algo: (params.algorithm || 'SHA1').toUpperCase(),
digits: parseInt(params.digits || '6', 10)
};
// 类型特定参数
if (type === 'totp') {
tokenData.period = parseInt(params.period || '30', 10);
} else if (type === 'hotp') {
tokenData.counter = parseInt(params.counter || '0', 10);
}
return tokenData;
} catch (error) {
console.error('[Error] Failed to parse OTP URL', {
error: error.message,
url: url.length > 100 ? url.substring(0, 100) + '...' : url
});
return null;
}
};
/**
* 验证令牌数据的有效性
* @param {Object} tokenData - 令牌数据对象
* @returns {string[]|null} 错误信息数组如果验证通过返回null
*/
const validateToken = (tokenData) => {
const errors = [];
// 验证必填字段
if (!tokenData.issuer || !tokenData.issuer.trim()) {
errors.push('服务名称不能为空');
}
if (!tokenData.secret || !tokenData.secret.trim()) {
errors.push('密钥不能为空');
}
// 验证算法
const validAlgos = ['SHA1', 'SHA256', 'SHA512'];
if (!validAlgos.includes(tokenData.algo)) {
errors.push(`不支持的算法: ${tokenData.algo}`);
}
// 验证位数
if (tokenData.digits < 6 || tokenData.digits > 8) {
errors.push('位数必须在6-8之间');
}
// 类型特定验证
if (tokenData.type === 'totp') {
if (tokenData.period < 15 || tokenData.period > 300) {
errors.push('更新周期必须在15-300秒之间');
}
} else if (tokenData.type === 'hotp') {
if (tokenData.counter < 0) {
errors.push('计数器值不能为负数');
}
} else {
errors.push('无效的令牌类型');
}
return errors.length > 0 ? errors : null;
};
module.exports = {
formatTime,
formatNumber,
parseURL,
validateToken
};