579 lines
No EOL
17 KiB
JavaScript
579 lines
No EOL
17 KiB
JavaScript
/**
|
||
* 云服务相关工具函数
|
||
*/
|
||
|
||
// 导入统一配置
|
||
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
|
||
}; |