otp/utils/cloud.js
2025-06-17 14:44:48 +08:00

579 lines
No EOL
17 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.

/**
* 云服务相关工具函数
*/
// 导入统一配置
const config = require('./config');
const auth = require('./auth');
/**
* 检查是否有有效的认证令牌
* @returns {boolean} 是否有有效的访问令牌和刷新令牌
*/
const hasValidTokens = () => {
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
// 检查token是否存在且未过期
if (!accessToken || !refreshToken) {
return false;
}
// 使用shouldRefreshToken验证token有效性
const { needsRefresh } = shouldRefreshToken();
return !needsRefresh;
};
/**
* 检查JWT token是否需要刷新
* @returns {Object} 包含needsRefresh和hasRefreshToken两个布尔值
*/
const shouldRefreshToken = () => {
try {
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
// 如果没有访问令牌或刷新令牌,则不需要刷新
if (!accessToken) {
return { needsRefresh: false, hasRefreshToken: !!refreshToken };
}
// 使用auth模块解析JWT token
const decoded = auth.parseToken(accessToken);
// 定义expirationTime变量
let expirationTime;
if (decoded && decoded.exp) {
expirationTime = decoded.exp * 1000; // 转换为毫秒
} else {
console.error('解析JWT token失败: 无法获取过期时间');
// 如果解析失败假设token将在1小时后过期
expirationTime = Date.now() + 3600 * 1000;
}
// 如果token将在5分钟内过期则需要刷新
const needsRefresh = Date.now() + config.JWT_CONFIG.refreshThreshold > expirationTime;
return { needsRefresh, hasRefreshToken: !!refreshToken };
} catch (error) {
console.error('Token解析失败:', error);
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
return { needsRefresh: false, hasRefreshToken: !!refreshToken };
}
};
/**
* 刷新JWT token
* @returns {Promise<boolean>} 刷新是否成功
*/
const refreshToken = async () => {
try {
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
if (!refreshToken) {
console.warn('没有可用的刷新令牌,无法刷新访问令牌');
return false;
}
// 使用Promise包装wx.request以便使用await
const response = await new Promise((resolve, reject) => {
wx.request({
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `${config.JWT_CONFIG.tokenPrefix}${refreshToken}`
},
success: (res) => {
// 确保response对象包含完整的信息
resolve({
...res,
requestOptions: {
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `${config.JWT_CONFIG.tokenPrefix}${refreshToken}`
}
}
});
},
fail: (error) => {
// 增强错误信息
const enhancedError = new Error(error.errMsg || '刷新令牌请求失败');
enhancedError.originalError = error;
enhancedError.requestOptions = {
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
method: 'POST'
};
reject(enhancedError);
}
});
});
if (response.statusCode === 200 && response.data.access_token) {
wx.setStorageSync(config.JWT_CONFIG.storage.access, response.data.access_token);
if (response.data.refresh_token) {
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, response.data.refresh_token);
}
console.log('Token刷新成功');
return true;
} else {
console.error(`Token刷新失败: ${response.statusCode}`);
// 清除所有token强制用户重新登录
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
return false;
}
} catch (error) {
console.error('Token刷新失败:', error);
// 清除所有token强制用户重新登录
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
return false;
}
};
/**
* 发送HTTP请求的通用函数
* @param {string} url - API URL
* @param {Object} options - 请求选项
* @returns {Promise<any>} 响应数据
*/
const request = async (url, options = {}) => {
// 对于登录请求跳过token刷新检查
const isLoginRequest = url === config.API_ENDPOINTS.AUTH.LOGIN;
if (!isLoginRequest) {
// 检查是否需要刷新token
const tokenStatus = shouldRefreshToken();
if (tokenStatus.needsRefresh && tokenStatus.hasRefreshToken) {
const refreshSuccess = await refreshToken();
if (!refreshSuccess) {
console.warn('Token刷新失败继续使用当前token');
}
}
}
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
const defaultOptions = {
method: 'GET',
header: {
'Content-Type': 'application/json'
}
};
// 只有在有访问令牌且不是登录请求时才添加Authorization头
if (accessToken && !isLoginRequest) {
defaultOptions.header['Authorization'] = `Bearer ${accessToken}`;
}
const requestOptions = {
...defaultOptions,
...options,
url: `${config.API_BASE_URL}${url}`
};
// 合并headers
requestOptions.header = {
...defaultOptions.header,
...options.header
};
try {
const response = await new Promise((resolve, reject) => {
const requestTask = wx.request({
...requestOptions,
success: (res) => {
// 对于404错误静默处理不要让wx.request打印错误日志
if (res.statusCode === 404 && url === config.API_ENDPOINTS.OTP.RECOVER) {
// 静默处理404错误
res.silent404 = true;
}
// 确保response对象包含完整的信息
resolve({
...res,
requestOptions: {
url: requestOptions.url,
method: requestOptions.method,
header: requestOptions.header
}
});
},
fail: (error) => {
// 增强错误信息
const enhancedError = new Error(error.errMsg || '网络请求失败');
enhancedError.originalError = error;
enhancedError.requestOptions = {
url: requestOptions.url,
method: requestOptions.method,
header: requestOptions.header
};
reject(enhancedError);
}
});
});
// 处理401错误token无效
if (response.statusCode === 401 && !isLoginRequest) {
// 只有在有刷新令牌的情况下才尝试刷新token
const tokenStatus = shouldRefreshToken();
if (tokenStatus.hasRefreshToken) {
const refreshSuccess = await refreshToken();
if (refreshSuccess) {
return request(url, options); // 递归调用使用新token重试
}
}
// 如果没有刷新令牌或刷新失败,抛出认证错误
throw new Error('认证失败,请重新登录');
}
if (response.statusCode >= 200 && response.statusCode < 300) {
return response.data;
}
// 增强错误信息
const error = new Error(response.data?.message || '请求失败');
error.response = response;
error.statusCode = response.statusCode;
throw error;
} catch (error) {
// 只有非404错误才打印错误日志
if (error.statusCode !== 404) {
console.error('API请求失败:', {
endpoint: url,
method: requestOptions.method,
status: error.statusCode || 'N/A',
error: error.message,
requestOptions: error.requestOptions || requestOptions,
originalError: error.originalError || error
});
}
// 重新抛出增强的错误
const enhancedError = new Error(error.message || '网络请求失败');
enhancedError.originalError = error;
enhancedError.requestOptions = error.requestOptions || requestOptions;
throw enhancedError;
}
};
/**
* 上传令牌数据到云端
* @param {Array} tokens - 令牌数据数组
* @returns {Promise<string>} 云端数据ID
*/
const uploadTokens = async (tokens) => {
if (!tokens || tokens.length === 0) {
throw new Error('没有可上传的数据');
}
try {
// 从access_token中获取用户ID
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!accessToken) {
throw new Error('未登录');
}
// 使用auth模块解析JWT token获取用户ID
const decoded = auth.parseToken(accessToken);
if (!decoded) {
throw new Error('无法解析访问令牌');
}
const userId = decoded.sub || decoded.user_id || decoded.userId; // 尝试不同的可能字段名
if (!userId) {
throw new Error('用户未登录');
}
// 处理令牌数据,确保符合后端要求
const processedTokens = tokens.map(token => {
// 创建一个新对象,只包含后端需要的字段
const processedToken = {
id: token.id,
issuer: token.issuer,
account: token.account,
secret: token.secret,
type: token.type,
period: token.period,
digits: token.digits,
algorithm: token.algorithm
};
// 只有HOTP类型的令牌才设置counter字段且必须大于等于0
if (token.type && token.type.toUpperCase() === 'HOTP') {
processedToken.counter = token.counter || 0;
}
// TOTP类型的令牌不设置counter字段
return processedToken;
});
const response = await request(config.API_ENDPOINTS.OTP.SAVE, {
method: 'POST',
data: {
tokens: processedTokens,
userId
}
});
if (response.success) {
return response.data.id;
}
throw new Error(response.message || '上传失败');
} catch (error) {
console.error('上传令牌数据失败:', error);
throw error;
}
};
/**
* 从云端获取最新的令牌数据
* @returns {Promise<Object>} 云端数据对象
*/
const fetchLatestTokens = async () => {
try {
// 从access_token中获取用户ID
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!accessToken) {
throw new Error('未登录');
}
// 使用auth模块解析JWT token获取用户ID
const decoded = auth.parseToken(accessToken);
if (!decoded) {
throw new Error('无法解析访问令牌');
}
const userId = decoded.sub || decoded.user_id || decoded.userId; // 尝试不同的可能字段名
if (!userId) {
throw new Error('无法获取用户ID');
}
const response = await request(config.API_ENDPOINTS.OTP.RECOVER, {
method: 'POST',
data: {
userId
}
});
if (response.success) {
// 当云端无数据时,返回空数组
if (!response.data?.tokens) {
return {
tokens: [],
timestamp: new Date().toISOString(),
num: 0
};
}
// 处理从后端获取的令牌数据,确保符合前端要求
const processedTokens = response.data.tokens.map(token => {
// 创建一个新对象,包含前端需要的字段
const processedToken = {
...token,
createTime: token.createTime || new Date().toISOString(),
lastUpdate: token.lastUpdate || new Date().toISOString(),
code: token.code || ''
};
// 确保HOTP类型的令牌有counter字段
if (token.type && token.type.toUpperCase() === 'HOTP') {
processedToken.counter = token.counter || 0;
}
// TOTP类型的令牌不需要counter字段
return processedToken;
});
return {
tokens: processedTokens,
timestamp: response.data.timestamp,
num: processedTokens.length
};
}
// 当云端无数据时后端应返回404状态码
if (response.statusCode === 404) {
return {
tokens: [],
timestamp: new Date().toISOString(),
num: 0
};
}
throw new Error(response.message || '获取云端数据失败');
} catch (error) {
// 当状态码为404时说明云端无数据返回空数组
if (error.statusCode === 404) {
return {
tokens: [],
timestamp: new Date().toISOString(),
num: 0
};
}
throw error;
}
};
/**
* 初始化云服务
* 检查认证状态并尝试恢复会话
* @returns {Promise<boolean>} 初始化是否成功
*/
const initCloud = async () => {
try {
// 检查是否有有效的令牌
if (!hasValidTokens()) {
console.log('未找到有效令牌,需要登录');
return false;
}
// 验证令牌有效性
const tokenStatus = shouldRefreshToken();
if (tokenStatus.needsRefresh) {
const refreshSuccess = await refreshToken();
if (!refreshSuccess) {
console.log('令牌刷新失败,需要重新登录');
return false;
}
}
return true;
} catch (error) {
console.warn('云服务初始化失败:', error);
// 清除所有认证信息
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
return false;
}
};
/**
* 比较本地和云端数据
* @param {Array} localTokens - 本地令牌数组
* @param {Array} cloudTokens - 云端令牌数组
* @returns {Object} 比较结果
*/
const compareTokens = (localTokens, cloudTokens) => {
const result = {
added: [],
removed: [],
modified: [],
unchanged: []
};
// 创建查找映射
const localMap = new Map(localTokens.map(t => [t.id, t]));
const cloudMap = new Map(cloudTokens.map(t => [t.id, t]));
// 查找添加和修改的令牌
cloudTokens.forEach(cloudToken => {
const localToken = localMap.get(cloudToken.id);
if (!localToken) {
result.added.push(cloudToken);
} else if (JSON.stringify(localToken) !== JSON.stringify(cloudToken)) {
result.modified.push(cloudToken);
} else {
result.unchanged.push(cloudToken);
}
});
// 查找删除的令牌
localTokens.forEach(localToken => {
if (!cloudMap.has(localToken.id)) {
result.removed.push(localToken);
}
});
return result;
};
/**
* 合并本地和云端数据
* @param {Array} localTokens - 本地令牌数组
* @param {Array} cloudTokens - 云端令牌数组
* @param {Object} options - 合并选项
* @param {boolean} [options.preferCloud=true] - 冲突时是否优先使用云端数据
* @returns {Array} 合并后的令牌数组
*/
const mergeTokens = (localTokens, cloudTokens, options = { preferCloud: true }) => {
const comparison = compareTokens(localTokens, cloudTokens);
const result = [...comparison.unchanged];
// 添加新令牌
result.push(...comparison.added);
// 处理修改的令牌
comparison.modified.forEach(cloudToken => {
if (options.preferCloud) {
result.push(cloudToken);
} else {
const localToken = localTokens.find(t => t.id === cloudToken.id);
result.push(localToken);
}
});
// 如果不优先使用云端数据,保留本地删除的令牌
if (!options.preferCloud) {
result.push(...comparison.removed);
}
return result;
};
/**
* 清空云端所有令牌数据
* @returns {Promise<boolean>} 是否成功
*/
const clearAllTokens = async () => {
try {
// 从access_token中获取用户ID
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!accessToken) {
throw new Error('未登录');
}
// 使用auth模块解析JWT token获取用户ID
const decoded = auth.parseToken(accessToken);
if (!decoded) {
throw new Error('无法解析访问令牌');
}
const userId = decoded.sub || decoded.user_id || decoded.userId;
if (!userId) {
throw new Error('无法获取用户ID');
}
const response = await request(config.API_ENDPOINTS.OTP.CLEAR_ALL, {
method: 'POST',
data: {
userId
}
});
if (response.success) {
return true;
}
throw new Error(response.message || '清空失败');
} catch (error) {
console.error('清空云端数据失败:', error);
throw error;
}
};
module.exports = {
uploadTokens,
fetchLatestTokens,
compareTokens,
mergeTokens,
initCloud,
shouldRefreshToken,
refreshToken,
request,
hasValidTokens,
clearAllTokens
};