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
这个提交包含在:
Guoguo
2026-04-28 18:12:30 -07:00
父节点 88adee7743
当前提交 91d5937d8e
修改 10 个文件,包含 307 行新增32 行删除
+7
查看文件
@@ -8,6 +8,7 @@
"name": "hox-scf-api",
"version": "1.0.0",
"dependencies": {
"bcryptjs": "^2.4.3",
"cos-nodejs-sdk-v5": "^2.14.7",
"dotenv": "^16.4.5",
"jsonwebtoken": "^9.0.2",
@@ -124,6 +125,12 @@
"tweetnacl": "^0.14.3"
}
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+1
查看文件
@@ -9,6 +9,7 @@
"db:init": "node scripts/init-db.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"cos-nodejs-sdk-v5": "^2.14.7",
"dotenv": "^16.4.5",
"jsonwebtoken": "^9.0.2",
+11 -2
查看文件
@@ -1,12 +1,21 @@
const crypto = require('crypto')
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const config = require('../config')
const { one } = require('./db')
function hashPassword(password, salt) {
function hashPasswordLegacy(password, salt) {
return crypto.createHash('sha256').update(String(password) + ':' + salt).digest('hex')
}
function hashPassword(password) {
return bcrypt.hashSync(password, 10)
}
function verifyPassword(password, hash) {
return bcrypt.compareSync(password, hash)
}
function randomHex(bytes) {
return crypto.randomBytes(bytes).toString('hex')
}
@@ -49,4 +58,4 @@ async function requireAdmin(ctx) {
}
}
module.exports = { hashPassword, randomHex, signUser, signAdmin, requireUser, requireAdmin }
module.exports = { hashPassword, hashPasswordLegacy, verifyPassword, randomHex, signUser, signAdmin, readBearer, requireUser, requireAdmin }
+13 -2
查看文件
@@ -1,6 +1,6 @@
const { one, query, limitClause } = require('../lib/db')
const { ok, fail } = require('../lib/response')
const { hashPassword, signAdmin, requireAdmin } = require('../lib/auth')
const { hashPassword, hashPasswordLegacy, verifyPassword, signAdmin, requireAdmin } = require('../lib/auth')
const { writeLog } = require('../lib/log')
function pageParams(ctx) {
@@ -14,7 +14,18 @@ function register(router) {
const username = ctx.body.username || ''
const password = ctx.body.password || ''
const admin = await one('SELECT * FROM admin_accounts WHERE username = :username AND status = 1', { username })
if (!admin || hashPassword(password, admin.password_salt) !== admin.password_hash) return fail(1001, '用户名或密码错误')
if (!admin) return fail(1001, '用户名或密码错误')
let matched = verifyPassword(password, admin.password_hash)
if (!matched) {
// Try legacy SHA-256 verification for migration
if (admin.password_salt && hashPasswordLegacy(password, admin.password_salt) === admin.password_hash) {
// Auto-migrate to bcrypt
const newHash = await hashPassword(password)
await query('UPDATE admin_accounts SET password_hash = :password_hash, password_salt = NULL WHERE admin_id = :admin_id', { password_hash: newHash, admin_id: admin.admin_id })
matched = true
}
}
if (!matched) return fail(1001, '用户名或密码错误')
const token = signAdmin(admin)
await writeLog({ admin_id: admin.admin_id, action: 'admin_login', detail: '管理员登录: ' + username, ip: ctx.ip })
return ok({ token, admin_id: String(admin.admin_id), username: admin.username, real_name: admin.real_name, role: admin.role })
+40 -2
查看文件
@@ -1,8 +1,10 @@
const jwt = require('jsonwebtoken')
const { one, query } = require('../lib/db')
const { ok, fail } = require('../lib/response')
const { signUser } = require('../lib/auth')
const { signUser, readBearer } = require('../lib/auth')
const { code2Session } = require('../lib/wechat')
const { writeLog } = require('../lib/log')
const config = require('../config')
function register(router) {
router.post('/api/v1/auth/login', async ctx => {
@@ -33,7 +35,43 @@ function register(router) {
})
router.post('/api/v1/auth/refresh', async ctx => {
return fail(2001, 'refresh_token 暂未启用,请重新登录')
const token = readBearer(ctx.headers)
if (!token) return fail(1001, 'token_expired')
let payload
try {
payload = jwt.verify(token, config.jwt.secret)
} catch (err) {
if (err.name === 'TokenExpiredError') {
try {
payload = jwt.verify(token, config.jwt.secret, { ignoreExpiration: true })
} catch (_) {
return fail(1001, 'token_expired')
}
const now = Math.floor(Date.now() / 1000)
const gracePeriod = 3 * 24 * 60 * 60
if (now - payload.exp > gracePeriod) {
return fail(1001, 'token_expired')
}
} else {
return fail(1001, 'token_expired')
}
}
if (payload.type !== 'user') return fail(1001, 'token_expired')
// Check if token is within 7 days of expiry (for non-expired tokens)
const now = Math.floor(Date.now() / 1000)
const sevenDays = 7 * 24 * 60 * 60
if (payload.exp && payload.exp > now && (payload.exp - now) > sevenDays) {
return ok({ token, expires_in: payload.exp - now })
}
const user = await one('SELECT * FROM users WHERE user_id = :user_id AND status = 1', { user_id: payload.user_id })
if (!user) return fail(1001, 'token_expired')
const newToken = signUser(user)
return ok({ token: newToken, expires_in: 604800 })
})
}
+8
查看文件
@@ -55,6 +55,14 @@ function register(router) {
await writeLog({ user_id: user.user_id, action: 'treatment_sync', detail: '同步护理记录: ' + sessionId, ip: ctx.ip })
return ok({ record_id: sessionId })
})
router.get('/api/v1/treatment/:record_id', async ctx => {
const user = await requireUser(ctx)
if (!user) return fail(1001, 'invalid_token')
const record = await one('SELECT * FROM treatment_records WHERE session_id = :session_id AND user_id = :user_id', { session_id: ctx.params.record_id, user_id: user.user_id })
if (!record) return fail(1005, 'record_not_found')
return ok(record)
})
}
module.exports = register