695 lines
No EOL
22 KiB
JavaScript
695 lines
No EOL
22 KiB
JavaScript
// index.js
|
||
const {
|
||
generateCode,
|
||
getRemainingSeconds,
|
||
formatTime,
|
||
syncTokens
|
||
} = require('../../utils/util');
|
||
const storage = require('../../utils/storage');
|
||
const eventManager = require('../../utils/eventManager');
|
||
|
||
Page({
|
||
data: {
|
||
tokens: [],
|
||
remainingSeconds: {}, // 存储每个令牌的剩余时间
|
||
loading: true,
|
||
syncing: false,
|
||
error: null
|
||
},
|
||
|
||
onLoad: function() {
|
||
// 初始化页面数据
|
||
this.loadTokens();
|
||
|
||
// 监听令牌更新事件
|
||
this.tokensUpdatedListener = (tokens) => {
|
||
this.loadTokens();
|
||
};
|
||
eventManager.on('tokensUpdated', this.tokensUpdatedListener);
|
||
|
||
// 初始化页面路由
|
||
this.pageRoutes = {
|
||
form: '/pages/form/form',
|
||
edit: '/pages/edit/edit'
|
||
};
|
||
},
|
||
|
||
onShow: function() {
|
||
// 先加载令牌列表
|
||
this.loadTokens();
|
||
// 然后延迟100ms刷新验证码,确保tokens已经加载
|
||
setTimeout(() => {
|
||
this.refreshTokens();
|
||
}, 100);
|
||
},
|
||
|
||
onPullDownRefresh: function() {
|
||
this.refreshTokens().then(() => {
|
||
wx.stopPullDownRefresh();
|
||
});
|
||
},
|
||
|
||
async loadTokens() {
|
||
try {
|
||
this.setData({ loading: true, error: null });
|
||
|
||
// 从本地存储加载令牌
|
||
const tokens = await wx.getStorageSync('tokens') || [];
|
||
// console.log('[DEBUG] Loaded tokens count:', tokens.length);
|
||
|
||
// 统计令牌类型
|
||
const typeCounts = tokens.reduce((acc, token) => {
|
||
acc[token.type] = (acc[token.type] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
// console.log('[DEBUG] Token types:', typeCounts);
|
||
|
||
// 初始化剩余时间
|
||
const remainingSeconds = {};
|
||
tokens.forEach(token => {
|
||
if (token.type === 'totp') {
|
||
const period = token.period || 30;
|
||
const remaining = getRemainingSeconds(period);
|
||
// console.log(`[DEBUG] Token ${token.id} period=${period}, remaining=${remaining}`);
|
||
remainingSeconds[token.id] = remaining;
|
||
}
|
||
});
|
||
|
||
// 获取当前时间戳
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
|
||
// 先更新验证码
|
||
let tokensToSet = [...tokens]; // 创建tokens的副本
|
||
if (tokensToSet.length > 0) {
|
||
const updatedTokens = await this.updateTokenCodes(tokensToSet);
|
||
if (Array.isArray(updatedTokens)) {
|
||
tokensToSet = updatedTokens;
|
||
}
|
||
}
|
||
|
||
// 设置状态
|
||
this.setData({
|
||
tokens: tokensToSet,
|
||
remainingSeconds,
|
||
loading: false
|
||
});
|
||
|
||
// 设置定时器,定期更新TOTP令牌
|
||
this.setupRefreshTimer();
|
||
} catch (error) {
|
||
console.error('Failed to load tokens:', error);
|
||
this.setData({
|
||
error: '加载令牌失败: ' + error.message,
|
||
loading: false
|
||
});
|
||
}
|
||
},
|
||
|
||
async refreshTokens() {
|
||
try {
|
||
const currentTokens = [...this.data.tokens]; // 创建tokens的副本
|
||
const updatedTokens = await this.updateTokenCodes(currentTokens);
|
||
|
||
// 更新剩余时间
|
||
const remainingSeconds = {};
|
||
updatedTokens.forEach(token => {
|
||
if (token.type === 'totp') {
|
||
remainingSeconds[token.id] = getRemainingSeconds(token.period || 30);
|
||
}
|
||
});
|
||
|
||
this.setData({
|
||
tokens: updatedTokens,
|
||
remainingSeconds
|
||
});
|
||
} catch (error) {
|
||
console.error('Failed to refresh tokens:', error);
|
||
this.setData({
|
||
error: '刷新令牌失败: ' + error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
setupRefreshTimer() {
|
||
// 清除现有的定时器
|
||
if (this.refreshTimer) {
|
||
clearInterval(this.refreshTimer);
|
||
}
|
||
|
||
// console.log('[DEBUG] Setting up refresh timer...');
|
||
|
||
// 设置新的定时器,每秒检查一次
|
||
this.refreshTimer = setInterval(() => {
|
||
// console.log('[DEBUG] Timer tick - checking tokens...');
|
||
|
||
const currentTokens = [...this.data.tokens]; // 创建tokens的副本
|
||
const currentRemainingSeconds = this.data.remainingSeconds || {};
|
||
const remainingSeconds = {};
|
||
const tokensToUpdate = [];
|
||
|
||
currentTokens.forEach(token => {
|
||
// 处理所有支持的OTP类型
|
||
const supportedTypes = ['totp', 'hotp', 'otpauth'];
|
||
if (!supportedTypes.includes(token.type)) {
|
||
// console.log(`[DEBUG] Skipping unsupported token type: ${token.type}`);
|
||
return;
|
||
}
|
||
|
||
// console.log(`[DEBUG] Processing ${token.type.toUpperCase()} token ${token.id || token.name}`);
|
||
|
||
// 针对不同类型做特殊处理
|
||
switch(token.type) {
|
||
case 'totp':
|
||
// 标准TOTP处理逻辑
|
||
break;
|
||
case 'hotp':
|
||
// HOTP特定处理
|
||
// console.log(`[DEBUG] HOTP token detected, counter: ${token.counter}`);
|
||
break;
|
||
case 'otpauth':
|
||
// 扫描二维码生成的令牌
|
||
// console.log(`[DEBUG] OTPAUTH URI token detected`);
|
||
break;
|
||
}
|
||
|
||
const period = token.period || 30;
|
||
const remaining = getRemainingSeconds(period);
|
||
const prevRemaining = currentRemainingSeconds[token.id];
|
||
|
||
// console.log(`[DEBUG] Token ${token.id}: prev=${prevRemaining}, curr=${remaining}`);
|
||
|
||
// 更新剩余时间
|
||
remainingSeconds[token.id] = remaining;
|
||
|
||
// 在以下情况下更新token:
|
||
// 1. 剩余时间为0(新的时间窗口开始)
|
||
// 2. 之前没有剩余时间记录(首次加载)
|
||
// 3. 时间窗口发生变化(prevRemaining > remaining,说明跨越了时间窗口)
|
||
if (remaining === 0 || prevRemaining === undefined || (prevRemaining > 0 && remaining < prevRemaining)) {
|
||
// console.log(`[DEBUG] Token ${token.id} needs update`);
|
||
tokensToUpdate.push({...token}); // 创建token的副本
|
||
}
|
||
});
|
||
|
||
// 只在有变化时更新剩余时间
|
||
if (Object.keys(remainingSeconds).length > 0) {
|
||
// console.log('[DEBUG] Updating remainingSeconds:', remainingSeconds);
|
||
this.setData({
|
||
'remainingSeconds': {
|
||
...currentRemainingSeconds,
|
||
...remainingSeconds
|
||
}
|
||
}, () => {
|
||
// console.log('[DEBUG] remainingSeconds updated');
|
||
});
|
||
}
|
||
|
||
// 只更新需要更新的token,并传入当前时间戳
|
||
if (tokensToUpdate.length > 0) {
|
||
// console.log('[DEBUG] Updating tokens:', tokensToUpdate.length);
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
this.updateSelectedTokens(tokensToUpdate, timestamp);
|
||
}
|
||
}, 1000);
|
||
},
|
||
|
||
// 只更新选定的token
|
||
async updateSelectedTokens(tokensToUpdate, timestamp) {
|
||
try {
|
||
const currentTokens = this.data.tokens;
|
||
// 获取当前时间戳,如果没有传入
|
||
const currentTimestamp = timestamp || Math.floor(Date.now() / 1000);
|
||
|
||
// 为每个token计算正确的时间戳
|
||
const updatePromises = tokensToUpdate.map(token => {
|
||
// 优先使用令牌中的时间戳(如果有的话)
|
||
const tokenTimestamp = token.timestamp || currentTimestamp;
|
||
|
||
if (token.type === 'totp') {
|
||
// 计算时间窗口的开始时间
|
||
const period = token.period || 30;
|
||
const windowStart = Math.floor(tokenTimestamp / period) * period;
|
||
return this.updateTokenCode(token, windowStart);
|
||
} else {
|
||
// 对于HOTP类型,直接使用时间戳
|
||
return this.updateTokenCode(token, tokenTimestamp);
|
||
}
|
||
});
|
||
|
||
const updatedTokens = await Promise.all(updatePromises);
|
||
|
||
// 创建token ID到更新后token的映射
|
||
const updatedTokenMap = {};
|
||
updatedTokens.forEach(token => {
|
||
updatedTokenMap[token.id] = token;
|
||
});
|
||
|
||
// 更新tokens数组中的特定token
|
||
const newTokens = currentTokens.map(token =>
|
||
updatedTokenMap[token.id] ? updatedTokenMap[token.id] : token
|
||
);
|
||
|
||
this.setData({ tokens: newTokens });
|
||
} catch (error) {
|
||
console.error('更新选定token失败:', error);
|
||
}
|
||
},
|
||
|
||
async updateTokenCodes(tokens) {
|
||
if (!Array.isArray(tokens) || tokens.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
// 获取当前时间戳
|
||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||
|
||
// 并行更新所有令牌的验证码,为每个令牌计算其时间窗口的开始时间
|
||
const updatePromises = tokens.map(token => {
|
||
// 优先使用令牌中的时间戳(如果有的话)
|
||
const tokenTimestamp = token.timestamp || currentTimestamp;
|
||
|
||
if (token.type === 'totp') {
|
||
// 计算时间窗口的开始时间
|
||
const period = token.period || 30;
|
||
const windowStart = Math.floor(tokenTimestamp / period) * period;
|
||
return this.updateTokenCode(token, windowStart);
|
||
} else {
|
||
// 对于HOTP类型,直接使用时间戳
|
||
return this.updateTokenCode(token, tokenTimestamp);
|
||
}
|
||
});
|
||
|
||
const updatedTokens = await Promise.all(updatePromises);
|
||
return updatedTokens;
|
||
} catch (error) {
|
||
console.error('更新验证码失败:', error);
|
||
// 出错时返回原始tokens,避免undefined
|
||
return tokens;
|
||
}
|
||
},
|
||
|
||
async updateTokenCode(token, timestamp) {
|
||
try {
|
||
// 生成新的验证码,使用传入的统一时间戳
|
||
const code = await generateCode({
|
||
type: token.type,
|
||
secret: token.secret,
|
||
algorithm: token.algorithm || 'SHA1',
|
||
digits: token.digits || 6,
|
||
period: token.period || 30,
|
||
counter: token.counter,
|
||
timestamp: timestamp // 使用传入的统一时间戳
|
||
});
|
||
|
||
// 更新令牌数据
|
||
token.code = code;
|
||
token.lastUpdate = formatTime(new Date());
|
||
|
||
return token;
|
||
} catch (error) {
|
||
console.error(`更新令牌 ${token.id} 失败:`, error);
|
||
token.error = error.message;
|
||
return token;
|
||
}
|
||
},
|
||
|
||
async refreshHotpToken(event) {
|
||
const tokenId = event.currentTarget.dataset.tokenId;
|
||
const token = this.data.tokens.find(t => t.id === tokenId);
|
||
|
||
if (!token) return;
|
||
|
||
try {
|
||
// 增加计数器值
|
||
const newCounter = (token.counter || 0) + 1;
|
||
|
||
// 生成新的验证码
|
||
const code = await generateCode({
|
||
type: 'hotp',
|
||
secret: token.secret,
|
||
algorithm: token.algorithm || 'SHA1',
|
||
counter: newCounter,
|
||
digits: token.digits || 6
|
||
});
|
||
|
||
// 更新令牌数据
|
||
token.code = code;
|
||
token.counter = newCounter;
|
||
token.lastUpdate = formatTime(new Date());
|
||
|
||
// 更新存储和状态
|
||
const tokens = this.data.tokens.map(t =>
|
||
t.id === token.id ? token : t
|
||
);
|
||
|
||
await wx.setStorageSync('tokens', tokens);
|
||
this.setData({ tokens });
|
||
|
||
// 同步到云端
|
||
if (this.data.syncing) {
|
||
await syncTokens(tokens);
|
||
}
|
||
} catch (error) {
|
||
console.error(`刷新HOTP令牌 ${token.id} 失败:`, error);
|
||
this.setData({
|
||
error: '刷新HOTP令牌失败: ' + error.message
|
||
});
|
||
}
|
||
},
|
||
|
||
async syncWithCloud() {
|
||
try {
|
||
this.setData({ syncing: true, error: null });
|
||
|
||
// 获取本地令牌
|
||
const localTokens = this.data.tokens;
|
||
|
||
// 同步令牌
|
||
const syncedTokens = await syncTokens(localTokens);
|
||
|
||
// 更新所有令牌的验证码
|
||
await this.updateTokenCodes(syncedTokens);
|
||
|
||
// 更新存储和状态
|
||
await wx.setStorageSync('tokens', syncedTokens);
|
||
this.setData({
|
||
tokens: syncedTokens,
|
||
syncing: false
|
||
});
|
||
} catch (error) {
|
||
console.error('同步失败:', error);
|
||
this.setData({
|
||
error: '同步失败: ' + error.message,
|
||
syncing: false
|
||
});
|
||
}
|
||
},
|
||
|
||
onUnload: function() {
|
||
// 清除定时器
|
||
if (this.refreshTimer) {
|
||
clearInterval(this.refreshTimer);
|
||
}
|
||
|
||
// 移除令牌更新事件监听
|
||
if (this.tokensUpdatedListener) {
|
||
eventManager.off('tokensUpdated', this.tokensUpdatedListener);
|
||
}
|
||
},
|
||
|
||
// 处理令牌点击事件
|
||
handleTokenTap(event) {
|
||
const tokenId = event.currentTarget.dataset.tokenId;
|
||
const token = this.data.tokens.find(t => t.id === tokenId);
|
||
|
||
if (!token) return;
|
||
|
||
// 复制验证码到剪贴板
|
||
wx.setClipboardData({
|
||
data: token.code,
|
||
success: () => {
|
||
wx.showToast({
|
||
title: '验证码已复制',
|
||
icon: 'success',
|
||
duration: 1500
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
// 显示添加令牌菜单
|
||
showAddTokenMenu() {
|
||
wx.showActionSheet({
|
||
itemList: ['扫描二维码', '手动添加'],
|
||
success: (res) => {
|
||
switch (res.tapIndex) {
|
||
case 0: // 扫描二维码
|
||
wx.scanCode({
|
||
onlyFromCamera: false,
|
||
scanType: ['qrCode'],
|
||
success: (res) => {
|
||
try {
|
||
// 解析二维码内容
|
||
const qrContent = res.result;
|
||
|
||
// 导入util.js中的parseURL函数
|
||
const { parseURL } = require('../../utils/util');
|
||
|
||
// 使用parseURL函数解析二维码内容
|
||
const parsedToken = parseURL(qrContent);
|
||
|
||
if (parsedToken) {
|
||
// 构建表单数据
|
||
const formData = {
|
||
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 (formData.type === 'totp' && (formData.period < 15 || formData.period > 60)) {
|
||
formData.period = 30;
|
||
console.warn('TOTP周期无效,已设置为默认值30秒');
|
||
}
|
||
|
||
// 检查必要参数是否完整
|
||
if (formData.secret && formData.type) {
|
||
// 直接添加令牌
|
||
this.addTokenDirectly(formData).then(() => {
|
||
wx.showToast({
|
||
title: '令牌已添加',
|
||
icon: 'success'
|
||
});
|
||
}).catch(error => {
|
||
console.error('添加令牌失败:', error);
|
||
// 失败后跳转到表单页面
|
||
const queryString = Object.entries(formData)
|
||
.filter(([_, value]) => value !== undefined)
|
||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||
.join('&');
|
||
|
||
wx.navigateTo({
|
||
url: `${this.pageRoutes.form}?${queryString}`
|
||
});
|
||
});
|
||
} else {
|
||
// 参数不完整,跳转到表单页面
|
||
const queryString = Object.entries(formData)
|
||
.filter(([_, value]) => value !== undefined)
|
||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||
.join('&');
|
||
|
||
wx.navigateTo({
|
||
url: `${this.pageRoutes.form}?${queryString}`
|
||
});
|
||
}
|
||
} else {
|
||
wx.showToast({
|
||
title: '无效的二维码格式',
|
||
icon: 'error'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('解析二维码失败:', error);
|
||
wx.showToast({
|
||
title: '解析二维码失败',
|
||
icon: 'error'
|
||
});
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('扫描二维码失败:', error);
|
||
if (error.errMsg.includes('cancel')) {
|
||
// 用户取消扫码,不显示错误提示
|
||
return;
|
||
}
|
||
wx.showToast({
|
||
title: '扫描失败',
|
||
icon: 'error'
|
||
});
|
||
}
|
||
});
|
||
break;
|
||
case 1: // 手动添加
|
||
wx.navigateTo({
|
||
url: '/pages/form/form',
|
||
events: {
|
||
// 监听form页面的事件
|
||
tokenAdded: () => {
|
||
this.loadTokens();
|
||
}
|
||
},
|
||
success: () => {
|
||
// 跳转成功后的回调
|
||
wx.showToast({
|
||
title: '请填写令牌信息',
|
||
icon: 'none',
|
||
duration: 1000
|
||
});
|
||
}
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 编辑令牌
|
||
editToken(event) {
|
||
const id = event.currentTarget.dataset.tokenId;
|
||
wx.navigateTo({
|
||
url: `${this.pageRoutes.edit}?token_id=${id}`
|
||
});
|
||
},
|
||
|
||
// 删除令牌
|
||
deleteToken(event) {
|
||
const id = event.currentTarget.dataset.id;
|
||
const that = this;
|
||
|
||
wx.showModal({
|
||
title: '确认删除',
|
||
content: '确定要删除这个令牌吗?',
|
||
success(res) {
|
||
if (res.confirm) {
|
||
// 从本地存储中删除
|
||
wx.getStorage({
|
||
key: 'tokens',
|
||
success(res) {
|
||
const tokens = res.data.filter(token => token.id !== id);
|
||
wx.setStorage({
|
||
key: 'tokens',
|
||
data: tokens,
|
||
success() {
|
||
// 更新UI
|
||
that.setData({ tokens });
|
||
wx.showToast({
|
||
title: '删除成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
// 如果正在同步,更新云端
|
||
if (that.data.syncing) {
|
||
syncTokens(tokens).catch(err => {
|
||
console.error('同步删除失败:', err);
|
||
});
|
||
}
|
||
},
|
||
fail() {
|
||
wx.showToast({
|
||
title: '删除失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 直接添加令牌
|
||
async addTokenDirectly(tokenData) {
|
||
try {
|
||
// 获取当前令牌列表
|
||
const tokens = await wx.getStorageSync('tokens') || [];
|
||
|
||
// 生成唯一ID和时间戳
|
||
// 生成唯一ID和时间戳
|
||
const newToken = {
|
||
...tokenData,
|
||
id: Date.now().toString(),
|
||
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);
|
||
const currentRemainingSeconds = this.data.remainingSeconds || {};
|
||
|
||
// 先更新剩余时间,避免闪烁
|
||
this.setData({
|
||
remainingSeconds: {
|
||
...currentRemainingSeconds,
|
||
[newToken.id]: getRemainingSeconds(period)
|
||
}
|
||
});
|
||
}
|
||
|
||
// 生成验证码
|
||
try {
|
||
// 确保类型是字符串且为小写
|
||
const type = (newToken.type || 'totp').toLowerCase();
|
||
// 确保digits是数字
|
||
const digits = parseInt(newToken.digits || '6', 10);
|
||
// 确保period是数字
|
||
const period = parseInt(newToken.period || '30', 10);
|
||
// 确保counter是数字(如果存在)
|
||
const counter = newToken.counter ? parseInt(newToken.counter, 10) : undefined;
|
||
|
||
const code = await generateCode({
|
||
type,
|
||
secret: newToken.secret,
|
||
algorithm: newToken.algorithm || 'SHA1',
|
||
digits,
|
||
period,
|
||
counter
|
||
});
|
||
newToken.code = code;
|
||
} catch (error) {
|
||
console.error('生成验证码失败:', error);
|
||
wx.showToast({
|
||
title: '生成验证码失败: ' + error.message,
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
throw error;
|
||
}
|
||
|
||
// 添加到列表并保存
|
||
const updatedTokens = [...tokens, newToken];
|
||
await wx.setStorageSync('tokens', updatedTokens);
|
||
|
||
// 更新UI,使用setData的回调确保UI更新完成
|
||
this.setData({ tokens: updatedTokens }, () => {
|
||
// 触发更新事件
|
||
eventManager.emit('tokensUpdated', updatedTokens);
|
||
|
||
// 如果正在同步,更新云端
|
||
if (this.data.syncing) {
|
||
syncTokens(updatedTokens).catch(err => {
|
||
console.error('同步新令牌失败:', err);
|
||
});
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('直接添加令牌失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// 阻止触摸移动
|
||
catchTouchMove() {
|
||
return false;
|
||
}
|
||
}); |