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
这个提交包含在:
@@ -113,6 +113,7 @@ import { get } from '../utils/request'
|
|||||||
import { formatDate as formatDateUtil } from '../utils/format'
|
import { formatDate as formatDateUtil } from '../utils/format'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
name: 'DashboardView',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
stats: {},
|
stats: {},
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
import { get, post } from '../utils/request'
|
import { get, post } from '../utils/request'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
name: 'SettingsView',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
var api = require('../../utils/api')
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
statusBarHeight: 44,
|
statusBarHeight: 44,
|
||||||
@@ -14,21 +16,32 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onLoad: function (options) {
|
onLoad: function (options) {
|
||||||
|
var self = this
|
||||||
var app = getApp()
|
var app = getApp()
|
||||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
self.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||||
|
|
||||||
var planMap = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用会员' }
|
var planMap = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用会员' }
|
||||||
var durationMap = { yearly: 365, monthly: 30, trial: 7 }
|
|
||||||
var plan = options.plan || 'yearly'
|
var plan = options.plan || 'yearly'
|
||||||
|
self.setData({ planName: planMap[plan] || '会员' })
|
||||||
|
|
||||||
var now = new Date()
|
// Fetch actual subscription expiry from server instead of computing client-side
|
||||||
now.setDate(now.getDate() + (durationMap[plan] || 365))
|
api.getSubscription().then(function (res) {
|
||||||
var y = now.getFullYear()
|
if (res && res.expire_time) {
|
||||||
var m = ('0' + (now.getMonth() + 1)).slice(-2)
|
var date = new Date(res.expire_time)
|
||||||
var d = ('0' + now.getDate()).slice(-2)
|
var y = date.getFullYear()
|
||||||
this.setData({
|
var m = ('0' + (date.getMonth() + 1)).slice(-2)
|
||||||
planName: planMap[plan] || '会员',
|
var d = ('0' + date.getDate()).slice(-2)
|
||||||
expiryDate: y + '年' + m + '月' + d + '日'
|
self.setData({ expiryDate: y + '年' + m + '月' + d + '日' })
|
||||||
|
}
|
||||||
|
}).catch(function () {
|
||||||
|
// Fallback: compute from current date if server call fails
|
||||||
|
var durationMap = { yearly: 365, monthly: 30, trial: 7 }
|
||||||
|
var now = new Date()
|
||||||
|
now.setDate(now.getDate() + (durationMap[plan] || 365))
|
||||||
|
var y = now.getFullYear()
|
||||||
|
var m = ('0' + (now.getMonth() + 1)).slice(-2)
|
||||||
|
var d = ('0' + now.getDate()).slice(-2)
|
||||||
|
self.setData({ expiryDate: y + '年' + m + '月' + d + '日' })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ function queryStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindDevice(userId, bindToken) {
|
function bindDevice(userId, bindToken) {
|
||||||
var userBytes = protocol.hexToBytes(userId)
|
var userBytes = protocol.uint32ToBytes(parseInt(userId, 10))
|
||||||
var tokenBytes = protocol.hexToBytes(bindToken)
|
var tokenBytes = protocol.hexToBytes(bindToken)
|
||||||
var ts = Math.floor(Date.now() / 1000)
|
var ts = Math.floor(Date.now() / 1000)
|
||||||
var tsBytes = protocol.uint32ToBytes(ts)
|
var tsBytes = protocol.uint32ToBytes(ts)
|
||||||
@@ -165,7 +165,7 @@ function bindDevice(userId, bindToken) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unbindDevice(userId) {
|
function unbindDevice(userId) {
|
||||||
var userBytes = protocol.hexToBytes(userId)
|
var userBytes = protocol.uint32ToBytes(parseInt(userId, 10))
|
||||||
var payload = [0x02].concat(userBytes)
|
var payload = [0x02].concat(userBytes)
|
||||||
return writeCommandWithRetry(protocol.CMD.UNBIND, payload)
|
return writeCommandWithRetry(protocol.CMD.UNBIND, payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,9 @@ function disconnect() {
|
|||||||
_chars = {}
|
_chars = {}
|
||||||
var commands = require('./commands')
|
var commands = require('./commands')
|
||||||
commands.clearPendingAcks()
|
commands.clearPendingAcks()
|
||||||
_listeners = {}
|
// Only emit disconnected event; do not clear _listeners so that
|
||||||
|
// subscribers (other modules) retain their registrations across reconnects.
|
||||||
|
emit('disconnect_cleanup', null)
|
||||||
wx.closeBluetoothAdapter({})
|
wx.closeBluetoothAdapter({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-2
@@ -12,10 +12,10 @@
|
|||||||
"cos-nodejs-sdk-v5": "^2.14.7",
|
"cos-nodejs-sdk-v5": "^2.14.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"express-rate-limit": "^8.5.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.11.3"
|
"mysql2": "^3.11.3"
|
||||||
},
|
}
|
||||||
"devDependencies": {}
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.6.0",
|
"version": "25.6.0",
|
||||||
@@ -600,6 +600,24 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.5.0.tgz",
|
||||||
|
"integrity": "sha512-XKhFohWaSBdVJNTi5TaHziqnPkv04I9UQV6q1Wy7Ui6GGQZVW12ojDFwqer14EvCXxjvPG0CyWXx7cAXpALB4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "10.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/mime-db": {
|
"node_modules/express/node_modules/mime-db": {
|
||||||
"version": "1.54.0",
|
"version": "1.54.0",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
@@ -965,6 +983,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"cos-nodejs-sdk-v5": "^2.14.7",
|
"cos-nodejs-sdk-v5": "^2.14.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"express-rate-limit": "^8.5.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.11.3"
|
"mysql2": "^3.11.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
|
const rateLimit = require('express-rate-limit')
|
||||||
const { ok, fail } = require('./lib/response')
|
const { ok, fail } = require('./lib/response')
|
||||||
const { authMiddleware } = require('./middleware/auth')
|
const { authMiddleware } = require('./middleware/auth')
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
// Rate limiting for auth-sensitive endpoints
|
||||||
|
const userLoginLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 10,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: { code: 2001, message: 'too_many_attempts' }
|
||||||
|
})
|
||||||
|
const adminLoginLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 5,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: { code: 2001, message: 'too_many_attempts' }
|
||||||
|
})
|
||||||
|
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.header('Access-Control-Allow-Origin', '*')
|
res.header('Access-Control-Allow-Origin', '*')
|
||||||
@@ -13,6 +30,9 @@ app.use((req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.use('/api/v1/auth/login', userLoginLimiter)
|
||||||
|
app.use('/api/v1/admin/login', adminLoginLimiter)
|
||||||
|
|
||||||
app.use(authMiddleware)
|
app.use(authMiddleware)
|
||||||
|
|
||||||
app.get('/health', (req, res) => res.json(ok({ status: 'ok' })))
|
app.get('/health', (req, res) => res.json(ok({ status: 'ok' })))
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const config = {
|
|||||||
if (config.nodeEnv === 'production') {
|
if (config.nodeEnv === 'production') {
|
||||||
if (config.jwt.secret === 'dev-user-secret') throw new Error('JWT_SECRET must be set in production')
|
if (config.jwt.secret === 'dev-user-secret') throw new Error('JWT_SECRET must be set in production')
|
||||||
if (config.jwt.adminSecret === 'dev-admin-secret') throw new Error('ADMIN_JWT_SECRET must be set in production')
|
if (config.jwt.adminSecret === 'dev-admin-secret') throw new Error('ADMIN_JWT_SECRET must be set in production')
|
||||||
|
if (config.admin.username === 'admin' || config.admin.password === 'admin') throw new Error('ADMIN_USERNAME and ADMIN_PASSWORD must be changed from defaults in production')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config
|
module.exports = config
|
||||||
|
|||||||
@@ -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
|
* Create a pending binding request with 10-minute expiry
|
||||||
* @param {number} userId
|
* @param {number} userId
|
||||||
@@ -193,6 +205,7 @@ async function countActiveByUser(userId) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
findActiveByUser,
|
findActiveByUser,
|
||||||
findDeviceExists,
|
findDeviceExists,
|
||||||
|
cancelPending,
|
||||||
createPending,
|
createPending,
|
||||||
confirmBind,
|
confirmBind,
|
||||||
mockBind,
|
mockBind,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const { query, one, transaction, limitClause } = require('../lib/db')
|
|||||||
*/
|
*/
|
||||||
async function findActive(userId) {
|
async function findActive(userId) {
|
||||||
return one(
|
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 }
|
{ 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 {number} userId
|
||||||
* @param {string} [orderId] - optional order ID
|
* @param {string} [orderId] - optional order ID
|
||||||
|
* @param {number} [days] - trial duration in days (default 7)
|
||||||
* @returns {Promise<Array>} query result
|
* @returns {Promise<Array>} query result
|
||||||
*/
|
*/
|
||||||
async function createTrial(userId, orderId) {
|
async function createTrial(userId, orderId, days) {
|
||||||
|
const trialDays = days || 7
|
||||||
return query(
|
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() }
|
{ 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 user has an active subscription, add days to current expire_time.
|
||||||
* If no active subscription, insert a new one starting NOW().
|
* If no active subscription, insert a new one starting NOW().
|
||||||
* @param {number} userId
|
* @param {number} userId
|
||||||
@@ -110,27 +112,29 @@ async function purchase(userId, plan, amount, orderId, days) {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function adminCreate(userId, plan, amount, orderId, days) {
|
async function adminCreate(userId, plan, amount, orderId, days) {
|
||||||
const active = await one(
|
return transaction(async conn => {
|
||||||
'SELECT subscription_id FROM subscriptions WHERE user_id = :user_id AND status = 1 AND expire_time > NOW() LIMIT 1',
|
const [rows] = await conn.execute(
|
||||||
{ user_id: userId }
|
'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1',
|
||||||
)
|
[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 }
|
|
||||||
)
|
)
|
||||||
} else {
|
if (rows.length > 0) {
|
||||||
// Expire any stale rows, then insert new
|
// Extend existing active subscription
|
||||||
await query(
|
await conn.execute(
|
||||||
'UPDATE subscriptions SET status = 2 WHERE user_id = :user_id AND status = 1',
|
'UPDATE subscriptions SET expire_time = DATE_ADD(expire_time, INTERVAL ? DAY), plan = ?, amount = amount + ? WHERE subscription_id = ?',
|
||||||
{ user_id: userId }
|
[days, plan, amount, rows[0].subscription_id]
|
||||||
)
|
)
|
||||||
await query(
|
} else {
|
||||||
'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))',
|
// Expire any stale rows, then insert new
|
||||||
{ user_id: userId, plan, amount, order_id: orderId, days }
|
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]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,4 +11,9 @@ async function getSettings() {
|
|||||||
return cache
|
return cache
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getSettings }
|
function invalidateCache() {
|
||||||
|
cache = null
|
||||||
|
cacheTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getSettings, invalidateCache }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const router = require('express').Router()
|
|||||||
const { ok, fail } = require('../lib/response')
|
const { ok, fail } = require('../lib/response')
|
||||||
const { hashPassword, hashPasswordLegacy, verifyPassword, signAdmin } = require('../lib/auth')
|
const { hashPassword, hashPasswordLegacy, verifyPassword, signAdmin } = require('../lib/auth')
|
||||||
const { requireAdmin } = require('../middleware/auth')
|
const { requireAdmin } = require('../middleware/auth')
|
||||||
|
const { invalidateCache } = require('../lib/settings-cache')
|
||||||
const adminDao = require('../dao/admin.dao')
|
const adminDao = require('../dao/admin.dao')
|
||||||
const deviceDao = require('../dao/device.dao')
|
const deviceDao = require('../dao/device.dao')
|
||||||
const bindingDao = require('../dao/binding.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
|
if (!ALLOWED_KEYS.includes(key)) continue
|
||||||
await settingsDao.update(key, req.body[key])
|
await settingsDao.update(key, req.body[key])
|
||||||
}
|
}
|
||||||
|
invalidateCache()
|
||||||
res.json(ok({ message: 'success' }))
|
res.json(ok({ message: 'success' }))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ router.post('/auth/refresh', wrap(async (req, res) => {
|
|||||||
return res.json(fail(1001, 'token_expired'))
|
return res.json(fail(1001, 'token_expired'))
|
||||||
}
|
}
|
||||||
const now = Math.floor(Date.now() / 1000)
|
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) {
|
if (now - payload.exp > gracePeriod) {
|
||||||
return res.json(fail(1001, 'token_expired'))
|
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)
|
const device = await bindingDao.findDeviceExists(deviceId)
|
||||||
if (!device) return res.json(fail(1005, 'DEVICE_NOT_FOUND'))
|
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)
|
const bindToken = randomHex(8)
|
||||||
await bindingDao.createPending(req.user.user_id, deviceId, bindToken)
|
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 })
|
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'))
|
if (!commandId) return res.json(fail(2001, 'command_id required'))
|
||||||
const cmd = await commandDao.findByIdForUser(commandId, req.user.user_id)
|
const cmd = await commandDao.findByIdForUser(commandId, req.user.user_id)
|
||||||
if (!cmd) return res.json(fail(1006, 'device_not_bound'))
|
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' }))
|
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)
|
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) => {
|
router.get('/admin/firmware', requireAdmin, wrap(async (req, res) => {
|
||||||
const rows = await firmwareDao.list()
|
const rows = await firmwareDao.list()
|
||||||
res.json(ok({ records: rows, total: rows.length }))
|
res.json(ok({ records: rows, total: rows.length }))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const router = require('express').Router()
|
|||||||
const { ok, fail } = require('../lib/response')
|
const { ok, fail } = require('../lib/response')
|
||||||
const { requireUser } = require('../middleware/auth')
|
const { requireUser } = require('../middleware/auth')
|
||||||
const { requireAdmin } = require('../middleware/auth')
|
const { requireAdmin } = require('../middleware/auth')
|
||||||
|
const { getSettings } = require('../lib/settings-cache')
|
||||||
const subscriptionDao = require('../dao/subscription.dao')
|
const subscriptionDao = require('../dao/subscription.dao')
|
||||||
const logDao = require('../dao/log.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, '已使用过试用'))
|
if (usedTrial) return res.json(fail(2001, '已使用过试用'))
|
||||||
const activeSub = await subscriptionDao.findActive(req.user.user_id)
|
const activeSub = await subscriptionDao.findActive(req.user.user_id)
|
||||||
if (activeSub) return res.json(fail(2001, '已有有效订阅'))
|
if (activeSub) return res.json(fail(2001, '已有有效订阅'))
|
||||||
await subscriptionDao.createTrial(req.user.user_id)
|
const settings = await getSettings()
|
||||||
res.json(ok({ status: 'active', plan: 'trial', remaining_days: 7 }))
|
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
|
// Temporary: admin-only until payment integration
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户