diff --git a/app.js b/app.js index 569f0b0..b40f29c 100644 --- a/app.js +++ b/app.js @@ -25,24 +25,45 @@ App({ async userLogin(options) { try { // 获取微信登录凭证 - const { code } = await new Promise((resolve, reject) => { + const loginResult = await new Promise((resolve, reject) => { wx.login({ - success: resolve, - fail: reject + success: (res) => { + if (res.code) { + resolve(res); + } else { + reject(new Error('获取微信登录凭证失败:' + (res.errMsg || '未知错误'))); + } + }, + fail: (error) => reject(new Error('微信登录失败:' + (error.errMsg || '未知错误'))) }); }); + if (!loginResult.code) { + throw new Error('未获取到微信登录凭证'); + } + // 调用后端登录接口 - const loginResult = await cloud.request({ - url: config.API_ENDPOINTS.AUTH.LOGIN, + const response = await cloud.request(config.API_ENDPOINTS.AUTH.LOGIN, { method: 'POST', - data: { code }, - needToken: false + data: { code: loginResult.code }, + header: { + 'Content-Type': 'application/json' + } }); - // 保存登录信息到storage - wx.setStorageSync(config.JWT_CONFIG.storage.access, loginResult.accessToken); - wx.setStorageSync(config.JWT_CONFIG.storage.refresh, loginResult.refreshToken); + // 检查响应数据 + if (!response || typeof response !== 'object') { + throw new Error('登录响应格式错误'); + } + + // 检查并保存登录信息到storage + if (!response.access_token || !response.refresh_token) { + console.error('登录响应数据:', response); + throw new Error('登录响应缺少必要的token信息'); + } + + wx.setStorageSync(config.JWT_CONFIG.storage.access, response.access_token); + wx.setStorageSync(config.JWT_CONFIG.storage.refresh, response.refresh_token); // 初始化基本用户信息 const userInfo = { @@ -51,9 +72,6 @@ App({ }; this.globalData.userInfo = userInfo; - // 同步OTP数据 - await this.syncOtpData(); - return userInfo; } catch (error) { console.error('登录失败:', error); @@ -62,28 +80,52 @@ App({ } }, - async syncOtpData() { - try { - // 获取本地数据 - const localTokens = this.globalData.otpList; - - // 使用util.js中的syncTokens函数进行同步 - // 注意:syncTokens函数已经被更新为使用cloud模块 - const syncedTokens = await syncTokens(localTokens); - - // 更新本地存储和全局数据 - wx.setStorageSync('tokens', syncedTokens); - this.globalData.otpList = syncedTokens; - - } catch (error) { - console.error('同步OTP数据失败:', error); - // 同步失败不显示错误提示,因为这是自动同步过程 + // 全局退出登录方法 + logout() { + // 清除token相关storage数据(保留tokens存储) + wx.removeStorageSync(config.JWT_CONFIG.storage.access); + wx.removeStorageSync(config.JWT_CONFIG.storage.refresh); + + // 保留用户自定义头像和昵称 + const customAvatar = wx.getStorageSync('customAvatar'); + const customNickName = wx.getStorageSync('customNickName'); + + // 清除其他用户数据 + wx.removeStorageSync('userAvatar'); + wx.removeStorageSync('userNickName'); + + // 恢复自定义设置 + if (customAvatar) wx.setStorageSync('userAvatar', customAvatar); + if (customNickName) wx.setStorageSync('userNickName', customNickName); + + // 重置全局数据(保留otpList) + this.globalData.userInfo = null; + + // 触发全局事件通知 + if (this.globalData.eventEmitter) { + this.globalData.eventEmitter.emit('logout'); } }, globalData: { version: 103, otpList: [], - userInfo: null + userInfo: null, + // 添加简单的事件发射器 + eventEmitter: { + listeners: {}, + on(event, callback) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + }, + emit(event, ...args) { + const callbacks = this.listeners[event]; + if (callbacks) { + callbacks.forEach(cb => cb(...args)); + } + } + } } }) \ No newline at end of file diff --git a/pages/edit/edit.js b/pages/edit/edit.js index a9824af..ec717a1 100644 --- a/pages/edit/edit.js +++ b/pages/edit/edit.js @@ -7,7 +7,7 @@ Page({ */ data: { issuer: '', - remark: '', + account: '', secret: '', type: '', counter: 0, @@ -64,14 +64,18 @@ Page({ return } + const type = (targetToken.type || 'totp').toLowerCase(); + const isHotp = type === 'hotp'; + self.setData({ tokens: tokens, issuer: targetToken.issuer || '', - remark: targetToken.remark || '', + account: targetToken.account || '', secret: targetToken.secret || '', - type: targetToken.type || 'totp', - counter: targetToken.counter || 0, - isHotp: targetToken.type === 'hotp', + type: type, + // 只有HOTP类型才设置counter + counter: isHotp ? (targetToken.counter || 0) : undefined, + isHotp: isHotp, hasLoaded: true }) }, @@ -160,18 +164,28 @@ Page({ } // 更新令牌数据 - updatedTokens[tokenIndex] = { + const type = this.data.type.toLowerCase(); + const isHotp = type === 'hotp'; + + // 创建基本的更新对象 + const updatedToken = { ...updatedTokens[tokenIndex], issuer: values.issuer.trim(), - remark: (values.remark || '').trim(), + account: (values.account || '').trim(), secret: this.data.secret, // 使用已加载的secret,而不是从表单获取 - type: this.data.type - } + type: type + }; // 如果是HOTP类型,更新计数器值 - if (this.data.isHotp && values.counter !== undefined) { - updatedTokens[tokenIndex].counter = parseInt(values.counter) + if (isHotp && values.counter !== undefined) { + updatedToken.counter = parseInt(values.counter); + } else if (!isHotp) { + // 如果是TOTP类型,删除counter字段 + delete updatedToken.counter; } + + // 更新令牌数组 + updatedTokens[tokenIndex] = updatedToken; wx.setStorage({ key: 'tokens', @@ -223,7 +237,7 @@ Page({ hasLoaded: false, isSubmitting: false, issuer: '', - remark: '', + account: '', secret: '', type: '', counter: 0, diff --git a/pages/edit/edit.wxml b/pages/edit/edit.wxml index 6d9be71..d376ba5 100644 --- a/pages/edit/edit.wxml +++ b/pages/edit/edit.wxml @@ -8,7 +8,7 @@ Account - + diff --git a/pages/form/form.js b/pages/form/form.js index be7e276..3d50fb6 100644 --- a/pages/form/form.js +++ b/pages/form/form.js @@ -20,16 +20,20 @@ Page({ */ onLoad: function (options) { // 准备初始数据 + const type = (options.type || 'totp').toLowerCase(); + const isHotp = type === 'hotp'; + const initialData = { - type: options.type || 'totp', + type: type, formData: { issuer: '', - remark: '', + account: '', secret: '', - algo: 'SHA1', + algorithm: 'SHA1', digits: '6', period: '30', - counter: '0' + // 只有HOTP类型才设置counter初始值 + ...(isHotp ? { counter: '0' } : {}) }, pageReady: true // 直接设置为ready状态 }; @@ -41,15 +45,19 @@ Page({ const parsedToken = parseURL(scanData); if (parsedToken) { - initialData.type = parsedToken.type; + const type = (parsedToken.type || 'totp').toLowerCase(); + const isHotp = type === 'hotp'; + + initialData.type = type; initialData.formData = { issuer: parsedToken.issuer || '', - remark: parsedToken.remark || '', + account: parsedToken.account || '', secret: parsedToken.secret || '', - algo: parsedToken.algo || 'SHA1', + algorithm: parsedToken.algorithm || 'SHA1', digits: parsedToken.digits || '6', period: parsedToken.period || '30', - counter: parsedToken.counter || '0' + // 只有HOTP类型才设置counter + ...(isHotp ? { counter: parsedToken.counter || '0' } : {}) }; // 立即显示成功提示 @@ -91,8 +99,8 @@ Page({ if (!values.issuer || !values.issuer.trim()) { throw new Error('请输入服务名称'); } - if (!values.remark || !values.remark.trim()) { - throw new Error('请输入账号备注'); + if (!values.account || !values.account.trim()) { + throw new Error('请输入账户名称'); } if (!values.secret || !values.secret.trim()) { throw new Error('请输入密钥'); @@ -100,16 +108,16 @@ Page({ // 格式化数据 const tokenData = { - type: values.type, + type: this.data.type, issuer: values.issuer.trim(), - remark: values.remark.trim(), + account: values.account.trim(), secret: values.secret.trim().toUpperCase(), - algo: values.algo, + algorithm: values.algorithm, digits: parseInt(values.digits, 10) }; // 类型特定字段 - if (values.type === 'totp') { + if (this.data.type === 'totp') { const period = parseInt(values.period, 10); if (isNaN(period) || period < 15 || period > 300) { throw new Error('更新周期必须在15-300秒之间'); diff --git a/pages/form/form.wxml b/pages/form/form.wxml index f13face..48fda66 100644 --- a/pages/form/form.wxml +++ b/pages/form/form.wxml @@ -10,7 +10,7 @@ Account - + KEY @@ -35,10 +35,10 @@ 高级设置 算法 - - SHA1 - SHA256 - SHA512 + + SHA1 + SHA256 + SHA512 diff --git a/pages/index/index.js b/pages/index/index.js index 7de06f9..9c7ef0f 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -222,14 +222,17 @@ Page({ // 为每个token计算正确的时间戳 const updatePromises = tokensToUpdate.map(token => { + // 优先使用令牌中的时间戳(如果有的话) + const tokenTimestamp = token.timestamp || currentTimestamp; + if (token.type === 'totp') { // 计算时间窗口的开始时间 const period = token.period || 30; - const windowStart = Math.floor(currentTimestamp / period) * period; + const windowStart = Math.floor(tokenTimestamp / period) * period; return this.updateTokenCode(token, windowStart); } else { - // 对于HOTP类型,直接使用当前时间戳 - return this.updateTokenCode(token, currentTimestamp); + // 对于HOTP类型,直接使用时间戳 + return this.updateTokenCode(token, tokenTimestamp); } }); @@ -263,14 +266,17 @@ Page({ // 并行更新所有令牌的验证码,为每个令牌计算其时间窗口的开始时间 const updatePromises = tokens.map(token => { + // 优先使用令牌中的时间戳(如果有的话) + const tokenTimestamp = token.timestamp || currentTimestamp; + if (token.type === 'totp') { // 计算时间窗口的开始时间 const period = token.period || 30; - const windowStart = Math.floor(currentTimestamp / period) * period; + const windowStart = Math.floor(tokenTimestamp / period) * period; return this.updateTokenCode(token, windowStart); } else { - // 对于HOTP类型,直接使用当前时间戳 - return this.updateTokenCode(token, currentTimestamp); + // 对于HOTP类型,直接使用时间戳 + return this.updateTokenCode(token, tokenTimestamp); } }); @@ -426,73 +432,33 @@ Page({ try { // 解析二维码内容 const qrContent = res.result; - // 如果是otpauth://格式的URL - if (qrContent.startsWith('otpauth://')) { - // 小程序兼容的URL解析 - const [protocolAndPath, search] = qrContent.split('?'); - const [protocol, path] = protocolAndPath.split('://'); - const type = protocol.replace('otpauth:', ''); - - // 解析路径部分 - const decodedPath = decodeURIComponent(path.substring(1)); // 移除开头的/ - let [issuer, remark] = decodedPath.split(':'); - if (!remark) { - remark = issuer; - issuer = ''; - } - - // 解析查询参数 - const params = {}; - if (search) { - search.split('&').forEach(pair => { - const [key, value] = pair.split('='); - if (key && value) { - params[key] = decodeURIComponent(value); - } - }); - } - - // 从参数中获取issuer(如果存在) - if (params.issuer) { - issuer = params.issuer; - } - - // 验证secret参数 - if (!params.secret) { - wx.showToast({ - title: '无效的二维码:缺少secret参数', - icon: 'none', - duration: 2000 - }); - return; - } - - // 将otpauth类型转换为实际类型 - let validType = type.toLowerCase(); - if (validType === 'otpauth') { - // 从URI路径中提取实际类型 - validType = path.split('/')[0].toLowerCase(); - } - - // 构建表单数据,确保数字类型参数正确转换 + + // 导入util.js中的parseURL函数 + const { parseURL } = require('../../utils/util'); + + // 使用parseURL函数解析二维码内容 + const parsedToken = parseURL(qrContent); + + if (parsedToken) { + // 构建表单数据 const formData = { - type: validType, - issuer, - remark, - secret: params.secret, - algorithm: params.algorithm || 'SHA1', - digits: params.digits ? parseInt(params.digits, 10) : 6, - period: validType === 'totp' ? (params.period ? parseInt(params.period, 10) : 30) : undefined, - counter: validType === 'hotp' ? (params.counter ? parseInt(params.counter, 10) : 0) : undefined + type: parsedToken.type, + issuer: parsedToken.issuer || '', + account: parsedToken.account || '', + secret: parsedToken.secret || '', + algorithm: parsedToken.algorithm || 'SHA1', + digits: parsedToken.digits || 6, + period: parsedToken.type === 'totp' ? (parsedToken.period || 30) : undefined, + counter: parsedToken.type === 'hotp' ? (parsedToken.counter || 0) : undefined }; - + // 验证必要参数 if (formData.digits < 6 || formData.digits > 8) { formData.digits = 6; console.warn('验证码位数无效,已设置为默认值6'); } - if (validType === 'totp' && (formData.period < 15 || formData.period > 60)) { + if (formData.type === 'totp' && (formData.period < 15 || formData.period > 60)) { formData.period = 30; console.warn('TOTP周期无效,已设置为默认值30秒'); } @@ -640,14 +606,22 @@ Page({ // 获取当前令牌列表 const tokens = await wx.getStorageSync('tokens') || []; + // 生成唯一ID和时间戳 // 生成唯一ID和时间戳 const newToken = { ...tokenData, id: Date.now().toString(), - createdAt: new Date().toISOString(), - lastUpdate: new Date().toISOString() + createTime: formatTime(new Date()), + lastUpdate: formatTime(new Date()), + code: '' // 初始化为空字符串 }; + // 对于HOTP类型,添加counter字段 + if (tokenData.type && tokenData.type.toUpperCase() === 'HOTP') { + newToken.counter = 0; // HOTP类型需要counter >= 0 + } + // 对于TOTP类型,不设置counter字段,让它在JSON序列化时被忽略 + // 如果是TOTP类型,先初始化剩余时间 if ((newToken.type || 'totp').toLowerCase() === 'totp') { const period = parseInt(newToken.period || '30', 10); diff --git a/pages/index/index.skeleton.wxml b/pages/index/index.skeleton.wxml new file mode 100644 index 0000000..473c636 --- /dev/null +++ b/pages/index/index.skeleton.wxml @@ -0,0 +1,61 @@ + + \ No newline at end of file diff --git a/pages/index/index.skeleton.wxss b/pages/index/index.skeleton.wxss new file mode 100644 index 0000000..5285e5f --- /dev/null +++ b/pages/index/index.skeleton.wxss @@ -0,0 +1,69 @@ +/* +此文件为开发者工具生成,生成时间: 2025/6/12上午10:26:00 + +在 D:\Project\go\src\otp\miniprogram\pages\index\index.wxss 中引入样式 +``` +@import "./index.skeleton.wxss"; +``` + +更多详细信息可以参考文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/skeleton.html +*/ +.sk-transparent { + color: transparent !important; + } +.sk-text-14-2857-186 { + background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important; + background-size: 100% 32.3077rpx; + position: relative !important; + } +.sk-text { + background-origin: content-box !important; + background-clip: content-box !important; + background-color: transparent !important; + color: transparent !important; + background-repeat: repeat-y !important; + } +.sk-text-14-2857-326 { + background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important; + background-size: 100% 43.0769rpx; + position: relative !important; + } +.sk-text-14-2857-212 { + background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important; + background-size: 100% 64.6154rpx; + position: relative !important; + } +.sk-text-14-2857-783 { + background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important; + background-size: 100% 43.0769rpx; + position: relative !important; + } +.sk-text-14-2857-193 { + background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important; + background-size: 100% 43.0769rpx; + position: relative !important; + } +.sk-text-14-2857-416 { + background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important; + background-size: 100% 32.3077rpx; + position: relative !important; + } +.sk-text-14-2857-977 { + background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important; + background-size: 100% 53.8462rpx; + position: relative !important; + } +.sk-text-14-2857-863 { + background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important; + background-size: 100% 37.6923rpx; + position: relative !important; + } +.sk-container { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: hidden; + background-color: transparent; + } diff --git a/pages/mine/mine.js b/pages/mine/mine.js index 7f1b5b8..27b49d6 100644 --- a/pages/mine/mine.js +++ b/pages/mine/mine.js @@ -6,6 +6,8 @@ const { showLoading, hideLoading } = require('../../utils/util'); +const config = require('../../utils/config'); +const { hasValidTokens, clearAllTokens } = require('../../utils/cloud'); Page({ /** @@ -14,6 +16,7 @@ Page({ data: { loading: false, uploading: false, + clearing: false, isLoggedIn: false, userInfo: null, currentYear: new Date().getFullYear() @@ -22,9 +25,27 @@ Page({ onShow: function() { // 每次显示页面时检查登录状态 const app = getApp(); + const isLoggedIn = hasValidTokens(); + + // 如果未登录,强制重置用户信息 + if (!isLoggedIn) { + this.setData({ + isLoggedIn: false, + userInfo: { + avatarUrl: '/images/default-avatar.png', + nickName: '微信用户' + } + }); + return; + } + + // 如果已登录,优先使用globalData中的用户信息 this.setData({ - isLoggedIn: !!app.globalData.token, - userInfo: app.globalData.userInfo + isLoggedIn: true, + userInfo: app.globalData.userInfo || { + avatarUrl: '/images/default-avatar.png', + nickName: '微信用户' + } }); }, @@ -114,7 +135,10 @@ Page({ showToast('数据恢复成功', 'success'); } catch (error) { - console.error('数据恢复失败:', error); + // 如果是404错误(云端无数据),不打印错误日志 + if (error.statusCode !== 404) { + console.error('数据恢复失败:', error); + } showToast('数据恢复失败,请重试'); } finally { this.setData({ loading: false }); @@ -131,21 +155,33 @@ Page({ try { showLoading('登录中...'); const app = getApp(); - await app.userLogin(); + const userInfo = await app.userLogin(); - // 更新登录状态,使用默认值 + // 使用hasValidTokens检查完整的登录状态 + const isLoggedIn = hasValidTokens(); + + if (!isLoggedIn) { + throw new Error('登录失败:未能获取有效的访问令牌'); + } + + // 检查是否有自定义设置 + const customAvatar = wx.getStorageSync('customAvatar'); + const customNickName = wx.getStorageSync('customNickName'); + + // 更新登录状态,优先使用自定义设置 this.setData({ - isLoggedIn: !!app.globalData.token, + isLoggedIn: true, userInfo: { - avatarUrl: wx.getStorageSync('userAvatar') || '/images/default-avatar.png', - nickName: wx.getStorageSync('userNickName') || '微信用户' + ...(userInfo || {}), + avatarUrl: customAvatar || wx.getStorageSync('userAvatar') || '/images/default-avatar.png', + nickName: customNickName || wx.getStorageSync('userNickName') || '微信用户' } }); showToast('登录成功', 'success'); } catch (error) { console.error('登录失败:', error); - showToast('登录失败,请重试'); + showToast(error.message || '登录失败,请重试'); } finally { hideLoading(); } @@ -164,12 +200,25 @@ Page({ 'userInfo.avatarUrl': avatarUrl }); - // 保存到本地存储 - wx.setStorageSync('userAvatar', avatarUrl); + // 使用异步存储操作 + await Promise.all([ + wx.setStorage({ + key: 'userAvatar', + data: avatarUrl + }), + wx.setStorage({ + key: 'customAvatar', + data: avatarUrl + }) + ]); showToast('头像更新成功', 'success'); } catch (error) { console.error('头像更新失败:', error); + // 恢复默认头像 + this.setData({ + 'userInfo.avatarUrl': '/images/default-avatar.png' + }); showToast('头像更新失败,请重试'); } finally { hideLoading(); @@ -191,6 +240,8 @@ Page({ // 保存到本地存储 wx.setStorageSync('userNickName', nickName); + // 额外保存为自定义昵称 + wx.setStorageSync('customNickName', nickName); } catch (error) { console.error('昵称更新失败:', error); showToast('昵称更新失败,请重试'); @@ -204,6 +255,107 @@ Page({ wx.stopPullDownRefresh(); }, + /** + * 退出登录 + */ + onLoad: function() { + const app = getApp(); + // 监听全局logout事件 + app.globalData.eventEmitter.on('logout', this.handleLogout.bind(this)); + }, + + onUnload: function() { + const app = getApp(); + // 移除事件监听 + if (app.globalData.eventEmitter && this.handleLogout) { + app.globalData.eventEmitter.off('logout', this.handleLogout); + } + }, + + handleLogout: function() { + // 重置页面数据 + this.setData({ + isLoggedIn: false, + userInfo: { + avatarUrl: '/images/default-avatar.png', + nickName: '微信用户' + } + }); + }, + + /** + * 清空云端数据 + */ + clearCloudData: async function() { + // 检查登录状态 + if (!this.data.isLoggedIn) { + showToast('请先登录'); + return; + } + + try { + // 显示确认对话框 + const confirmed = await new Promise(resolve => { + wx.showModal({ + title: '清空云端数据', + content: '确定清空云端所有备份数据?此操作不可恢复!', + confirmText: '确定', + confirmColor: '#ff9c10', + success: res => resolve(res.confirm) + }); + }); + + if (!confirmed) return; + + this.setData({ clearing: true }); + showLoading('正在清空...'); + + // 调用清空接口 + await clearAllTokens(); + + showToast('云端数据已清空', 'success'); + } catch (error) { + console.error('清空云端数据失败:', error); + showToast(error.message || '清空失败,请重试'); + } finally { + this.setData({ clearing: false }); + hideLoading(); + } + }, + + logout: async function() { + try { + // 显示确认对话框 + const confirmed = await new Promise(resolve => { + wx.showModal({ + title: '退出登录', + content: '确定要退出登录吗?', + confirmText: '确定', + confirmColor: '#ff9c10', + success: res => resolve(res.confirm) + }); + }); + + if (!confirmed) return; + + showLoading('正在退出...'); + + // 调用全局logout方法 + const app = getApp(); + app.logout(); + + // 本地UI更新 + this.handleLogout(); + + showToast('已退出登录', 'success'); + } catch (error) { + console.error('退出登录失败:', error); + showToast('退出登录失败,请重试'); + } finally { + hideLoading(); + } + }, + /** * 转发 */ diff --git a/pages/mine/mine.wxml b/pages/mine/mine.wxml index fa0df1c..b403a51 100644 --- a/pages/mine/mine.wxml +++ b/pages/mine/mine.wxml @@ -5,13 +5,13 @@ @@ -20,6 +20,8 @@ + + diff --git a/pages/mine/mine.wxss b/pages/mine/mine.wxss index d8e327a..d236ef2 100644 --- a/pages/mine/mine.wxss +++ b/pages/mine/mine.wxss @@ -89,7 +89,8 @@ input.userinfo-nickname { opacity: 0.9; } -.user-bg image { +/* 修改选择器,避免影响头像图片 */ +.user-bg > image { width: 400rpx; height: 200rpx; } @@ -110,6 +111,12 @@ input.userinfo-nickname { background: #ffffff; color: #ff9c10; } + +.logout-btn { + background: #ffffff; + color: #ff3a30; + border: 2px solid #ffcccb; +} .footer{ position: absolute; bottom: 20px; diff --git a/project.config.json b/project.config.json index 37a0e35..59af078 100644 --- a/project.config.json +++ b/project.config.json @@ -3,7 +3,7 @@ "es6": true, "postcss": true, "minified": true, - "uglifyFileName": false, + "uglifyFileName": true, "enhance": true, "packNpmRelationList": [], "babelSetting": { @@ -20,6 +20,6 @@ "ignore": [], "include": [] }, - "appid": "wxb6599459668b6b55", + "appid": "wx57d1033974eb5250", "editorSetting": {} } \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json index 493b9cf..1787f80 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -1,6 +1,6 @@ { "libVersion": "3.8.6", - "projectname": "%25E5%258A%25A8%25E6%2580%2581%25E4%25BB%25A4%25E7%2589%258C", + "projectname": "otp", "setting": { "urlCheck": true, "coverView": true, diff --git a/utils/auth.js b/utils/auth.js index 2c6e6f8..48646ad 100644 --- a/utils/auth.js +++ b/utils/auth.js @@ -92,22 +92,31 @@ const refreshToken = async () => { /** * 用户登录 - * @param {string} username - 用户名 - * @param {string} password - 密码 * @returns {Promise} 登录结果,包含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) { diff --git a/utils/cloud.js b/utils/cloud.js index 9956fdc..30c3ea0 100644 --- a/utils/cloud.js +++ b/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} + * @returns {Promise} 刷新是否成功 */ 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} 响应数据 */ 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} 是否成功 + */ +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 }; \ No newline at end of file diff --git a/utils/config.js b/utils/config.js index 08814eb..d646a58 100644 --- a/utils/config.js +++ b/utils/config.js @@ -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 }; \ No newline at end of file diff --git a/utils/format.js b/utils/format.js index 7981399..35e0d67 100644 --- a/utils/format.js +++ b/utils/format.js @@ -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}`); } // 验证位数 diff --git a/utils/hmac.js b/utils/hmac.js index dbec0aa..252cbb8 100644 --- a/utils/hmac.js +++ b/utils/hmac.js @@ -39,13 +39,6 @@ if (!sjcl.codec.bytes) { }; } -// 微信小程序原生 createHMAC 接口映射表 -const HASH_ALGORITHMS = { - SHA1: 'sha1', - SHA256: 'sha256', - SHA512: 'sha512' -}; - /** * 生成 HMAC-SHA1/SHA256/SHA512 值 * diff --git a/utils/hotp.js b/utils/hotp.js index 1037cbd..080b4d2 100644 --- a/utils/hotp.js +++ b/utils/hotp.js @@ -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 }; \ No newline at end of file diff --git a/utils/otp.js b/utils/otp.js index 7ffccfd..e9c05a2 100644 --- a/utils/otp.js +++ b/utils/otp.js @@ -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 }); } diff --git a/utils/totp.js b/utils/totp.js index 2d62fc8..03c9216 100644 --- a/utils/totp.js +++ b/utils/totp.js @@ -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 }; \ No newline at end of file diff --git a/utils/util.js b/utils/util.js index 640b23f..4095a9f 100644 --- a/utils/util.js +++ b/utils/util.js @@ -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} 同步后的令牌列表 */ 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} 云端的令牌列表 */ 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相关功能 ============