fix mine
This commit is contained in:
parent
17a02ea47e
commit
70e7a113e6
22 changed files with 967 additions and 285 deletions
|
@ -92,22 +92,31 @@ const refreshToken = async () => {
|
|||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {string} username - 用户名
|
||||
* @param {string} password - 密码
|
||||
* @returns {Promise<Object>} 登录结果,包含success和message字段
|
||||
*/
|
||||
const login = async (username, password) => {
|
||||
const login = async () => {
|
||||
try {
|
||||
// 获取微信登录凭证
|
||||
const loginResult = await wx.login();
|
||||
if (!loginResult.code) {
|
||||
return {
|
||||
success: false,
|
||||
message: '获取微信登录凭证失败'
|
||||
};
|
||||
}
|
||||
|
||||
// 发送code到后端
|
||||
const response = await wx.request({
|
||||
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.LOGIN}`,
|
||||
method: 'POST',
|
||||
data: { username, password }
|
||||
data: { code: loginResult.code }
|
||||
});
|
||||
|
||||
if (response.statusCode === 200 && response.data.access_token) {
|
||||
const { access_token, refresh_token } = response.data;
|
||||
const { access_token, refresh_token, openid } = response.data;
|
||||
wx.setStorageSync(config.JWT_CONFIG.storage.access, access_token);
|
||||
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, refresh_token);
|
||||
wx.setStorageSync('openid', openid);
|
||||
|
||||
// 触发登录成功事件
|
||||
eventManager.emit('auth:login', parseToken(access_token));
|
||||
|
@ -120,7 +129,7 @@ const login = async (username, password) => {
|
|||
|
||||
return {
|
||||
success: false,
|
||||
message: response.data?.message || '登录失败'
|
||||
message: response.data?.errmsg || response.data?.message || '登录失败'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
|
@ -138,6 +147,7 @@ const logout = () => {
|
|||
// 清除所有认证信息
|
||||
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
|
||||
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||
wx.removeStorageSync('openid');
|
||||
|
||||
// 触发登出事件
|
||||
eventManager.emit('auth:logout');
|
||||
|
@ -149,7 +159,8 @@ const logout = () => {
|
|||
*/
|
||||
const isLoggedIn = () => {
|
||||
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||
if (!token) return false;
|
||||
const openid = wx.getStorageSync('openid');
|
||||
if (!token || !openid) return false;
|
||||
|
||||
try {
|
||||
// 解析JWT token(不验证签名)
|
||||
|
@ -170,7 +181,8 @@ const isLoggedIn = () => {
|
|||
*/
|
||||
const getCurrentUser = () => {
|
||||
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||
if (!token) return null;
|
||||
const openid = wx.getStorageSync('openid');
|
||||
if (!token || !openid) return null;
|
||||
|
||||
try {
|
||||
// 解析JWT token(不验证签名)
|
||||
|
@ -179,7 +191,7 @@ const getCurrentUser = () => {
|
|||
|
||||
return {
|
||||
id: decoded.sub,
|
||||
username: decoded.username,
|
||||
openid: openid,
|
||||
// 其他用户信息...
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
393
utils/cloud.js
393
utils/cloud.js
|
@ -4,46 +4,110 @@
|
|||
|
||||
// 导入统一配置
|
||||
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 {boolean}
|
||||
* @returns {Object} 包含needsRefresh和hasRefreshToken两个布尔值
|
||||
*/
|
||||
const shouldRefreshToken = () => {
|
||||
try {
|
||||
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||
if (!token) return true;
|
||||
|
||||
// 解析JWT token(不验证签名)
|
||||
const [, payload] = token.split('.');
|
||||
const { exp } = JSON.parse(atob(payload));
|
||||
const expirationTime = exp * 1000; // 转换为毫秒
|
||||
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||
|
||||
// 如果token将在5分钟内过期,则刷新
|
||||
return Date.now() + config.JWT_CONFIG.refreshThreshold > expirationTime;
|
||||
// 如果没有访问令牌或刷新令牌,则不需要刷新
|
||||
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);
|
||||
return true;
|
||||
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||
return { needsRefresh: false, hasRefreshToken: !!refreshToken };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新JWT token
|
||||
* @returns {Promise<void>}
|
||||
* @returns {Promise<boolean>} 刷新是否成功
|
||||
*/
|
||||
const refreshToken = async () => {
|
||||
try {
|
||||
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
console.warn('没有可用的刷新令牌,无法刷新访问令牌');
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await wx.request({
|
||||
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Authorization': `Bearer ${refreshToken}`
|
||||
}
|
||||
// 使用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) {
|
||||
|
@ -51,15 +115,21 @@ const refreshToken = async () => {
|
|||
if (response.data.refresh_token) {
|
||||
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, response.data.refresh_token);
|
||||
}
|
||||
console.log('Token刷新成功');
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('Token refresh failed');
|
||||
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);
|
||||
throw new Error('认证已过期,请重新登录');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -70,20 +140,33 @@ const refreshToken = async () => {
|
|||
* @returns {Promise<any>} 响应数据
|
||||
*/
|
||||
const request = async (url, options = {}) => {
|
||||
// 检查并刷新token
|
||||
if (shouldRefreshToken()) {
|
||||
await refreshToken();
|
||||
// 对于登录请求,跳过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': `Bearer ${accessToken}`
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
// 只有在有访问令牌且不是登录请求时才添加Authorization头
|
||||
if (accessToken && !isLoginRequest) {
|
||||
defaultOptions.header['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
|
@ -98,33 +181,81 @@ const request = async (url, options = {}) => {
|
|||
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
const requestTask = wx.request({
|
||||
...requestOptions,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
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) {
|
||||
// 尝试刷新token并重试请求
|
||||
await refreshToken();
|
||||
return request(url, options); // 递归调用,使用新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;
|
||||
}
|
||||
|
||||
throw new Error(response.data?.message || '请求失败');
|
||||
} catch (error) {
|
||||
console.error('API请求失败:', {
|
||||
endpoint: url,
|
||||
method: requestOptions.method,
|
||||
status: error.statusCode || 'N/A',
|
||||
error: error.message
|
||||
});
|
||||
// 增强错误信息
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -139,11 +270,51 @@ const uploadTokens = async (tokens) => {
|
|||
}
|
||||
|
||||
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,
|
||||
timestamp: Date.now()
|
||||
tokens: processedTokens,
|
||||
userId
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -164,24 +335,86 @@ const uploadTokens = async (tokens) => {
|
|||
*/
|
||||
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: {
|
||||
timestamp: Date.now()
|
||||
userId
|
||||
}
|
||||
});
|
||||
|
||||
if (response.success && response.data?.tokens) {
|
||||
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: response.data.tokens,
|
||||
tokens: processedTokens,
|
||||
timestamp: response.data.timestamp,
|
||||
num: response.data.tokens.length
|
||||
num: processedTokens.length
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(response.message || '未找到云端数据');
|
||||
// 当云端无数据时,后端应返回404状态码
|
||||
if (response.statusCode === 404) {
|
||||
return {
|
||||
tokens: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
num: 0
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(response.message || '获取云端数据失败');
|
||||
} catch (error) {
|
||||
console.error('获取云端数据失败:', error);
|
||||
// 当状态码为404时,说明云端无数据,返回空数组
|
||||
if (error.statusCode === 404) {
|
||||
return {
|
||||
tokens: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
num: 0
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
@ -193,16 +426,20 @@ const fetchLatestTokens = async () => {
|
|||
*/
|
||||
const initCloud = async () => {
|
||||
try {
|
||||
// 检查是否有有效的访问令牌
|
||||
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||
if (!accessToken) {
|
||||
console.log('未找到访问令牌,需要登录');
|
||||
// 检查是否有有效的令牌
|
||||
if (!hasValidTokens()) {
|
||||
console.log('未找到有效令牌,需要登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证令牌有效性
|
||||
if (shouldRefreshToken()) {
|
||||
await refreshToken();
|
||||
const tokenStatus = shouldRefreshToken();
|
||||
if (tokenStatus.needsRefresh) {
|
||||
const refreshSuccess = await refreshToken();
|
||||
if (!refreshSuccess) {
|
||||
console.log('令牌刷新失败,需要重新登录');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
@ -287,6 +524,47 @@ const mergeTokens = (localTokens, cloudTokens, options = { preferCloud: true })
|
|||
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,
|
||||
|
@ -294,5 +572,8 @@ module.exports = {
|
|||
mergeTokens,
|
||||
initCloud,
|
||||
shouldRefreshToken,
|
||||
refreshToken
|
||||
refreshToken,
|
||||
request,
|
||||
hasValidTokens,
|
||||
clearAllTokens
|
||||
};
|
|
@ -7,7 +7,6 @@
|
|||
const baseConfig = {
|
||||
// 生产环境配置
|
||||
API_BASE_URL: 'https://otpm.zeroc.net',
|
||||
API_VERSION: 'v1',
|
||||
|
||||
// API端点配置 (统一使用微信登录端点)
|
||||
API_ENDPOINTS: {
|
||||
|
@ -17,26 +16,26 @@ const baseConfig = {
|
|||
},
|
||||
OTP: {
|
||||
SAVE: '/otp/save',
|
||||
RECOVER: '/otp/recover'
|
||||
RECOVER: '/otp/recover',
|
||||
CLEAR_ALL: '/otp/clear_all'
|
||||
}
|
||||
},
|
||||
|
||||
// JWT配置
|
||||
JWT_CONFIG: {
|
||||
storage: {
|
||||
access: 'jwt_access_token',
|
||||
refresh: 'jwt_refresh_token'
|
||||
},
|
||||
refreshThreshold: 5 * 60 * 1000, // Token过期前5分钟开始刷新
|
||||
headerKey: 'Authorization',
|
||||
tokenPrefix: 'Bearer '
|
||||
}
|
||||
};
|
||||
|
||||
// JWT配置
|
||||
const JWT_CONFIG = {
|
||||
storage: {
|
||||
access: 'jwt_access_token', // 访问令牌的存储键
|
||||
refresh: 'jwt_refresh_token' // 刷新令牌的存储键
|
||||
},
|
||||
refreshThreshold: 5 * 60 * 1000, // 令牌刷新阈值(5分钟)
|
||||
headerKey: 'Authorization', // 请求头中的认证键名
|
||||
tokenPrefix: 'Bearer ' // 令牌前缀
|
||||
};
|
||||
|
||||
// 导出合并后的配置
|
||||
module.exports = {
|
||||
...baseConfig.API_ENDPOINTS,
|
||||
...baseConfig.JWT_CONFIG,
|
||||
API_BASE_URL: baseConfig.API_BASE_URL,
|
||||
API_VERSION: baseConfig.API_VERSION
|
||||
API_ENDPOINTS: baseConfig.API_ENDPOINTS,
|
||||
JWT_CONFIG, // 导出JWT配置
|
||||
API_BASE_URL: baseConfig.API_BASE_URL
|
||||
};
|
|
@ -40,42 +40,73 @@ const parseURL = (url) => {
|
|||
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 protocolAndRest = url.split('://');
|
||||
if (protocolAndRest.length !== 2) return null;
|
||||
|
||||
const typeAndRest = protocolAndRest[1].split('/', 2);
|
||||
if (typeAndRest.length !== 2) return null;
|
||||
|
||||
const type = typeAndRest[0].toLowerCase();
|
||||
|
||||
// 分离路径和查询参数
|
||||
const pathAndQuery = typeAndRest[1].split('?');
|
||||
if (pathAndQuery.length !== 2) return null;
|
||||
|
||||
// 解析路径部分(issuer和account)
|
||||
const pathParts = decodeURIComponent(pathAndQuery[0]).split(':');
|
||||
let issuer = pathParts[0];
|
||||
const account = pathParts.length > 1 ? 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);
|
||||
const queryParts = pathAndQuery[1].split('&');
|
||||
for (const part of queryParts) {
|
||||
const [key, value] = part.split('=');
|
||||
if (key && value) {
|
||||
params[key.toLowerCase()] = decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
|
||||
return tokenData;
|
||||
// 如果params中有issuer参数,优先使用它
|
||||
if (params.issuer) {
|
||||
issuer = params.issuer;
|
||||
}
|
||||
|
||||
// 验证必要参数
|
||||
if (!params.secret) {
|
||||
throw new Error('缺少必要参数: secret');
|
||||
}
|
||||
|
||||
// 验证并规范化参数
|
||||
const digits = parseInt(params.digits) || 6;
|
||||
if (digits < 6 || digits > 8) {
|
||||
throw new Error('验证码位数必须在6-8之间');
|
||||
}
|
||||
|
||||
const period = parseInt(params.period) || (type === 'totp' ? 30 : undefined);
|
||||
if (type === 'totp' && (period < 15 || period > 60)) {
|
||||
throw new Error('TOTP周期必须在15-60秒之间');
|
||||
}
|
||||
|
||||
const counter = parseInt(params.counter) || (type === 'hotp' ? 0 : undefined);
|
||||
if (type === 'hotp' && counter < 0) {
|
||||
throw new Error('HOTP计数器不能为负数');
|
||||
}
|
||||
|
||||
// 构建返回对象
|
||||
return {
|
||||
type,
|
||||
issuer,
|
||||
account,
|
||||
secret: params.secret.toUpperCase(), // 统一转换为大写
|
||||
algorithm: (params.algorithm || 'SHA1').toUpperCase(),
|
||||
digits,
|
||||
period,
|
||||
counter
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Error] Failed to parse OTP URL', {
|
||||
error: error.message,
|
||||
url: url.length > 100 ? url.substring(0, 100) + '...' : url
|
||||
});
|
||||
console.error('解析OTP URL失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
@ -97,9 +128,9 @@ const validateToken = (tokenData) => {
|
|||
}
|
||||
|
||||
// 验证算法
|
||||
const validAlgos = ['SHA1', 'SHA256', 'SHA512'];
|
||||
if (!validAlgos.includes(tokenData.algo)) {
|
||||
errors.push(`不支持的算法: ${tokenData.algo}`);
|
||||
const validAlgorithms = ['SHA1', 'SHA256', 'SHA512'];
|
||||
if (!validAlgorithms.includes(tokenData.algorithm)) {
|
||||
errors.push(`不支持的算法: ${tokenData.algorithm}`);
|
||||
}
|
||||
|
||||
// 验证位数
|
||||
|
|
|
@ -39,13 +39,6 @@ if (!sjcl.codec.bytes) {
|
|||
};
|
||||
}
|
||||
|
||||
// 微信小程序原生 createHMAC 接口映射表
|
||||
const HASH_ALGORITHMS = {
|
||||
SHA1: 'sha1',
|
||||
SHA256: 'sha256',
|
||||
SHA512: 'sha512'
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成 HMAC-SHA1/SHA256/SHA512 值
|
||||
*
|
||||
|
|
|
@ -3,7 +3,7 @@ const { base32Decode } = require('./base32.js');
|
|||
const { constantTimeEqual, safeIntegerParse } = require('./crypto.js');
|
||||
|
||||
// 支持的哈希算法
|
||||
const HASH_ALGOS = {
|
||||
const HASH_ALGORITHMS = {
|
||||
'SHA1': 'SHA1',
|
||||
'SHA256': 'SHA256',
|
||||
'SHA512': 'SHA512'
|
||||
|
@ -11,7 +11,7 @@ const HASH_ALGOS = {
|
|||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG = {
|
||||
algorithm: HASH_ALGOS.SHA1, // 默认使用SHA1
|
||||
algorithm: HASH_ALGORITHMS.SHA1, // 默认使用SHA1
|
||||
digits: 6, // 默认6位数字
|
||||
window: 1 // 默认验证前后1个计数值
|
||||
};
|
||||
|
@ -42,7 +42,7 @@ async function generateHOTP(secret, counter, options = {}) {
|
|||
const config = { ...DEFAULT_CONFIG, ...options };
|
||||
|
||||
// 验证算法
|
||||
if (!HASH_ALGOS[config.algorithm]) {
|
||||
if (!HASH_ALGORITHMS[config.algorithm]) {
|
||||
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
||||
}
|
||||
|
||||
|
@ -180,7 +180,7 @@ function generateHOTPUri(secret, accountName, issuer, counter, options = {}) {
|
|||
const config = { ...DEFAULT_CONFIG, ...options };
|
||||
|
||||
// 验证算法
|
||||
if (!HASH_ALGOS[config.algorithm]) {
|
||||
if (!HASH_ALGORITHMS[config.algorithm]) {
|
||||
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
||||
}
|
||||
|
||||
|
@ -209,5 +209,5 @@ module.exports = {
|
|||
generateHOTP,
|
||||
verifyHOTP,
|
||||
generateHOTPUri,
|
||||
HASH_ALGOS
|
||||
HASH_ALGORITHMS
|
||||
};
|
|
@ -40,7 +40,7 @@ async function generateOTP(type, secret, options = {}) {
|
|||
...options,
|
||||
...(parsed.type === 'totp' ? { period: parsed.period } : {}),
|
||||
...(parsed.type === 'hotp' ? { counter: parsed.counter } : {}),
|
||||
algorithm: parsed.algo,
|
||||
algorithm: parsed.algorithm,
|
||||
digits: parsed.digits
|
||||
});
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ async function verifyOTP(token, type, secret, options = {}) {
|
|||
...options,
|
||||
...(parsed.type === 'totp' ? { period: parsed.period } : {}),
|
||||
...(parsed.type === 'hotp' ? { counter: parsed.counter } : {}),
|
||||
algorithm: parsed.algo,
|
||||
algorithm: parsed.algorithm,
|
||||
digits: parsed.digits
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ const { decode: base32Decode } = require('./base32.js');
|
|||
const { constantTimeEqual, safeIntegerParse } = require('./crypto.js');
|
||||
|
||||
// 支持的哈希算法
|
||||
const HASH_ALGOS = {
|
||||
const HASH_ALGORITHMS = {
|
||||
'SHA1': 'SHA1',
|
||||
'SHA256': 'SHA256',
|
||||
'SHA512': 'SHA512'
|
||||
|
@ -11,7 +11,7 @@ const HASH_ALGOS = {
|
|||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG = {
|
||||
algorithm: HASH_ALGOS.SHA1, // 默认使用SHA1
|
||||
algorithm: HASH_ALGORITHMS.SHA1, // 默认使用SHA1
|
||||
period: 30, // 默认30秒时间窗口
|
||||
digits: 6, // 默认6位数字
|
||||
timestamp: null, // 默认使用当前时间
|
||||
|
@ -40,7 +40,7 @@ async function generateTOTP(secret, options = {}) {
|
|||
const config = { ...DEFAULT_CONFIG, ...options };
|
||||
|
||||
// 验证算法
|
||||
if (!HASH_ALGOS[config.algorithm]) {
|
||||
if (!HASH_ALGORITHMS[config.algorithm]) {
|
||||
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
||||
}
|
||||
|
||||
|
@ -219,7 +219,7 @@ function generateTOTPUri(secret, accountName, issuer, options = {}) {
|
|||
const config = { ...DEFAULT_CONFIG, ...options };
|
||||
|
||||
// 验证算法
|
||||
if (!HASH_ALGOS[config.algorithm]) {
|
||||
if (!HASH_ALGORITHMS[config.algorithm]) {
|
||||
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
||||
}
|
||||
|
||||
|
@ -255,5 +255,5 @@ module.exports = {
|
|||
verifyTOTP,
|
||||
getRemainingSeconds,
|
||||
generateTOTPUri,
|
||||
HASH_ALGOS
|
||||
HASH_ALGORITHMS
|
||||
};
|
|
@ -43,7 +43,7 @@ function verifyTokenWindow(token, timestamp, period) {
|
|||
const generateCode = async (config) => {
|
||||
try {
|
||||
const options = {
|
||||
algorithm: config.algorithm || config.algo || 'SHA1',
|
||||
algorithm: config.algorithm || 'SHA1',
|
||||
digits: Number(config.digits) || 6,
|
||||
_forceRefresh: !!config._forceRefresh,
|
||||
timestamp: config.timestamp || otp.getCurrentTimestamp()
|
||||
|
@ -94,7 +94,7 @@ const generateCode = async (config) => {
|
|||
const verifyCode = async (token, config) => {
|
||||
try {
|
||||
const options = {
|
||||
algorithm: config.algorithm || config.algo || 'SHA1',
|
||||
algorithm: config.algorithm || 'SHA1',
|
||||
digits: Number(config.digits) || 6,
|
||||
timestamp: otp.getCurrentTimestamp() // 使用统一的时间戳获取方法
|
||||
};
|
||||
|
@ -150,12 +150,21 @@ const addToken = async (tokenData) => {
|
|||
|
||||
// 生成唯一ID
|
||||
const id = `token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const now = formatTime(new Date());
|
||||
const token = {
|
||||
...tokenData,
|
||||
id,
|
||||
createTime: formatTime(new Date())
|
||||
createTime: now,
|
||||
lastUpdate: now,
|
||||
code: '' // 初始化为空字符串
|
||||
};
|
||||
|
||||
// 对于HOTP类型,添加counter字段
|
||||
if (tokenData.type && tokenData.type.toUpperCase() === 'HOTP') {
|
||||
token.counter = 0; // HOTP类型需要counter >= 0
|
||||
}
|
||||
// 对于TOTP类型,不设置counter字段,让它在JSON序列化时被忽略
|
||||
|
||||
// 验证是否可以生成代码
|
||||
try {
|
||||
await generateCode(token);
|
||||
|
@ -235,7 +244,26 @@ const formatDate = (date) => {
|
|||
* @returns {Promise<Array>} 同步后的令牌列表
|
||||
*/
|
||||
const syncTokens = async (tokens) => {
|
||||
return await cloud.syncTokens(tokens);
|
||||
try {
|
||||
// 上传本地令牌到云端
|
||||
await cloud.uploadTokens(tokens);
|
||||
|
||||
// 获取云端最新数据
|
||||
const cloudData = await cloud.fetchLatestTokens();
|
||||
|
||||
// 使用cloud.js中的mergeTokens函数合并本地和云端数据
|
||||
const mergedTokens = cloud.mergeTokens(tokens, cloudData.tokens, { preferCloud: true });
|
||||
|
||||
// 更新所有令牌的时间戳
|
||||
for (const token of mergedTokens) {
|
||||
token.timestamp = cloudData.timestamp;
|
||||
}
|
||||
|
||||
return mergedTokens;
|
||||
} catch (error) {
|
||||
console.error('同步令牌失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -243,7 +271,16 @@ const syncTokens = async (tokens) => {
|
|||
* @returns {Promise<Array>} 云端的令牌列表
|
||||
*/
|
||||
const getCloudTokens = async () => {
|
||||
return await cloud.getTokens();
|
||||
try {
|
||||
const cloudData = await cloud.fetchLatestTokens();
|
||||
return cloudData.tokens;
|
||||
} catch (error) {
|
||||
// 如果是404错误(云端无数据),不打印错误日志
|
||||
if (error.statusCode !== 404) {
|
||||
console.error('获取云端令牌失败:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ============ UI相关功能 ============
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue