feat: implement P0 security and reliability improvements
- bcrypt password hashing with auto-migration from SHA-256 - BLE command retry (3 attempts, 500ms delay, skip on disconnect) - BLE auto-reconnect with service re-discovery on disconnect - Treatment page disconnect/reconnect event handling - Token refresh endpoint with 3-day grace period - Client-side token auto-refresh when <24h remaining - Single treatment record detail API with ownership check
这个提交包含在:
+77
-20
@@ -1,38 +1,95 @@
|
||||
var config = require('../config/env')
|
||||
var API_BASE = config.API_BASE
|
||||
var _refreshing = false
|
||||
var _refreshQueue = []
|
||||
|
||||
function httpRequest(options) {
|
||||
var token = wx.getStorageSync('token')
|
||||
function getTokenExpiry() {
|
||||
return parseInt(wx.getStorageSync('token_expiry') || '0', 10)
|
||||
}
|
||||
|
||||
function shouldRefresh() {
|
||||
var expiry = getTokenExpiry()
|
||||
if (!expiry) return false
|
||||
var remaining = expiry - Math.floor(Date.now() / 1000)
|
||||
return remaining > 0 && remaining < 86400
|
||||
}
|
||||
|
||||
function refreshToken() {
|
||||
if (_refreshing) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
_refreshQueue.push({ resolve: resolve, reject: reject })
|
||||
})
|
||||
}
|
||||
_refreshing = true
|
||||
return new Promise(function (resolve, reject) {
|
||||
var token = wx.getStorageSync('token')
|
||||
wx.request({
|
||||
url: API_BASE + options.path,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: Object.assign({
|
||||
'Authorization': token ? 'Bearer ' + token : '',
|
||||
'Content-Type': 'application/json',
|
||||
'X-App-Version': '1.0.0',
|
||||
'X-Platform': 'wechat'
|
||||
}, options.header || {}),
|
||||
url: API_BASE + '/api/v1/auth/refresh',
|
||||
method: 'POST',
|
||||
header: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
data: {},
|
||||
success: function (res) {
|
||||
if (res.data && res.data.code === 0) {
|
||||
resolve(res.data.data)
|
||||
} else if (res.data && res.data.code === 1001 || res.data && res.data.code === 1002) {
|
||||
wx.removeStorageSync('token')
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
reject(res.data)
|
||||
if (res.data && res.data.code === 0 && res.data.data && res.data.data.token) {
|
||||
wx.setStorageSync('token', res.data.data.token)
|
||||
wx.setStorageSync('token_expiry', String(Math.floor(Date.now() / 1000) + (res.data.data.expires_in || 604800)))
|
||||
resolve(res.data.data.token)
|
||||
_refreshQueue.forEach(function (q) { q.resolve(res.data.data.token) })
|
||||
} else {
|
||||
reject(res.data || { code: -1, message: '请求失败' })
|
||||
resolve(null)
|
||||
_refreshQueue.forEach(function (q) { q.resolve(null) })
|
||||
}
|
||||
_refreshing = false
|
||||
_refreshQueue = []
|
||||
},
|
||||
fail: function (err) {
|
||||
reject({ code: 2002, message: err.errMsg || '网络异常' })
|
||||
fail: function () {
|
||||
resolve(null)
|
||||
_refreshQueue.forEach(function (q) { q.resolve(null) })
|
||||
_refreshing = false
|
||||
_refreshQueue = []
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function httpRequest(options) {
|
||||
var doRequest = function () {
|
||||
var token = wx.getStorageSync('token')
|
||||
return new Promise(function (resolve, reject) {
|
||||
wx.request({
|
||||
url: API_BASE + options.path,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: Object.assign({
|
||||
'Authorization': token ? 'Bearer ' + token : '',
|
||||
'Content-Type': 'application/json',
|
||||
'X-App-Version': '1.0.0',
|
||||
'X-Platform': 'wechat'
|
||||
}, options.header || {}),
|
||||
success: function (res) {
|
||||
if (res.data && res.data.code === 0) {
|
||||
resolve(res.data.data)
|
||||
} else if (res.data && res.data.code === 1001 || res.data && res.data.code === 1002) {
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('token_expiry')
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
reject(res.data)
|
||||
} else {
|
||||
reject(res.data || { code: -1, message: '请求失败' })
|
||||
}
|
||||
},
|
||||
fail: function (err) {
|
||||
reject({ code: 2002, message: err.errMsg || '网络异常' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldRefresh() && options.path !== '/api/v1/auth/refresh') {
|
||||
return refreshToken().then(function () { return doRequest() })
|
||||
}
|
||||
return doRequest()
|
||||
}
|
||||
|
||||
function request(options) {
|
||||
return httpRequest(options)
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户