This commit is contained in:
“xHuPo” 2025-06-09 13:35:15 +08:00
commit 2b8870a40e
51 changed files with 5845 additions and 0 deletions

721
pages/index/index.js Normal file
View file

@ -0,0 +1,721 @@
// 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 => {
if (token.type === 'totp') {
// 计算时间窗口的开始时间
const period = token.period || 30;
const windowStart = Math.floor(currentTimestamp / period) * period;
return this.updateTokenCode(token, windowStart);
} else {
// 对于HOTP类型直接使用当前时间戳
return this.updateTokenCode(token, currentTimestamp);
}
});
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 => {
if (token.type === 'totp') {
// 计算时间窗口的开始时间
const period = token.period || 30;
const windowStart = Math.floor(currentTimestamp / period) * period;
return this.updateTokenCode(token, windowStart);
} else {
// 对于HOTP类型直接使用当前时间戳
return this.updateTokenCode(token, currentTimestamp);
}
});
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;
// 如果是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();
}
// 构建表单数据,确保数字类型参数正确转换
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
};
// 验证必要参数
if (formData.digits < 6 || formData.digits > 8) {
formData.digits = 6;
console.warn('验证码位数无效已设置为默认值6');
}
if (validType === '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和时间戳
const newToken = {
...tokenData,
id: Date.now().toString(),
createdAt: new Date().toISOString(),
lastUpdate: new Date().toISOString()
};
// 如果是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;
}
});