fix mine
This commit is contained in:
parent
17a02ea47e
commit
70e7a113e6
22 changed files with 967 additions and 285 deletions
102
app.js
102
app.js
|
@ -25,24 +25,45 @@ App({
|
||||||
async userLogin(options) {
|
async userLogin(options) {
|
||||||
try {
|
try {
|
||||||
// 获取微信登录凭证
|
// 获取微信登录凭证
|
||||||
const { code } = await new Promise((resolve, reject) => {
|
const loginResult = await new Promise((resolve, reject) => {
|
||||||
wx.login({
|
wx.login({
|
||||||
success: resolve,
|
success: (res) => {
|
||||||
fail: reject
|
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({
|
const response = await cloud.request(config.API_ENDPOINTS.AUTH.LOGIN, {
|
||||||
url: config.API_ENDPOINTS.AUTH.LOGIN,
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { code },
|
data: { code: loginResult.code },
|
||||||
needToken: false
|
header: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保存登录信息到storage
|
// 检查响应数据
|
||||||
wx.setStorageSync(config.JWT_CONFIG.storage.access, loginResult.accessToken);
|
if (!response || typeof response !== 'object') {
|
||||||
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, loginResult.refreshToken);
|
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 = {
|
const userInfo = {
|
||||||
|
@ -51,9 +72,6 @@ App({
|
||||||
};
|
};
|
||||||
this.globalData.userInfo = userInfo;
|
this.globalData.userInfo = userInfo;
|
||||||
|
|
||||||
// 同步OTP数据
|
|
||||||
await this.syncOtpData();
|
|
||||||
|
|
||||||
return userInfo;
|
return userInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('登录失败:', error);
|
||||||
|
@ -62,28 +80,52 @@ App({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async syncOtpData() {
|
// 全局退出登录方法
|
||||||
try {
|
logout() {
|
||||||
// 获取本地数据
|
// 清除token相关storage数据(保留tokens存储)
|
||||||
const localTokens = this.globalData.otpList;
|
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
|
||||||
|
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||||
// 使用util.js中的syncTokens函数进行同步
|
|
||||||
// 注意:syncTokens函数已经被更新为使用cloud模块
|
// 保留用户自定义头像和昵称
|
||||||
const syncedTokens = await syncTokens(localTokens);
|
const customAvatar = wx.getStorageSync('customAvatar');
|
||||||
|
const customNickName = wx.getStorageSync('customNickName');
|
||||||
// 更新本地存储和全局数据
|
|
||||||
wx.setStorageSync('tokens', syncedTokens);
|
// 清除其他用户数据
|
||||||
this.globalData.otpList = syncedTokens;
|
wx.removeStorageSync('userAvatar');
|
||||||
|
wx.removeStorageSync('userNickName');
|
||||||
} catch (error) {
|
|
||||||
console.error('同步OTP数据失败:', error);
|
// 恢复自定义设置
|
||||||
// 同步失败不显示错误提示,因为这是自动同步过程
|
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: {
|
globalData: {
|
||||||
version: 103,
|
version: 103,
|
||||||
otpList: [],
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -7,7 +7,7 @@ Page({
|
||||||
*/
|
*/
|
||||||
data: {
|
data: {
|
||||||
issuer: '',
|
issuer: '',
|
||||||
remark: '',
|
account: '',
|
||||||
secret: '',
|
secret: '',
|
||||||
type: '',
|
type: '',
|
||||||
counter: 0,
|
counter: 0,
|
||||||
|
@ -64,14 +64,18 @@ Page({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const type = (targetToken.type || 'totp').toLowerCase();
|
||||||
|
const isHotp = type === 'hotp';
|
||||||
|
|
||||||
self.setData({
|
self.setData({
|
||||||
tokens: tokens,
|
tokens: tokens,
|
||||||
issuer: targetToken.issuer || '',
|
issuer: targetToken.issuer || '',
|
||||||
remark: targetToken.remark || '',
|
account: targetToken.account || '',
|
||||||
secret: targetToken.secret || '',
|
secret: targetToken.secret || '',
|
||||||
type: targetToken.type || 'totp',
|
type: type,
|
||||||
counter: targetToken.counter || 0,
|
// 只有HOTP类型才设置counter
|
||||||
isHotp: targetToken.type === 'hotp',
|
counter: isHotp ? (targetToken.counter || 0) : undefined,
|
||||||
|
isHotp: isHotp,
|
||||||
hasLoaded: true
|
hasLoaded: true
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -160,18 +164,28 @@ Page({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新令牌数据
|
// 更新令牌数据
|
||||||
updatedTokens[tokenIndex] = {
|
const type = this.data.type.toLowerCase();
|
||||||
|
const isHotp = type === 'hotp';
|
||||||
|
|
||||||
|
// 创建基本的更新对象
|
||||||
|
const updatedToken = {
|
||||||
...updatedTokens[tokenIndex],
|
...updatedTokens[tokenIndex],
|
||||||
issuer: values.issuer.trim(),
|
issuer: values.issuer.trim(),
|
||||||
remark: (values.remark || '').trim(),
|
account: (values.account || '').trim(),
|
||||||
secret: this.data.secret, // 使用已加载的secret,而不是从表单获取
|
secret: this.data.secret, // 使用已加载的secret,而不是从表单获取
|
||||||
type: this.data.type
|
type: type
|
||||||
}
|
};
|
||||||
|
|
||||||
// 如果是HOTP类型,更新计数器值
|
// 如果是HOTP类型,更新计数器值
|
||||||
if (this.data.isHotp && values.counter !== undefined) {
|
if (isHotp && values.counter !== undefined) {
|
||||||
updatedTokens[tokenIndex].counter = parseInt(values.counter)
|
updatedToken.counter = parseInt(values.counter);
|
||||||
|
} else if (!isHotp) {
|
||||||
|
// 如果是TOTP类型,删除counter字段
|
||||||
|
delete updatedToken.counter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新令牌数组
|
||||||
|
updatedTokens[tokenIndex] = updatedToken;
|
||||||
|
|
||||||
wx.setStorage({
|
wx.setStorage({
|
||||||
key: 'tokens',
|
key: 'tokens',
|
||||||
|
@ -223,7 +237,7 @@ Page({
|
||||||
hasLoaded: false,
|
hasLoaded: false,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
issuer: '',
|
issuer: '',
|
||||||
remark: '',
|
account: '',
|
||||||
secret: '',
|
secret: '',
|
||||||
type: '',
|
type: '',
|
||||||
counter: 0,
|
counter: 0,
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</view>
|
</view>
|
||||||
<view class="input-box">
|
<view class="input-box">
|
||||||
<text>Account</text>
|
<text>Account</text>
|
||||||
<input name="remark" placeholder="帐号备注" placeholder-style="color: #666; font-size: 1rem;" value="{{remark}}"/>
|
<input name="account" placeholder="账户名称" placeholder-style="color: #666; font-size: 1rem;" value="{{account}}"/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
|
@ -20,16 +20,20 @@ Page({
|
||||||
*/
|
*/
|
||||||
onLoad: function (options) {
|
onLoad: function (options) {
|
||||||
// 准备初始数据
|
// 准备初始数据
|
||||||
|
const type = (options.type || 'totp').toLowerCase();
|
||||||
|
const isHotp = type === 'hotp';
|
||||||
|
|
||||||
const initialData = {
|
const initialData = {
|
||||||
type: options.type || 'totp',
|
type: type,
|
||||||
formData: {
|
formData: {
|
||||||
issuer: '',
|
issuer: '',
|
||||||
remark: '',
|
account: '',
|
||||||
secret: '',
|
secret: '',
|
||||||
algo: 'SHA1',
|
algorithm: 'SHA1',
|
||||||
digits: '6',
|
digits: '6',
|
||||||
period: '30',
|
period: '30',
|
||||||
counter: '0'
|
// 只有HOTP类型才设置counter初始值
|
||||||
|
...(isHotp ? { counter: '0' } : {})
|
||||||
},
|
},
|
||||||
pageReady: true // 直接设置为ready状态
|
pageReady: true // 直接设置为ready状态
|
||||||
};
|
};
|
||||||
|
@ -41,15 +45,19 @@ Page({
|
||||||
const parsedToken = parseURL(scanData);
|
const parsedToken = parseURL(scanData);
|
||||||
|
|
||||||
if (parsedToken) {
|
if (parsedToken) {
|
||||||
initialData.type = parsedToken.type;
|
const type = (parsedToken.type || 'totp').toLowerCase();
|
||||||
|
const isHotp = type === 'hotp';
|
||||||
|
|
||||||
|
initialData.type = type;
|
||||||
initialData.formData = {
|
initialData.formData = {
|
||||||
issuer: parsedToken.issuer || '',
|
issuer: parsedToken.issuer || '',
|
||||||
remark: parsedToken.remark || '',
|
account: parsedToken.account || '',
|
||||||
secret: parsedToken.secret || '',
|
secret: parsedToken.secret || '',
|
||||||
algo: parsedToken.algo || 'SHA1',
|
algorithm: parsedToken.algorithm || 'SHA1',
|
||||||
digits: parsedToken.digits || '6',
|
digits: parsedToken.digits || '6',
|
||||||
period: parsedToken.period || '30',
|
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()) {
|
if (!values.issuer || !values.issuer.trim()) {
|
||||||
throw new Error('请输入服务名称');
|
throw new Error('请输入服务名称');
|
||||||
}
|
}
|
||||||
if (!values.remark || !values.remark.trim()) {
|
if (!values.account || !values.account.trim()) {
|
||||||
throw new Error('请输入账号备注');
|
throw new Error('请输入账户名称');
|
||||||
}
|
}
|
||||||
if (!values.secret || !values.secret.trim()) {
|
if (!values.secret || !values.secret.trim()) {
|
||||||
throw new Error('请输入密钥');
|
throw new Error('请输入密钥');
|
||||||
|
@ -100,16 +108,16 @@ Page({
|
||||||
|
|
||||||
// 格式化数据
|
// 格式化数据
|
||||||
const tokenData = {
|
const tokenData = {
|
||||||
type: values.type,
|
type: this.data.type,
|
||||||
issuer: values.issuer.trim(),
|
issuer: values.issuer.trim(),
|
||||||
remark: values.remark.trim(),
|
account: values.account.trim(),
|
||||||
secret: values.secret.trim().toUpperCase(),
|
secret: values.secret.trim().toUpperCase(),
|
||||||
algo: values.algo,
|
algorithm: values.algorithm,
|
||||||
digits: parseInt(values.digits, 10)
|
digits: parseInt(values.digits, 10)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 类型特定字段
|
// 类型特定字段
|
||||||
if (values.type === 'totp') {
|
if (this.data.type === 'totp') {
|
||||||
const period = parseInt(values.period, 10);
|
const period = parseInt(values.period, 10);
|
||||||
if (isNaN(period) || period < 15 || period > 300) {
|
if (isNaN(period) || period < 15 || period > 300) {
|
||||||
throw new Error('更新周期必须在15-300秒之间');
|
throw new Error('更新周期必须在15-300秒之间');
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</view>
|
</view>
|
||||||
<view class="input-box">
|
<view class="input-box">
|
||||||
<text>Account</text>
|
<text>Account</text>
|
||||||
<input name="remark" placeholder="帐号备注" value="{{formData.remark}}" placeholder-style="color: #666; font-size: 1rem;" />
|
<input name="account" placeholder="账户名称" value="{{formData.account}}" placeholder-style="color: #666; font-size: 1rem;" />
|
||||||
</view>
|
</view>
|
||||||
<view class="input-box">
|
<view class="input-box">
|
||||||
<text>KEY</text>
|
<text>KEY</text>
|
||||||
|
@ -35,10 +35,10 @@
|
||||||
<view class="section-title">高级设置</view>
|
<view class="section-title">高级设置</view>
|
||||||
<view class="input-box">
|
<view class="input-box">
|
||||||
<text>算法</text>
|
<text>算法</text>
|
||||||
<radio-group name="algo">
|
<radio-group name="algorithm">
|
||||||
<radio value="SHA1" checked="{{!formData.algo || formData.algo === 'SHA1'}}">SHA1</radio>
|
<radio value="SHA1" checked="{{!formData.algorithm || formData.algorithm === 'SHA1'}}">SHA1</radio>
|
||||||
<radio value="SHA256" checked="{{formData.algo === 'SHA256'}}">SHA256</radio>
|
<radio value="SHA256" checked="{{formData.algorithm === 'SHA256'}}">SHA256</radio>
|
||||||
<radio value="SHA512" checked="{{formData.algo === 'SHA512'}}">SHA512</radio>
|
<radio value="SHA512" checked="{{formData.algorithm === 'SHA512'}}">SHA512</radio>
|
||||||
</radio-group>
|
</radio-group>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
|
@ -222,14 +222,17 @@ Page({
|
||||||
|
|
||||||
// 为每个token计算正确的时间戳
|
// 为每个token计算正确的时间戳
|
||||||
const updatePromises = tokensToUpdate.map(token => {
|
const updatePromises = tokensToUpdate.map(token => {
|
||||||
|
// 优先使用令牌中的时间戳(如果有的话)
|
||||||
|
const tokenTimestamp = token.timestamp || currentTimestamp;
|
||||||
|
|
||||||
if (token.type === 'totp') {
|
if (token.type === 'totp') {
|
||||||
// 计算时间窗口的开始时间
|
// 计算时间窗口的开始时间
|
||||||
const period = token.period || 30;
|
const period = token.period || 30;
|
||||||
const windowStart = Math.floor(currentTimestamp / period) * period;
|
const windowStart = Math.floor(tokenTimestamp / period) * period;
|
||||||
return this.updateTokenCode(token, windowStart);
|
return this.updateTokenCode(token, windowStart);
|
||||||
} else {
|
} else {
|
||||||
// 对于HOTP类型,直接使用当前时间戳
|
// 对于HOTP类型,直接使用时间戳
|
||||||
return this.updateTokenCode(token, currentTimestamp);
|
return this.updateTokenCode(token, tokenTimestamp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -263,14 +266,17 @@ Page({
|
||||||
|
|
||||||
// 并行更新所有令牌的验证码,为每个令牌计算其时间窗口的开始时间
|
// 并行更新所有令牌的验证码,为每个令牌计算其时间窗口的开始时间
|
||||||
const updatePromises = tokens.map(token => {
|
const updatePromises = tokens.map(token => {
|
||||||
|
// 优先使用令牌中的时间戳(如果有的话)
|
||||||
|
const tokenTimestamp = token.timestamp || currentTimestamp;
|
||||||
|
|
||||||
if (token.type === 'totp') {
|
if (token.type === 'totp') {
|
||||||
// 计算时间窗口的开始时间
|
// 计算时间窗口的开始时间
|
||||||
const period = token.period || 30;
|
const period = token.period || 30;
|
||||||
const windowStart = Math.floor(currentTimestamp / period) * period;
|
const windowStart = Math.floor(tokenTimestamp / period) * period;
|
||||||
return this.updateTokenCode(token, windowStart);
|
return this.updateTokenCode(token, windowStart);
|
||||||
} else {
|
} else {
|
||||||
// 对于HOTP类型,直接使用当前时间戳
|
// 对于HOTP类型,直接使用时间戳
|
||||||
return this.updateTokenCode(token, currentTimestamp);
|
return this.updateTokenCode(token, tokenTimestamp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -426,73 +432,33 @@ Page({
|
||||||
try {
|
try {
|
||||||
// 解析二维码内容
|
// 解析二维码内容
|
||||||
const qrContent = res.result;
|
const qrContent = res.result;
|
||||||
// 如果是otpauth://格式的URL
|
|
||||||
if (qrContent.startsWith('otpauth://')) {
|
// 导入util.js中的parseURL函数
|
||||||
// 小程序兼容的URL解析
|
const { parseURL } = require('../../utils/util');
|
||||||
const [protocolAndPath, search] = qrContent.split('?');
|
|
||||||
const [protocol, path] = protocolAndPath.split('://');
|
// 使用parseURL函数解析二维码内容
|
||||||
const type = protocol.replace('otpauth:', '');
|
const parsedToken = parseURL(qrContent);
|
||||||
|
|
||||||
// 解析路径部分
|
if (parsedToken) {
|
||||||
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 = {
|
const formData = {
|
||||||
type: validType,
|
type: parsedToken.type,
|
||||||
issuer,
|
issuer: parsedToken.issuer || '',
|
||||||
remark,
|
account: parsedToken.account || '',
|
||||||
secret: params.secret,
|
secret: parsedToken.secret || '',
|
||||||
algorithm: params.algorithm || 'SHA1',
|
algorithm: parsedToken.algorithm || 'SHA1',
|
||||||
digits: params.digits ? parseInt(params.digits, 10) : 6,
|
digits: parsedToken.digits || 6,
|
||||||
period: validType === 'totp' ? (params.period ? parseInt(params.period, 10) : 30) : undefined,
|
period: parsedToken.type === 'totp' ? (parsedToken.period || 30) : undefined,
|
||||||
counter: validType === 'hotp' ? (params.counter ? parseInt(params.counter, 10) : 0) : undefined
|
counter: parsedToken.type === 'hotp' ? (parsedToken.counter || 0) : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
// 验证必要参数
|
// 验证必要参数
|
||||||
if (formData.digits < 6 || formData.digits > 8) {
|
if (formData.digits < 6 || formData.digits > 8) {
|
||||||
formData.digits = 6;
|
formData.digits = 6;
|
||||||
console.warn('验证码位数无效,已设置为默认值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;
|
formData.period = 30;
|
||||||
console.warn('TOTP周期无效,已设置为默认值30秒');
|
console.warn('TOTP周期无效,已设置为默认值30秒');
|
||||||
}
|
}
|
||||||
|
@ -640,14 +606,22 @@ Page({
|
||||||
// 获取当前令牌列表
|
// 获取当前令牌列表
|
||||||
const tokens = await wx.getStorageSync('tokens') || [];
|
const tokens = await wx.getStorageSync('tokens') || [];
|
||||||
|
|
||||||
|
// 生成唯一ID和时间戳
|
||||||
// 生成唯一ID和时间戳
|
// 生成唯一ID和时间戳
|
||||||
const newToken = {
|
const newToken = {
|
||||||
...tokenData,
|
...tokenData,
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
createdAt: new Date().toISOString(),
|
createTime: formatTime(new Date()),
|
||||||
lastUpdate: new Date().toISOString()
|
lastUpdate: formatTime(new Date()),
|
||||||
|
code: '' // 初始化为空字符串
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 对于HOTP类型,添加counter字段
|
||||||
|
if (tokenData.type && tokenData.type.toUpperCase() === 'HOTP') {
|
||||||
|
newToken.counter = 0; // HOTP类型需要counter >= 0
|
||||||
|
}
|
||||||
|
// 对于TOTP类型,不设置counter字段,让它在JSON序列化时被忽略
|
||||||
|
|
||||||
// 如果是TOTP类型,先初始化剩余时间
|
// 如果是TOTP类型,先初始化剩余时间
|
||||||
if ((newToken.type || 'totp').toLowerCase() === 'totp') {
|
if ((newToken.type || 'totp').toLowerCase() === 'totp') {
|
||||||
const period = parseInt(newToken.period || '30', 10);
|
const period = parseInt(newToken.period || '30', 10);
|
||||||
|
|
61
pages/index/index.skeleton.wxml
Normal file
61
pages/index/index.skeleton.wxml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<!--
|
||||||
|
此文件为开发者工具生成,生成时间: 2025/6/12上午10:26:00
|
||||||
|
使用方法:
|
||||||
|
在 D:\Project\go\src\otp\miniprogram\pages\index\index.wxml 引入模板
|
||||||
|
|
||||||
|
```
|
||||||
|
<import src="index.skeleton.wxml"/>
|
||||||
|
<template is="skeleton" wx:if="{{loading}}" />
|
||||||
|
```
|
||||||
|
|
||||||
|
在 D:\Project\go\src\otp\miniprogram\pages\index\index.wxss 中引入样式
|
||||||
|
```
|
||||||
|
@import "./index.skeleton.wxss";
|
||||||
|
```
|
||||||
|
|
||||||
|
更多详细信息可以参考文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/skeleton.html
|
||||||
|
-->
|
||||||
|
<template name="skeleton">
|
||||||
|
<view class="sk-container">
|
||||||
|
<view class="container">
|
||||||
|
<scroll-view class="scroll-view" enable-flex="true" scroll-y="true" enable-back-to-top="true">
|
||||||
|
<view class="token-list">
|
||||||
|
<view class="token-item" data-token-id="token_1749645769332_ifg0whqr6">
|
||||||
|
<view class="token-content">
|
||||||
|
<view class="token-header">
|
||||||
|
<text class="token-type totp sk-transparent sk-text-14-2857-186 sk-text">TOTP</text>
|
||||||
|
<text class="token-issuer sk-transparent sk-text-14-2857-326 sk-text">abc</text>
|
||||||
|
</view>
|
||||||
|
<view class="token-body">
|
||||||
|
<view class="code-container">
|
||||||
|
<text class="code sk-transparent sk-text-14-2857-212 sk-text">823945</text>
|
||||||
|
<view class="code-actions">
|
||||||
|
<view class="edit-btn" data-token-id="token_1749645769332_ifg0whqr6" hover-class="button-hover">
|
||||||
|
<text class="edit-icon sk-transparent sk-text-14-2857-783 sk-text">✏️</text>
|
||||||
|
</view>
|
||||||
|
<view class="delete-btn" data-id="token_1749645769332_ifg0whqr6" hover-class="button-hover">
|
||||||
|
<text class="delete-icon sk-transparent sk-text-14-2857-193 sk-text">🗑️</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="token-info">
|
||||||
|
<view class="totp-info">
|
||||||
|
<text class="remaining-time sk-transparent sk-text-14-2857-416 sk-text">剩余 29 秒</text>
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress" style="width: 96.66666666666667%"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="ad-container"></view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="add-button" hover-class="button-hover">
|
||||||
|
<text class="add-icon sk-transparent sk-text-14-2857-977 sk-text">+</text>
|
||||||
|
<text class="add-text sk-transparent sk-text-14-2857-863 sk-text">添加验证器</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
69
pages/index/index.skeleton.wxss
Normal file
69
pages/index/index.skeleton.wxss
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ const {
|
||||||
showLoading,
|
showLoading,
|
||||||
hideLoading
|
hideLoading
|
||||||
} = require('../../utils/util');
|
} = require('../../utils/util');
|
||||||
|
const config = require('../../utils/config');
|
||||||
|
const { hasValidTokens, clearAllTokens } = require('../../utils/cloud');
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
/**
|
/**
|
||||||
|
@ -14,6 +16,7 @@ Page({
|
||||||
data: {
|
data: {
|
||||||
loading: false,
|
loading: false,
|
||||||
uploading: false,
|
uploading: false,
|
||||||
|
clearing: false,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
userInfo: null,
|
userInfo: null,
|
||||||
currentYear: new Date().getFullYear()
|
currentYear: new Date().getFullYear()
|
||||||
|
@ -22,9 +25,27 @@ Page({
|
||||||
onShow: function() {
|
onShow: function() {
|
||||||
// 每次显示页面时检查登录状态
|
// 每次显示页面时检查登录状态
|
||||||
const app = getApp();
|
const app = getApp();
|
||||||
|
const isLoggedIn = hasValidTokens();
|
||||||
|
|
||||||
|
// 如果未登录,强制重置用户信息
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
this.setData({
|
||||||
|
isLoggedIn: false,
|
||||||
|
userInfo: {
|
||||||
|
avatarUrl: '/images/default-avatar.png',
|
||||||
|
nickName: '微信用户'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已登录,优先使用globalData中的用户信息
|
||||||
this.setData({
|
this.setData({
|
||||||
isLoggedIn: !!app.globalData.token,
|
isLoggedIn: true,
|
||||||
userInfo: app.globalData.userInfo
|
userInfo: app.globalData.userInfo || {
|
||||||
|
avatarUrl: '/images/default-avatar.png',
|
||||||
|
nickName: '微信用户'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -114,7 +135,10 @@ Page({
|
||||||
showToast('数据恢复成功', 'success');
|
showToast('数据恢复成功', 'success');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('数据恢复失败:', error);
|
// 如果是404错误(云端无数据),不打印错误日志
|
||||||
|
if (error.statusCode !== 404) {
|
||||||
|
console.error('数据恢复失败:', error);
|
||||||
|
}
|
||||||
showToast('数据恢复失败,请重试');
|
showToast('数据恢复失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
this.setData({ loading: false });
|
this.setData({ loading: false });
|
||||||
|
@ -131,21 +155,33 @@ Page({
|
||||||
try {
|
try {
|
||||||
showLoading('登录中...');
|
showLoading('登录中...');
|
||||||
const app = getApp();
|
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({
|
this.setData({
|
||||||
isLoggedIn: !!app.globalData.token,
|
isLoggedIn: true,
|
||||||
userInfo: {
|
userInfo: {
|
||||||
avatarUrl: wx.getStorageSync('userAvatar') || '/images/default-avatar.png',
|
...(userInfo || {}),
|
||||||
nickName: wx.getStorageSync('userNickName') || '微信用户'
|
avatarUrl: customAvatar || wx.getStorageSync('userAvatar') || '/images/default-avatar.png',
|
||||||
|
nickName: customNickName || wx.getStorageSync('userNickName') || '微信用户'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
showToast('登录成功', 'success');
|
showToast('登录成功', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('登录失败:', error);
|
||||||
showToast('登录失败,请重试');
|
showToast(error.message || '登录失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
|
@ -164,12 +200,25 @@ Page({
|
||||||
'userInfo.avatarUrl': avatarUrl
|
'userInfo.avatarUrl': avatarUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保存到本地存储
|
// 使用异步存储操作
|
||||||
wx.setStorageSync('userAvatar', avatarUrl);
|
await Promise.all([
|
||||||
|
wx.setStorage({
|
||||||
|
key: 'userAvatar',
|
||||||
|
data: avatarUrl
|
||||||
|
}),
|
||||||
|
wx.setStorage({
|
||||||
|
key: 'customAvatar',
|
||||||
|
data: avatarUrl
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
showToast('头像更新成功', 'success');
|
showToast('头像更新成功', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('头像更新失败:', error);
|
console.error('头像更新失败:', error);
|
||||||
|
// 恢复默认头像
|
||||||
|
this.setData({
|
||||||
|
'userInfo.avatarUrl': '/images/default-avatar.png'
|
||||||
|
});
|
||||||
showToast('头像更新失败,请重试');
|
showToast('头像更新失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
@ -191,6 +240,8 @@ Page({
|
||||||
|
|
||||||
// 保存到本地存储
|
// 保存到本地存储
|
||||||
wx.setStorageSync('userNickName', nickName);
|
wx.setStorageSync('userNickName', nickName);
|
||||||
|
// 额外保存为自定义昵称
|
||||||
|
wx.setStorageSync('customNickName', nickName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('昵称更新失败:', error);
|
console.error('昵称更新失败:', error);
|
||||||
showToast('昵称更新失败,请重试');
|
showToast('昵称更新失败,请重试');
|
||||||
|
@ -204,6 +255,107 @@ Page({
|
||||||
wx.stopPullDownRefresh();
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转发
|
* 转发
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
<view class="userinfo">
|
<view class="userinfo">
|
||||||
<block wx:if="{{isLoggedIn && userInfo}}">
|
<block wx:if="{{isLoggedIn && userInfo}}">
|
||||||
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
|
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
|
||||||
<image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="aspectFit"></image>
|
<image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="aspectFill"></image>
|
||||||
</button>
|
</button>
|
||||||
<input type="nickname" class="userinfo-nickname" placeholder="请输入昵称" value="{{userInfo.nickName}}" bindchange="onNicknameChange"/>
|
<input type="nickname" class="userinfo-nickname" placeholder="请输入昵称" value="{{userInfo.nickName}}" bindchange="onNicknameChange"/>
|
||||||
</block>
|
</block>
|
||||||
<block wx:else>
|
<block wx:else>
|
||||||
<button class="login-btn" bindtap="goAuth">
|
<button class="login-btn" bindtap="goAuth">
|
||||||
<image class="userinfo-avatar" src="/images/default-avatar.png" mode="aspectFit"></image>
|
<image class="userinfo-avatar" src="/images/default-avatar.png" mode="aspectFill"></image>
|
||||||
<text class="userinfo-nickname">点击登录</text>
|
<text class="userinfo-nickname">点击登录</text>
|
||||||
</button>
|
</button>
|
||||||
</block>
|
</block>
|
||||||
|
@ -20,6 +20,8 @@
|
||||||
<view class="btns">
|
<view class="btns">
|
||||||
<button class="btn" loading="{{uploading}}" disabled="{{uploading}}" bindtap="uploadData">数据备份</button>
|
<button class="btn" loading="{{uploading}}" disabled="{{uploading}}" bindtap="uploadData">数据备份</button>
|
||||||
<button class="btn" loading="{{loading}}" disabled="{{loading}}" bindtap="restoreData">数据恢复</button>
|
<button class="btn" loading="{{loading}}" disabled="{{loading}}" bindtap="restoreData">数据恢复</button>
|
||||||
|
<button wx:if="{{isLoggedIn}}" class="btn" loading="{{clearing}}" disabled="{{clearing}}" bindtap="clearCloudData" type="warn">删除云端数据</button>
|
||||||
|
<button wx:if="{{isLoggedIn}}" class="btn logout-btn" bindtap="logout">退出登录</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="footer">
|
<view class="footer">
|
||||||
<navigator url="/pages/info/info" hover-class="none">
|
<navigator url="/pages/info/info" hover-class="none">
|
||||||
|
|
|
@ -89,7 +89,8 @@ input.userinfo-nickname {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-bg image {
|
/* 修改选择器,避免影响头像图片 */
|
||||||
|
.user-bg > image {
|
||||||
width: 400rpx;
|
width: 400rpx;
|
||||||
height: 200rpx;
|
height: 200rpx;
|
||||||
}
|
}
|
||||||
|
@ -110,6 +111,12 @@ input.userinfo-nickname {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #ff9c10;
|
color: #ff9c10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #ff3a30;
|
||||||
|
border: 2px solid #ffcccb;
|
||||||
|
}
|
||||||
.footer{
|
.footer{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"postcss": true,
|
"postcss": true,
|
||||||
"minified": true,
|
"minified": true,
|
||||||
"uglifyFileName": false,
|
"uglifyFileName": true,
|
||||||
"enhance": true,
|
"enhance": true,
|
||||||
"packNpmRelationList": [],
|
"packNpmRelationList": [],
|
||||||
"babelSetting": {
|
"babelSetting": {
|
||||||
|
@ -20,6 +20,6 @@
|
||||||
"ignore": [],
|
"ignore": [],
|
||||||
"include": []
|
"include": []
|
||||||
},
|
},
|
||||||
"appid": "wxb6599459668b6b55",
|
"appid": "wx57d1033974eb5250",
|
||||||
"editorSetting": {}
|
"editorSetting": {}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"libVersion": "3.8.6",
|
"libVersion": "3.8.6",
|
||||||
"projectname": "%25E5%258A%25A8%25E6%2580%2581%25E4%25BB%25A4%25E7%2589%258C",
|
"projectname": "otp",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": true,
|
"urlCheck": true,
|
||||||
"coverView": true,
|
"coverView": true,
|
||||||
|
|
|
@ -92,22 +92,31 @@ const refreshToken = async () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户登录
|
* 用户登录
|
||||||
* @param {string} username - 用户名
|
|
||||||
* @param {string} password - 密码
|
|
||||||
* @returns {Promise<Object>} 登录结果,包含success和message字段
|
* @returns {Promise<Object>} 登录结果,包含success和message字段
|
||||||
*/
|
*/
|
||||||
const login = async (username, password) => {
|
const login = async () => {
|
||||||
try {
|
try {
|
||||||
|
// 获取微信登录凭证
|
||||||
|
const loginResult = await wx.login();
|
||||||
|
if (!loginResult.code) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '获取微信登录凭证失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送code到后端
|
||||||
const response = await wx.request({
|
const response = await wx.request({
|
||||||
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.LOGIN}`,
|
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.LOGIN}`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { username, password }
|
data: { code: loginResult.code }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200 && response.data.access_token) {
|
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.access, access_token);
|
||||||
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, refresh_token);
|
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, refresh_token);
|
||||||
|
wx.setStorageSync('openid', openid);
|
||||||
|
|
||||||
// 触发登录成功事件
|
// 触发登录成功事件
|
||||||
eventManager.emit('auth:login', parseToken(access_token));
|
eventManager.emit('auth:login', parseToken(access_token));
|
||||||
|
@ -120,7 +129,7 @@ const login = async (username, password) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: response.data?.message || '登录失败'
|
message: response.data?.errmsg || response.data?.message || '登录失败'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('登录失败:', error);
|
||||||
|
@ -138,6 +147,7 @@ const logout = () => {
|
||||||
// 清除所有认证信息
|
// 清除所有认证信息
|
||||||
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
|
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
|
||||||
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
|
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||||
|
wx.removeStorageSync('openid');
|
||||||
|
|
||||||
// 触发登出事件
|
// 触发登出事件
|
||||||
eventManager.emit('auth:logout');
|
eventManager.emit('auth:logout');
|
||||||
|
@ -149,7 +159,8 @@ const logout = () => {
|
||||||
*/
|
*/
|
||||||
const isLoggedIn = () => {
|
const isLoggedIn = () => {
|
||||||
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||||
if (!token) return false;
|
const openid = wx.getStorageSync('openid');
|
||||||
|
if (!token || !openid) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 解析JWT token(不验证签名)
|
// 解析JWT token(不验证签名)
|
||||||
|
@ -170,7 +181,8 @@ const isLoggedIn = () => {
|
||||||
*/
|
*/
|
||||||
const getCurrentUser = () => {
|
const getCurrentUser = () => {
|
||||||
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||||
if (!token) return null;
|
const openid = wx.getStorageSync('openid');
|
||||||
|
if (!token || !openid) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 解析JWT token(不验证签名)
|
// 解析JWT token(不验证签名)
|
||||||
|
@ -179,7 +191,7 @@ const getCurrentUser = () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: decoded.sub,
|
id: decoded.sub,
|
||||||
username: decoded.username,
|
openid: openid,
|
||||||
// 其他用户信息...
|
// 其他用户信息...
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
393
utils/cloud.js
393
utils/cloud.js
|
@ -4,46 +4,110 @@
|
||||||
|
|
||||||
// 导入统一配置
|
// 导入统一配置
|
||||||
const config = require('./config');
|
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是否需要刷新
|
* 检查JWT token是否需要刷新
|
||||||
* @returns {boolean}
|
* @returns {Object} 包含needsRefresh和hasRefreshToken两个布尔值
|
||||||
*/
|
*/
|
||||||
const shouldRefreshToken = () => {
|
const shouldRefreshToken = () => {
|
||||||
try {
|
try {
|
||||||
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||||
if (!token) return true;
|
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||||
|
|
||||||
// 解析JWT token(不验证签名)
|
|
||||||
const [, payload] = token.split('.');
|
|
||||||
const { exp } = JSON.parse(atob(payload));
|
|
||||||
const expirationTime = exp * 1000; // 转换为毫秒
|
|
||||||
|
|
||||||
// 如果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) {
|
} catch (error) {
|
||||||
console.error('Token解析失败:', error);
|
console.error('Token解析失败:', error);
|
||||||
return true;
|
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||||
|
return { needsRefresh: false, hasRefreshToken: !!refreshToken };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新JWT token
|
* 刷新JWT token
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<boolean>} 刷新是否成功
|
||||||
*/
|
*/
|
||||||
const refreshToken = async () => {
|
const refreshToken = async () => {
|
||||||
try {
|
try {
|
||||||
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
|
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new Error('No refresh token available');
|
console.warn('没有可用的刷新令牌,无法刷新访问令牌');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await wx.request({
|
// 使用Promise包装wx.request以便使用await
|
||||||
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
|
const response = await new Promise((resolve, reject) => {
|
||||||
method: 'POST',
|
wx.request({
|
||||||
header: {
|
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
|
||||||
'Authorization': `Bearer ${refreshToken}`
|
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) {
|
if (response.statusCode === 200 && response.data.access_token) {
|
||||||
|
@ -51,15 +115,21 @@ const refreshToken = async () => {
|
||||||
if (response.data.refresh_token) {
|
if (response.data.refresh_token) {
|
||||||
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, response.data.refresh_token);
|
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, response.data.refresh_token);
|
||||||
}
|
}
|
||||||
|
console.log('Token刷新成功');
|
||||||
|
return true;
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Token刷新失败:', error);
|
console.error('Token刷新失败:', error);
|
||||||
// 清除所有token,强制用户重新登录
|
// 清除所有token,强制用户重新登录
|
||||||
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
|
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
|
||||||
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
|
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
|
||||||
throw new Error('认证已过期,请重新登录');
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,20 +140,33 @@ const refreshToken = async () => {
|
||||||
* @returns {Promise<any>} 响应数据
|
* @returns {Promise<any>} 响应数据
|
||||||
*/
|
*/
|
||||||
const request = async (url, options = {}) => {
|
const request = async (url, options = {}) => {
|
||||||
// 检查并刷新token
|
// 对于登录请求,跳过token刷新检查
|
||||||
if (shouldRefreshToken()) {
|
const isLoginRequest = url === config.API_ENDPOINTS.AUTH.LOGIN;
|
||||||
await refreshToken();
|
|
||||||
|
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 accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
header: {
|
header: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json'
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 只有在有访问令牌且不是登录请求时才添加Authorization头
|
||||||
|
if (accessToken && !isLoginRequest) {
|
||||||
|
defaultOptions.header['Authorization'] = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...options,
|
...options,
|
||||||
|
@ -98,33 +181,81 @@ const request = async (url, options = {}) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await new Promise((resolve, reject) => {
|
const response = await new Promise((resolve, reject) => {
|
||||||
wx.request({
|
const requestTask = wx.request({
|
||||||
...requestOptions,
|
...requestOptions,
|
||||||
success: resolve,
|
success: (res) => {
|
||||||
fail: reject
|
// 对于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无效)
|
// 处理401错误(token无效)
|
||||||
if (response.statusCode === 401) {
|
if (response.statusCode === 401 && !isLoginRequest) {
|
||||||
// 尝试刷新token并重试请求
|
// 只有在有刷新令牌的情况下才尝试刷新token
|
||||||
await refreshToken();
|
const tokenStatus = shouldRefreshToken();
|
||||||
return request(url, options); // 递归调用,使用新token重试
|
if (tokenStatus.hasRefreshToken) {
|
||||||
|
const refreshSuccess = await refreshToken();
|
||||||
|
if (refreshSuccess) {
|
||||||
|
return request(url, options); // 递归调用,使用新token重试
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有刷新令牌或刷新失败,抛出认证错误
|
||||||
|
throw new Error('认证失败,请重新登录');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(response.data?.message || '请求失败');
|
// 增强错误信息
|
||||||
} catch (error) {
|
const error = new Error(response.data?.message || '请求失败');
|
||||||
console.error('API请求失败:', {
|
error.response = response;
|
||||||
endpoint: url,
|
error.statusCode = response.statusCode;
|
||||||
method: requestOptions.method,
|
|
||||||
status: error.statusCode || 'N/A',
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
throw error;
|
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 {
|
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, {
|
const response = await request(config.API_ENDPOINTS.OTP.SAVE, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
tokens,
|
tokens: processedTokens,
|
||||||
timestamp: Date.now()
|
userId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -164,24 +335,86 @@ const uploadTokens = async (tokens) => {
|
||||||
*/
|
*/
|
||||||
const fetchLatestTokens = async () => {
|
const fetchLatestTokens = async () => {
|
||||||
try {
|
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, {
|
const response = await request(config.API_ENDPOINTS.OTP.RECOVER, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
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 {
|
return {
|
||||||
tokens: response.data.tokens,
|
tokens: processedTokens,
|
||||||
timestamp: response.data.timestamp,
|
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) {
|
} catch (error) {
|
||||||
console.error('获取云端数据失败:', error);
|
// 当状态码为404时,说明云端无数据,返回空数组
|
||||||
|
if (error.statusCode === 404) {
|
||||||
|
return {
|
||||||
|
tokens: [],
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
num: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -193,16 +426,20 @@ const fetchLatestTokens = async () => {
|
||||||
*/
|
*/
|
||||||
const initCloud = async () => {
|
const initCloud = async () => {
|
||||||
try {
|
try {
|
||||||
// 检查是否有有效的访问令牌
|
// 检查是否有有效的令牌
|
||||||
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
|
if (!hasValidTokens()) {
|
||||||
if (!accessToken) {
|
console.log('未找到有效令牌,需要登录');
|
||||||
console.log('未找到访问令牌,需要登录');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证令牌有效性
|
// 验证令牌有效性
|
||||||
if (shouldRefreshToken()) {
|
const tokenStatus = shouldRefreshToken();
|
||||||
await refreshToken();
|
if (tokenStatus.needsRefresh) {
|
||||||
|
const refreshSuccess = await refreshToken();
|
||||||
|
if (!refreshSuccess) {
|
||||||
|
console.log('令牌刷新失败,需要重新登录');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -287,6 +524,47 @@ const mergeTokens = (localTokens, cloudTokens, options = { preferCloud: true })
|
||||||
return result;
|
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 = {
|
module.exports = {
|
||||||
uploadTokens,
|
uploadTokens,
|
||||||
fetchLatestTokens,
|
fetchLatestTokens,
|
||||||
|
@ -294,5 +572,8 @@ module.exports = {
|
||||||
mergeTokens,
|
mergeTokens,
|
||||||
initCloud,
|
initCloud,
|
||||||
shouldRefreshToken,
|
shouldRefreshToken,
|
||||||
refreshToken
|
refreshToken,
|
||||||
|
request,
|
||||||
|
hasValidTokens,
|
||||||
|
clearAllTokens
|
||||||
};
|
};
|
|
@ -7,7 +7,6 @@
|
||||||
const baseConfig = {
|
const baseConfig = {
|
||||||
// 生产环境配置
|
// 生产环境配置
|
||||||
API_BASE_URL: 'https://otpm.zeroc.net',
|
API_BASE_URL: 'https://otpm.zeroc.net',
|
||||||
API_VERSION: 'v1',
|
|
||||||
|
|
||||||
// API端点配置 (统一使用微信登录端点)
|
// API端点配置 (统一使用微信登录端点)
|
||||||
API_ENDPOINTS: {
|
API_ENDPOINTS: {
|
||||||
|
@ -17,26 +16,26 @@ const baseConfig = {
|
||||||
},
|
},
|
||||||
OTP: {
|
OTP: {
|
||||||
SAVE: '/otp/save',
|
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 = {
|
module.exports = {
|
||||||
...baseConfig.API_ENDPOINTS,
|
API_ENDPOINTS: baseConfig.API_ENDPOINTS,
|
||||||
...baseConfig.JWT_CONFIG,
|
JWT_CONFIG, // 导出JWT配置
|
||||||
API_BASE_URL: baseConfig.API_BASE_URL,
|
API_BASE_URL: baseConfig.API_BASE_URL
|
||||||
API_VERSION: baseConfig.API_VERSION
|
|
||||||
};
|
};
|
|
@ -40,42 +40,73 @@ const parseURL = (url) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析URL
|
// 解析类型和路径
|
||||||
const urlObj = new URL(url);
|
const protocolAndRest = url.split('://');
|
||||||
const type = urlObj.hostname.toLowerCase();
|
if (protocolAndRest.length !== 2) return null;
|
||||||
const pathParts = urlObj.pathname.substring(1).split(':');
|
|
||||||
const issuer = decodeURIComponent(pathParts[0]);
|
const typeAndRest = protocolAndRest[1].split('/', 2);
|
||||||
const account = pathParts.length > 1 ? decodeURIComponent(pathParts[1]) : '';
|
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 = {};
|
const params = {};
|
||||||
urlObj.searchParams.forEach((value, key) => {
|
const queryParts = pathAndQuery[1].split('&');
|
||||||
params[key.toLowerCase()] = decodeURIComponent(value);
|
for (const part of queryParts) {
|
||||||
});
|
const [key, value] = part.split('=');
|
||||||
|
if (key && value) {
|
||||||
// 构建令牌数据
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('[Error] Failed to parse OTP URL', {
|
console.error('解析OTP URL失败:', error);
|
||||||
error: error.message,
|
|
||||||
url: url.length > 100 ? url.substring(0, 100) + '...' : url
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -97,9 +128,9 @@ const validateToken = (tokenData) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证算法
|
// 验证算法
|
||||||
const validAlgos = ['SHA1', 'SHA256', 'SHA512'];
|
const validAlgorithms = ['SHA1', 'SHA256', 'SHA512'];
|
||||||
if (!validAlgos.includes(tokenData.algo)) {
|
if (!validAlgorithms.includes(tokenData.algorithm)) {
|
||||||
errors.push(`不支持的算法: ${tokenData.algo}`);
|
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 值
|
* 生成 HMAC-SHA1/SHA256/SHA512 值
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,7 +3,7 @@ const { base32Decode } = require('./base32.js');
|
||||||
const { constantTimeEqual, safeIntegerParse } = require('./crypto.js');
|
const { constantTimeEqual, safeIntegerParse } = require('./crypto.js');
|
||||||
|
|
||||||
// 支持的哈希算法
|
// 支持的哈希算法
|
||||||
const HASH_ALGOS = {
|
const HASH_ALGORITHMS = {
|
||||||
'SHA1': 'SHA1',
|
'SHA1': 'SHA1',
|
||||||
'SHA256': 'SHA256',
|
'SHA256': 'SHA256',
|
||||||
'SHA512': 'SHA512'
|
'SHA512': 'SHA512'
|
||||||
|
@ -11,7 +11,7 @@ const HASH_ALGOS = {
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
algorithm: HASH_ALGOS.SHA1, // 默认使用SHA1
|
algorithm: HASH_ALGORITHMS.SHA1, // 默认使用SHA1
|
||||||
digits: 6, // 默认6位数字
|
digits: 6, // 默认6位数字
|
||||||
window: 1 // 默认验证前后1个计数值
|
window: 1 // 默认验证前后1个计数值
|
||||||
};
|
};
|
||||||
|
@ -42,7 +42,7 @@ async function generateHOTP(secret, counter, options = {}) {
|
||||||
const config = { ...DEFAULT_CONFIG, ...options };
|
const config = { ...DEFAULT_CONFIG, ...options };
|
||||||
|
|
||||||
// 验证算法
|
// 验证算法
|
||||||
if (!HASH_ALGOS[config.algorithm]) {
|
if (!HASH_ALGORITHMS[config.algorithm]) {
|
||||||
throw new Error(`Unsupported algorithm: ${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 };
|
const config = { ...DEFAULT_CONFIG, ...options };
|
||||||
|
|
||||||
// 验证算法
|
// 验证算法
|
||||||
if (!HASH_ALGOS[config.algorithm]) {
|
if (!HASH_ALGORITHMS[config.algorithm]) {
|
||||||
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,5 +209,5 @@ module.exports = {
|
||||||
generateHOTP,
|
generateHOTP,
|
||||||
verifyHOTP,
|
verifyHOTP,
|
||||||
generateHOTPUri,
|
generateHOTPUri,
|
||||||
HASH_ALGOS
|
HASH_ALGORITHMS
|
||||||
};
|
};
|
|
@ -40,7 +40,7 @@ async function generateOTP(type, secret, options = {}) {
|
||||||
...options,
|
...options,
|
||||||
...(parsed.type === 'totp' ? { period: parsed.period } : {}),
|
...(parsed.type === 'totp' ? { period: parsed.period } : {}),
|
||||||
...(parsed.type === 'hotp' ? { counter: parsed.counter } : {}),
|
...(parsed.type === 'hotp' ? { counter: parsed.counter } : {}),
|
||||||
algorithm: parsed.algo,
|
algorithm: parsed.algorithm,
|
||||||
digits: parsed.digits
|
digits: parsed.digits
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ async function verifyOTP(token, type, secret, options = {}) {
|
||||||
...options,
|
...options,
|
||||||
...(parsed.type === 'totp' ? { period: parsed.period } : {}),
|
...(parsed.type === 'totp' ? { period: parsed.period } : {}),
|
||||||
...(parsed.type === 'hotp' ? { counter: parsed.counter } : {}),
|
...(parsed.type === 'hotp' ? { counter: parsed.counter } : {}),
|
||||||
algorithm: parsed.algo,
|
algorithm: parsed.algorithm,
|
||||||
digits: parsed.digits
|
digits: parsed.digits
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ const { decode: base32Decode } = require('./base32.js');
|
||||||
const { constantTimeEqual, safeIntegerParse } = require('./crypto.js');
|
const { constantTimeEqual, safeIntegerParse } = require('./crypto.js');
|
||||||
|
|
||||||
// 支持的哈希算法
|
// 支持的哈希算法
|
||||||
const HASH_ALGOS = {
|
const HASH_ALGORITHMS = {
|
||||||
'SHA1': 'SHA1',
|
'SHA1': 'SHA1',
|
||||||
'SHA256': 'SHA256',
|
'SHA256': 'SHA256',
|
||||||
'SHA512': 'SHA512'
|
'SHA512': 'SHA512'
|
||||||
|
@ -11,7 +11,7 @@ const HASH_ALGOS = {
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
algorithm: HASH_ALGOS.SHA1, // 默认使用SHA1
|
algorithm: HASH_ALGORITHMS.SHA1, // 默认使用SHA1
|
||||||
period: 30, // 默认30秒时间窗口
|
period: 30, // 默认30秒时间窗口
|
||||||
digits: 6, // 默认6位数字
|
digits: 6, // 默认6位数字
|
||||||
timestamp: null, // 默认使用当前时间
|
timestamp: null, // 默认使用当前时间
|
||||||
|
@ -40,7 +40,7 @@ async function generateTOTP(secret, options = {}) {
|
||||||
const config = { ...DEFAULT_CONFIG, ...options };
|
const config = { ...DEFAULT_CONFIG, ...options };
|
||||||
|
|
||||||
// 验证算法
|
// 验证算法
|
||||||
if (!HASH_ALGOS[config.algorithm]) {
|
if (!HASH_ALGORITHMS[config.algorithm]) {
|
||||||
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +219,7 @@ function generateTOTPUri(secret, accountName, issuer, options = {}) {
|
||||||
const config = { ...DEFAULT_CONFIG, ...options };
|
const config = { ...DEFAULT_CONFIG, ...options };
|
||||||
|
|
||||||
// 验证算法
|
// 验证算法
|
||||||
if (!HASH_ALGOS[config.algorithm]) {
|
if (!HASH_ALGORITHMS[config.algorithm]) {
|
||||||
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
throw new Error(`Unsupported algorithm: ${config.algorithm}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,5 +255,5 @@ module.exports = {
|
||||||
verifyTOTP,
|
verifyTOTP,
|
||||||
getRemainingSeconds,
|
getRemainingSeconds,
|
||||||
generateTOTPUri,
|
generateTOTPUri,
|
||||||
HASH_ALGOS
|
HASH_ALGORITHMS
|
||||||
};
|
};
|
|
@ -43,7 +43,7 @@ function verifyTokenWindow(token, timestamp, period) {
|
||||||
const generateCode = async (config) => {
|
const generateCode = async (config) => {
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
algorithm: config.algorithm || config.algo || 'SHA1',
|
algorithm: config.algorithm || 'SHA1',
|
||||||
digits: Number(config.digits) || 6,
|
digits: Number(config.digits) || 6,
|
||||||
_forceRefresh: !!config._forceRefresh,
|
_forceRefresh: !!config._forceRefresh,
|
||||||
timestamp: config.timestamp || otp.getCurrentTimestamp()
|
timestamp: config.timestamp || otp.getCurrentTimestamp()
|
||||||
|
@ -94,7 +94,7 @@ const generateCode = async (config) => {
|
||||||
const verifyCode = async (token, config) => {
|
const verifyCode = async (token, config) => {
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
algorithm: config.algorithm || config.algo || 'SHA1',
|
algorithm: config.algorithm || 'SHA1',
|
||||||
digits: Number(config.digits) || 6,
|
digits: Number(config.digits) || 6,
|
||||||
timestamp: otp.getCurrentTimestamp() // 使用统一的时间戳获取方法
|
timestamp: otp.getCurrentTimestamp() // 使用统一的时间戳获取方法
|
||||||
};
|
};
|
||||||
|
@ -150,12 +150,21 @@ const addToken = async (tokenData) => {
|
||||||
|
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
const id = `token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
const id = `token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const now = formatTime(new Date());
|
||||||
const token = {
|
const token = {
|
||||||
...tokenData,
|
...tokenData,
|
||||||
id,
|
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 {
|
try {
|
||||||
await generateCode(token);
|
await generateCode(token);
|
||||||
|
@ -235,7 +244,26 @@ const formatDate = (date) => {
|
||||||
* @returns {Promise<Array>} 同步后的令牌列表
|
* @returns {Promise<Array>} 同步后的令牌列表
|
||||||
*/
|
*/
|
||||||
const syncTokens = async (tokens) => {
|
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>} 云端的令牌列表
|
* @returns {Promise<Array>} 云端的令牌列表
|
||||||
*/
|
*/
|
||||||
const getCloudTokens = async () => {
|
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相关功能 ============
|
// ============ UI相关功能 ============
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue