fix: address critical security and data integrity issues from cross-audit
- Add expire_time > NOW() filter to findActive() preventing stale subscriptions - Add express-rate-limit on login endpoints (user: 10/15min, admin: 5/15min) - Add production guard for default admin credentials - Fix BLE bindDevice userId encoding (uint32 instead of hexToBytes on numeric) - Wrap adminCreate in transaction to prevent race condition - Add settings cache invalidation after admin saves - Read trial_days from settings instead of hardcoding 7 - Fix double JSON.stringify in commandDao.finish call - Cancel stale pending bindings before creating new ones - Reduce token refresh grace period from 3 days to 1 day - Fix subscribe-success to fetch expiry from server (correct for renewals) - Add keep-alive name property to DashboardView and SettingsView - Fix BLE disconnect() to preserve listener registrations across reconnects
这个提交包含在:
@@ -2,6 +2,7 @@ const router = require('express').Router()
|
||||
const { ok, fail } = require('../lib/response')
|
||||
const { hashPassword, hashPasswordLegacy, verifyPassword, signAdmin } = require('../lib/auth')
|
||||
const { requireAdmin } = require('../middleware/auth')
|
||||
const { invalidateCache } = require('../lib/settings-cache')
|
||||
const adminDao = require('../dao/admin.dao')
|
||||
const deviceDao = require('../dao/device.dao')
|
||||
const bindingDao = require('../dao/binding.dao')
|
||||
@@ -229,6 +230,7 @@ router.post('/settings', requireAdmin, wrap(async (req, res) => {
|
||||
if (!ALLOWED_KEYS.includes(key)) continue
|
||||
await settingsDao.update(key, req.body[key])
|
||||
}
|
||||
invalidateCache()
|
||||
res.json(ok({ message: 'success' }))
|
||||
}))
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ router.post('/auth/refresh', wrap(async (req, res) => {
|
||||
return res.json(fail(1001, 'token_expired'))
|
||||
}
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const gracePeriod = 3 * 24 * 60 * 60
|
||||
const gracePeriod = 1 * 24 * 60 * 60 // 1 day
|
||||
if (now - payload.exp > gracePeriod) {
|
||||
return res.json(fail(1001, 'token_expired'))
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ router.post('/device/bind', requireUser, wrap(async (req, res) => {
|
||||
const device = await bindingDao.findDeviceExists(deviceId)
|
||||
if (!device) return res.json(fail(1005, 'DEVICE_NOT_FOUND'))
|
||||
|
||||
// Cancel any stale pending bindings for this user before creating a new one
|
||||
await bindingDao.cancelPending(req.user.user_id)
|
||||
|
||||
const bindToken = randomHex(8)
|
||||
await bindingDao.createPending(req.user.user_id, deviceId, bindToken)
|
||||
await logDao.write({ user_id: req.user.user_id, action: 'device_bind_request', detail: '申请绑定设备: ' + deviceId, ip: req.ip })
|
||||
@@ -108,7 +111,7 @@ router.post('/device/command/result', requireUser, wrap(async (req, res) => {
|
||||
if (!commandId) return res.json(fail(2001, 'command_id required'))
|
||||
const cmd = await commandDao.findByIdForUser(commandId, req.user.user_id)
|
||||
if (!cmd) return res.json(fail(1006, 'device_not_bound'))
|
||||
await commandDao.finish(commandId, success, JSON.stringify(req.body))
|
||||
await commandDao.finish(commandId, success, req.body)
|
||||
res.json(ok({ message: 'success' }))
|
||||
}))
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ const logDao = require('../dao/log.dao')
|
||||
|
||||
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
|
||||
|
||||
// NOTE: Admin firmware routes use /admin/firmware paths but are mounted at /api/v1
|
||||
// (not under the /api/v1/admin router). This is intentional — firmware management is
|
||||
// grouped in a single file alongside the user-facing /firmware/latest endpoint for
|
||||
// cohesion, rather than splitting across the admin router and a separate user router.
|
||||
// The requireAdmin middleware still protects these routes.
|
||||
|
||||
router.get('/admin/firmware', requireAdmin, wrap(async (req, res) => {
|
||||
const rows = await firmwareDao.list()
|
||||
res.json(ok({ records: rows, total: rows.length }))
|
||||
|
||||
@@ -2,6 +2,7 @@ const router = require('express').Router()
|
||||
const { ok, fail } = require('../lib/response')
|
||||
const { requireUser } = require('../middleware/auth')
|
||||
const { requireAdmin } = require('../middleware/auth')
|
||||
const { getSettings } = require('../lib/settings-cache')
|
||||
const subscriptionDao = require('../dao/subscription.dao')
|
||||
const logDao = require('../dao/log.dao')
|
||||
|
||||
@@ -64,8 +65,10 @@ router.post('/subscription/trial', requireUser, wrap(async (req, res) => {
|
||||
if (usedTrial) return res.json(fail(2001, '已使用过试用'))
|
||||
const activeSub = await subscriptionDao.findActive(req.user.user_id)
|
||||
if (activeSub) return res.json(fail(2001, '已有有效订阅'))
|
||||
await subscriptionDao.createTrial(req.user.user_id)
|
||||
res.json(ok({ status: 'active', plan: 'trial', remaining_days: 7 }))
|
||||
const settings = await getSettings()
|
||||
const trialDays = Number(settings.trial_days) || 7
|
||||
await subscriptionDao.createTrial(req.user.user_id, undefined, trialDays)
|
||||
res.json(ok({ status: 'active', plan: 'trial', remaining_days: trialDays }))
|
||||
}))
|
||||
|
||||
// Temporary: admin-only until payment integration
|
||||
|
||||
在新工单中引用
屏蔽一个用户