- 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
88 行
4.1 KiB
JavaScript
88 行
4.1 KiB
JavaScript
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')
|
|
|
|
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
|
|
|
|
const PLANS = {
|
|
trial: { amount: 0, days: 7 },
|
|
monthly: { amount: 99, days: 30 },
|
|
yearly: { amount: 899, days: 365 }
|
|
}
|
|
|
|
router.get('/subscription/plans', wrap(async (req, res) => {
|
|
const settingsDao = require('../dao/settings.dao')
|
|
const settings = await settingsDao.getAll()
|
|
res.json(ok({
|
|
plans: [
|
|
{ key: 'monthly', name: '月卡', price: Number(settings.monthly_price) || PLANS.monthly.amount, days: 30 },
|
|
{ key: 'yearly', name: '年卡', price: Number(settings.yearly_price) || PLANS.yearly.amount, days: 365 },
|
|
{ key: 'trial', name: '试用', price: 0, days: Number(settings.trial_days) || 7 }
|
|
]
|
|
}))
|
|
}))
|
|
|
|
router.get('/subscription', requireUser, wrap(async (req, res) => {
|
|
const sub = await subscriptionDao.findActive(req.user.user_id)
|
|
const trialUsed = await subscriptionDao.findTrial(req.user.user_id)
|
|
if (!sub) return res.json(ok({ status: 'inactive', plan: 'none', remaining_days: 0, trial_used: !!trialUsed }))
|
|
res.json(ok({
|
|
status: sub.remaining_days > 0 ? 'active' : 'expired',
|
|
plan: sub.plan,
|
|
start_time: sub.start_time,
|
|
expire_time: sub.expire_time,
|
|
remaining_days: sub.remaining_days,
|
|
trial_used: !!trialUsed
|
|
}))
|
|
}))
|
|
|
|
router.post('/subscription/purchase', requireUser, wrap(async (req, res) => {
|
|
const plan = req.body.plan || req.body.plan_type
|
|
if (!PLANS[plan]) return res.json(fail(2001, 'invalid plan'))
|
|
const orderId = 'ORD' + Date.now()
|
|
res.json(ok({ order_id: orderId, payment_params: {}, plan, amount: PLANS[plan].amount }))
|
|
}))
|
|
|
|
router.post('/subscription/mock-purchase', requireUser, wrap(async (req, res) => {
|
|
const config = require('../config')
|
|
if (config.nodeEnv === 'production') return res.json(fail(2001, 'not available in production'))
|
|
const plan = req.body.plan || req.body.plan_type
|
|
if (!PLANS[plan] || plan === 'trial') return res.json(fail(2001, 'invalid plan'))
|
|
const p = PLANS[plan]
|
|
const orderId = 'MOCK' + Date.now()
|
|
await subscriptionDao.purchase(req.user.user_id, plan, p.amount, orderId, p.days)
|
|
await logDao.write({ user_id: req.user.user_id, action: 'subscription_mock_purchase', detail: '模拟购买: ' + plan, ip: req.ip })
|
|
const sub = await subscriptionDao.findActive(req.user.user_id)
|
|
res.json(ok({ status: 'active', plan, remaining_days: sub ? sub.remaining_days : p.days }))
|
|
}))
|
|
|
|
router.post('/subscription/trial', requireUser, wrap(async (req, res) => {
|
|
const usedTrial = await subscriptionDao.findTrial(req.user.user_id)
|
|
if (usedTrial) return res.json(fail(2001, '已使用过试用'))
|
|
const activeSub = await subscriptionDao.findActive(req.user.user_id)
|
|
if (activeSub) return res.json(fail(2001, '已有有效订阅'))
|
|
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
|
|
router.post('/subscription/verify', requireAdmin, wrap(async (req, res) => {
|
|
const userId = req.body.user_id
|
|
if (!userId) return res.json(fail(2001, 'user_id required'))
|
|
const plan = req.body.plan || req.body.plan_type || 'monthly'
|
|
if (!PLANS[plan]) return res.json(fail(2001, 'invalid plan'))
|
|
const p = PLANS[plan]
|
|
await subscriptionDao.purchase(userId, plan, p.amount, req.body.order_id || 'ORD' + Date.now(), p.days)
|
|
await logDao.write({ admin_id: req.admin.admin_id, action: 'subscription_verify', detail: '订阅生效: ' + plan + ' user:' + userId, ip: req.ip })
|
|
const sub = await subscriptionDao.findActive(userId)
|
|
res.json(ok({ status: 'active', plan, remaining_days: sub ? sub.remaining_days : p.days }))
|
|
}))
|
|
|
|
module.exports = router
|