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
这个提交包含在:
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
在新工单中引用
屏蔽一个用户