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
这个提交包含在:
Guoguo
2026-05-05 02:33:25 -07:00
父节点 031678c03f
当前提交 2eb38195f1
修改 17 个文件,包含 147 行新增45 行删除
+13
查看文件
@@ -42,6 +42,18 @@ async function findDeviceExists(deviceId, conn) {
)
}
/**
* Cancel any existing pending bindings for a user (set bind_status=4)
* @param {number} userId
* @returns {Promise<Array>} query result
*/
async function cancelPending(userId) {
return query(
'UPDATE bindings SET bind_status = 4 WHERE user_id = :user_id AND bind_status = 3',
{ user_id: userId }
)
}
/**
* Create a pending binding request with 10-minute expiry
* @param {number} userId
@@ -193,6 +205,7 @@ async function countActiveByUser(userId) {
module.exports = {
findActiveByUser,
findDeviceExists,
cancelPending,
createPending,
confirmBind,
mockBind,
+29 -25
查看文件
@@ -7,7 +7,7 @@ const { query, one, transaction, limitClause } = require('../lib/db')
*/
async function findActive(userId) {
return one(
'SELECT *, GREATEST(DATEDIFF(expire_time, NOW()), 0) AS remaining_days FROM subscriptions WHERE user_id = :user_id AND status = 1 ORDER BY expire_time DESC LIMIT 1',
'SELECT *, GREATEST(DATEDIFF(expire_time, NOW()), 0) AS remaining_days FROM subscriptions WHERE user_id = :user_id AND status = 1 AND expire_time > NOW() ORDER BY expire_time DESC LIMIT 1',
{ user_id: userId }
)
}
@@ -49,14 +49,16 @@ async function findAnyActive(userId) {
}
/**
* Create a trial subscription (7 days, amount=0)
* Create a trial subscription (configurable days, amount=0)
* @param {number} userId
* @param {string} [orderId] - optional order ID
* @param {number} [days] - trial duration in days (default 7)
* @returns {Promise<Array>} query result
*/
async function createTrial(userId, orderId) {
async function createTrial(userId, orderId, days) {
const trialDays = days || 7
return query(
"INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, 'trial', 1, 0, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))",
"INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, 'trial', 1, 0, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL " + Number(trialDays) + " DAY))",
{ user_id: userId, order_id: orderId || 'TRIAL' + Date.now() }
)
}
@@ -99,7 +101,7 @@ async function purchase(userId, plan, amount, orderId, days) {
}
/**
* Admin-create a subscription: extend existing or create new
* Admin-create a subscription: extend existing or create new (transactional)
* If user has an active subscription, add days to current expire_time.
* If no active subscription, insert a new one starting NOW().
* @param {number} userId
@@ -110,27 +112,29 @@ async function purchase(userId, plan, amount, orderId, days) {
* @returns {Promise<void>}
*/
async function adminCreate(userId, plan, amount, orderId, days) {
const active = await one(
'SELECT subscription_id FROM subscriptions WHERE user_id = :user_id AND status = 1 AND expire_time > NOW() LIMIT 1',
{ user_id: userId }
)
if (active) {
// Extend existing active subscription
await query(
'UPDATE subscriptions SET expire_time = DATE_ADD(expire_time, INTERVAL :days DAY), plan = :plan, amount = amount + :amount WHERE subscription_id = :sid',
{ days, plan, amount, sid: active.subscription_id }
return transaction(async conn => {
const [rows] = await conn.execute(
'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1',
[userId]
)
} else {
// Expire any stale rows, then insert new
await query(
'UPDATE subscriptions SET status = 2 WHERE user_id = :user_id AND status = 1',
{ user_id: userId }
)
await query(
'INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, :plan, 1, :amount, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL :days DAY))',
{ user_id: userId, plan, amount, order_id: orderId, days }
)
}
if (rows.length > 0) {
// Extend existing active subscription
await conn.execute(
'UPDATE subscriptions SET expire_time = DATE_ADD(expire_time, INTERVAL ? DAY), plan = ?, amount = amount + ? WHERE subscription_id = ?',
[days, plan, amount, rows[0].subscription_id]
)
} else {
// Expire any stale rows, then insert new
await conn.execute(
'UPDATE subscriptions SET status = 2 WHERE user_id = ? AND status = 1',
[userId]
)
await conn.execute(
'INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (?, ?, 1, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY))',
[userId, plan, amount, orderId, days]
)
}
})
}
/**